repos
/ orchard main

orchard

mirror

Every 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

51.0 KB · 1900 lines · SCSS Raw History
   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}