orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1// Cassette futurism, held to instrument-panel discipline rather than costume.
2// The reference points are the Nostromo's MU/TH/UR terminals, the LAPD consoles
3// in Blade Runner 2049 and Lumon's basement, and what they share is not neon,
4// it is typographic restraint over a visible structural grid.
5//
6// Three rules the rest of this file follows:
7//
8// 1. Warm dark, never pure black. Phosphor sat on warm glass and #000 reads
9// as a hole in the page rather than a screen.
10// 2. Amber is the instrument, not the data. Chrome, labels and brackets are
11// amber. The only saturated colour on the page is a market direction,
12// which is the one thing worth looking at first.
13// 3. Every texture is drawn in CSS. No bitmap overlays, so the grain scales
14// to any viewport and costs no request.
15
16:root {
17 // Warm near-black, five steps.
18 --void: #0a0908;
19 --bg: #0d0b09;
20 --panel: #12100d;
21 --panel-2: #171410;
22 --line: #2f2820;
23 --line-hot: #47392a;
24
25 // Warm off-white through to dim, all carrying the same amber cast so nothing
26 // on the page looks like it came from a different machine.
27 --text: #ece5d8;
28 --muted: #bcaf99;
29 --dim: #9c8f79;
30 --faint: #7b7060;
31
32 // P3 amber, the phosphor this whole thing is pretending to be.
33 --amber: #ffb000;
34 --amber-dim: #b37b00;
35 --amber-glow: rgba(255, 176, 0, 0.16);
36
37 // Direction. Desaturated enough to live in a warm world and still read
38 // instantly at a glance across eight cards.
39 --up: #5ddc82;
40 --up-soft: rgba(93, 220, 130, 0.085);
41 --down: #ff5f4d;
42 --down-soft: rgba(255, 95, 77, 0.085);
43 --flat: #8a8172;
44
45 --warn: #ffb000;
46
47 --gap: 12px;
48 --pad: 14px;
49
50 --mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
51 --grotesk: "Space Grotesk", var(--mono);
52
53 // One tracking value, used everywhere a label is set in caps.
54 --track: 0.14em;
55}
56
57* { box-sizing: border-box; }
58
59html {
60 color-scheme: dark;
61 // The page is a screen, so the scrollbar should not be a bright OS artifact
62 // sitting on top of it.
63 scrollbar-color: var(--line-hot) var(--void);
64}
65
66body {
67 margin: 0;
68 min-height: 100vh;
69 display: flex;
70 flex-direction: column;
71 background: var(--bg);
72 color: var(--text);
73 font-family: var(--mono);
74 font-size: 13px;
75 line-height: 1.45;
76 font-variant-numeric: tabular-nums;
77 -webkit-font-smoothing: antialiased;
78
79 // The structural grid, faint enough to read as texture rather than as a
80 // table. 32px so it lines up with the panel padding rhythm.
81 background-image:
82 linear-gradient(rgba(255, 176, 0, 0.022) 1px, transparent 1px),
83 linear-gradient(90deg, rgba(255, 176, 0, 0.022) 1px, transparent 1px);
84 background-size: 32px 32px;
85 background-position: center top;
86}
87
88a { color: var(--amber); text-decoration: none; }
89a:hover { color: #ffc84d; }
90
91.dim { color: var(--dim); }
92
93// CRT overlays ---------------------------------------------------------------
94
95// Scanlines at 3px so they read as structure at any zoom. The opacity is low
96// on purpose: at anything stronger this stops being a dashboard and starts
97// being a screensaver, which is the failure mode of the whole genre.
98.crt {
99 position: fixed;
100 inset: 0;
101 z-index: 9000;
102 pointer-events: none;
103 background: repeating-linear-gradient(
104 180deg,
105 rgba(0, 0, 0, 0.16) 0px,
106 rgba(0, 0, 0, 0.16) 1px,
107 transparent 1px,
108 transparent 3px
109 );
110 mix-blend-mode: multiply;
111}
112
113// Corner falloff, so the panel edges sit in glass rather than running flat to
114// the browser chrome.
115.vignette {
116 position: fixed;
117 inset: 0;
118 z-index: 9001;
119 pointer-events: none;
120 background: radial-gradient(
121 ellipse 120% 100% at 50% 45%,
122 transparent 55%,
123 rgba(0, 0, 0, 0.16) 90%,
124 rgba(0, 0, 0, 0.3) 100%
125 );
126}
127
128// Status rail ----------------------------------------------------------------
129
130.rail {
131 position: sticky;
132 top: 0;
133 z-index: 100;
134 display: flex;
135 align-items: stretch;
136 gap: var(--gap);
137 padding: 0 var(--pad);
138 height: 42px;
139 background: linear-gradient(180deg, var(--panel-2), var(--panel));
140 border-bottom: 1px solid var(--line-hot);
141}
142
143.rail-brand {
144 display: flex;
145 align-items: center;
146 gap: 9px;
147
148 .mark {
149 color: var(--amber);
150 font-size: 13px;
151 // The one place a glow is worth the cost, because it is the only mark on
152 // the page and it is what sells the phosphor.
153 text-shadow: 0 0 10px var(--amber-glow);
154 }
155
156 .wordmark {
157 font-family: var(--grotesk);
158 font-weight: 700;
159 font-size: 15px;
160 letter-spacing: 0.2em;
161 color: var(--text);
162 }
163
164 .rail-sub {
165 font-size: 10px;
166 letter-spacing: var(--track);
167 color: var(--faint);
168 border-left: 1px solid var(--line);
169 padding-left: 9px;
170 }
171}
172
173.rail-readouts {
174 margin-left: auto;
175 display: flex;
176 align-items: stretch;
177}
178
179.readout {
180 display: flex;
181 flex-direction: column;
182 justify-content: center;
183 gap: 1px;
184 padding: 0 12px;
185 border-left: 1px solid var(--line);
186 min-width: 92px;
187
188 .readout-key {
189 font-size: 10px;
190 letter-spacing: var(--track);
191 color: var(--faint);
192 }
193
194 .readout-val {
195 font-size: 12px;
196 color: var(--text);
197 display: flex;
198 align-items: center;
199 gap: 6px;
200 }
201}
202
203// The link pip is the one animated thing in the chrome, and it breathes rather
204// than blinks so a page left open on a second monitor is not movement in the
205// corner of an eye.
206.pip {
207 width: 6px;
208 height: 6px;
209 flex: none;
210 background: var(--dim);
211 clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
212}
213
214.readout.link {
215 &[data-state="live"] {
216 .readout-val { color: var(--up); }
217 .pip { background: var(--up); animation: breathe 2.6s ease-in-out infinite; }
218 }
219 &[data-state="down"] {
220 .readout-val { color: var(--down); }
221 .pip { background: var(--down); }
222 }
223}
224
225@keyframes breathe {
226 0%, 100% { opacity: 1; }
227 50% { opacity: 0.25; }
228}
229
230// Layout ---------------------------------------------------------------------
231
232main {
233 // flex: 1 is what pushes the status line to the bottom of a short viewport.
234 flex: 1;
235 padding: var(--gap);
236 display: flex;
237 flex-direction: column;
238 gap: var(--gap);
239 max-width: 1760px;
240 margin: 0 auto;
241 width: 100%;
242}
243
244// Two bands rather than a stack of full width strips. A headline set across
245// 1900px is unreadable no matter how good the type is, and a row of panels that
246// all end at the same line reads as an instrument where a ragged one reads as a
247// page.
248.band {
249 display: grid;
250 gap: var(--gap);
251 align-items: stretch;
252
253 > .panel { display: flex; flex-direction: column; }
254}
255
256// The feeds hold ten rows each and are naturally close in height, so they are
257// not forced level the way the short readouts above them are.
258.news-band { align-items: start; }
259
260// Four short readouts, weighted by how much each carries rather than equal:
261// the curve is four rows, conditions and earnings are the wide ones.
262.market-band {
263 grid-template-columns: 0.8fr 1.05fr 1.15fr 1.4fr;
264}
265
266// The five readouts, all figures rather than prose, so they take equal width.
267.local-band {
268 grid-template-columns: repeat(6, 1fr);
269}
270
271// The three feeds on a row of their own. Equal thirds puts a headline on about
272// sixty characters, which is where prose stops being a chore to scan.
273.news-band {
274 grid-template-columns: repeat(3, 1fr);
275}
276
277// Panels ---------------------------------------------------------------------
278
279// Bordered, never floating. No drop shadows anywhere in this file: a panel that
280// hovers reads as a web card, and this is meant to read as an inset instrument.
281.panel {
282 position: relative;
283 background: var(--panel);
284 border: 1px solid var(--line);
285 padding: var(--pad);
286}
287
288// Corner brackets, drawn rather than imaged. They are what make a plain
289// rectangle read as a framed readout.
290.bracket {
291 position: absolute;
292 width: 7px;
293 height: 7px;
294 border: 1px solid var(--amber-dim);
295 pointer-events: none;
296
297 &.tl { top: -1px; left: -1px; border-right: 0; border-bottom: 0; }
298 &.tr { top: -1px; right: -1px; border-left: 0; border-bottom: 0; }
299 &.bl { bottom: -1px; left: -1px; border-right: 0; border-top: 0; }
300 &.br { bottom: -1px; right: -1px; border-left: 0; border-top: 0; }
301}
302
303.panel-head {
304 display: flex;
305 align-items: center;
306 gap: 10px;
307 margin-bottom: 12px;
308
309 .idx {
310 font-size: 10px;
311 letter-spacing: 0.08em;
312 color: var(--void);
313 background: var(--amber-dim);
314 padding: 2px 5px;
315 line-height: 1;
316 }
317
318 h2 {
319 margin: 0;
320 font-family: var(--grotesk);
321 font-size: 12px;
322 font-weight: 700;
323 letter-spacing: var(--track);
324 color: var(--amber);
325 white-space: nowrap;
326 }
327
328 // The rule fills whatever is left, which is what keeps a header reading as
329 // one continuous instrument label rather than as floating words.
330 .rule {
331 flex: 1;
332 height: 1px;
333 min-width: 12px;
334 background: repeating-linear-gradient(
335 90deg,
336 var(--line-hot) 0 2px,
337 transparent 2px 4px
338 );
339 }
340
341 .panel-meta {
342 font-size: 10px;
343 letter-spacing: var(--track);
344 color: var(--dim);
345 white-space: nowrap;
346 }
347 a.panel-meta:hover { color: var(--amber); }
348}
349
350// Markets --------------------------------------------------------------------
351
352.markets .panel-head {
353 .session {
354 display: flex;
355 align-items: center;
356 gap: 7px;
357
358 .badge {
359 font-size: 10px;
360 letter-spacing: var(--track);
361 text-transform: uppercase;
362 color: var(--muted);
363 }
364 .phase {
365 font-size: 10px;
366 letter-spacing: 0.06em;
367 color: var(--dim);
368 white-space: nowrap;
369 }
370
371 &[data-session="regular"] { .pip { background: var(--up); } .badge { color: var(--up); } }
372 &[data-session="pre"],
373 &[data-session="post"] { .pip { background: var(--warn); } .badge { color: var(--warn); } }
374 }
375
376 .drawdown {
377 display: flex;
378 align-items: baseline;
379 gap: 7px;
380 padding-left: 12px;
381 border-left: 1px solid var(--line);
382 white-space: nowrap;
383
384 .k { font-size: 10px; letter-spacing: var(--track); color: var(--dim); }
385 .v { font-size: 13px; color: var(--text); }
386 }
387}
388
389.cards {
390 display: grid;
391 grid-template-columns: repeat(8, 1fr);
392 gap: 1px;
393 background: var(--line);
394 border: 1px solid var(--line);
395}
396
397// One hairline between cards instead of eight separate boxes, so the strip
398// reads as a single instrument with eight channels.
399.card {
400 position: relative;
401 background: var(--panel-2);
402 padding: 9px 10px 0;
403 display: flex;
404 flex-direction: column;
405 overflow: hidden;
406
407 header {
408 display: flex;
409 flex-direction: column;
410 gap: 1px;
411 min-height: 26px;
412 }
413
414 h3 {
415 margin: 0;
416 font-family: var(--grotesk);
417 font-size: 11px;
418 font-weight: 500;
419 letter-spacing: 0.08em;
420 color: var(--text);
421 white-space: nowrap;
422 overflow: hidden;
423 text-overflow: ellipsis;
424 }
425
426 .sym {
427 font-size: 10px;
428 letter-spacing: 0.06em;
429 color: var(--muted);
430 white-space: nowrap;
431 overflow: hidden;
432 text-overflow: ellipsis;
433 }
434
435 .price {
436 margin: 7px 0 0;
437 font-size: 20px;
438 font-weight: 500;
439 letter-spacing: -0.02em;
440 color: var(--text);
441 line-height: 1.1;
442 }
443
444 .move {
445 margin: 2px 0 7px;
446 font-size: 11px;
447 display: flex;
448 align-items: center;
449 gap: 5px;
450
451 .glyph {
452 width: 0;
453 height: 0;
454 border-left: 3px solid transparent;
455 border-right: 3px solid transparent;
456 }
457 .pct { color: var(--muted); }
458 }
459
460 // Direction colours the move and the trace, and leaves the price white. The
461 // price is the figure being read, the move is the judgement about it, and
462 // colouring both makes eight cards shout at once.
463 &[data-dir="up"] {
464 .pts, .glyph { color: var(--up); }
465 .glyph { border-bottom: 4px solid var(--up); }
466 .spark-line { stroke: var(--up); }
467 .spark-area { fill: var(--up-soft); }
468 }
469 &[data-dir="down"] {
470 .pts, .glyph { color: var(--down); }
471 .glyph { border-top: 4px solid var(--down); }
472 .spark-line { stroke: var(--down); }
473 .spark-area { fill: var(--down-soft); }
474 }
475 &[data-dir="flat"] {
476 .pts { color: var(--flat); }
477 .spark-line { stroke: var(--flat); }
478 .spark-area { fill: rgba(138, 129, 114, 0.07); }
479 }
480
481 // A value that just changed flashes once. This is the only place motion is
482 // tied to data, and it is what makes "live" something you can see rather
483 // than something the corner of the page claims.
484 .price.tick { animation: tick 620ms ease-out; }
485}
486
487@keyframes tick {
488 0% { background: var(--amber-glow); color: var(--amber); }
489 100% { background: transparent; color: var(--text); }
490}
491
492.spark {
493 display: block;
494 width: calc(100% + 20px);
495 margin: 0 -10px;
496 height: 40px;
497
498 // vector-effect keeps the stroke one pixel after the viewBox is stretched to
499 // the card width, which would otherwise scale it into a smear.
500 .spark-line {
501 fill: none;
502 stroke-width: 1.25;
503 vector-effect: non-scaling-stroke;
504 stroke-linejoin: round;
505 stroke-linecap: round;
506 }
507
508 .spark-base {
509 stroke: var(--faint);
510 stroke-width: 1;
511 stroke-dasharray: 1 4;
512 vector-effect: non-scaling-stroke;
513 }
514
515 .spark-area { stroke: none; }
516
517 // The live end of a session that has not finished. The card is the whole
518 // trading day, so this is where the day has got to and everything right of
519 // it has not happened yet.
520 .spark-now {
521 stroke: var(--amber);
522 stroke-width: 1;
523 stroke-dasharray: 2 3;
524 opacity: 0.5;
525 vector-effect: non-scaling-stroke;
526 }
527
528 // Where this card's market shut while the rest of the strip kept trading, which
529 // is every night for the VIX. Same dashed rule as the live cursor so it reads
530 // as a deliberate variant, in faint rather than amber because it means the
531 // opposite thing. The space right of it is left alone, since the dotted
532 // baseline already runs the width of the card and a filled block there shouted
533 // over everything else on the page.
534 .spark-shut {
535 stroke: var(--faint);
536 stroke-width: 1;
537 stroke-dasharray: 2 3;
538 vector-effect: non-scaling-stroke;
539 }
540}
541
542// Feeds ----------------------------------------------------------------------
543
544.stories {
545 list-style: none;
546 margin: 0;
547 padding: 0;
548
549 li {
550 display: flex;
551 gap: 10px;
552 padding: 7px 0;
553 border-bottom: 1px solid var(--line);
554 &:last-child { border-bottom: 0; }
555 }
556
557 .rank {
558 font-size: 11px;
559 color: var(--faint);
560 padding-top: 2px;
561 flex: none;
562 width: 18px;
563 }
564
565 .body { min-width: 0; }
566
567 .title {
568 display: block;
569 color: var(--text);
570 font-size: 13px;
571 line-height: 1.35;
572 }
573 .title:hover { color: var(--amber); }
574
575 .meta {
576 display: flex;
577 flex-wrap: wrap;
578 gap: 11px;
579 margin-top: 3px;
580 font-size: 11px;
581 color: var(--dim);
582
583 .host { color: var(--muted); }
584 .unit { margin-left: 3px; color: var(--faint); }
585 .pts { color: var(--muted); }
586 a.cmt { color: var(--muted); }
587 a.cmt:hover { color: var(--amber); }
588 }
589
590 li.empty .body { color: var(--dim); font-size: 11px; letter-spacing: var(--track); }
591}
592
593// Atmos ----------------------------------------------------------------------
594
595// ATMOS reads top to bottom as now, today, next, and the length of the day.
596.weather-body {
597 .temp-row {
598 display: flex;
599 align-items: center;
600 gap: 14px;
601 }
602
603 .temp {
604 font-size: 40px;
605 line-height: 1;
606 color: var(--amber);
607 text-shadow: 0 0 18px var(--amber-glow);
608
609 .deg { font-size: 22px; color: var(--amber-dim); }
610 }
611
612 .cond {
613 display: flex;
614 flex-direction: column;
615 gap: 2px;
616 .cond-label { font-size: 12px; letter-spacing: 0.06em; color: var(--text); text-transform: uppercase; }
617 .cond-feels { font-size: 10px; letter-spacing: var(--track); color: var(--dim); }
618 }
619
620 // The day's range, pushed to the far edge so the header has two anchors
621 // rather than one and a stretch of empty panel.
622 .range {
623 margin-left: auto;
624 text-align: right;
625 display: flex;
626 flex-direction: column;
627 gap: 2px;
628 font-size: 12px;
629
630 .hi { color: var(--text); }
631 .lo { color: var(--dim); }
632 }
633
634 // Rain and wind are plain figures with no band behind them, so they read as
635 // a line of text here rather than pretending to be gauges like the three
636 // below.
637 .now-row {
638 display: flex;
639 gap: 18px;
640 margin-top: 11px;
641 font-size: 11px;
642 color: var(--text);
643
644 b {
645 font-weight: 400;
646 letter-spacing: var(--track);
647 color: var(--dim);
648 }
649 }
650}
651
652// UV, air quality and pollen are three readings on three unrelated scales, and
653// drawing each as the same gauge is what makes them comparable at a glance.
654// Pollen used to be a full width cell under two half ones, which is what made
655// this corner look assembled rather than designed.
656.gauges {
657 display: grid;
658 grid-template-columns: repeat(3, 1fr);
659 gap: 1px;
660 margin-top: 12px;
661 background: var(--line);
662 border: 1px solid var(--line);
663}
664
665.gauge {
666 background: var(--panel-2);
667 padding: 8px 9px;
668 display: flex;
669 flex-direction: column;
670 gap: 4px;
671 min-width: 0;
672
673 .gk { font-size: 9px; letter-spacing: var(--track); color: var(--dim); }
674 .gv { font-size: 17px; line-height: 1; color: var(--text); }
675
676 .gbar {
677 height: 4px;
678 background: var(--void);
679 border: 1px solid var(--line-hot);
680 overflow: hidden;
681
682 i {
683 display: block;
684 height: 100%;
685 background: var(--flat);
686 transition: width 0.4s ease;
687 }
688 }
689
690 .gb {
691 font-size: 9px;
692 letter-spacing: var(--track);
693 color: var(--dim);
694 }
695
696 // The allergens behind a pollen reading, which is the one of the three that
697 // has anything to name.
698 .gn {
699 font-size: 9px;
700 letter-spacing: 0.04em;
701 color: var(--faint);
702 overflow: hidden;
703 text-overflow: ellipsis;
704 }
705
706 // Only the bottom band is green. A moderate reading drawn in the same colour
707 // as a good one is the panel agreeing with itself about nothing.
708 &[data-level="0"] .gbar i { background: var(--up); }
709 &[data-level="1"] { .gv { color: var(--text); } .gbar i { background: var(--muted); } }
710 &[data-level="2"] { .gv { color: var(--warn); } .gbar i { background: var(--warn); } }
711 &[data-level="3"] { .gv { color: var(--down); } .gbar i { background: var(--down); } }
712}
713
714// The next eight hours. Each hour shows the chance of rain twice, as a bar and
715// as a figure, because the bar alone is unreadable on a dry day and the figure
716// alone gives no shape across the evening.
717.hours {
718 display: flex;
719 flex-direction: column;
720 flex: 1;
721 margin-top: 14px;
722
723 .hours-head {
724 display: flex;
725 justify-content: space-between;
726 align-items: baseline;
727 margin-bottom: 6px;
728
729 .k { font-size: 9px; letter-spacing: var(--track); color: var(--dim); }
730 .v { font-size: 9px; letter-spacing: var(--track); color: var(--faint); }
731 }
732
733 // The bars share one plot, so they share a floor and a half way rule. Giving
734 // each bar its own eleven pixel border drew eight ticks instead of a baseline,
735 // and holding the plot to a fixed height left it floating at the top of a
736 // panel the band had stretched. It takes the slack now and the bars stand on
737 // the bottom of it.
738 .hour-plot {
739 position: relative;
740 display: grid;
741 grid-template-columns: repeat(8, 1fr);
742 align-items: end;
743 flex: 1;
744 min-height: 46px;
745 border-bottom: 1px solid var(--line-hot);
746
747 &::before {
748 content: "";
749 position: absolute;
750 left: 0;
751 right: 0;
752 top: 50%;
753 border-top: 1px dashed var(--line-hot);
754 pointer-events: none;
755 }
756 }
757
758 .hbar {
759 width: 11px;
760 justify-self: center;
761 display: flex;
762 flex-direction: column;
763 justify-content: flex-end;
764 height: 100%;
765
766 i {
767 display: block;
768 width: 100%;
769 // A chance that rounds to nothing still draws, so a dry hour reads as dry
770 // rather than as a column that failed to load.
771 min-height: 1px;
772 background: var(--amber-dim);
773 }
774
775 // Worth taking a coat for.
776 &[data-wet="yes"] i { background: var(--amber); }
777 }
778
779 .hour-cols {
780 display: grid;
781 grid-template-columns: repeat(8, 1fr);
782 margin-top: 4px;
783 }
784}
785
786.hour {
787 display: flex;
788 flex-direction: column;
789 align-items: center;
790 gap: 1px;
791 min-width: 0;
792
793 .hp { font-size: 9px; color: var(--dim); }
794 .hv { font-size: 11px; color: var(--text); }
795 .ht { font-size: 8px; letter-spacing: 0.04em; color: var(--faint); }
796
797 &[data-wet="yes"] .hp { color: var(--amber); }
798
799 // Warm to cool across the strip's own range, so an even evening still shows a
800 // gradient rather than eight identical numbers.
801 &[data-warm="3"] .hv { color: var(--amber); }
802 &[data-warm="2"] .hv { color: var(--text); }
803 &[data-warm="1"] .hv { color: var(--muted); }
804 &[data-warm="0"] .hv { color: var(--dim); }
805}
806
807.daylight {
808 display: flex;
809 align-items: center;
810 gap: 10px;
811 margin-top: 14px;
812
813 .t { font-size: 10px; color: var(--dim); white-space: nowrap; }
814
815 .track {
816 flex: 1;
817 height: 3px;
818 background: var(--line-hot);
819 position: relative;
820
821 i {
822 display: block;
823 height: 100%;
824 background: linear-gradient(90deg, var(--amber-dim), var(--amber));
825 }
826 }
827}
828
829// Systems --------------------------------------------------------------------
830
831.systems {
832 .rows {
833 list-style: none;
834 margin: 0;
835 padding: 0;
836 flex: 1;
837 display: flex;
838 flex-direction: column;
839 justify-content: space-around;
840
841 li {
842 display: flex;
843 align-items: center;
844 gap: 8px;
845 padding: 5px 0;
846 font-size: 12px;
847 }
848
849 .name {
850 color: var(--text);
851 white-space: nowrap;
852 letter-spacing: 0.04em;
853 }
854 a.name:hover { color: var(--amber); }
855
856 // A dotted leader between the name and its numbers, the way a real readout
857 // ties a label to a value across a gap.
858 .bar {
859 flex: 1;
860 height: 1px;
861 min-width: 8px;
862 background: repeating-linear-gradient(
863 90deg,
864 var(--line-hot) 0 1px,
865 transparent 1px 4px
866 );
867 }
868
869 .latency { font-size: 11px; color: var(--muted); min-width: 46px; text-align: right; }
870 .errors { font-size: 11px; color: var(--dim); min-width: 34px; text-align: right; }
871 .errors[data-any="yes"] { color: var(--warn); }
872
873 li[data-state="up"] .pip { background: var(--up); }
874 li[data-state="cached"] .pip { background: var(--warn); }
875 li[data-state="down"] .pip { background: var(--down); }
876 li[data-state="down"] .name { color: var(--down); }
877 li[data-state="unknown"] .name { color: var(--dim); }
878 }
879
880 .footnote {
881 margin: 11px 0 0;
882 font-size: 9px;
883 letter-spacing: var(--track);
884 color: var(--dim);
885 }
886}
887
888.uplink {
889 .rows {
890 list-style: none;
891 margin: 0;
892 padding: 0;
893 flex: 1;
894 display: flex;
895 flex-direction: column;
896 justify-content: space-around;
897
898 li {
899 display: flex;
900 align-items: center;
901 gap: 8px;
902 padding: 4px 0;
903 font-size: 11px;
904 }
905
906 .name { color: var(--muted); letter-spacing: 0.08em; white-space: nowrap; }
907
908 .bar {
909 flex: 1;
910 height: 1px;
911 min-width: 8px;
912 background: repeating-linear-gradient(
913 90deg,
914 var(--line-hot) 0 1px,
915 transparent 1px 4px
916 );
917 }
918
919 .age { font-size: 11px; color: var(--dim); min-width: 30px; text-align: right; }
920
921 // Calls out of this hour's budget. Amber only past half, since anything
922 // under that is not a number worth looking at.
923 .load {
924 font-size: 10px;
925 color: var(--faint);
926 min-width: 52px;
927 text-align: right;
928 }
929 .load[data-hot="yes"] { color: var(--warn); }
930
931 li[data-state="ok"] .pip { background: var(--up); }
932 li[data-state="degraded"] .pip { background: var(--warn); }
933 li[data-state="open"] { .pip { background: var(--down); } .name { color: var(--down); } }
934 li[data-state="idle"] .pip { background: var(--faint); }
935 }
936
937 .footnote {
938 margin: 10px 0 0;
939 font-size: 9px;
940 letter-spacing: var(--track);
941 color: var(--dim);
942 }
943}
944
945// Alerts ---------------------------------------------------------------------
946
947// Above everything, and absent entirely when there is nothing active, because a
948// permanent "no warnings" row trains you to stop seeing the space it occupies.
949.alerts {
950 display: grid;
951 gap: var(--gap);
952}
953.alerts:empty { display: none; }
954
955// A stack of short rows rather than one long line. The event name runs to four
956// words on its own, so anything that sets it beside the rest of the row is one
957// narrow viewport away from wrapping into a paragraph.
958.alert {
959 padding: 10px 12px 10px 14px;
960 border: 1px solid var(--line-hot);
961
962 // Severity is in the stripe as well as the colour, so it survives being read
963 // on a phone in sunlight and by anyone who cannot tell amber from red.
964 border-left: 3px solid var(--warn);
965 background-color: var(--panel);
966 background-image: linear-gradient(
967 90deg,
968 rgba(255, 176, 0, 0.08),
969 rgba(255, 176, 0, 0.022) 30%,
970 rgba(255, 176, 0, 0.014)
971 );
972
973 // Inline flow rather than a flex row, so a name long enough to wrap cannot
974 // leave the pip stranded on a line of its own and the chip follows the last
975 // word instead of dropping below it.
976 .alert-head { line-height: 1.35; }
977
978 .pip {
979 display: inline-block;
980 vertical-align: 0.15em;
981 margin-right: 8px;
982 background: var(--warn);
983 }
984
985 .ev {
986 font-family: var(--grotesk);
987 font-weight: 700;
988 font-size: 13px;
989 letter-spacing: 0.1em;
990 color: var(--warn);
991 // EXCESSIVE HEAT WARNING is three long words and has to break somewhere.
992 overflow-wrap: anywhere;
993 }
994
995 // Beside the event name rather than out at the right edge, since it qualifies
996 // the name and lines up with nothing over there.
997 .sev {
998 display: inline-block;
999 vertical-align: 0.1em;
1000 margin-left: 9px;
1001 padding: 2px 6px;
1002 border: 1px solid rgba(255, 176, 0, 0.4);
1003 font-size: 9px;
1004 letter-spacing: var(--track);
1005 text-transform: uppercase;
1006 color: var(--warn);
1007 white-space: nowrap;
1008 }
1009
1010 // Only present when a forecaster wrote something the template did not.
1011 .hl {
1012 margin: 7px 0 0;
1013 font-size: 12px;
1014 line-height: 1.5;
1015 color: var(--muted);
1016 }
1017
1018 .alert-meta {
1019 display: flex;
1020 flex-wrap: wrap;
1021 align-items: baseline;
1022 gap: 3px 16px;
1023 margin-top: 7px;
1024
1025 .m { display: flex; align-items: baseline; gap: 6px; white-space: nowrap; }
1026 .m[hidden] { display: none; }
1027 .k { font-size: 9px; letter-spacing: var(--track); color: var(--faint); }
1028 .v { font-size: 11px; letter-spacing: 0.04em; color: var(--text); }
1029 .left .v { color: var(--warn); }
1030 }
1031
1032 &[data-severity="severe"],
1033 &[data-severity="extreme"] {
1034 border-left-color: var(--down);
1035 background-image: linear-gradient(
1036 90deg,
1037 rgba(255, 95, 77, 0.1),
1038 rgba(255, 95, 77, 0.028) 30%,
1039 rgba(255, 95, 77, 0.016)
1040 );
1041
1042 .pip { background: var(--down); }
1043 .ev { color: var(--down); }
1044 .sev { color: var(--down); border-color: rgba(255, 95, 77, 0.45); }
1045 .alert-meta .left .v { color: var(--down); }
1046 }
1047
1048 // Immediate is the NWS saying to act now rather than to keep an eye out, and
1049 // it is the one thing here allowed to move.
1050 &[data-urgency="immediate"] .pip { animation: breathe 1.6s ease-in-out infinite; }
1051}
1052
1053// Desktop has the width to put the whole row on one line, and three stacked
1054// rows across 1700px above the markets is a lot of page for two facts. The meta
1055// falls back to its own line on its own when it stops fitting.
1056@media (min-width: 1025px) {
1057 .alert {
1058 display: flex;
1059 align-items: center;
1060 flex-wrap: wrap;
1061 gap: 8px 20px;
1062
1063 .alert-meta { margin-top: 0; margin-left: auto; justify-content: flex-end; }
1064 }
1065
1066 // A written headline is a third line whatever the width, and pinning the meta
1067 // to the right of it leaves the row split across an empty middle.
1068 .alert:has(.hl) {
1069 display: block;
1070 .alert-meta { margin-top: 7px; margin-left: 0; justify-content: flex-start; }
1071 }
1072}
1073
1074// Conditions -------------------------------------------------------------------
1075
1076.conditions {
1077 > div[data-signal] {
1078 flex: 1;
1079 display: flex;
1080 flex-direction: column;
1081 }
1082
1083 .verdict {
1084 margin: 0 0 11px;
1085 font-family: var(--grotesk);
1086 font-size: 12px;
1087 font-weight: 500;
1088 letter-spacing: 0.06em;
1089 line-height: 1.45;
1090 padding: 8px 10px;
1091 border-left: 2px solid var(--flat);
1092 background: rgba(255, 255, 255, 0.015);
1093 color: var(--muted);
1094
1095 &[data-level="dip"] { border-left-color: var(--warn); color: var(--text); }
1096 &[data-level="stress"] { border-left-color: var(--amber); color: var(--amber); }
1097 &[data-level="deep"] { border-left-color: var(--up); color: var(--up); }
1098 }
1099
1100 ul.conditions {
1101 list-style: none;
1102 margin: 0;
1103 padding: 0;
1104 flex: 1;
1105 display: flex;
1106 flex-direction: column;
1107 justify-content: space-around;
1108
1109 li {
1110 display: grid;
1111 grid-template-columns: 1fr auto auto;
1112 grid-template-areas: "k v n" "meter meter meter";
1113 align-items: baseline;
1114 gap: 5px 10px;
1115 padding: 6px 0;
1116 border-bottom: 1px solid var(--line);
1117 &:last-child { border-bottom: 0; }
1118 }
1119
1120 .k { grid-area: k; font-size: 10px; letter-spacing: var(--track); color: var(--dim); }
1121 .v { grid-area: v; font-size: 14px; color: var(--text); }
1122 .n { grid-area: n; font-size: 9px; letter-spacing: var(--track); color: var(--faint); min-width: 92px; text-align: right; }
1123
1124 // Where the reading sits between calm and the worst end of its own scale,
1125 // with the thresholds ruled across it. A state word says which band a
1126 // number landed in and nothing about how close it is to the next one.
1127 .meter {
1128 grid-area: meter;
1129 position: relative;
1130 display: block;
1131 height: 6px;
1132 background: var(--void);
1133 border: 1px solid var(--line-hot);
1134 overflow: hidden;
1135
1136 .fill {
1137 position: absolute;
1138 inset: 0 auto 0 0;
1139 background: var(--flat);
1140 // A reading barely off the calm end still has to draw something, or a
1141 // quiet market looks like a panel that failed to load.
1142 min-width: 2px;
1143 transition: width 0.4s ease;
1144 }
1145
1146 // The band edges, so the bar says how far it is to the next threshold
1147 // rather than only which side of one it is on. Light enough to read over
1148 // the empty track and over a filled bar both.
1149 .tick {
1150 position: absolute;
1151 top: 0;
1152 bottom: 0;
1153 width: 1px;
1154 background: var(--dim);
1155 opacity: 0.55;
1156 }
1157 }
1158
1159 li[data-state="dip"] { .v { color: var(--warn); } .fill { background: var(--warn); } }
1160 li[data-state="stress"] { .v { color: var(--amber); } .fill { background: var(--amber); } }
1161 li[data-state="deep"] { .v { color: var(--up); } .fill { background: var(--up); } }
1162 }
1163
1164 .footnote {
1165 margin: 11px 0 0;
1166 font-size: 9px;
1167 letter-spacing: var(--track);
1168 color: var(--faint);
1169 }
1170}
1171
1172// Rates ------------------------------------------------------------------------
1173
1174.rates {
1175 .rate-rows {
1176 list-style: none;
1177 margin: 0;
1178 padding: 0;
1179 display: flex;
1180 flex-direction: column;
1181
1182 // The rows share the panel equally, so four of them read as one stepped
1183 // scale rather than as four items with gaps between them.
1184 flex: 1;
1185
1186 li {
1187 display: grid;
1188 grid-template-columns: 34px auto 1fr auto;
1189 align-items: center;
1190 gap: 10px;
1191 flex: 1;
1192 padding: 6px 0;
1193 border-bottom: 1px solid var(--line);
1194 &:last-child { border-bottom: 0; }
1195 }
1196
1197 .k { font-size: 11px; letter-spacing: var(--track); color: var(--dim); }
1198 .v { font-size: 16px; color: var(--text); min-width: 62px; }
1199 .d { font-size: 11px; color: var(--flat); min-width: 40px; text-align: right; }
1200
1201 // Reading the four bars down the panel gives the shape of the curve, which
1202 // four numbers in a column do not. The scale runs from zero, so a bar is
1203 // proportional to the yield and not to the gap between the smallest and the
1204 // largest, which would turn a quarter point of spread into a cliff.
1205 .scale {
1206 height: 5px;
1207 background: var(--void);
1208 border: 1px solid var(--line-hot);
1209 overflow: hidden;
1210
1211 i {
1212 display: block;
1213 height: 100%;
1214 background: linear-gradient(90deg, var(--amber-dim), var(--amber));
1215 transition: width 0.4s ease;
1216 }
1217 }
1218
1219 li[data-dir="up"] .d { color: var(--down); }
1220 li[data-dir="down"] .d { color: var(--up); }
1221 }
1222
1223 // Yields up is bad for anything already held, so the colours are inverted
1224 // against the equity cards on purpose. A falling yield is a rising bond.
1225
1226 .curve {
1227 display: flex;
1228 justify-content: space-between;
1229 align-items: baseline;
1230 margin: auto 0 0;
1231 padding-top: 9px;
1232 border-top: 1px solid var(--line-hot);
1233
1234 .k { font-size: 10px; letter-spacing: var(--track); color: var(--dim); }
1235 .v { font-size: 14px; color: var(--text); margin-left: auto; margin-right: 9px; }
1236
1237 .shape { font-size: 9px; letter-spacing: var(--track); color: var(--faint); }
1238
1239 &[data-curve-state="inverted"] {
1240 .v,
1241 .shape { color: var(--down); }
1242 }
1243 &[data-curve-state="flat"] .shape { color: var(--warn); }
1244 }
1245}
1246
1247// Sector board -----------------------------------------------------------------
1248
1249.heat {
1250 display: grid;
1251 grid-template-columns: repeat(3, 1fr);
1252 // Four equal rows taking whatever height the band settled on, so the board
1253 // ends level with the panels beside it instead of stopping short.
1254 grid-template-rows: repeat(4, 1fr);
1255 flex: 1;
1256 gap: 1px;
1257 background: var(--line);
1258 border: 1px solid var(--line);
1259}
1260
1261.heat .cell {
1262 background: var(--panel-2);
1263 padding: 7px 8px;
1264 display: flex;
1265 flex-direction: column;
1266 justify-content: center;
1267 gap: 2px;
1268
1269 .s { font-size: 9px; letter-spacing: var(--track); color: var(--muted); }
1270 .p { font-size: 13px; color: var(--text); }
1271
1272 // Four steps either side of flat, so the board reads as a gradient rather
1273 // than as two colours.
1274 &[data-dir="up"][data-heat="1"] { background: rgba(93, 220, 130, 0.07); }
1275 &[data-dir="up"][data-heat="2"] { background: rgba(93, 220, 130, 0.14); }
1276 &[data-dir="up"][data-heat="3"] { background: rgba(93, 220, 130, 0.24); }
1277 &[data-dir="up"][data-heat="4"] { background: rgba(93, 220, 130, 0.36); }
1278 &[data-dir="up"] .p { color: var(--up); }
1279
1280 &[data-dir="down"][data-heat="1"] { background: rgba(255, 95, 77, 0.07); }
1281 &[data-dir="down"][data-heat="2"] { background: rgba(255, 95, 77, 0.14); }
1282 &[data-dir="down"][data-heat="3"] { background: rgba(255, 95, 77, 0.24); }
1283 &[data-dir="down"][data-heat="4"] { background: rgba(255, 95, 77, 0.36); }
1284 &[data-dir="down"] .p { color: var(--down); }
1285
1286 // The index is the line the rest are being read against, so it is marked
1287 // rather than left to be picked out of eleven lookalike cells.
1288 &[data-benchmark="yes"] {
1289 outline: 1px solid var(--amber-dim);
1290 outline-offset: -1px;
1291 .s { color: var(--amber); }
1292 }
1293}
1294
1295// Earnings ---------------------------------------------------------------------
1296
1297.earning-rows {
1298 list-style: none;
1299 margin: 0;
1300 padding: 0;
1301 flex: 1;
1302 display: flex;
1303 flex-direction: column;
1304 justify-content: space-around;
1305
1306 li {
1307 display: grid;
1308 grid-template-columns: 52px 1fr auto;
1309 align-items: baseline;
1310 gap: 2px 10px;
1311 padding: 5px 0;
1312 border-bottom: 1px solid var(--line);
1313 }
1314
1315 .tkr { font-family: var(--grotesk); font-weight: 700; font-size: 12px; color: var(--amber); letter-spacing: 0.06em; }
1316 .co { font-size: 12px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1317 .day { font-size: 10px; letter-spacing: var(--track); color: var(--muted); white-space: nowrap; }
1318 .when { color: var(--faint); }
1319 .move { font-family: var(--grotesk); font-size: 12px; font-weight: 700; white-space: nowrap; }
1320
1321 // The second line of every row: the numbers behind the word, under the name
1322 // rather than beside it, since the panel is a third of the page wide.
1323 .detail {
1324 grid-column: 2 / -1;
1325 display: flex;
1326 flex-wrap: wrap;
1327 align-items: baseline;
1328 gap: 3px 8px;
1329 font-size: 10px;
1330 letter-spacing: var(--track);
1331 color: var(--dim);
1332 }
1333 .detail b { font-weight: 700; font-family: var(--grotesk); }
1334 .detail .figs { color: var(--muted); letter-spacing: 0.04em; }
1335 .detail .note { color: var(--amber); }
1336
1337 b[data-verdict="BEAT"] { color: var(--up); }
1338 b[data-verdict="MISS"] { color: var(--down); }
1339 b[data-verdict="MET"] { color: var(--muted); }
1340
1341 li[data-dir="up"] .move { color: var(--up); }
1342 li[data-dir="down"] .move { color: var(--down); }
1343 li[data-dir="flat"] .move { color: var(--muted); }
1344
1345 // Where the results stop and the calendar starts. A reported row carries a
1346 // verdict and a move and an upcoming one carries an estimate, so the two
1347 // halves need marking and do not need naming.
1348 li.done + li.next { border-top: 1px solid var(--line-hot); padding-top: 9px; }
1349
1350 li.empty {
1351 grid-column: 1 / -1;
1352 display: block;
1353 font-size: 11px;
1354 letter-spacing: var(--track);
1355 color: var(--dim);
1356 border-bottom: 0;
1357 }
1358}
1359
1360// Daylight and air -------------------------------------------------------------
1361
1362// Pinned to the bottom of the panel, so ATMOS ends on its one bar instead of
1363
1364// Outdoors ---------------------------------------------------------------------
1365
1366.outlook {
1367 list-style: none;
1368 margin: 0;
1369 padding: 0;
1370 flex: 1;
1371 display: flex;
1372 flex-direction: column;
1373
1374 // The three days share the panel, and each one now carries a reading under
1375 // every word, so growing them fills the band with forecast rather than with
1376 // the gaps between days that made this too airy the first time.
1377 gap: 10px;
1378
1379 li {
1380 flex: 1;
1381 display: flex;
1382 flex-direction: column;
1383 justify-content: center;
1384 gap: 6px;
1385 padding: 0 0 10px;
1386 border-bottom: 1px solid var(--line);
1387 &:last-child { border-bottom: 0; padding-bottom: 0; }
1388 }
1389
1390 .head {
1391 display: flex;
1392 justify-content: space-between;
1393 align-items: baseline;
1394 gap: 10px;
1395 }
1396
1397 .d {
1398 font-family: var(--grotesk);
1399 font-weight: 700;
1400 font-size: 12px;
1401 letter-spacing: 0.06em;
1402 color: var(--text);
1403 }
1404
1405 .v { font-size: 11px; letter-spacing: var(--track); color: var(--muted); }
1406
1407 .temps {
1408 font-size: 11px;
1409 color: var(--muted);
1410 margin-left: auto;
1411 margin-right: 10px;
1412 white-space: nowrap;
1413
1414 i { font-style: normal; color: var(--faint); }
1415 }
1416
1417 // One column per thing being graded, each with its own bar. The verdict is a
1418 // sum of four scores, so a day that reads MARGINAL should say which of the
1419 // four made it that rather than leaving four adjectives to be compared.
1420 .factors {
1421 display: grid;
1422 grid-template-columns: repeat(4, 1fr);
1423 gap: 6px;
1424 margin-top: 2px;
1425 }
1426
1427 .factor {
1428 display: flex;
1429 flex-direction: column;
1430 gap: 3px;
1431 min-width: 0;
1432
1433 .fk {
1434 font-size: 9px;
1435 letter-spacing: var(--track);
1436 color: var(--faint);
1437 }
1438
1439 .fbar {
1440 height: 4px;
1441 background: var(--up);
1442 }
1443
1444 .fv {
1445 font-size: 10px;
1446 letter-spacing: 0.04em;
1447 color: var(--dim);
1448 overflow: hidden;
1449 text-overflow: ellipsis;
1450 white-space: nowrap;
1451 }
1452
1453 // The reading the word was graded from, so a call that was close is
1454 // visible as a number rather than only as an adjective.
1455 .fd {
1456 font-size: 9px;
1457 letter-spacing: 0.04em;
1458 color: var(--faint);
1459 }
1460
1461 // Nought is the good end here, since the score is how much this factor
1462 // costs the day.
1463 &[data-score="1"] { .fbar { background: var(--text); } }
1464 &[data-score="2"] { .fbar { background: var(--warn); } .fv { color: var(--muted); } }
1465 &[data-score="3"] { .fbar { background: var(--down); } .fv { color: var(--down); } }
1466 }
1467
1468 li[data-verdict="OPTIMAL"] .v { color: var(--up); }
1469 li[data-verdict="GOOD"] .v { color: var(--text); }
1470 li[data-verdict="MARGINAL"] .v { color: var(--warn); }
1471 li[data-verdict="POOR"] .v { color: var(--down); }
1472
1473 li.empty { color: var(--dim); font-size: 11px; letter-spacing: var(--track); }
1474}
1475
1476// Steam ------------------------------------------------------------------------
1477
1478.games {
1479 list-style: none;
1480 margin: 0;
1481 padding: 0;
1482 flex: 1;
1483 display: flex;
1484 flex-direction: column;
1485 justify-content: space-around;
1486
1487 li {
1488 display: grid;
1489 grid-template-columns: 1fr auto;
1490 column-gap: 10px;
1491 row-gap: 2px;
1492 padding: 6px 0;
1493 border-bottom: 1px solid var(--line);
1494 &:last-child { border-bottom: 0; }
1495 }
1496
1497 a {
1498 color: var(--text);
1499 font-size: 12px;
1500 overflow: hidden;
1501 text-overflow: ellipsis;
1502 white-space: nowrap;
1503 }
1504 a:hover { color: var(--amber); }
1505
1506 .pr { font-size: 11px; color: var(--dim); white-space: nowrap; text-align: right; }
1507 .disc { color: var(--up); }
1508
1509 // Steam's own review percentage, its bar, and how many reviews are behind it.
1510 // The count matters as much as the number, since 94% of four hundred thousand
1511 // and 94% of eleven are not the same claim.
1512 .rating {
1513 grid-column: 1 / -1;
1514 display: grid;
1515 grid-template-columns: 34px 1fr auto;
1516 align-items: center;
1517 gap: 7px;
1518 margin-top: 1px;
1519
1520 .pct { font-size: 10px; color: var(--muted); }
1521 .n { font-size: 9px; letter-spacing: 0.06em; color: var(--faint); }
1522
1523 // Four, because border-box means the 1px border top and bottom leaves a 3px
1524 // track only 1px of fill, and rows land on fractional pixels here, which
1525 // antialiased that hairline away to nothing on some of them.
1526 .track {
1527 height: 4px;
1528 background: var(--panel-2);
1529 border: 1px solid var(--line);
1530 overflow: hidden;
1531
1532 i {
1533 display: block;
1534 height: 100%;
1535 background: var(--flat);
1536 }
1537 }
1538
1539 &[data-band="good"] { .pct { color: var(--up); } .track i { background: var(--up); } }
1540 &[data-band="mixed"] { .pct { color: var(--warn); } .track i { background: var(--warn); } }
1541 &[data-band="poor"],
1542 &[data-band="bad"] { .pct { color: var(--down); } .track i { background: var(--down); } }
1543 }
1544
1545 .meta {
1546 grid-column: 1 / -1;
1547 display: flex;
1548 flex-wrap: wrap;
1549 gap: 4px 8px;
1550 font-size: 9px;
1551 letter-spacing: 0.06em;
1552 color: var(--faint);
1553 }
1554 .tag { color: var(--dim); }
1555 .players { color: var(--up); }
1556
1557 li.empty { display: block; color: var(--dim); font-size: 11px; letter-spacing: var(--track); }
1558}
1559
1560// Streaming ---------------------------------------------------------------------
1561
1562.titles {
1563 list-style: none;
1564 margin: 0;
1565 padding: 0;
1566 flex: 1;
1567 display: flex;
1568 flex-direction: column;
1569
1570 li {
1571 flex: 1;
1572 display: grid;
1573 grid-template-columns: 1fr auto;
1574 align-content: center;
1575 column-gap: 10px;
1576 row-gap: 2px;
1577 padding: 5px 0;
1578 border-bottom: 1px solid var(--line);
1579 &:last-child { border-bottom: 0; }
1580 }
1581
1582 .name {
1583 font-size: 12px;
1584 color: var(--text);
1585 overflow: hidden;
1586 text-overflow: ellipsis;
1587 white-space: nowrap;
1588 }
1589 a.name:hover { color: var(--amber); }
1590
1591 .svc {
1592 font-size: 9px;
1593 letter-spacing: var(--track);
1594 color: var(--amber-dim);
1595 white-space: nowrap;
1596 }
1597
1598 // The IMDb score and the tomatometer averaged onto one bar, drawn the way the
1599 // Steam ratings are so the two panels beside each other read the same way.
1600 // The label on the right says which numbers went into it, since an average of
1601 // one is not the same claim as an average of two.
1602 .rating {
1603 grid-column: 1 / -1;
1604 display: grid;
1605 grid-template-columns: 34px 1fr auto;
1606 align-items: center;
1607 gap: 7px;
1608 margin-top: 1px;
1609
1610 .pct { font-size: 10px; color: var(--muted); }
1611 .n { font-size: 9px; letter-spacing: 0.06em; color: var(--faint); }
1612
1613 // Four, because border-box means the 1px border top and bottom leaves a 3px
1614 // track only 1px of fill, and rows land on fractional pixels here, which
1615 // antialiased that hairline away to nothing on some of them.
1616 .track {
1617 height: 4px;
1618 background: var(--panel-2);
1619 border: 1px solid var(--line);
1620 overflow: hidden;
1621
1622 i {
1623 display: block;
1624 height: 100%;
1625 background: var(--flat);
1626 }
1627 }
1628
1629 &[data-band="good"] { .pct { color: var(--up); } .track i { background: var(--up); } }
1630 &[data-band="fair"] { .pct { color: var(--warn); } .track i { background: var(--warn); } }
1631 &[data-band="poor"] { .pct { color: var(--down); } .track i { background: var(--down); } }
1632 }
1633
1634 .meta {
1635 grid-column: 1 / -1;
1636 display: flex;
1637 flex-wrap: wrap;
1638 gap: 4px 8px;
1639 font-size: 9px;
1640 letter-spacing: 0.06em;
1641 color: var(--faint);
1642 }
1643
1644 // Each score graded on its own, so a good number reads as good without
1645 // having to be compared against the other five rows.
1646 .score { color: var(--dim); }
1647 .score[data-grade="good"] { color: var(--up); }
1648 .score[data-grade="fair"] { color: var(--muted); }
1649 .score[data-grade="poor"] { color: var(--down); }
1650
1651 li.empty {
1652 display: block;
1653 color: var(--dim);
1654 font-size: 11px;
1655 letter-spacing: var(--track);
1656 }
1657}
1658
1659// Wire -----------------------------------------------------------------------
1660
1661// Source, headline, age, on one line. The boxed pill on a line of its own under
1662// each headline gave every row a different height and read as a stack of cards
1663// rather than a wire. Same three column shape the earnings and story rows use.
1664.headlines {
1665 list-style: none;
1666 margin: 0;
1667 padding: 0;
1668
1669 li {
1670 display: grid;
1671 grid-template-columns: 38px 1fr auto;
1672 align-items: baseline;
1673 column-gap: 10px;
1674 padding: 7px 0;
1675 border-bottom: 1px solid var(--line);
1676 &:last-child { border-bottom: 0; }
1677 }
1678
1679 .tag {
1680 font-size: 9px;
1681 letter-spacing: var(--track);
1682 color: var(--amber-dim);
1683 white-space: nowrap;
1684 }
1685
1686 a { color: var(--text); font-size: 13px; line-height: 1.4; }
1687 a:hover { color: var(--amber); }
1688
1689 .age { font-size: 11px; color: var(--dim); white-space: nowrap; }
1690
1691 li.empty {
1692 display: block;
1693 color: var(--dim);
1694 font-size: 11px;
1695 letter-spacing: var(--track);
1696 }
1697}
1698
1699// Traffic meter --------------------------------------------------------------
1700
1701// Four bars against the busiest site on this machine, because busy is only
1702// meaningful relative to what this particular set of sites does.
1703.traffic {
1704 display: inline-flex;
1705 align-items: flex-end;
1706 gap: 2px;
1707 height: 10px;
1708 flex: none;
1709
1710 i {
1711 display: block;
1712 width: 3px;
1713 background: var(--line-hot);
1714 }
1715 i:nth-child(1) { height: 3px; }
1716 i:nth-child(2) { height: 5px; }
1717 i:nth-child(3) { height: 7px; }
1718 i:nth-child(4) { height: 10px; }
1719
1720 &[data-level="1"] i:nth-child(-n + 1),
1721 &[data-level="2"] i:nth-child(-n + 2),
1722 &[data-level="3"] i:nth-child(-n + 3),
1723 &[data-level="4"] i:nth-child(-n + 4) { background: var(--amber-dim); }
1724
1725 // Against this site's own preceding week, so an unusual day reads as unusual
1726 // for that site rather than for the internet.
1727 &[data-trend="up"] i:nth-child(-n + 4) { background: var(--amber); }
1728 &[data-trend="down"] i { background: var(--line-hot); }
1729 &[data-trend="down"] i:nth-child(-n + 1) { background: var(--dim); }
1730}
1731
1732// Status line ----------------------------------------------------------------
1733
1734.statusline {
1735 display: flex;
1736 align-items: center;
1737 gap: 0;
1738 height: 26px;
1739 padding: 0 var(--pad);
1740 border-top: 1px solid var(--line-hot);
1741 background: var(--panel);
1742 font-size: 10px;
1743 letter-spacing: var(--track);
1744 color: var(--dim);
1745
1746 .seg {
1747 padding: 0 12px;
1748 border-right: 1px solid var(--line);
1749 white-space: nowrap;
1750 &:first-child { padding-left: 0; }
1751 &:last-child { border-right: 0; }
1752 }
1753
1754 // Every upstream the page reads, generated from the same list the UPLINK
1755 // panel uses. It is the one segment allowed to be cut short, since the
1756 // panel names them all anyway.
1757 .srcs {
1758 min-width: 0;
1759 overflow: hidden;
1760 text-overflow: ellipsis;
1761
1762 i {
1763 font-style: normal;
1764 padding: 0 5px;
1765 color: var(--line-hot);
1766 }
1767 }
1768 .grow { flex: 1; border-right: 0; }
1769 a { color: var(--dim); }
1770 a:hover { color: var(--amber); }
1771}
1772
1773.notfound {
1774 text-align: center;
1775 padding: 70px 20px;
1776
1777 .code {
1778 margin: 0;
1779 font-size: 56px;
1780 color: var(--amber);
1781 text-shadow: 0 0 22px var(--amber-glow);
1782 }
1783 .msg {
1784 margin: 6px 0 18px;
1785 font-size: 11px;
1786 letter-spacing: var(--track);
1787 color: var(--muted);
1788 }
1789 a { font-size: 11px; letter-spacing: var(--track); }
1790}
1791
1792// Responsive -----------------------------------------------------------------
1793//
1794// Four sizes, each a real machine rather than a round number: a large desktop
1795// where the third column can breathe, a laptop where the market strip has to
1796// wrap, a tablet where the three columns become one, and a phone where the
1797// status rail has more in it than fits.
1798
1799// Large desktop. The strip is the only thing that benefits from more width, so
1800// it is the only thing that gets it.
1801@media (min-width: 1900px) {
1802 main { max-width: 2100px; }
1803 .card .price { font-size: 22px; }
1804}
1805
1806// Laptop. Eight cards across stops being readable well before the layout
1807// breaks, so they go to two rows of four first.
1808@media (max-width: 1700px) {
1809 .local-band { grid-template-columns: repeat(3, 1fr); }
1810}
1811
1812@media (max-width: 1560px) {
1813 .cards { grid-template-columns: repeat(4, 1fr); }
1814 .market-band { grid-template-columns: 1fr 1fr; }
1815}
1816
1817@media (max-width: 1340px) {
1818 .local-band { grid-template-columns: repeat(2, 1fr); }
1819 .news-band { grid-template-columns: 1fr 1fr; }
1820}
1821
1822// Tablet.
1823@media (max-width: 1024px) {
1824 .band { grid-template-columns: 1fr; }
1825 .heat { grid-template-columns: repeat(3, 1fr); }
1826
1827 .rail { height: auto; flex-wrap: wrap; padding: 8px var(--pad); }
1828 .rail-readouts { margin-left: auto; }
1829 .readout { min-width: 78px; padding: 0 9px; }
1830
1831}
1832
1833@media (max-width: 820px) {
1834 .cards { grid-template-columns: repeat(2, 1fr); }
1835
1836 // The drawdown readout is the first thing to go: it is a nice to have and
1837 // the session state is not.
1838 .markets .panel-head .drawdown { display: none; }
1839}
1840
1841// Phone. The rail keeps the clock and the link state, since those are the two
1842// things worth knowing before scrolling, and drops the rest.
1843@media (max-width: 620px) {
1844 :root { --gap: 8px; --pad: 10px; }
1845
1846 main { padding: var(--gap); }
1847 .panel { padding: 11px; }
1848
1849 .rail-brand .rail-sub { display: none; }
1850 .readout[data-readout="utc"],
1851 .readout[data-readout="date"] { display: none; }
1852 .readout { min-width: 0; padding: 0 8px; }
1853
1854 .cards { grid-template-columns: repeat(2, 1fr); }
1855 .card { padding: 9px 9px 0; }
1856 .card .price { font-size: 17px; }
1857 .card h3 { font-size: 10px; }
1858 .card .sym { font-size: 9px; }
1859 .card .move { font-size: 10px; }
1860 .spark { height: 30px; }
1861
1862 .markets .panel-head {
1863 flex-wrap: wrap;
1864 .rule { display: none; }
1865 }
1866
1867 // The wire keeps its three columns down here, the source is four characters
1868 // and stacking it above the headline is what made these rows look like cards.
1869 .headlines li { grid-template-columns: 32px 1fr auto; column-gap: 8px; }
1870
1871 // Three gauges still fit at this width, but a band like SENSITIVE GROUPS has
1872 // to be allowed onto a second line rather than pushing the cell wider.
1873 .gauges { grid-template-columns: repeat(3, 1fr); }
1874 .gauge .gb { overflow-wrap: anywhere; }
1875 .heat { grid-template-columns: repeat(2, 1fr); }
1876 .earning-rows li { grid-template-columns: 48px 1fr auto; }
1877 .conditions ul.conditions li { grid-template-columns: 1fr auto; .n { display: none; } }
1878 .alert {
1879 padding: 9px 10px 9px 12px;
1880 .ev { font-size: 12px; letter-spacing: 0.07em; }
1881 .alert-meta { gap: 3px 12px; }
1882 }
1883
1884 .statusline {
1885 height: auto;
1886 flex-wrap: wrap;
1887 padding: 8px var(--pad);
1888 row-gap: 4px;
1889 .seg { padding: 0 8px; }
1890 .grow { display: none; }
1891 }
1892}
1893
1894// Anything that moves here is decoration on top of a page that already works,
1895// so all of it comes off rather than being slowed down.
1896@media (prefers-reduced-motion: reduce) {
1897 .pip,
1898 .price.tick { animation: none !important; }
1899}