Repository navigation
Expand file tree
/
Copy pathindex.html
More file actions
985 lines (958 loc) · 69.9 KB
/
Copy pathindex.html
File metadata and controls
985 lines (958 loc) · 69.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Primary SEO metadata -->
<title>Shawn Corvec — Meteorologist & Atmospheric Scientist</title>
<meta name="description" content="Meteorologist and atmospheric scientist Shawn Corvec: numerical weather prediction, forecasting and energy markets, with live products from open model data.">
<meta name="author" content="Shawn Corvec">
<link rel="canonical" href="https://scorvec.com/">
<meta name="robots" content="index, follow">
<!-- Open Graph (link previews on social, Slack, iMessage) -->
<meta property="og:type" content="website">
<meta property="og:title" content="Shawn Corvec — Meteorologist & Atmospheric Scientist">
<meta property="og:description" content="Quantitative meteorologist working on numerical weather prediction, operational forecasting, and energy markets. Live weather and climate products from open model data.">
<meta property="og:url" content="https://scorvec.com/">
<meta property="og:image" content="https://scorvec.com/hero-supercell-1280.webp">
<meta property="og:image:alt" content="A supercell thunderstorm over green prairie under a blue sky">
<meta property="og:site_name" content="Shawn Corvec">
<meta property="og:locale" content="en_US">
<!-- Twitter card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Shawn Corvec — Meteorologist & Atmospheric Scientist">
<meta name="twitter:description" content="Meteorologist working on numerical weather prediction, operational forecasting, and energy markets.">
<meta name="twitter:image" content="https://scorvec.com/hero-supercell-1280.webp">
<!-- Structured data: a person, and the site itself -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Person",
"@id": "https://scorvec.com/#person",
"name": "Shawn Corvec",
"givenName": "Shawn",
"familyName": "Corvec",
"alternateName": "S. Corvec",
"url": "https://scorvec.com/",
"image": "https://scorvec.com/profile.jpg",
"jobTitle": "Meteorologist & Atmospheric Scientist",
"worksFor": { "@type": "Organization", "name": "bp", "url": "https://www.bp.com/" },
"alumniOf": [
{ "@type": "CollegeOrUniversity", "name": "University of Waterloo", "url": "https://uwaterloo.ca/" },
{ "@type": "CollegeOrUniversity", "name": "Dalhousie University", "url": "https://www.dal.ca/" }
],
"description": "Quantitative meteorologist with experience in numerical weather prediction, operational forecasting, and energy markets.",
"knowsAbout": [
"Numerical weather prediction",
"Operational forecasting",
"Energy markets",
"Atmospheric science",
"El Niño–Southern Oscillation",
"Madden–Julian Oscillation",
"Wind generation forecasting"
],
"sameAs": [
"https://www.linkedin.com/in/shawn-corvec-35895b231/",
"https://www.researchgate.net/profile/Shawn-Corvec",
"https://scholar.google.com/citations?user=EYLRCJIAAAAJ&hl=en",
"https://github.com/scorvec",
"https://orcid.org/0000-0002-6948-5697"
]
},
{
"@type": "ProfilePage",
"@id": "https://scorvec.com/#profilepage",
"url": "https://scorvec.com/",
"name": "Shawn Corvec — Meteorologist & Atmospheric Scientist",
"mainEntity": { "@id": "https://scorvec.com/#person" },
"isPartOf": { "@id": "https://scorvec.com/#website" }
},
{
"@type": "WebSite",
"@id": "https://scorvec.com/#website",
"url": "https://scorvec.com/",
"name": "Shawn Corvec",
"author": { "@id": "https://scorvec.com/#person" },
"inLanguage": "en"
}
]
}
</script>
<!-- The hero photograph is the largest contentful paint: fetch it before the stylesheet parses. -->
<link rel="preload" as="image"
imagesrcset="/hero-supercell-800.webp 800w, /hero-supercell-1280.webp 1280w, /hero-supercell-1920.webp 1920w"
imagesizes="100vw">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,500;0,8..60,600;1,8..60,400&family=Source+Sans+3:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/site.css?v=08996a48dc">
<style>
/* Palette sampled from the photograph: sky, cloud, storm base, hills, prairie. */
:root {
--sky: #4878af;
--sky-deep: #274b7a;
--cloud: #eef0eb;
--cloud-2: #e3e6df;
--storm: #5c6b73;
--prairie: #29321f;
--grass: #9a8342; /* outline / underline accent (non-text, 3:1 is enough) */
--grass-ink: #74622f; /* the same gold as text: 5.2:1 on the row background */
--measure: 66rem;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
body {
background: var(--cloud);
color: var(--prairie);
font-family: var(--sans);
font-size: 1.0625rem;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
a { color: inherit; }
a:focus-visible { outline: 2px solid var(--sky); outline-offset: 3px; }
/* ── Hero: the photograph, full viewport, nothing on top of the storm ── */
.hero {
position: relative;
height: clamp(24rem, 58vh, 38rem); /* fallback for browsers without svh (Safari < 15.4) */
height: clamp(24rem, 58svh, 38rem);
overflow: hidden;
background: var(--sky);
}
.hero img {
position: absolute; inset: 0;
width: 100%; height: 100%;
object-fit: cover;
/* keep the updraft in frame when the viewport is narrower than 4:3 */
object-position: 42% 58%;
}
/* a low band of shade over the fields so the name reads; the sky stays untouched */
.hero::after {
content: '';
position: absolute; left: 0; right: 0; bottom: 0; height: 64%;
background: linear-gradient(to top, rgba(14, 20, 11, 0.92) 0%, rgba(14, 20, 11, 0.72) 32%, rgba(14, 20, 11, 0.36) 66%, rgba(14, 20, 11, 0) 100%);
pointer-events: none;
}
/* a second, narrower band at the top so the menu reads against a bright sky */
.hero::before {
content: '';
position: absolute; left: 0; right: 0; top: 0; height: 150px; z-index: 1;
background: linear-gradient(to bottom, rgba(10, 16, 24, 0.62) 0%, rgba(10, 16, 24, 0.28) 55%, rgba(10, 16, 24, 0) 100%);
pointer-events: none;
}
.hero-text {
position: absolute; z-index: 2;
left: 50%; transform: translateX(-50%);
bottom: clamp(2rem, 6vh, 4.5rem);
width: min(44rem, calc(100% - 2.5rem));
text-align: center;
color: #fff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55), 0 6px 24px rgba(0, 0, 0, 0.35);
}
.hero-text h1 {
font-family: var(--serif);
font-size: var(--t-hero);
font-weight: 400; line-height: 1.02; letter-spacing: -0.012em;
font-variation-settings: 'opsz' 72;
margin-bottom: 0.55rem;
}
.hero-text .role {
font-family: var(--serif); font-style: italic;
font-size: clamp(1.35rem, 2.2vw, 1.7rem);
font-variation-settings: 'opsz' 24;
line-height: 1.3;
color: rgba(255, 255, 255, 0.92);
margin-bottom: 1.1rem;
}
.hero-text p {
font-size: 1.2rem; line-height: 1.55; max-width: 36rem;
margin: 0 auto; /* the paragraph is narrower than the block: centre it too */
color: rgba(255, 255, 255, 0.95);
}
/* the hub's one call to action: the catalogue, as a quiet outlined pill on the photograph */
.hero-text .hero-find {
display: inline-block; margin-left: 0.4rem; padding: 0.12rem 0.75rem; border-radius: 999px;
color: #fff; text-decoration: none; border: 1px solid rgba(255, 255, 255, 0.6); background: rgba(255, 255, 255, 0.1);
font-size: 1rem; text-shadow: none; white-space: nowrap;
}
.hero-text .hero-find:hover { background: rgba(255, 255, 255, 0.22); }
.wrap { max-width: 74rem; margin: 0 auto; padding: 0 clamp(1rem, 4vw, 3rem); }
/* wide screens: the content takes the header's width and padding (site.css .sh-in), so its edges line up with the search
box and the About menu instead of a 74rem column with ~430 px empty on each side at 2160 px */
@media (min-width: 1100px) { .wrap { max-width: min(94vw, 2000px); padding: 0 clamp(1rem, 3vw, 2.5rem); } }
/* ── Right now: compact live readings, few and uncluttered. Every number is read live (midterms results_summary.json, the
ENSO feeds); on any failure the dashes stay and the card still links. ── */
.now { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; margin-top: clamp(1.75rem, 4vw, 2.75rem); }
.nc {
position: relative; display: flex; flex-direction: column; min-width: 0;
padding: 1.05rem 1.2rem 1rem; background: #fff; text-decoration: none;
border: 1px solid var(--cloud-2); border-top: 4px solid var(--acc, var(--sky-deep)); border-radius: 10px;
transition: box-shadow .15s;
}
.nc:hover { box-shadow: 0 2px 14px rgba(39, 75, 122, 0.13); }
.nc:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; }
.nc .k { /* the card's title: the theme tiles' heading below */
font-family: var(--serif); font-weight: 500; font-size: 1.32rem; line-height: 1.2;
font-variation-settings: 'opsz' 24; color: var(--acc, var(--sky-deep));
}
.nc b {
font-family: var(--serif); font-weight: 400; font-size: clamp(2rem, 3.2vw, 2.6rem); line-height: 1.05;
font-variation-settings: 'opsz' 72; color: var(--acc, var(--sky-deep)); font-variant-numeric: tabular-nums;
}
.nc .s { margin-top: 0.55rem; font-size: 0.88rem; line-height: 1.4; color: var(--storm); }
.nc .s.src { margin-top: 0.4rem; font-size: 0.76rem; }
.nc .go { margin-top: auto; padding-top: 0.6rem; font-size: 0.9rem; font-weight: 500; color: var(--acc, var(--sky-deep));
text-decoration: underline; text-underline-offset: 0.2em; text-decoration-color: transparent; }
.nc:hover .go { text-decoration-color: currentColor; }
/* El Niño card: a warm wash, the state in CPC's terms as a pill, three readings side by side, then the year and the
models' forecast, warm above zero and cool below, the models' range shaded */
.nc--en { --acc: #9a3f14; background: linear-gradient(180deg, #fff6f0 0%, #fff 48%); }
.nc--en:hover { box-shadow: 0 4px 22px rgba(154, 63, 20, 0.16); }
.en-top { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
.en-pill { flex: none; padding: 0.2rem 0.7rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; color: #fff;
background: var(--acc); white-space: nowrap; }
.en-pill.cool { background: #2c5f8a; }
.en-pill.neutral { background: #6f6b64; }
.en-pill[hidden] { display: none; }
.en-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.5rem 1rem; margin-top: 0.65rem; }
.en-stats > span { display: flex; flex-direction: column; min-width: 0; padding-left: 0.8rem; border-left: 2px solid #f1d8c8; }
.en-stats > span:first-child { padding-left: 0; border-left: 0; }
.en-stats small { font-size: 0.86rem; color: var(--prairie); line-height: 1.3; }
.en-stats em { margin-top: 0.15rem; font-style: normal; font-size: 0.78rem; line-height: 1.35; color: var(--storm); }
/* midterm card: the forecast page's navy band and seat arcs (its colours: night #0e2140, ratings Solid D .. Solid R),
every seat filled in on load, the odds counting up under each arc */
.nc--mt { --acc: #9cc0ff; background: #0e2140; border-color: #0e2140; color: #fff; }
.nc--mt:hover { box-shadow: 0 4px 22px rgba(14, 33, 64, 0.35); }
.nc--mt .k { color: #fff; }
.nc--mt .s { color: #b9c5da; }
.mt-top { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
.mt-arcs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.4rem 1.2rem; margin-top: 0.6rem; }
.mt-ch { display: flex; flex-direction: column; align-items: center; min-width: 0; text-align: center; }
.mt-arc { display: block; width: 100%; height: auto; }
.mt-arc[hidden] { display: none; }
.mt-arc circle { opacity: 0; transition: opacity 0.2s var(--d, 0s); }
.mt-arc.on circle { opacity: 1; }
.mt-arc line { stroke: rgba(255, 255, 255, 0.55); stroke-width: 1.2; stroke-dasharray: 3 2; }
/* the arcs scale with the card (~0.8 on a desktop card, ~0.5 on a phone), so the majority label goes on phones; the
line under the arcs gives both thresholds */
.mt-arc text { font-family: var(--sans); font-size: 14px; fill: #b9c5da; }
.nc--mt .mt-ch b { margin-top: 0.2rem; font-size: clamp(2.1rem, 3.4vw, 2.8rem); }
.mt-ch small { margin-top: 0.15rem; font-size: 0.86rem; line-height: 1.3; color: #b9c5da; }
.mt-key { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem 0.9rem; margin-top: 0.75rem;
font-size: 0.78rem; color: #b9c5da; }
.mt-key span { display: inline-flex; align-items: center; gap: 0.35rem; }
.mt-key i { display: inline-block; width: 0.62rem; height: 0.62rem; border-radius: 50%; }
.mt-key .sw { display: inline-flex; gap: 2px; }
.mt-key .ring { background: transparent; border: 1.6px solid #4a8af0; }
.nc--mt .go { color: #9cc0ff; }
@media (prefers-reduced-motion: reduce) { .mt-arc circle { opacity: 1; transition: none; } }
@media (max-width: 640px) { .mt-arc text { display: none; } }
/* drawn at its real pixel width (viewBox = the element's width, height fixed): a fixed 320-wide viewBox stretched to
a 560 px card would blow the labels up to ~17 px and the chart to 160 px */
.spark { display: block; width: 100%; flex: 1 1 170px; min-height: 170px; margin-top: 0.8rem; } /* fills the card's height */
.spark text { font-family: var(--sans); font-size: 11.5px; fill: var(--storm); }
.spark .lab { font-weight: 600; fill: #9a3f14; }
.spark .lab.cool { fill: #2c5f8a; }
/* SSW card: the whole row; the outlook page's three readings beside each ensemble's chance run by run, in the
stratosphere figures' model colours, named at the line ends and in the key */
.nc--ssw { --acc: #3b3f8c; grid-column: 1 / -1; background: linear-gradient(180deg, #f3f3fb 0%, #fff 48%); }
.nc--ssw:hover { box-shadow: 0 4px 22px rgba(59, 63, 140, 0.16); }
.nc--ssw .en-stats > span { border-left-color: #d8daef; }
.ss-body { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 1.6rem; align-items: start; }
.ss-text, .ss-chart { display: flex; flex-direction: column; min-width: 0; }
.ss-chart[hidden] { display: none; }
.ss-chart .spark { flex: none; height: 190px; margin-top: 0.65rem; }
.ss-key { display: flex; flex-wrap: wrap; gap: 0.25rem 0.9rem; margin-top: 0.35rem; font-size: 0.78rem; color: var(--storm); }
.ss-key span { display: inline-flex; align-items: center; gap: 0.35rem; }
.ss-key i { display: inline-block; width: 0.9rem; height: 2px; border-radius: 1px; }
@media (max-width: 820px) { .ss-body { grid-template-columns: 1fr; } }
/* ── The themes: the menus' six groups as tiles, their key pages as plain links (the descriptions are tooltips and
live on the pages and in the catalogue); the rest of a theme's pages sit behind "n more". ── */
.themes { padding: clamp(2.5rem, 6vw, 4rem) 0 clamp(2rem, 5vw, 3rem); }
/* heading with its note underneath (spread to opposite ends, the note would float ~1,000 px from its heading once
the content takes the header's width) */
.themes-head { display: flex; flex-direction: column; align-items: flex-start; gap: 0.35rem; margin-bottom: 1.25rem; }
.themes-head h2 {
font-family: var(--serif); font-weight: 400; font-size: clamp(1.7rem, 3vw, 2.3rem); line-height: 1.1;
letter-spacing: -0.01em; font-variation-settings: 'opsz' 60;
}
.themes-head p { color: var(--storm); font-size: 0.98rem; max-width: 38rem; }
.themes-head p a { text-underline-offset: 0.2em; text-decoration-color: rgba(92, 107, 115, 0.5); }
/* three stacks (apply_chrome.HOME_COLUMNS), each tile as tall as its list: no half-empty boxes, no holes beside them */
.sgs { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; align-items: start; }
.sg-col { display: grid; gap: 1rem; align-content: start; min-width: 0; }
.sg {
display: flex; flex-direction: column; min-width: 0;
padding: 1.1rem 1.25rem 0.95rem; background: #fff;
border: 1px solid var(--cloud-2); border-top: 4px solid var(--acc, var(--sky-deep)); border-radius: 10px;
}
.sg[data-topic="models"] { --acc: #1f6466; }
.sg[data-topic="weather"] { --acc: #274b7a; }
.sg[data-topic="drivers"] { --acc: #9a3f14; }
.sg[data-topic="circulation"] { --acc: #5b4a8a; }
.sg[data-topic="outlooks"] { --acc: #3f6b3a; }
.sg[data-topic="research"] { --acc: #4f5d65; }
.sg[data-topic="tools"] { --acc: #74622f; }
.sg h3 {
font-family: var(--serif); font-weight: 500; font-size: 1.32rem; line-height: 1.2;
font-variation-settings: 'opsz' 24; color: var(--acc, var(--sky-deep));
}
.sg > p { margin-top: 0.25rem; font-size: 0.92rem; line-height: 1.4; color: var(--storm); }
.sg ul { list-style: none; margin-top: 0.7rem; }
.sg li { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; border-top: 1px solid var(--cloud-2); }
.sg li a {
flex: 1 1 auto; min-width: 0; padding: 0.5rem 0; font-size: 1rem; line-height: 1.3; text-decoration: none; color: var(--prairie);
}
.sg li a:hover { text-decoration: underline; text-underline-offset: 0.18em; text-decoration-color: var(--acc); }
.sg-more { border-top: 1px solid var(--cloud-2); }
.sg-more summary { /* block, not flex: Safari ignored display:flex on <summary> before 16 */
display: block; cursor: pointer; padding: 0.5rem 0; font-size: 0.92rem; color: var(--storm); list-style: none;
}
.sg-more summary::-webkit-details-marker { display: none; }
.sg-more summary::before { content: '+'; display: inline-block; width: 1em; margin-right: 0.35rem; text-align: center; font-weight: 500; color: var(--acc); }
.sg-more[open] summary::before { content: '−'; }
.sg-more summary:hover { color: var(--prairie); }
.sg-more ul { margin-top: 0; }
.sg-all {
margin-top: auto; padding-top: 0.7rem; align-self: flex-start;
font-size: 0.9rem; font-weight: 500; color: var(--acc); text-underline-offset: 0.2em; text-decoration-color: transparent;
}
.sg-all::after { content: ' →'; }
.sg-all:hover { text-decoration-color: currentColor; }
.sg a:focus-visible, .sg-more summary:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; border-radius: 3px; }
.note { margin-top: 1.75rem; font-size: 0.9rem; line-height: 1.5; color: var(--storm);
border-left: 3px solid var(--cloud-2); padding-left: 0.9rem; }
@media (max-width: 1020px) {
.sgs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.sg-col { display: contents; } /* two columns: the tiles in one grid, stacks in reading order */
}
@media (max-width: 640px) {
.sgs, .now { grid-template-columns: 1fr; }
.sg li a { padding: 0.62rem 0; } /* 44 px touch rows */
}
footer { border-top: 1px solid var(--cloud-2); padding: 1.4rem 0 2rem; font-size: 0.9rem; color: var(--storm); }
footer .wrap { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.5rem 2rem; }
footer a { text-decoration: underline; text-underline-offset: 0.2em; text-decoration-color: rgba(92,107,115,0.45); }
@media (max-width: 520px) {
.hero img { object-position: 40% 55%; }
/* narrow phones: the text sets the hero's height (never below the usual one), so the title cannot rise under
the menu; 84 px = the overlay header's row */
.hero {
height: auto; min-height: clamp(24rem, 58vh, 38rem); min-height: clamp(24rem, 58svh, 38rem);
display: flex; flex-direction: column; justify-content: flex-end; padding-top: 84px;
}
.hero-text { position: relative; left: auto; bottom: auto; transform: none; margin: 0 auto clamp(2rem, 6vh, 4.5rem); }
}
</style>
<script src="/assets/site.js?v=6a624e5f1e" defer></script>
<script data-goatcounter="https://scorvec.goatcounter.com/count" async src="https://gc.zgo.at/count.v4.js" integrity="sha384-nRw6qfbWyJha9LhsOtSb2YJDyZdKvvCFh0fJYlkquSFjUxp9FVNugbfy8q1jdxI+" crossorigin="anonymous"></script>
</head>
<body>
<!-- sh:start -->
<a class="skip-link" href="#main">Skip to main content</a>
<header class="sh sh--overlay" id="site-header">
<div class="sh-in">
<a class="sh-brand" href="/">Shawn Corvec</a>
<button class="sh-find" type="button" aria-haspopup="dialog" aria-keyshortcuts="Control+K Meta+K" title="Find a plot (Ctrl K)"><svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true" focusable="false"><circle cx="8.5" cy="8.5" r="5.5" style="fill:none;stroke:currentColor"/><path d="M12.6 12.6 17 17" style="fill:none;stroke:currentColor"/></svg><span class="sh-find-t">Find a plot</span><kbd>Ctrl K</kbd></button>
<button class="sh-toggle" type="button" aria-expanded="false" aria-controls="sh-menu">Menu</button>
<nav class="sh-nav" id="sh-menu" aria-label="Site">
<ul class="sh-list">
<li class="sh-item sh-has-menu">
<button class="sh-link sh-menubtn" type="button" aria-expanded="false" aria-controls="sh-g0">Forecasts</button>
<div class="sh-menu sh-menu--cols" id="sh-g0">
<div class="sh-col">
<div>
<h3>Model maps</h3>
<ul>
<li><a href="/models.html"><span class="sh-t">North America, short range</span><span class="sh-d">HRRR, RRFS and RDPS: radar, snow, storms</span></a></li>
<li><a href="/samerica-models.html"><span class="sh-t">South America, to day 10</span><span class="sh-d">IFS control and GDPS, with soundings</span></a></li>
<li><a href="/ens-anomalies.html"><span class="sh-t">World ensembles, to day 15</span><span class="sh-d">Four ensembles: anomalies and regimes</span></a></li>
</ul>
</div>
</div>
<div class="sh-col">
<div>
<h3>Days 1–15</h3>
<ul>
<li><a href="/ar.html"><span class="sh-t">Atmospheric rivers</span><span class="sh-d">Moisture plumes, landfall and their odds</span></a></li>
<li><a href="/circulation.html#dt"><span class="sh-t">Jet stream and tropopause</span><span class="sh-d">Tropopause, Pacific jet and wave trains</span></a></li>
<li><a href="/circulation.html#telecon15"><span class="sh-t">Teleconnections</span><span class="sh-d">NAO, PNA, EPO and more, calibrated</span></a></li>
<li><a href="/circulation.html#tc"><span class="sh-t">Tropical cyclones</span><span class="sh-d">Tracks and recurvature from 100 members</span></a></li>
<li><a href="/recon.html"><span class="sh-t">Hurricane reconnaissance</span><span class="sh-d">Flights, dropsondes and vortex fixes</span></a></li>
</ul>
</div>
</div>
</div>
</li>
<li class="sh-item sh-has-menu">
<button class="sh-link sh-menubtn" type="button" aria-expanded="false" aria-controls="sh-g1">Tropics</button>
<div class="sh-menu sh-menu--cols" id="sh-g1">
<div class="sh-col">
<div>
<h3>ENSO and the MJO</h3>
<ul>
<li><a href="/enso.html"><span class="sh-t">El Niño monitor</span><span class="sh-d">ONI, RONI, the subsurface and winds</span></a></li>
<li><a href="/mjo.html"><span class="sh-t">MJO forecast</span><span class="sh-d">Four ensembles on one phase diagram</span></a></li>
<li><a href="/tropical-waves.html"><span class="sh-t">Tropical waves</span><span class="sh-d">Kelvin and Rossby waves, now and ahead</span></a></li>
<li><a href="/circulation.html#mslp"><span class="sh-t">Pacific pressure and satellite</span><span class="sh-d">Pressure, wind and simulated IR</span></a></li>
</ul>
</div>
</div>
<div class="sh-col">
<div>
<h3>Circulation</h3>
<ul>
<li><a href="/circulation.html#walker"><span class="sh-t">Walker and Hadley cells</span><span class="sh-d">The overturning, its outflow and descent</span></a></li>
<li><a href="/circulation.html#jets"><span class="sh-t">Jets and angular momentum</span><span class="sh-d">Jet cores, momentum and torques</span></a></li>
<li><a href="/circulation.html#rainfc"><span class="sh-t">Rain forecast, weeks 1–2</span><span class="sh-d">Four ensembles, bias-adjusted, blended</span></a></li>
</ul>
</div>
</div>
</div>
</li>
<li class="sh-item sh-has-menu">
<button class="sh-link sh-menubtn" type="button" aria-expanded="false" aria-controls="sh-g2">Stratosphere</button>
<div class="sh-menu sh-menu--cols" id="sh-g2">
<div class="sh-col">
<div>
<h3>Forecasts</h3>
<ul>
<li><a href="/ssw.html"><span class="sh-t">SSW outlook</span><span class="sh-d">Is a sudden warming coming?</span></a></li>
<li><a href="/stratosphere.html#vortex"><span class="sh-t">The next two weeks</span><span class="sh-d">The vortex, its waves and the surface</span></a></li>
<li><a href="/stratosphere.html#vortex35"><span class="sh-t">Weeks 3–5 and the season</span><span class="sh-d">GEPS, GEFS, C3S and SEAS5</span></a></li>
<li><a href="/strat-models.html"><span class="sh-t">Stratosphere model maps</span><span class="sh-d">Polar maps at every level, zoomable</span></a></li>
</ul>
</div>
</div>
<div class="sh-col">
<div>
<h3>Analyses and history</h3>
<ul>
<li><a href="/stratosphere.html#qbo"><span class="sh-t">The QBO and today's analyses</span><span class="sh-d">QBO, momentum and the Brewer–Dobson</span></a></li>
<li><a href="/stratosphere.html#shwinters"><span class="sh-t">History and studies</span><span class="sh-d">Every winter and warming since 1980</span></a></li>
</ul>
</div>
</div>
</div>
</li>
<li class="sh-item sh-has-menu">
<button class="sh-link sh-menubtn" type="button" aria-expanded="false" aria-controls="sh-g3">Weeks to seasons</button>
<div class="sh-menu sh-menu--cols" id="sh-g3">
<div class="sh-col">
<div>
<h3>Extended range</h3>
<ul>
<li><a href="/subseasonal.html"><span class="sh-t">GEPS, to day 35</span><span class="sh-d">Canada's extended ensemble</span></a></li>
<li><a href="/gefs.html"><span class="sh-t">GEFS, to day 35</span><span class="sh-d">NOAA's extended ensemble</span></a></li>
<li><a href="/sfs-subseasonal.html"><span class="sh-t">SFS, to six weeks</span><span class="sh-d">NOAA's experimental new system</span></a></li>
</ul>
</div>
</div>
<div class="sh-col">
<div>
<h3>Months and seasons</h3>
<ul>
<li><a href="/monthly-outlook.html"><span class="sh-t">Claude's long-range forecast</span><span class="sh-d">The month and three seasons ahead</span></a></li>
<li><a href="/seasonal.html"><span class="sh-t">Seasonal model forecasts</span><span class="sh-d">Seven C3S systems and NOAA's SFS</span></a></li>
<li><a href="/enso-forecasts.html"><span class="sh-t">ENSO forecasts</span><span class="sh-d">Niño-3.4 from five centres</span></a></li>
</ul>
</div>
</div>
</div>
</li>
<li class="sh-item sh-has-menu">
<button class="sh-link sh-menubtn" type="button" aria-expanded="false" aria-controls="sh-g4">Studies and tools</button>
<div class="sh-menu sh-menu--cols" id="sh-g4">
<div class="sh-col">
<div>
<h3>Studies</h3>
<ul>
<li><a href="/enso.html#imp_reg"><span class="sh-t">ENSO impacts on the Americas</span><span class="sh-d">CMIP6 and the observed record</span></a></li>
<li><a href="/mjo.html#mi"><span class="sh-t">MJO impacts</span><span class="sh-d">Each phase, season by season</span></a></li>
<li><a href="/aifs-aam-budget.html"><span class="sh-t">AI models and conservation laws</span><span class="sh-d">Does the AIFS keep the budgets?</span></a></li>
<li><a href="/torque-enso.html"><span class="sh-t">El Niño and angular momentum</span><span class="sh-d">Why the atmosphere spins up</span></a></li>
<li><a href="/topics/"><span class="sh-t">Explainers</span><span class="sh-d">The physics behind the products</span></a></li>
</ul>
</div>
</div>
<div class="sh-col">
<div>
<h3>Verification</h3>
<ul>
<li><a href="/verification.html"><span class="sh-t">Global models against observations</span><span class="sh-d">Scorecards, maps and the database</span></a></li>
<li><a href="/aifs-verify.html"><span class="sh-t">AIFS single versus ensemble control</span><span class="sh-d">Two AIFS configurations compared</span></a></li>
</ul>
</div>
<div>
<h3>Tools</h3>
<ul>
<li><a href="/catalog.html"><span class="sh-t">Every plot on the site</span><span class="sh-d">Search and filter every figure</span></a></li>
<li><a href="/skewt/"><span class="sh-t">Sounding explorer</span><span class="sh-d">Radiosondes worldwide, live</span></a></li>
<li><a href="/asos5.html"><span class="sh-t">Five-minute airport observations</span><span class="sh-d">22 major US airports, live</span></a></li>
<li><a href="/climate.html"><span class="sh-t">US climate trends</span><span class="sh-d">Every county and month since 1895</span></a></li>
<li><a href="/fci.html"><span class="sh-t">US credit conditions</span><span class="sh-d">Financial conditions, weekly since 1990</span></a></li>
</ul>
</div>
</div>
</div>
</li>
<li class="sh-item sh-item--special"><a class="sh-special" href="/midterms/">2026 Midterms</a></li>
<li class="sh-item sh-has-menu sh-item--me">
<button class="sh-link sh-menubtn sh-me" type="button" aria-expanded="false" aria-controls="sh-me">About</button>
<div class="sh-menu sh-menu--list sh-menu--me" id="sh-me">
<ul>
<li><a href="/about.html">About me</a></li>
<li><a href="/resume.html">Resume</a></li>
<li><a href="/research.html">Publications</a></li>
<li class="sh-me-ext"><a href="https://github.com/scorvec">GitHub</a></li>
<li><a href="https://scholar.google.com/citations?user=EYLRCJIAAAAJ&hl=en">Google Scholar</a></li>
<li><a href="https://www.linkedin.com/in/shawn-corvec-35895b231/">LinkedIn</a></li>
</ul>
</div>
</li>
</ul>
</nav>
</div>
</header>
<!-- sh:end -->
<header class="hero">
<img src="/hero-supercell-1920.webp"
srcset="/hero-supercell-800.webp 800w, /hero-supercell-1280.webp 1280w, /hero-supercell-1920.webp 1920w"
sizes="100vw" fetchpriority="high" decoding="async"
alt="A supercell thunderstorm builds over green prairie under a blue sky, its anvil spreading downwind.">
<div class="hero-text">
<h1>Weather and climate diagnostics</h1>
<p class="role">Forecasts, climate drivers and studies</p>
<p>Live products computed from open model and observation data and rebuilt as new runs arrive, from the next two weeks to the coming seasons. <a class="hero-find" href="/catalog.html">Browse every plot</a></p>
</div>
</header>
<main id="main" class="wrap">
<section class="now" aria-label="Right now">
<a class="nc nc--mt" href="/midterms/">
<span class="mt-top"><span class="k">2026 midterm results</span></span>
<span class="mt-arcs" aria-live="polite">
<span class="mt-ch"><svg class="mt-arc" id="mt-ah" viewBox="0 -18 300 170" role="img" aria-label="House seats as called" hidden></svg><b id="mt-h">–</b><small id="mt-hl">House, Democratic–Republican</small></span>
<span class="mt-ch"><svg class="mt-arc" id="mt-as" viewBox="0 -18 300 170" role="img" aria-label="Senate seats as called" hidden></svg><b id="mt-s">–</b><small id="mt-sl">Senate, Democratic–Republican</small></span>
</span>
<span class="mt-key" id="mt-key" hidden>
<span><i style="background:#4a8af0"></i>Democratic</span>
<span><i style="background:#e6dcc5"></i>Not called</span>
<span><i style="background:#de4f3e"></i>Republican</span>
<span><i class="ring"></i>Senate seat not up</span>
</span>
<span class="s" id="mt-sub">Every race as called, next to the forecast frozen before the polls closed.</span>
<span class="go">The races still out, every result and how the forecast did</span>
</a>
<a class="nc nc--en" href="/enso.html">
<span class="en-top"><span class="k">El Niño and La Niña</span><span class="en-pill" id="en-pill" hidden></span></span>
<span class="en-stats" aria-live="polite">
<span><b id="en-now">–</b><small>Niño-3.4 today, °C</small><em id="en-nowd"></em></span>
<span><b id="en-roni">–</b><small id="en-ronil">RONI this season</small><em id="en-ronid"></em></span>
<span><b id="en-pk">–</b><small id="en-pkl">expected peak, models</small><em id="en-pkd"></em></span>
</span>
<svg class="spark" id="en-spark" viewBox="0 0 320 170" role="img" aria-label="Niño-3.4 over the past year and the model forecast" hidden></svg>
<span class="s" id="en-sub">Sea temperatures, the official indices and the seasonal models.</span>
<span class="s src">NOAA OISST v2.1 and CPC; C3S seasonal forecasts. Contains modified Copernicus Climate Change Service information 2026.</span>
<span class="go">El Niño monitor</span>
</a>
<a class="nc nc--ssw" href="/ssw.html">
<span class="en-top"><span class="k">Sudden stratospheric warming</span><span class="en-pill" id="ss-pill" hidden></span></span>
<span class="ss-body">
<span class="ss-text">
<span class="en-stats" aria-live="polite">
<span><b id="ss-p">–</b><small id="ss-pl">chance in the next 15 days</small><em id="ss-pd"></em></span>
<span><b id="ss-kind">–</b><small>wave driving</small><em id="ss-kd"></em></span>
<span><b id="ss-sfc">–</b><small>surface pattern after day 15</small><em id="ss-sd"></em></span>
</span>
<span class="s" id="ss-sub">The 10 hPa wind at 60°N turning easterly in each ensemble's members, what drives it and the surface after.</span>
<span class="s src">ECMWF IFS-ENS and AIFS-ENS open data (CC BY 4.0), NOAA GEFS, ECCC GEPS.</span>
</span>
<span class="ss-chart" id="ss-chart" hidden>
<svg class="spark" id="ss-spark" role="img" aria-label="Each ensemble's chance of a sudden warming, run by run"></svg>
<span class="ss-key" id="ss-key"></span>
</span>
</span>
<span class="go">SSW outlook</span>
</a>
</section>
<section class="themes" id="products" aria-labelledby="themes-title">
<div class="themes-head">
<!-- one plain heading; "Browse every plot" and "Find a plot" stay in the hero/header -->
<h2 id="themes-title">Weather analysis and model data — charts and maps</h2>
</div>
<!-- hp:start -->
<div class="sgs">
<div class="sg-col">
<section class="sg" data-topic="weather">
<h3>Forecasts</h3>
<p>The newest model runs, zoomable, and the next two weeks' storms and flow aloft</p>
<ul>
<li><a href="/models.html" title="Zoomable maps of the newest HRRR, RRFS and RDPS runs for aviation, severe and winter weather and renewables: radar with precipitation type, accumulation, snow-band diagnostics (frontogenesis, EPV*, dendritic-zone lift), ceiling, visibility, temperature, dewpoint, wind, HRRR entraining CAPE (ECAPE), smoke and sunshine, with contours and hover values.">North America, short range</a></li>
<li><a href="/samerica-models.html" title="Zoomable maps of the newest IFS control (ECMWF's unperturbed 9 km run) and GDPS runs over South America to day 10: temperature, wind at the surface and at 925 and 850 hPa, rain and its type, accumulation, precipitable water, 850 hPa θe and simulated IR, with ECAPE, CAPE, shear and helicity computed on every column, airport meteograms, forecast soundings, contours and hover values.">South America, to day 10</a></li>
<li><a href="/ens-anomalies.html" title="Zoomable world maps of the newest IFS-ENS, AIFS-ENS, GEFS and GEPS runs to day 15: 2 m temperature (the IFS-ENS on its 0.1° grid), 500 hPa height and sea-level pressure against the 1991–2020 normal where the members agree on the sign, and the significant changes from the previous run, for days 1–5, 6–10, 11–15 and each day, with contours and hover values; each member's weather regime over the Euro-Atlantic and North America, with each regime's scenario maps.">World ensembles, to day 15</a></li>
<li><a href="/ar.html" title="Integrated vapour transport maps to day 7 from AIFS, IFS, GFS and GDPS; West Coast landfall and AR-scale ranks; AR probabilities to day 15 from four ensembles and their blend; GEFS weeks 3–5 AR-frequency anomalies where significant.">Atmospheric rivers</a></li>
<li><a href="/circulation.html#dt" title="The flow aloft: the 2-PVU dynamic tropopause and PV on the 330 and 350 K isentropes from the IFS control (ECMWF's unperturbed 9 km run), the North Pacific jet from every member with its extension and shift phases and what they usually bring downstream, and the Takaya–Nakamura wave activity flux at 250 hPa, the ensemble's quasi-stationary wave trains.">Jet stream and tropopause</a></li>
<li><a href="/circulation.html#telecon15" title="The NAO, PNA, EPO, WPO and seven more 500 hPa teleconnection patterns from every member of the IFS-ENS, AIFS-ENS, GEFS and GEPS runs, each ensemble calibrated against its own archived runs, with the chance of each phase only at the leads where it has shown skill and how the forecast has moved from run to run.">Teleconnections</a></li>
</ul>
<details class="sg-more"><summary>2 more</summary>
<ul>
<li><a href="/circulation.html#tc" title="Every storm ECMWF's two ensembles carry: where their 100 members take it, the chance it recurves by day 10 and when, when it becomes post-tropical and how deep it gets after; each storm on the IFS control's dynamic tropopause and in phase space; its shear and environment, profile, tilt and ventilation; and the track record of the odds against best tracks.">Tropical cyclones</a></li>
<li><a href="/recon.html" title="Atlantic hurricane hunter flights decoded and mapped: the track by SFMR or flight-level wind, dropsonde skew-Ts, vortex fixes, best track and forecast, with the aircraft's quality flags respected.">Hurricane reconnaissance</a></li>
</ul>
</details>
<a class="sg-all" href="/catalog.html?topic=weather">All plots in this theme</a>
</section>
<section class="sg" data-topic="outlooks">
<h3>Weeks to seasons</h3>
<p>Extended ensembles to day 35, Claude's long-range forecast and the seasonal models</p>
<ul>
<li><a href="/subseasonal.html" title="Environment Canada's extended ensemble to day 35 for the Americas and the tropics: weekly anomalies against its own reforecast, terciles, teleconnections, regimes and change since the previous run (its polar vortex is on the stratosphere page).">GEPS, to day 35</a></li>
<li><a href="/gefs.html" title="NOAA's GEFS extended ensemble to day 35 in the same layout: weekly anomalies against its own reforecast and the change since a week earlier (its drift-corrected polar vortex is on the stratosphere page).">GEFS, to day 35</a></li>
<li><a href="/monthly-outlook.html" title="A forecast designed and run by Claude, issued on the 15th: next month and the next three seasons of 2 m temperature, precipitation and 500 hPa height over the Northern Hemisphere, as anomalies and tercile probabilities, and Niño-3.4 for twelve months from SST and the TAO moorings' heat content. Claude's statistical forecasts are blended with the C3S seasonal systems, GEFS and GEPS where the hindcast shows they help, dotted where it shows skill, with a running record against ERA5.">Claude's long-range forecast</a></li>
<li><a href="/seasonal.html" title="Seven C3S systems from six centres plus NOAA's SFS beta: anomaly maps and tercile probabilities for every C3S model and their mean, the member-level ECMWF SEAS5 outlook (indices, teleconnections, impacts; its stratosphere is on the stratosphere page) and the 31-member SFS plumes and monthly maps.">Seasonal model forecasts</a></li>
<li><a href="/enso-forecasts.html" title="Every member of five centres' seasonal models for Niño-3.4, with percentile fans and a record of how past forecasts did.">ENSO forecasts</a></li>
</ul>
<details class="sg-more"><summary>1 more</summary>
<ul>
<li><a href="/sfs-subseasonal.html" title="NOAA's experimental SFS beta2, 31 members from the first of each month, up to six weeks in the same layout against its own 1991–2020 reforecast, with its hindcast skill against GEFS, GEPS and CFSv2.">SFS, to six weeks</a></li>
</ul>
</details>
<a class="sg-all" href="/catalog.html?topic=outlooks">All plots in this theme</a>
</section>
</div>
<div class="sg-col">
<section class="sg" data-topic="drivers">
<h3>Tropics</h3>
<p>El Niño, the MJO, the tropical waves and the overturning cells</p>
<ul>
<li><a href="/enso.html" title="Daily ONI and RONI, Niño-region sea surface temperatures, the subsurface, winds and convection, with SST anomaly maps and animation.">El Niño monitor</a></li>
<li><a href="/mjo.html" title="Real-time multivariate MJO index from the AIFS-ENS, IFS-ENS, GEFS and GEPS ensembles on one phase diagram, standard or ENSO-removed, with the observed track and what each phase has meant for temperature and rain.">MJO forecast</a></li>
<li><a href="/tropical-waves.html" title="Where the equatorial waves are now: Kelvin and Rossby wave trackers along the equator with the forecast, the OLR wave decomposition and ocean Kelvin waves.">Tropical waves</a></li>
<li><a href="/circulation.html#walker" title="The tropical overturning: the Walker circulation and the Hadley cells in every analysed week since July 2025 and in the AIFS-ENS, IFS-ENS and GEFS forecasts to day 15 against ERA5, by ocean basin; the cells' outflow, descent and turnover; 200 hPa velocity potential and the GDPS 150 hPa outflow; and how much of it the rain itself explains (a Gill model forced by IMERG and AIFS-ENS rainfall).">Walker and Hadley cells</a></li>
<li><a href="/circulation.html#jets" title="Speed and latitude of each hemisphere's subtropical jet core in the AIFS ensemble against the normal, the zonal-mean wind through the 15-day forecast, and global atmospheric angular momentum with the mountain and friction torques that change it.">Jets and angular momentum</a></li>
<li><a href="/circulation.html#rainfc" title="Bias-adjusted rain for weeks 1 and 2 from four ensembles (IFS-ENS, AIFS-ENS, GEFS, GEPS) and their verified blend over the tropics and the globe: corrected rain, its anomaly, the favoured tercile against the GPCP normal and the change since the previous day's runs, where significant.">Rain forecast, weeks 1–2</a></li>
</ul>
<details class="sg-more"><summary>1 more</summary>
<ul>
<li><a href="/circulation.html#mslp" title="Sea-level pressure and 10 m wind over the tropical and subtropical Pacific to day 15 (the AIFS and IFS ensembles, separately and blended, and the AIFS, IFS and GDPS single runs), and Environment Canada's GDPS drawn as an infrared satellite loop, 3-hourly to day 10.">Pacific pressure and satellite</a></li>
</ul>
</details>
<a class="sg-all" href="/catalog.html?topic=drivers">All plots in this theme</a>
</section>
<section class="sg" data-topic="circulation">
<h3>Stratosphere</h3>
<p>The polar vortex from the next two weeks to the season, and the QBO</p>
<ul>
<li><a href="/ssw.html" title="Is a sudden warming coming in the next 15 days, what kind of event it is (wave-1 or wave-2, bottom-up or top-down, reflecting or absorbing) and whether it will reach the surface: one summary of every ensemble.">SSW outlook</a></li>
<li><a href="/stratosphere.html#vortex" title="Every model's vortex to day 15 (its strength and the chance of a sudden warming, split or displaced member by member, the 10 hPa temperature, the vortex winds, the dripping paint, the upper stratosphere to 1 hPa), the waves driving it (the 100 hPa heat flux with the warning meter, waves 1 and 2, the E–P flux) and whether its anomaly is coming down to the surface (the polar cap, the Arctic Oscillation and NAO from every ensemble's members).">The next two weeks</a></li>
<li><a href="/stratosphere.html#vortex35" title="GEPS and GEFS to day 35 (drift-corrected vortex diagnostics, loops and dripping paint, the 100 hPa heat flux), the C3S multi-model December vortex calibrated on its hindcasts, SEAS5's vortex, QBO and annular mode, and this winter's odds from the QBO and ENSO.">Weeks 3–5 and the season</a></li>
<li><a href="/strat-models.html" title="Zoomable polar maps of the newest GFS (to day 16, 100 to 0.4 hPa), GDPS (to day 10, 100 to 1 hPa), AIFS single (100, 50 and 10 hPa) and GEOS FP (100 to 0.4 hPa) runs at every stratospheric level they publish: temperature, height, wind speed and zonal wind, the GFS's smoothed vertical motion, and Ertel potential vorticity on isentropic surfaces from 350 to 1600 K (GFS) and GEOS FP's own, both hemispheres, with contours and hover values.">Stratosphere model maps</a></li>
<li><a href="/stratosphere.html#qbo" title="The QBO from radiosondes since the 1950s and its zero-wind line forecast, the momentum budget, the Brewer–Dobson circulation and the water-vapour tape recorder.">The QBO and today's analyses</a></li>
<li><a href="/stratosphere.html#shwinters" title="Every winter and sudden warming since 1980 in MERRA-2 and 15,000 more in CMIP6, with what followed at the surface.">History and studies</a></li>
</ul>
<a class="sg-all" href="/catalog.html?topic=circulation">All plots in this theme</a>
</section>
</div>
<div class="sg-col">
<section class="sg" data-topic="research">
<h3>Studies and tools</h3>
<p>What the record and the models say, how forecasts score, and the data to explore</p>
<ul>
<li><a href="/verification.html" title="GFS, ECMWF IFS, AIFS and Canada's GDPS to day 10 and four ensembles to day 15, scored on the same surface stations, radiosondes, Stage IV rain and ERA5, with the uncertainty of every score and a database of every run. Scored with mverify, an open-source verification package.">Global models against observations</a></li>
<li><a href="/aifs-verify.html" title="The two ECMWF AIFS configurations compared as deterministic models against radiosondes and ERA5, with the ensemble member spectrally matched to the single model and the ensemble mean for reference.">AIFS single versus ensemble control</a></li>
<li><a href="/enso.html#imp_reg" title="Thousands of El Niño and La Niña events in 16 CMIP6 models against the observed record: regressions, composites, super and east-based El Niños, 500 hPa wave trains, and why the PDO mostly follows the atmosphere rather than driving it. Significant results only.">ENSO impacts on the Americas</a></li>
<li><a href="/mjo.html#mi" title="What each MJO phase has meant for temperature and rain, season by season, in CMIP6 models and the observed record. Significant results only.">MJO impacts</a></li>
<li><a href="/aifs-aam-budget.html" title="Do AI weather models obey the conservation laws? ECMWF's AI ensemble (AIFS-ENS), the single AIFS and the physics ensemble against angular momentum and its torques, dry-air mass, water and energy. Winter 2025–26, days 1–15.">AI models and conservation laws</a></li>
<li><a href="/torque-enso.html" title="Why the atmosphere spins up in El Niño: a standing shift in the Andes mountain torque supplies the extra angular momentum, and ERA5's model loses westerly momentum that its analysis puts back, mostly over the oceans. ERA5 1959–2026, 41 CMIP6 models, the 2026 budget hour by hour. Significant results only.">El Niño and angular momentum</a></li>
<li><a href="/catalog.html" title="One searchable sheet of every figure and loop, filterable by topic, model, forecast range and region, each linked to its exact view.">Every plot on the site</a></li>
<li><a href="/skewt/" title="Real-time and archived radiosondes worldwide, drawn and analysed in the browser, with launch history and record rings.">Sounding explorer</a></li>
<li><a href="/climate.html" title="Every US county and calendar month since 1895: temperature, precipitation and degree-day trends per decade with significance, the 1991–2020 normal and what normal is now.">US climate trends</a></li>
</ul>
<details class="sg-more"><summary>3 more</summary>
<ul>
<li><a href="/topics/" title="How to read the products: the equations and the physics behind each diagnostic.">Explainers</a></li>
<li><a href="/asos5.html" title="Five-minute ASOS observations at 22 major US airports: temperature, dewpoint, wind, visibility, ceiling and weather, with running daily highs and lows.">Five-minute airport observations</a></li>
<li><a href="/fci.html" title="How tight credit is in the US, weekly since 1990: corporate bond spreads, commercial paper, bank lending and the loan officer survey, mortgage rates, the fed funds rate, the yield curve, stocks and the dollar, with sub-indices for companies, mortgages, rates, markets, liquidity and private credit.">US credit conditions</a></li>
</ul>
</details>
<a class="sg-all" href="/catalog.html?topic=research">All plots in this theme</a>
</section>
</div>
</div>
<!-- hp:end -->
<p class="note"><strong>A note on reliability.</strong> The site is under constant development: products change without
notice and pipelines break. Products are generated automatically and published without review by a person; each page
describes its automated quality checks and significance tests. Nothing here should be relied on for decisions of any
kind.</p>
</section>
</main>
<footer>
<div class="wrap">
<span>Unique viewers: <span id="count">—</span>, <a href="/stats.html">by location</a></span>
<span>Photograph and site by Shawn Corvec</span>
</div>
</footer>
<script>
const $set = (id, t) => { const el = document.getElementById(id); if (el) el.textContent = t; };
const getJSON = u => fetch(u, { cache: 'no-cache' }).then(r => r.ok ? r.json() : null).catch(() => null);
const day = iso => new Date(iso.slice(0, 10) + 'T12:00:00Z').toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' });
// midterm card: each chamber's seats as called, on the forecast page's seat arcs (results_summary.json carries one
// character per seat, most Democratic first), and how the frozen forecast did; on any failure the dashes stay and the
// card still links
const FILL = ['#4a8af0', '#8db4f2', '#c6d8f6', '#e6dcc5', '#f5c3b9', '#ec8a7b', '#de4f3e'], FILLI = ['#a57bdc', '#c2a6e8', '#ddd0f2'];
// seats in concentric rows filled left to right in the given order, laid out as on the forecast page: 0 a Democratic
// win, 3 not called, 6 a Republican win, an independent's seat ('a') purple, a Senate seat not up ('D'/'R') a ring
function arc(svg, seats, maj, what) {
const n = seats.length, R = 138, cx = 150, cy = 146, R0 = 0.38;
let k = 2;
while (Math.PI * k * (1 + R0) / (2 * n) < (1 - R0) / (k - 1)) k++;
const radii = [...Array(k)].map((_, i) => R0 + (1 - R0) * i / (k - 1)), tot = radii.reduce((a, b) => a + b);
const cnt = radii.map(r => Math.floor(n * r / tot));
radii.map((r, i) => [n * r / tot - cnt[i], i]).sort((a, b) => b[0] - a[0])
.slice(0, n - cnt.reduce((a, b) => a + b)).forEach(([, i]) => cnt[i]++);
const pts = [];
radii.forEach((r, i) => { for (let j = 0; j < cnt[i]; j++) pts.push({ a: Math.PI * (1 - j / (cnt[i] - 1)), r }); });
pts.sort((p, q) => q.a - p.a || p.r - q.r);
const dot = (Math.min(Math.PI * k * (1 + R0) / (2 * n), (1 - R0) / (k - 1)) * R * 0.43).toFixed(2);
const X = (r, a) => (cx + R * r * Math.cos(a)).toFixed(1), Y = (r, a) => (cy - R * r * Math.sin(a)).toFixed(1);
const am = (pts[maj - 1].a + pts[maj].a) / 2;
svg.innerHTML = pts.map((p, i) => {
const c = seats[i], held = c === 'D' || c === 'R';
const f = held ? FILL[c === 'D' ? 0 : 6] : 'abc'.includes(c) ? FILLI['abc'.indexOf(c)] : FILL[+c];
return `<circle cx="${X(p.r, p.a)}" cy="${Y(p.r, p.a)}" r="${dot}" style="--d:${Math.round(i * 650 / n)}ms" ` +
(held ? `fill="#0e2140" stroke="${f}" stroke-width="1.8"/>` : `fill="${f}"/>`);
}).join('') +
`<line x1="${X(R0 - 0.07, am)}" y1="${Y(R0 - 0.07, am)}" x2="${X(1.04, am)}" y2="${Y(1.04, am)}"/>` +
`<text x="${X(1.08, am)}" y="${Y(1.08, am)}" text-anchor="middle">${maj} ${what}</text>`;
svg.removeAttribute('hidden'); // SVG elements have no .hidden property
}
getJSON('/midterms/data/results_summary.json').then(s => {
if (!s || !s.house) return;
const H = s.house, S = s.senate, other = c => (c.i ? `, ${c.i} other` : '');
const arcs = [['mt-ah', 'mt-h', H, 218, 'for a majority', 'House'], ['mt-as', 'mt-s', S, 51, 'for control', 'Senate']];
for (const [id, num, c, maj, what, name] of arcs) {
$set(num, `${c.d}–${c.r}`);
$set(num + 'l', `${name}, Democratic–Republican${other(c)}${c.open ? `; ${c.open} not called` : ''}`);
const svg = document.getElementById(id);
arc(svg, c.seats, maj, what);
svg.setAttribute('aria-label', `${name} seats as called: ${c.d} Democratic, ${c.r} Republican${other(c)}, ${c.open} not called`);
}
const ck = s.check;
const nat = s.national == null ? '' : `, national House vote ${Math.abs(s.national) < 0.05 ? 'even' : (s.national > 0 ? 'D+' : 'R+') + Math.abs(s.national).toFixed(1)}`;
const med = H.median && S.median ? `Median outcome with the races still out: House ${H.median.join('–')}, Senate ${S.median.join('–')}${nat}. ` : '';
$set('mt-sub', med + (ck ? `The forecast's favourite won ${ck.won} of the ${ck.called} races called so far. ` : '') +
`Forecast: ${H.forecast_d} Democratic House seats, ${S.forecast_d} Senate seats. Updated ${day(s.generated)}.`);
if (H.i || S.i)
document.querySelector('#mt-key .ring').parentElement.insertAdjacentHTML('beforebegin', `<span><i style="background:${FILLI[0]}"></i>Independent</span>`);
document.getElementById('mt-key').hidden = false;
// the seats fill in from the left once painted
requestAnimationFrame(() => requestAnimationFrame(() => arcs.forEach(([id]) => document.getElementById(id).classList.add('on'))));
});
// El Niño card: Niño-3.4 today; our RONI estimate for the season so far beside CPC's latest official season, whose
// strength in CPC's terms is the pill; the model-mean Niño-3.4 peak with the range of the model means (DWD left out,
// as on the ENSO forecasts page) and its value on the RONI scale; the past year and the models' forecast, warm above
// zero and cool below, the models' range shaded
Promise.all(['/assets/sst/data/enso_daily.json', '/assets/sst/data/enso_forecast.json', '/assets/sst/data/cpc_official.json'].map(getJSON)).then(([D, F, O]) => {
const sg = v => (v >= 0 ? '+' : '−') + Math.abs(v).toFixed(1);
const SEAS = 'JFMAMJJASONDJF';
const season = ym => { const m = +ym.slice(5, 7), k = m - 2 < 0 ? 11 : m - 2; return SEAS.slice(k, k + 3); };
const nextMonth = ym => { const y = +ym.slice(0, 4), m = +ym.slice(5, 7); return m === 12 ? `${y + 1}-01` : `${y}-${String(m + 1).padStart(2, '0')}`; };
const mean = a => a.reduce((x, y) => x + y, 0) / a.length;
const strength = v => { const a = Math.abs(v); return a >= 2 ? 'Very strong' : a >= 1.5 ? 'Strong' : a >= 1 ? 'Moderate' : 'Weak'; };
if (D && D.latest) {
const L = D.latest;
$set('en-now', sg(L.nino34));
$set('en-nowd', `${day(L.date)}, ${L.nino34_d30 >= 0 ? 'up' : 'down'} ${Math.abs(L.nino34_d30).toFixed(1)} in 30 days` +
(D.final_through && L.date > D.final_through ? ', preliminary' : ''));
// a centred season is still open until its last month has closed
if (L.roni != null) {
$set('en-roni', sg(L.roni));
$set('en-ronil', `RONI, ${season(L.oni_month)}${L.date.slice(0, 7) <= nextMonth(L.oni_month) ? ' so far' : ''}`);
}
}
if (O && O.roni) {
const k = Object.keys(O.roni).sort().pop();
if (k) {
const v = O.roni[k], pill = document.getElementById('en-pill');
$set('en-ronid', `our estimate from OISST; CPC's official ${sg(v)} for ${season(k)}`);
pill.textContent = Math.abs(v) < 0.5 ? 'ENSO-neutral' : `${strength(v)} ${v > 0 ? 'El Niño' : 'La Niña'}`;
pill.title = `CPC's official RONI for ${season(k)}: ${sg(v)} °C`;
pill.className = 'en-pill' + (Math.abs(v) < 0.5 ? ' neutral' : v < 0 ? ' cool' : '');
pill.hidden = false;
}
}
let mmm = null, lo = null, hi = null;
if (F && F.models) {
const models = Object.entries(F.models).filter(([k]) => !/^DWD\b/.test(k)).map(([, v]) => v), nL = F.valid_months.length;
const per = models.map(m => m.n34.map(mean)); // each model's ensemble mean by lead
mmm = [...Array(nL).keys()].map(l => mean(per.map(p => p[l])));
lo = [...Array(nL).keys()].map(l => Math.min(...per.map(p => p[l])));
hi = [...Array(nL).keys()].map(l => Math.max(...per.map(p => p[l])));
const s3 = (p, l) => (p[l] + p[l + 1] + p[l + 2]) / 3;
let best = -1, bv = -9;
for (let l = 0; l + 2 < nL; l++) { const v = s3(mmm, l); if (v > bv) { bv = v; best = l; } }
if (best >= 0) {
const pk = per.map(p => s3(p, best));
let rel = '';
if (models.every(m => m.rnino)) {
const rp = models.map(m => m.rnino.map(mean)), rm = [...Array(nL).keys()].map(l => mean(rp.map(p => p[l])));
let rb = 0;
for (let l = 1; l + 2 < nL; l++) if (s3(rm, l) > s3(rm, rb)) rb = l;
rel = `; ${sg(s3(rm, rb))} on the RONI scale` + (rb !== best ? ` (${season(F.valid_months[rb + 1])})` : '');
}
$set('en-pk', sg(bv));
$set('en-pkl', `expected peak, ${season(F.valid_months[best + 1])}`);
$set('en-pkd', `${models.length} models, ${sg(Math.min(...pk))} to ${sg(Math.max(...pk))}${rel}`);
$set('en-sub', `The peak is the 3-month mean of ${models.length} C3S models' Niño-3.4, each against its own 1993–2016 ` +
'hindcast; the shading is their range.');
}
}
if (D && D.daily && D.daily.nino34) {
const dd = D.daily.dates, v = D.daily.nino34, n = dd.length, k0 = Math.max(0, n - 365);
const t = s => Date.parse(s.length === 7 ? s + '-15' : s);
const obs = dd.slice(k0).map((s, i) => [t(s), v[k0 + i]]).filter(p => p[1] != null);
if (obs.length < 2) return;
const last = obs[obs.length - 1];
// the forecast from today's value on: the model mean and the models' range by valid month
const ahead = mmm ? F.valid_months.map((m, i) => [t(m), mmm[i], lo[i], hi[i]]).filter(p => p[0] > last[0]) : [];
const fc = ahead.length ? [[last[0], last[1], last[1], last[1]]].concat(ahead) : [];
const x0 = obs[0][0], x1 = (fc.length ? fc[fc.length - 1] : last)[0];
const ys = obs.map(p => p[1]).concat(...fc.map(p => [p[2], p[3]]));
const y0 = Math.floor(Math.min(-1, ...ys)), y1 = Math.ceil(Math.max(1, ...ys));
const svg = document.getElementById('en-spark');
svg.hidden = false;
const top = 8;
const draw = () => {
const r = svg.getBoundingClientRect(), W = Math.max(220, Math.round(r.width) || 320), L0 = 26, R0 = 8;
const H = Math.max(170, Math.round(r.height) || 170), bot = H - 22;
svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
const X = x => L0 + (W - L0 - R0) * (x - x0) / (x1 - x0), Y = y => top + (bot - top) * (y1 - y) / (y1 - y0);
const f1 = z => z.toFixed(1), z0 = f1(Y(0));
const path = (pts, k = 1) => pts.map((p, i) => (i ? 'L' : 'M') + f1(X(p[0])) + ' ' + f1(Y(p[k]))).join('');
const line = path(obs);
const area = `${line}L${f1(X(last[0]))} ${z0}L${f1(X(obs[0][0]))} ${z0}Z`;
let g = '<defs><clipPath id="en-up"><rect x="0" y="0" width="' + W + '" height="' + z0 + '"/></clipPath>' +
'<clipPath id="en-dn"><rect x="0" y="' + z0 + '" width="' + W + '" height="' + H + '"/></clipPath>' +
'<linearGradient id="en-warm" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#d9622b" stop-opacity="0.42"/>' +
'<stop offset="1" stop-color="#d9622b" stop-opacity="0.06"/></linearGradient>' +
'<linearGradient id="en-cool" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#3f7fb5" stop-opacity="0.06"/>' +
'<stop offset="1" stop-color="#3f7fb5" stop-opacity="0.38"/></linearGradient></defs>';
for (let y = y0; y <= y1; y++) // a line and a label for every whole degree
g += `<line x1="${L0}" x2="${W - R0}" y1="${f1(Y(y))}" y2="${f1(Y(y))}" stroke="${y ? '#ece9e2' : '#bdb8ad'}" stroke-width="1"/>` +
`<text x="${L0 - 5}" y="${f1(Y(y) + 4)}" text-anchor="end">${y > 0 ? '+' + y : y < 0 ? '−' + -y : '0'}</text>`;
for (const [th, name, cls] of [[0.5, 'El Niño', ''], [-0.5, 'La Niña', ' cool']]) // the thresholds, where in range
if (th > y0 && th < y1)
g += `<line x1="${L0}" x2="${W - R0}" y1="${f1(Y(th))}" y2="${f1(Y(th))}" stroke="${cls ? '#3f7fb5' : '#d9622b'}" stroke-width="1" stroke-dasharray="3 3" opacity="0.6"/>` +
`<text class="lab${cls}" x="${W - R0 - 2}" y="${f1(Y(th) + (th > 0 ? -4 : 12))}" text-anchor="end">${name}</text>`;
g += `<path d="${area}" fill="url(#en-warm)" clip-path="url(#en-up)"/><path d="${area}" fill="url(#en-cool)" clip-path="url(#en-dn)"/>`;
if (fc.length > 1) {
const band = fc.map(p => [p[0], p[3]]).concat(fc.slice().reverse().map(p => [p[0], p[2]]));
g += `<path d="${path(band)}Z" fill="#9a3f14" opacity="0.13"/>` +
`<path d="${path(fc)}" fill="none" stroke="#9a3f14" stroke-width="2" stroke-dasharray="5 4"/>` +
`<text x="${f1((X(last[0]) + X(x1)) / 2)}" y="${f1(Math.max(top + 10, Y(Math.max(...fc.map(p => p[3]))) - 6))}" text-anchor="middle">models</text>`;
}
g += `<path d="${line}" fill="none" stroke="#9a3f14" stroke-width="2.2" stroke-linejoin="round"/>` +
`<circle cx="${f1(X(last[0]))}" cy="${f1(Y(last[1]))}" r="4" fill="#9a3f14" stroke="#fff" stroke-width="1.6"/>`;
// month ticks along the bottom, every third month, the year at January and at the first tick
const d0 = new Date(x0), ticks = [];
for (let d = new Date(Date.UTC(d0.getUTCFullYear(), d0.getUTCMonth() + 1, 1)); d.getTime() <= x1; d.setUTCMonth(d.getUTCMonth() + 1))
if (d.getUTCMonth() % 3 === 0) ticks.push(new Date(d));
ticks.forEach((d, i) => {
const x = X(d.getTime()), mon = d.toLocaleDateString('en-US', { month: 'short', timeZone: 'UTC' });
if (x > W - R0 - 14) return;
g += `<line x1="${f1(x)}" x2="${f1(x)}" y1="${bot}" y2="${bot + 4}" stroke="#bdb8ad"/>` +
`<text x="${f1(x)}" y="${bot + 17}" text-anchor="middle">${mon}${d.getUTCMonth() === 0 || i === 0 ? ' ' + d.getUTCFullYear() : ''}</text>`;
});
svg.innerHTML = g;
};
draw();
// drawn again whenever the card's size changes: the window, or the midterm card beside it filling in
let rt; new ResizeObserver(() => { clearTimeout(rt); rt = setTimeout(draw, 100); }).observe(svg);
}
});
// SSW card: the outlook page's summary: the four ensembles' chance of a sudden warming in the next 15 days (their
// members' mean share, as on ssw.html), what drives it and the chance of the post-warming surface pattern after day
// 15, beside each ensemble's chance in its runs of the last three weeks
getJSON('/assets/sst/data/ssw_summary.json').then(j => {
if (!j || !j.ssw) return;
const S = j.ssw, W = j.waves, D = j.descent, F = j.surface, N = j.names, br = S.base_rate;
const pct = v => v == null ? '–' : Math.round(100 * v) + ' %';
const share = S.share_mean;
if (share != null) {
$set('ss-p', pct(share));
$set('ss-pl', `chance, ${day(j.window[0])} to ${day(j.window[1])}`);
$set('ss-pd', `members, four ensembles · since 1980: ${br.with_onset} of ${br.winters} winters` + (j.ssw_season ? '' : ' · outside Nov–Mar'));
const pill = document.getElementById('ss-pill');
pill.textContent = share >= 0.6 ? 'Likely' : share >= 0.3 ? 'Possible' : share >= 0.05 ? 'In a few members' : 'None';
pill.className = 'en-pill' + (share < 0.3 ? ' neutral' : '');
pill.hidden = false;
}
if (W && W.kind) {
$set('ss-kind', { 'wave-1': 'Wave-1', 'wave-2': 'Wave-2', mixed: 'Mixed' }[W.kind] || '–');
$set('ss-kd', `${pct(W.wave2_share_mean)} wave-2` + (D && D.kind && D.kind !== 'none' ? ` · ${D.kind}` : ''));
}
if (F && F.p_positive_mean != null) {
$set('ss-sfc', pct(F.p_positive_mean));
$set('ss-sd', 'chance; normal 50 %');
}
$set('ss-sub', `The 10 hPa wind at 60°N turning easterly in each ensemble's members; newest run ${day(j.newest_init)} ${j.newest_init.slice(11)}.`);
const COL = { aifs: '#b4541f', ifs: '#1f4b6e', gefs: '#3b88c4', geps: '#2f7d4f' }; // the stratosphere figures'
const t = s => Date.parse(s + ':00:00Z');
const x1 = Math.max(...Object.keys(COL).map(m => S.models[m] && S.models[m].runs ? t(S.models[m].runs.at(-1)[0]) : 0)), x0 = x1 - 21 * 864e5;
const runs = Object.fromEntries(Object.keys(COL).map(m => [m, ((S.models[m] || {}).runs || []).filter(r => r[1] != null && t(r[0]) >= x0)]));
const ens = Object.keys(COL).filter(m => runs[m].length);
if (!ens.length) return;
const svg = document.getElementById('ss-spark');
svg.setAttribute('aria-label', 'Each ensemble\'s chance of a sudden warming, run by run; newest: ' +
ens.map(m => `${N[m]} ${pct(runs[m].at(-1)[1])}`).join(', '));
document.getElementById('ss-key').innerHTML = ens.map(m => `<span><i style="background:${COL[m]}"></i>${N[m]} ${pct(S.models[m].share)}</span>`).join('');
document.getElementById('ss-chart').hidden = false;
const draw = () => {
const r = svg.getBoundingClientRect(), Wd = Math.max(220, Math.round(r.width) || 320), H = Math.max(170, Math.round(r.height) || 190);
const L0 = 34, R0 = 62, top = 20, bot = H - 22;
svg.setAttribute('viewBox', `0 0 ${Wd} ${H}`);
const X = x => L0 + (Wd - L0 - R0) * (x - x0) / (x1 - x0 || 1), Y = v => top + (bot - top) * (1 - v);
const f1 = z => z.toFixed(1);
let g = `<text x="${L0}" y="11">each run's chance in its next 15 days</text>`;
for (const v of [0, 0.5, 1])
g += `<line x1="${L0}" x2="${Wd - R0}" y1="${f1(Y(v))}" y2="${f1(Y(v))}" stroke="${v ? '#ece9e2' : '#bdb8ad'}" stroke-width="1"/>` +
`<text x="${L0 - 5}" y="${f1(Y(v) + 4)}" text-anchor="end">${100 * v}%</text>`;
// a tick a week from the first whole day
for (let d = Math.ceil(x0 / 864e5) * 864e5; d <= x1; d += 7 * 864e5) {
const x = X(d);
if (x > Wd - R0 - 16) break;
g += `<line x1="${f1(x)}" x2="${f1(x)}" y1="${bot}" y2="${bot + 4}" stroke="#bdb8ad"/>` +
`<text x="${f1(x)}" y="${bot + 17}" text-anchor="middle">${day(new Date(d).toISOString())}</text>`;
}
const ends = ens.map(m => {
const p = runs[m], e = p.at(-1);
g += `<path d="${p.map((q, i) => (i ? 'L' : 'M') + f1(X(t(q[0]))) + ' ' + f1(Y(q[1]))).join('')}" fill="none" stroke="${COL[m]}" stroke-width="2" stroke-linejoin="round"/>`;
return { m, x: X(t(e[0])), y: Y(e[1]) };
});
ends.forEach(e => { g += `<circle cx="${f1(e.x)}" cy="${f1(e.y)}" r="3.5" fill="${COL[e.m]}" stroke="#fff" stroke-width="1.5"/>`; });
// the names beside the line ends, pushed apart where the ensembles end together
ends.sort((a, b) => a.y - b.y);
let prev = -Infinity;
for (const e of ends) prev = e.ly = Math.max(e.y + 4, prev + 13);
const over = prev - (bot + 4);
ends.forEach(e => { g += `<text x="${f1(Wd - R0 + 8)}" y="${f1(e.ly - Math.max(0, over))}">${N[e.m]}</text>`; });
svg.innerHTML = g;
};
draw();
let rt; new ResizeObserver(() => { clearTimeout(rt); rt = setTimeout(draw, 100); }).observe(svg);
});
fetch('https://scorvec.goatcounter.com/counter/TOTAL.json')
.then(res => res.json())
.then(data => {
document.getElementById('count').textContent = data.count_unique || data.count;
})
.catch(() => {
document.getElementById('count').textContent = '—';
});
</script>
</body>
</html>