repos
/ finance-rust master

finance-rust

mirror archived upstream

Single-binary self-hosted market watcher for stocks, ETFs, indexes, and futures: live charts, key stats, fundamentals, SEC filings, and SSE streaming.

axumdockerfinancerustself-hostedsqlitestocksvite

45.6 KB · 2180 lines · SCSS Raw History
   1@use "../../base/styles/variables" as *;
   2@use "../../base/styles/mixins" as *;
   3
   4/* ---------- header ----------
   5   Identity (ticker + tag chips), name, and live quote stack as three
   6   rows. When a stock has a synthesised health read, a circular % badge
   7   anchors the top-right and spans those three rows so id / name / quote
   8   read as one column on the left and the badge sits alone on the right
   9   at desktop. On phone it slides under the quote so the ticker keeps
  10   the full width of the screen. The double-rule under the header echoes
  11   the new .section-title ledger underline so the page reads as a single
  12   accounting register. */
  13.sym-head {
  14  display: grid;
  15  grid-template-columns: minmax(0, 1fr);
  16  gap: var(--sp-3) var(--sp-5);
  17  margin: var(--sp-1) 0 var(--sp-5);
  18  padding-bottom: var(--sp-4);
  19  border-bottom: 1px solid var(--rule-strong);
  20  box-shadow: 0 3px 0 -2px var(--rule);
  21}
  22
  23/* With a health badge, the header becomes a 2-column grid at every width:
  24   left column flexes, right column is auto-sized to the badge. Stacking
  25   on phone is handled inside .sym-head__health (it justifies left and
  26   the badge shrinks). */
  27.sym-head--has-health {
  28  grid-template-columns: minmax(0, 1fr) auto;
  29}
  30
  31.sym-head__id {
  32  display: flex;
  33  align-items: center;
  34  gap: var(--sp-3);
  35  flex-wrap: wrap;
  36  min-width: 0;
  37  grid-column: 1;
  38}
  39
  40.sym-head__ticker {
  41  @include mono;
  42  font-weight: 700;
  43  font-size: var(--fs-2xl);
  44  letter-spacing: -0.02em;
  45}
  46
  47.sym-head__tag {
  48  @include eyebrow;
  49  font-size: var(--eye-chip);
  50  color: var(--ink-dim);
  51  padding: 3px 8px;
  52  border: 1px solid var(--rule-strong);
  53  border-radius: 4px;
  54}
  55
  56// Phase 15: sector / industry tag, linked to its /industries page. Same shape
  57// as a plain `.sym-head__tag`, with a hover ramp that lifts to ink.
  58.sym-head__tag--link {
  59  color: var(--ink-dim);
  60  text-decoration: none;
  61  transition: color 0.1s, border-color 0.1s, background 0.1s;
  62
  63  &:hover {
  64    color: var(--ink);
  65    border-color: var(--ink);
  66    background: var(--well);
  67  }
  68}
  69
  70/* Name + quote span the full header width by default; the desktop rule
  71   below pulls them back to column 1 when the health badge is present
  72   (the badge then anchors the right column across all three rows). */
  73.sym-head__name {
  74  grid-column: 1 / -1;
  75  color: var(--ink-dim);
  76  font-size: var(--fs-md);
  77  margin-top: -2px;
  78}
  79
  80.sym-head__quote {
  81  display: flex;
  82  align-items: baseline;
  83  flex-wrap: wrap;
  84  gap: 4px var(--sp-3);
  85  grid-column: 1 / -1;
  86}
  87
  88.sym-head__price {
  89  @include mono;
  90  font-size: var(--fs-2xl);
  91  font-weight: 700;
  92  letter-spacing: -0.01em;
  93}
  94
  95.sym-head__chg {
  96  @include mono;
  97  font-size: var(--fs-md);
  98  font-weight: 700;
  99}
 100
 101.sym-head__asof {
 102  font-size: var(--fs-xs);
 103  color: var(--ink-faint);
 104  letter-spacing: 0;
 105  text-transform: none;
 106  /* a hairline divider before the asof so the "At close · quoted Nov 14"
 107     reads as a separate annotation, not part of the % change figure */
 108  padding-left: var(--sp-3);
 109  border-left: 1px solid var(--rule);
 110  margin-left: 2px;
 111}
 112
 113/* ---------- mobile health verdict line (Phase 6) ----------
 114   The donut's read, flattened to a single "● Verdict · NN% · trajectory ↑"
 115   row sitting directly under the price, where a phone reads it first. Hidden
 116   at $bp-md, where the donut (below) is the desktop treatment. Coloured by the
 117   overall grade; works for a stock (with a trajectory clause) or an ETF. */
 118.sym-verdict {
 119  grid-column: 1 / -1;
 120  display: flex;
 121  align-items: baseline;
 122  flex-wrap: wrap;
 123  gap: 4px var(--sp-3);
 124  margin-top: -2px;
 125  font-size: var(--fs-sm);
 126}
 127
 128.sym-verdict__dot {
 129  align-self: center;
 130  width: 9px;
 131  height: 9px;
 132  border-radius: 50%;
 133  background: var(--ink-faint);
 134}
 135.sym-verdict--good .sym-verdict__dot { background: var(--up); }
 136.sym-verdict--ok   .sym-verdict__dot { background: var(--warn); }
 137.sym-verdict--bad  .sym-verdict__dot { background: var(--down); }
 138
 139.sym-verdict__text {
 140  @include eyebrow;
 141  font-size: var(--fs-xs);
 142  font-weight: 700;
 143  color: var(--ink-dim);
 144}
 145.sym-verdict--good .sym-verdict__text { color: var(--up); }
 146.sym-verdict--ok   .sym-verdict__text { color: var(--warn); }
 147.sym-verdict--bad  .sym-verdict__text { color: var(--down); }
 148
 149.sym-verdict__pct {
 150  @include mono;
 151  font-weight: 700;
 152  color: var(--ink-dim);
 153}
 154
 155/* trajectory clause: a quiet annotation, divided off like .sym-head__asof */
 156.sym-verdict__traj {
 157  font-size: var(--fs-xs);
 158  color: var(--ink-faint);
 159  padding-left: var(--sp-3);
 160  border-left: 1px solid var(--rule);
 161}
 162.sym-verdict__traj--good { color: var(--up); }
 163.sym-verdict__traj--bad  { color: var(--down); }
 164
 165.sym-verdict__arrow {
 166  @include mono;
 167  margin-right: 3px;
 168  font-weight: 700;
 169}
 170
 171/* ---------- header health badge (Phase 17 redesign) ----------
 172   A circular % donut anchors the header's top-right. The previous
 173   full-width Stock health section is gone; the three sub-readings
 174   (fundamentals / trajectory / leadership) live in a popover that
 175   shows on hover and on keyboard / touch focus.
 176
 177   The donut uses the well-known stroke-dasharray trick: r = 50/π so the
 178   circle's circumference is exactly 100, and "stroke-dasharray: N, 100"
 179   then fills exactly N% of the ring with no maths in the template. */
 180.sym-head__health {
 181  grid-column: 2;
 182  grid-row: 1;
 183  align-self: center;
 184  justify-self: end;
 185  position: relative;
 186  /* Phase 6: on phones the flattened .sym-verdict line carries the read; the
 187     donut is the desktop treatment, switched back on at $bp-md below. */
 188  display: none;
 189}
 190
 191.health-badge {
 192  appearance: none;
 193  display: grid;
 194  grid-template-columns: auto auto;
 195  align-items: center;
 196  gap: 0 var(--sp-3);
 197  padding: 6px 14px 6px 6px;
 198  background: var(--paper);
 199  border: 1px solid var(--rule-strong);
 200  border-radius: 999px;
 201  color: var(--ink);
 202  cursor: help;
 203  font: inherit;
 204  text-align: left;
 205  transition: border-color 0.12s, box-shadow 0.12s, transform 0.12s;
 206
 207  &:hover,
 208  &:focus-visible {
 209    border-color: var(--ink);
 210    box-shadow: 0 1px 0 0 var(--rule);
 211  }
 212}
 213
 214.health-badge__ring {
 215  grid-row: 1 / span 2;
 216  width: 52px;
 217  height: 52px;
 218  display: block;
 219  /* rotate -90deg so the 0% mark sits at the top */
 220  transform: rotate(-90deg);
 221}
 222
 223.health-badge__track {
 224  stroke: var(--rule);
 225  stroke-width: 3.2;
 226}
 227
 228.health-badge__fill {
 229  stroke-width: 3.2;
 230  stroke-linecap: round;
 231  transition: stroke-dasharray 0.4s ease;
 232}
 233
 234.health-badge--good .health-badge__fill { stroke: var(--up); }
 235.health-badge--ok   .health-badge__fill { stroke: var(--warn); }
 236.health-badge--bad  .health-badge__fill { stroke: var(--down); }
 237.health-badge--unknown .health-badge__fill { stroke: var(--ink-faint); }
 238
 239.health-badge__num {
 240  @include mono;
 241  font-weight: 700;
 242  font-size: var(--fs-lg);
 243  line-height: 1;
 244}
 245
 246.health-badge--good .health-badge__num { color: var(--up); }
 247.health-badge--ok   .health-badge__num { color: var(--warn); }
 248.health-badge--bad  .health-badge__num { color: var(--down); }
 249
 250.health-badge__unit {
 251  font-size: 0.65em;
 252  margin-left: 1px;
 253  color: var(--ink-faint);
 254  font-weight: 600;
 255}
 256
 257.health-badge__caption {
 258  @include eyebrow;
 259  font-size: var(--eye-chip);
 260  color: var(--ink-dim);
 261  letter-spacing: 0.06em;
 262}
 263
 264/* Hover / focus popover. Pure CSS — opens on the badge's :hover, on
 265   keyboard focus (:focus-visible bubbles through :focus-within), and
 266   stays open while the pointer is on the popover itself so a user
 267   moving the cursor in to read it does not flicker the popover shut. */
 268.health-pop {
 269  position: absolute;
 270  top: calc(100% + 8px);
 271  right: 0;
 272  z-index: 30;
 273  width: min(320px, calc(100vw - 32px));
 274  padding: var(--sp-3) var(--sp-3) var(--sp-2);
 275  background: var(--paper);
 276  border: 1px solid var(--rule-strong);
 277  border-radius: var(--radius-sm);
 278  box-shadow: 0 8px 24px -8px rgba(33, 31, 26, 0.18);
 279  opacity: 0;
 280  pointer-events: none;
 281  transform: translateY(-4px);
 282  transition: opacity 0.12s, transform 0.12s;
 283}
 284
 285.sym-head__health:hover .health-pop,
 286.sym-head__health:focus-within .health-pop {
 287  opacity: 1;
 288  pointer-events: auto;
 289  transform: translateY(0);
 290}
 291
 292.health-pop__head {
 293  margin: 0 0 var(--sp-2);
 294  padding-bottom: var(--sp-2);
 295  border-bottom: 1px solid var(--rule);
 296  font-size: var(--fs-sm);
 297  color: var(--ink-dim);
 298}
 299
 300.health-pop__verdict--good { color: var(--up); }
 301.health-pop__verdict--ok   { color: var(--warn); }
 302.health-pop__verdict--bad  { color: var(--down); }
 303.health-pop__verdict--unknown { color: var(--ink-faint); }
 304
 305.health-pop__rows {
 306  margin: 0;
 307  padding: 0;
 308  list-style: none;
 309  display: grid;
 310  gap: 4px;
 311}
 312
 313.health-pop__row {
 314  display: grid;
 315  grid-template-columns: 1fr auto;
 316  align-items: baseline;
 317  gap: var(--sp-3);
 318  padding: 6px var(--sp-2);
 319  border-left: 3px solid var(--rule);
 320  background: var(--well);
 321  border-radius: var(--radius-sm);
 322}
 323
 324.health-pop__row--good { border-left-color: var(--up); }
 325.health-pop__row--ok   { border-left-color: var(--warn); }
 326.health-pop__row--bad  { border-left-color: var(--down); }
 327.health-pop__row--unknown { border-left-color: var(--ink-faint); }
 328
 329.health-pop__label {
 330  @include eyebrow;
 331  color: var(--ink);
 332}
 333
 334.health-pop__value {
 335  @include mono;
 336  font-weight: 700;
 337  font-size: var(--fs-sm);
 338}
 339
 340.health-pop__row--good .health-pop__value { color: var(--up); }
 341.health-pop__row--ok   .health-pop__value { color: var(--warn); }
 342.health-pop__row--bad  .health-pop__value { color: var(--down); }
 343.health-pop__row--unknown .health-pop__value { color: var(--ink-faint); }
 344
 345.health-pop__src {
 346  margin: var(--sp-2) 0 0;
 347  font-size: var(--fs-2xs);
 348  color: var(--ink-faint);
 349  line-height: 1.45;
 350  font-style: italic;
 351}
 352
 353/* ---------- chart panel ---------- */
 354.chart-panel {
 355  padding: var(--sp-3) var(--sp-3) var(--sp-2);
 356}
 357
 358/* Phase 31: a stricter two-row toolbar. Row 1 is the time-range bar with
 359   the range-summary chip on the right; row 2 is the indicator toggle row
 360   on a quieter hairline. Splitting them stops the crammed-into-one-row
 361   look that earlier crowded the chart-bar at narrow widths. */
 362.chart-bar {
 363  display: flex;
 364  flex-wrap: wrap;
 365  align-items: center;
 366  justify-content: space-between;
 367  gap: var(--sp-2) var(--sp-4);
 368  margin-bottom: var(--sp-2);
 369}
 370
 371.range-bar {
 372  display: flex;
 373  flex-wrap: wrap;
 374  align-items: center;
 375  gap: var(--sp-1);
 376}
 377
 378/* Phase 6: a hairline after 1W sets the intraday ranges (1D / 1W) apart from
 379   the daily ones without a heavier divider element. */
 380.range-btn--group-end {
 381  margin-right: var(--sp-2);
 382  position: relative;
 383}
 384.range-btn--group-end::after {
 385  content: "";
 386  position: absolute;
 387  right: calc(var(--sp-1) * -1);
 388  top: 50%;
 389  transform: translateY(-50%);
 390  height: 60%;
 391  border-right: 1px solid var(--rule-strong);
 392}
 393
 394/* the range-change chip: % and absolute move over the whole visible range */
 395.range-summary {
 396  display: flex;
 397  flex-direction: column;
 398  align-items: flex-end;
 399  gap: 1px;
 400  margin-left: auto;
 401  line-height: 1.25;
 402  text-align: right;
 403}
 404
 405/* `display: flex` would override the [hidden] attribute, as on .chart-readout */
 406.range-summary[hidden] {
 407  display: none;
 408}
 409
 410.range-summary__chg {
 411  font-size: 1rem;
 412  font-weight: 700;
 413}
 414
 415.range-summary[data-dir="up"] .range-summary__chg {
 416  color: var(--up);
 417}
 418
 419.range-summary[data-dir="down"] .range-summary__chg {
 420  color: var(--down);
 421}
 422
 423.range-summary__abs {
 424  margin-left: 8px;
 425  color: var(--ink-dim);
 426  font-weight: 600;
 427}
 428
 429.range-summary__cap {
 430  @include eyebrow;
 431  font-size: 0.56rem;
 432  color: var(--ink-faint);
 433}
 434
 435/* ---------- indicator toggle row ---------- */
 436.ind-bar {
 437  display: flex;
 438  flex-wrap: wrap;
 439  gap: 6px;
 440  margin-bottom: 14px;
 441  padding-top: 10px;
 442  border-top: 1px solid var(--rule);
 443}
 444
 445.ind-btn {
 446  @include mono;
 447  display: inline-flex;
 448  align-items: center;
 449  gap: 7px;
 450  min-height: 44px;
 451  padding: 0 12px;
 452  border-radius: var(--radius-sm);
 453  border: 1px solid var(--rule);
 454  background: var(--surface);
 455  color: var(--ink-faint);
 456  font-size: 0.76rem;
 457  font-weight: 500;
 458  cursor: pointer;
 459  transition: color 0.13s, border-color 0.13s;
 460}
 461
 462/* `display: inline-flex` above would override the [hidden] attribute's
 463   display:none, so re-assert it. Without this the benchmark toggle (hidden on
 464   symbols with no benchmark) and the daily-only toggles (hidden on the 1D / 1W
 465   intraday ranges, Phase 6) would still show. */
 466.ind-btn[hidden] {
 467  display: none;
 468}
 469
 470.ind-btn:hover {
 471  color: var(--ink-dim);
 472  border-color: var(--rule-strong);
 473}
 474
 475.ind-btn.is-active {
 476  color: var(--ink);
 477  border-color: var(--ink);
 478}
 479
 480/* the swatch echoes the indicator's line color on the chart (chart.js paints
 481   it); dimmed while the indicator is toggled off */
 482.ind-btn__dot {
 483  width: 18px;
 484  height: 3px;
 485  border-radius: 2px;
 486  background: var(--ink-faint);
 487  flex: none;
 488  opacity: 0.4;
 489  transition: opacity 0.13s;
 490}
 491
 492.ind-btn.is-active .ind-btn__dot {
 493  opacity: 1;
 494}
 495
 496.range-btn {
 497  @include mono;
 498  min-height: 44px;
 499  min-width: 52px;
 500  padding: 0 13px;
 501  border-radius: var(--radius-sm);
 502  border: 1px solid var(--rule-strong);
 503  background: var(--surface);
 504  color: var(--ink-dim);
 505  font-size: 0.78rem;
 506  font-weight: 500;
 507  cursor: pointer;
 508  transition: color 0.13s, background 0.13s, border-color 0.13s;
 509}
 510
 511.range-btn:hover {
 512  color: var(--ink);
 513  border-color: var(--ink);
 514}
 515
 516.range-btn.is-active {
 517  color: var(--paper);
 518  background: var(--ink);
 519  border-color: var(--ink);
 520  font-weight: 700;
 521}
 522
 523#chart {
 524  position: relative;
 525  width: 100%;
 526  height: 360px;
 527  transition: opacity 0.15s;
 528  /* horizontal drags drive the measure tool; vertical still scrolls the page */
 529  touch-action: pan-y;
 530}
 531
 532#chart.is-loading {
 533  opacity: 0.45;
 534}
 535
 536/* ---------- chart measure tool ---------- */
 537/* Shaded band spanning a click-dragged interval; bottom inset clears the
 538   time axis. Pointer-transparent so the drag stays bound to #chart. */
 539.chart-band {
 540  position: absolute;
 541  top: 0;
 542  bottom: 26px;
 543  z-index: 3;
 544  background: rgba(33, 31, 26, 0.07);
 545  border-left: 1px solid var(--rule-strong);
 546  border-right: 1px solid var(--rule-strong);
 547  pointer-events: none;
 548}
 549
 550.chart-readout {
 551  position: absolute;
 552  top: 8px;
 553  z-index: 4;
 554  display: flex;
 555  flex-direction: column;
 556  gap: 1px;
 557  padding: 5px 9px;
 558  background: var(--surface);
 559  border: 1px solid var(--rule-strong);
 560  border-radius: var(--radius-sm);
 561  box-shadow: var(--lift-hover);
 562  white-space: nowrap;
 563  pointer-events: none;
 564}
 565
 566/* `display: flex` above would override the [hidden] attribute's display:none,
 567   leaving the readout chip stuck on screen after a selection is cleared, so
 568   re-assert it (same fix as `.health-banner[hidden]`). */
 569.chart-readout[hidden] {
 570  display: none;
 571}
 572
 573.chart-readout__pct {
 574  @include mono;
 575  font-size: 0.92rem;
 576  font-weight: 700;
 577}
 578
 579.chart-readout[data-dir="up"] .chart-readout__pct {
 580  color: var(--up);
 581}
 582
 583.chart-readout[data-dir="down"] .chart-readout__pct {
 584  color: var(--down);
 585}
 586
 587.chart-readout__sub {
 588  @include mono;
 589  font-size: 0.66rem;
 590  color: var(--ink-faint);
 591}
 592
 593/* ---------- key stats: gauges ---------- */
 594.keystats {
 595  display: grid;
 596  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
 597  gap: 12px;
 598}
 599
 600.gauge {
 601  @include card;
 602  padding: 16px 16px 8px;
 603}
 604
 605.gauge__row {
 606  display: flex;
 607  align-items: baseline;
 608  justify-content: space-between;
 609  gap: 12px;
 610}
 611
 612.gauge__label {
 613  @include eyebrow;
 614  color: var(--ink);
 615}
 616
 617.gauge__meta {
 618  font-size: 0.74rem;
 619  color: var(--ink-faint);
 620  text-align: right;
 621}
 622
 623/* the rail: pips need vertical room above the track for their dot heads */
 624.gauge__track {
 625  height: 8px;
 626  margin: 24px 0 10px;
 627}
 628
 629.gauge__ends {
 630  display: flex;
 631  justify-content: space-between;
 632  gap: 8px;
 633  font-size: 0.82rem;
 634  color: var(--ink-dim);
 635}
 636
 637.gauge__ends-r {
 638  text-align: right;
 639}
 640
 641.gauge__ends-c {
 642  text-align: center;
 643}
 644
 645.gauge__cap {
 646  @include eyebrow;
 647  font-size: 0.56rem;
 648  margin-right: 3px;
 649}
 650
 651/* ---------- gauge legend ---------- */
 652.legend {
 653  margin-top: 12px;
 654}
 655
 656.legend__item {
 657  display: grid;
 658  grid-template-columns: 1fr auto auto;
 659  align-items: baseline;
 660  gap: 10px;
 661  padding: 8px 0;
 662  border-top: 1px solid var(--rule);
 663}
 664
 665.legend__item dt {
 666  display: flex;
 667  align-items: center;
 668  gap: 7px;
 669  font-size: 0.85rem;
 670  color: var(--ink-dim);
 671}
 672
 673.legend__item dd {
 674  font-size: 0.9rem;
 675  font-weight: 600;
 676}
 677
 678.legend__delta {
 679  font-size: 0.78rem;
 680  font-weight: 600;
 681  min-width: 4.5ch;
 682  text-align: right;
 683}
 684
 685/* legend dots echo the markers on the rail */
 686.legend__dot {
 687  width: 9px;
 688  height: 9px;
 689  border-radius: 50%;
 690  background: var(--ink-dim);
 691  flex: none;
 692}
 693
 694.legend__dot--ghost {
 695  background: transparent;
 696  border: 2px solid var(--ink-faint);
 697}
 698
 699.legend__dot--up {
 700  background: var(--up);
 701}
 702
 703.legend__dot--down {
 704  background: var(--down);
 705}
 706
 707/* ---------- fundamentals: graded ratio cards ---------- */
 708.fund-basis {
 709  margin: 2px 0 14px;
 710  color: var(--ink-faint);
 711  font-size: 0.82rem;
 712}
 713
 714/* Stale-earnings caveat: the latest SEC quarter is old enough that the
 715   price-based ratios may lag. Amber, sparing — a confidence flag, not an alarm. */
 716.fund-stale {
 717  margin: -8px 0 16px;
 718  padding-left: 10px;
 719  border-left: 2px solid var(--warn);
 720  color: var(--warn);
 721  font-size: 0.82rem;
 722}
 723
 724/* "no recent trading data" banner for a dormant / delisted / renamed symbol,
 725   sitting above the header so it is read first (see STALE_DATA_DAYS). */
 726.stale-banner {
 727  margin: 4px 0 16px;
 728  padding: 10px 14px;
 729  border: 1px solid var(--warn);
 730  border-left-width: 3px;
 731  border-radius: 4px;
 732  background: var(--warn-soft);
 733  color: var(--ink);
 734  font-size: 0.86rem;
 735  line-height: 1.45;
 736
 737  strong { color: var(--warn); }
 738}
 739
 740/* the rolled-up strong / fair / weak standing badge with a one-line reading,
 741   sitting above the per-ratio grid (Phase 20) */
 742.fund-standing {
 743  display: flex;
 744  align-items: flex-start;
 745  gap: 12px;
 746  margin: 2px 0 16px;
 747}
 748
 749/* the badge reads larger here than the inline app-wide pill */
 750.fund-standing .vbadge {
 751  font-size: 0.72rem;
 752  padding: 6px 13px;
 753}
 754
 755.fund-standing__text {
 756  max-width: 70ch;
 757  color: var(--ink-dim);
 758  font-size: 0.86rem;
 759}
 760
 761.ratios {
 762  display: grid;
 763  grid-template-columns: repeat(auto-fit, minmax(248px, 1fr));
 764  gap: var(--sp-3);
 765}
 766
 767.ratio {
 768  @include card;
 769  display: flex;
 770  flex-direction: column;
 771  padding: var(--sp-3) var(--sp-4) var(--sp-3);
 772}
 773
 774.ratio__head {
 775  display: flex;
 776  align-items: center;
 777  justify-content: space-between;
 778  gap: var(--sp-2);
 779}
 780
 781.ratio__label {
 782  @include eyebrow;
 783  color: var(--ink);
 784}
 785
 786/* the verdict badge: a quiet pill that carries the semantic color */
 787.ratio__badge {
 788  @include eyebrow;
 789  flex: none;
 790  padding: 2px 8px;
 791  border-radius: 999px;
 792  font-size: var(--eye-chip);
 793}
 794
 795.ratio__badge--good     { color: var(--up);        background: var(--up-soft); }
 796.ratio__badge--ok       { color: var(--warn);      background: var(--warn-soft); }
 797.ratio__badge--bad      { color: var(--down);      background: var(--down-soft); }
 798.ratio__badge--unknown  { color: var(--ink-faint); background: var(--well); }
 799
 800/* the figure itself, colored to match the grade */
 801.ratio__value {
 802  @include mono;
 803  font-size: var(--fs-xl);
 804  font-weight: 700;
 805  letter-spacing: -0.02em;
 806  margin: var(--sp-2) 0 2px;
 807  line-height: 1.05;
 808}
 809
 810.ratio--good .ratio__value     { color: var(--up); }
 811.ratio--ok .ratio__value       { color: var(--warn); }
 812.ratio--bad .ratio__value      { color: var(--down); }
 813.ratio--unknown .ratio__value  { color: var(--ink-faint); }
 814
 815.ratio__reading {
 816  font-size: var(--fs-xs);
 817  color: var(--ink-dim);
 818  line-height: 1.45;
 819}
 820
 821/* the static "how to read it" note, set off below a hairline. Phase 31
 822   muted it further (smaller + tighter) — 9 ratios × the old explainer
 823   block dominated the page; the value + verdict + reading carry the
 824   primary signal and the explainer reads as a footnote. */
 825.ratio__explain {
 826  margin-top: var(--sp-2);
 827  padding-top: var(--sp-2);
 828  border-top: 1px solid var(--rule);
 829  font-size: var(--fs-2xs);
 830  color: var(--ink-faint);
 831  line-height: 1.4;
 832}
 833
 834/* shown while the SEC sweep has not yet reached this symbol */
 835.fund-pending {
 836  @include card;
 837  padding: 18px 20px;
 838  color: var(--ink-dim);
 839  font-size: 0.88rem;
 840}
 841
 842.fund-pending a {
 843  text-decoration: underline;
 844  text-underline-offset: 2px;
 845}
 846
 847/* ---------- financials table + period toggle ---------- */
 848.fin {
 849  padding: 14px 16px;
 850}
 851
 852.fin__toggle {
 853  display: flex;
 854  gap: 6px;
 855  margin-bottom: 14px;
 856}
 857
 858.fin__tab {
 859  @include mono;
 860  min-height: 40px;
 861  padding: 0 15px;
 862  border-radius: var(--radius-sm);
 863  border: 1px solid var(--rule-strong);
 864  background: var(--surface);
 865  color: var(--ink-dim);
 866  font-size: 0.78rem;
 867  font-weight: 500;
 868  cursor: pointer;
 869  transition: color 0.13s, background 0.13s, border-color 0.13s;
 870}
 871
 872.fin__tab:hover {
 873  color: var(--ink);
 874  border-color: var(--ink);
 875}
 876
 877.fin__tab.is-active {
 878  color: var(--paper);
 879  background: var(--ink);
 880  border-color: var(--ink);
 881  font-weight: 700;
 882}
 883
 884/* wide tables scroll within the card rather than the page on a phone */
 885.fin__scroll {
 886  overflow-x: auto;
 887}
 888
 889.fin__table {
 890  width: 100%;
 891  border-collapse: collapse;
 892  white-space: nowrap;
 893}
 894
 895.fin__table th,
 896.fin__table td {
 897  padding: 9px 10px;
 898  text-align: right;
 899  font-size: 0.85rem;
 900}
 901
 902.fin__table th:first-child,
 903.fin__table td:first-child {
 904  padding-left: 0;
 905}
 906
 907.fin__table th:last-child,
 908.fin__table td:last-child {
 909  padding-right: 0;
 910}
 911
 912.fin__table thead th {
 913  @include eyebrow;
 914  font-size: 0.58rem;
 915  color: var(--ink-faint);
 916  border-bottom: 1px solid var(--rule-strong);
 917}
 918
 919.fin__table tbody tr {
 920  border-top: 1px solid var(--rule);
 921}
 922
 923.fin__table tbody tr:first-child {
 924  border-top: none;
 925}
 926
 927.fin__table tbody th {
 928  text-align: left;
 929  font-weight: 500;
 930  color: var(--ink-dim);
 931}
 932
 933.fin__table tbody td {
 934  font-weight: 600;
 935}
 936
 937.fin__empty {
 938  padding: 8px 0;
 939  color: var(--ink-dim);
 940  font-size: 0.86rem;
 941}
 942
 943/* period-over-period growth cue (Phase 24): a small arrow on every changed
 944   figure, plus a semantic colour where a rise has a clear good/bad reading */
 945.fin__table td.fin-cell--good {
 946  color: var(--up);
 947}
 948
 949.fin__table td.fin-cell--bad {
 950  color: var(--down);
 951}
 952
 953.fin-cell__dir {
 954  margin-left: 4px;
 955  font-size: 0.62rem;
 956  vertical-align: 0.06em;
 957  /* faint by default; a graded cell re-colours it to match the figure */
 958  color: var(--ink-faint);
 959}
 960
 961.fin__table td.fin-cell--good .fin-cell__dir,
 962.fin__table td.fin-cell--bad .fin-cell__dir {
 963  color: inherit;
 964}
 965
 966.fin-cell__dir--up::before {
 967  content: "\25B4";
 968}
 969
 970.fin-cell__dir--down::before {
 971  content: "\25BE";
 972}
 973
 974/* ---------- recent SEC filings ----------
 975   The .filing / .lead-change / .anomaly rows all share the shape
 976   "left-anchored tag · body · meta · external icon". The styles here
 977   are the canonical version; .lead-change and .anomaly only override
 978   what differs from a filing row. */
 979.filings {
 980  padding: 2px var(--sp-4);
 981}
 982
 983.filing-list {
 984  margin: 0;
 985  padding: 0;
 986  list-style: none;
 987}
 988
 989.filing {
 990  border-top: 1px solid var(--rule);
 991}
 992
 993.filing:first-child {
 994  border-top: none;
 995}
 996
 997.filing__link {
 998  display: flex;
 999  align-items: center;
1000  gap: var(--sp-3);
1001  padding: 10px 0;
1002  color: var(--ink);
1003  transition: color 0.1s;
1004}
1005
1006.filing__link:hover {
1007  color: var(--ink-dim);
1008}
1009
1010.filing__form {
1011  @include mono;
1012  flex: none;
1013  min-width: 7ch;
1014  text-align: center;
1015  padding: 4px 7px;
1016  border: 1px solid var(--rule-strong);
1017  border-radius: 4px;
1018  font-size: var(--fs-2xs);
1019  font-weight: 700;
1020  letter-spacing: 0.02em;
1021  color: var(--ink);
1022}
1023
1024.filing__body {
1025  flex: 1;
1026  min-width: 0;
1027  display: flex;
1028  flex-direction: column;
1029  gap: 1px;
1030}
1031
1032.filing__desc {
1033  font-size: var(--fs-sm);
1034  font-weight: 500;
1035}
1036
1037.filing__meta {
1038  font-size: var(--fs-xs);
1039  color: var(--ink-faint);
1040}
1041
1042.filing__ext {
1043  flex: none;
1044  width: 14px;
1045  height: 14px;
1046  color: var(--ink-faint);
1047  transition: color 0.1s, transform 0.1s;
1048}
1049
1050.filing__link:hover .filing__ext {
1051  color: var(--ink-dim);
1052  transform: translate(2px, -2px);
1053}
1054
1055/* ---------- leadership: roster + change feed ---------- */
1056.leadership {
1057  padding: var(--sp-3) var(--sp-4);
1058}
1059
1060.roster {
1061  margin: 0;
1062  padding: 0;
1063  list-style: none;
1064}
1065
1066.roster__row {
1067  display: flex;
1068  flex-wrap: wrap;
1069  align-items: baseline;
1070  justify-content: space-between;
1071  gap: 2px var(--sp-4);
1072  padding: var(--sp-2) 0;
1073  border-top: 1px solid var(--rule);
1074}
1075
1076.roster__row:first-child {
1077  border-top: none;
1078}
1079
1080.roster__name {
1081  font-size: var(--fs-md);
1082  font-weight: 600;
1083}
1084
1085.roster__role {
1086  font-size: var(--fs-xs);
1087  color: var(--ink-dim);
1088  text-align: right;
1089}
1090
1091/* the provenance note, set off below a hairline like .ratio__explain */
1092.roster__src {
1093  margin-top: var(--sp-3);
1094  padding-top: var(--sp-3);
1095  border-top: 1px solid var(--rule);
1096  font-size: var(--fs-xs);
1097  color: var(--ink-faint);
1098  line-height: 1.45;
1099}
1100
1101/* recent officer/director changes, from 8-K item 5.02 */
1102.lead-changes {
1103  margin-top: var(--sp-4);
1104  padding-top: var(--sp-3);
1105  border-top: 1px solid var(--rule-strong);
1106}
1107
1108.lead-changes__title {
1109  font-family: var(--font-serif);
1110  font-weight: 600;
1111  font-size: var(--fs-md);
1112  letter-spacing: -0.01em;
1113  color: var(--ink);
1114  margin-bottom: var(--sp-1);
1115}
1116
1117.lead-changes__list {
1118  margin: 0;
1119  padding: 0;
1120  list-style: none;
1121}
1122
1123.lead-change {
1124  border-top: 1px solid var(--rule);
1125}
1126
1127.lead-change:first-child {
1128  border-top: none;
1129}
1130
1131.lead-change__link {
1132  display: flex;
1133  align-items: center;
1134  gap: var(--sp-3);
1135  padding: 10px 0;
1136  color: var(--ink);
1137  transition: color 0.1s;
1138}
1139
1140.lead-change__link:hover {
1141  color: var(--ink-dim);
1142}
1143
1144.lead-change__body {
1145  flex: 1;
1146  min-width: 0;
1147  display: flex;
1148  flex-direction: column;
1149  gap: 1px;
1150}
1151
1152.lead-change__desc {
1153  font-size: var(--fs-sm);
1154  font-weight: 500;
1155}
1156
1157.lead-change__meta {
1158  font-size: var(--fs-xs);
1159  color: var(--ink-faint);
1160}
1161
1162.lead-change__link:hover .filing__ext {
1163  color: var(--ink-dim);
1164  transform: translate(2px, -2px);
1165}
1166
1167/* ---------- per-ticker anomaly feed (Phase 16, redesigned) ----------
1168   Every row uses the same 4-column grid so the date, the glyph, the
1169   headline body, and the external-link icon land in the same x at
1170   every row. The previous flex layout let each column flow to its
1171   content width, which made dates of different widths (e.g. May 1 vs
1172   May 14) push the glyph and body around — defeating the "scan
1173   straight down" purpose of the feed.
1174
1175   A faint background tint on each row carries the event's polarity
1176   (good / bad / neutral) so a quick scan reveals the recent trend
1177   without reading any words. */
1178.anomalies {
1179  padding: var(--sp-2) var(--sp-3);
1180}
1181
1182.anomaly-list {
1183  margin: 0;
1184  padding: 0;
1185  list-style: none;
1186  display: grid;
1187  gap: 2px;
1188}
1189
1190.anomaly {
1191  border-radius: var(--radius-sm);
1192  border-left: 3px solid transparent;
1193}
1194
1195.anomaly--good {
1196  background: color-mix(in oklab, var(--up) 8%, transparent);
1197  border-left-color: color-mix(in oklab, var(--up) 65%, transparent);
1198}
1199.anomaly--bad {
1200  background: color-mix(in oklab, var(--down) 9%, transparent);
1201  border-left-color: color-mix(in oklab, var(--down) 65%, transparent);
1202}
1203.anomaly--neutral {
1204  background: var(--well);
1205  border-left-color: var(--rule-strong);
1206}
1207
1208.anomaly__link {
1209  display: grid;
1210  /* date | glyph | body | ext-icon — all fixed widths except body,
1211     so every row's glyph and body line up exactly under each other. */
1212  /* date col fits "Jul 25, 2025" (the shortdate filter adds the year for
1213     prior calendar years), glyph col fixed, body flexes, ext-icon fixed. */
1214  grid-template-columns: 6.25rem 1.5rem minmax(0, 1fr) 1.25rem;
1215  align-items: center;
1216  gap: var(--sp-3);
1217  padding: 9px var(--sp-3);
1218  color: var(--ink);
1219  text-decoration: none;
1220  transition: color 0.1s;
1221}
1222
1223a.anomaly__link:hover .anomaly__body {
1224  color: var(--ink-dim);
1225}
1226
1227.anomaly__date {
1228  @include mono;
1229  font-size: var(--fs-xs);
1230  color: var(--ink-faint);
1231  font-variant-numeric: tabular-nums;
1232}
1233
1234.anomaly__glyph {
1235  text-align: center;
1236  font-weight: 700;
1237  font-size: var(--fs-md);
1238  color: var(--ink-faint);
1239  line-height: 1;
1240}
1241
1242.anomaly__glyph--up        { color: var(--up); }
1243.anomaly__glyph--down      { color: var(--down); }
1244.anomaly__glyph--drawdown  { color: var(--down); }
1245.anomaly__glyph--fund-up   { color: var(--up); }
1246.anomaly__glyph--fund-down { color: var(--down); }
1247.anomaly__glyph--leader    { color: var(--ink-dim); }
1248
1249.anomaly__body {
1250  min-width: 0;
1251  font-size: var(--fs-sm);
1252  line-height: 1.4;
1253}
1254
1255/* ext icon slot is always rendered so the body column stays the same
1256   width whether or not a row has an outbound link. Empty span is fine. */
1257.anomaly__ext {
1258  display: flex;
1259  align-items: center;
1260  justify-content: center;
1261  color: var(--ink-faint);
1262}
1263.anomaly__ext .filing__ext {
1264  width: 14px;
1265  height: 14px;
1266}
1267
1268.anomaly-list__src {
1269  margin: var(--sp-3) 0 0;
1270  padding-top: var(--sp-2);
1271  border-top: 1px solid var(--rule);
1272  font-size: var(--fs-xs);
1273  color: var(--ink-faint);
1274  font-style: italic;
1275  line-height: 1.45;
1276}
1277
1278/* ---------- ETF fund profile ---------- */
1279.fund {
1280  padding: 16px 18px;
1281}
1282
1283.fund__stats {
1284  display: flex;
1285  flex-wrap: wrap;
1286  gap: 12px 36px;
1287}
1288
1289.fund__stat {
1290  display: flex;
1291  flex-direction: column;
1292  gap: 3px;
1293}
1294
1295.fund__stat-cap {
1296  @include eyebrow;
1297  font-size: 0.58rem;
1298}
1299
1300.fund__stat-val {
1301  font-size: 1.4rem;
1302  font-weight: 700;
1303}
1304
1305/* the physical-bullion note for a commodity-trust ETF (GLD, SLV) */
1306.fund__note {
1307  margin-top: 15px;
1308  padding-top: 15px;
1309  border-top: 1px solid var(--rule);
1310  color: var(--ink-dim);
1311  font-size: 0.86rem;
1312}
1313
1314/* asset-class mix: a thin segmented bar plus a compact legend. Segments are
1315   ink shades, not semantic green/amber/red — a fund's composition is not a
1316   good/ok/bad judgement (same reasoning as the chart indicator palette). */
1317.fund__mix {
1318  margin-top: 16px;
1319  padding-top: 16px;
1320  border-top: 1px solid var(--rule);
1321}
1322
1323.mixbar {
1324  display: flex;
1325  height: 10px;
1326  border-radius: 999px;
1327  overflow: hidden;
1328  background: var(--well);
1329}
1330
1331.mixbar__seg {
1332  height: 100%;
1333  border-right: 1px solid var(--surface);
1334}
1335
1336.mixbar__seg:last-child {
1337  border-right: none;
1338}
1339
1340.mixbar__seg:nth-child(1) {
1341  background: var(--ink);
1342}
1343
1344.mixbar__seg:nth-child(2) {
1345  background: var(--ink-dim);
1346}
1347
1348.mixbar__seg:nth-child(3) {
1349  background: var(--ink-faint);
1350}
1351
1352.mixbar__seg:nth-child(n + 4) {
1353  background: var(--rule-strong);
1354}
1355
1356.mixlegend {
1357  display: flex;
1358  flex-wrap: wrap;
1359  gap: 6px 22px;
1360  margin-top: 13px;
1361}
1362
1363.mixlegend__item {
1364  display: flex;
1365  align-items: baseline;
1366  gap: 9px;
1367}
1368
1369.mixlegend__item dt {
1370  display: flex;
1371  align-items: center;
1372  gap: 7px;
1373  font-size: 0.83rem;
1374  color: var(--ink-dim);
1375}
1376
1377.mixlegend__item dd {
1378  font-size: 0.83rem;
1379  font-weight: 600;
1380}
1381
1382.mixlegend__dot {
1383  width: 9px;
1384  height: 9px;
1385  border-radius: 2px;
1386  flex: none;
1387  background: var(--ink-faint);
1388}
1389
1390.mixlegend__item:nth-child(1) .mixlegend__dot {
1391  background: var(--ink);
1392}
1393
1394.mixlegend__item:nth-child(2) .mixlegend__dot {
1395  background: var(--ink-dim);
1396}
1397
1398.mixlegend__item:nth-child(3) .mixlegend__dot {
1399  background: var(--ink-faint);
1400}
1401
1402.mixlegend__item:nth-child(n + 4) .mixlegend__dot {
1403  background: var(--rule-strong);
1404}
1405
1406/* ---------- top holdings ---------- */
1407.holdings {
1408  padding: 2px var(--sp-4);
1409}
1410
1411/* Phase 31: 2-up on desktop so a 25-row holdings list doesn't reach off
1412   the page when the panel has the width to spare. The CSS columns layout
1413   keeps the existing single-list markup intact (no template change) and
1414   preserves reading order down-then-across. The break-inside avoids a
1415   row splitting across a column boundary. */
1416.hold-list {
1417  margin: 0;
1418  padding: 0;
1419  list-style: none;
1420}
1421
1422@media (min-width: $bp-md) {
1423  .hold-list {
1424    columns: 2;
1425    column-gap: var(--sp-6);
1426  }
1427}
1428
1429/* each row carries a faint left-anchored fill sized to the holding's weight,
1430   scaled so the largest holding shown fills the rail */
1431.hold {
1432  position: relative;
1433  display: grid;
1434  grid-template-columns: 2.6ch 1fr auto auto;
1435  align-items: baseline;
1436  gap: 2px var(--sp-3);
1437  padding: 9px 7px;
1438  border-top: 1px solid var(--rule);
1439  break-inside: avoid;
1440}
1441
1442.hold:first-child {
1443  border-top: none;
1444}
1445
1446/* with the 2-column layout the visual "first child" rule needs to apply
1447   to whichever row starts each column — easiest via CSS column-rule */
1448@media (min-width: $bp-md) {
1449  .hold-list {
1450    column-rule: 1px solid var(--rule);
1451  }
1452}
1453
1454.hold__fill {
1455  position: absolute;
1456  left: 0;
1457  top: 0;
1458  bottom: 0;
1459  z-index: 0;
1460  background: var(--well);
1461  border-radius: 3px;
1462}
1463
1464/* the row text rides above the weight fill */
1465.hold > :not(.hold__fill) {
1466  position: relative;
1467  z-index: 1;
1468}
1469
1470.hold__rank {
1471  font-size: 0.74rem;
1472  color: var(--ink-faint);
1473}
1474
1475.hold__name {
1476  min-width: 0;
1477  overflow: hidden;
1478  text-overflow: ellipsis;
1479  white-space: nowrap;
1480  font-size: 0.88rem;
1481  font-weight: 500;
1482}
1483
1484.hold__wt {
1485  font-size: 0.88rem;
1486  font-weight: 700;
1487}
1488
1489.hold__val {
1490  min-width: 6.5ch;
1491  text-align: right;
1492  font-size: 0.82rem;
1493  color: var(--ink-dim);
1494}
1495
1496/* ── Phase 28: ETF "About this fund" + returns + growth-of-$10k ────────── */
1497
1498/* About-this-fund panel: the headline ETF figures (expense / yield / NAV /
1499   inception / family / category) over the issuer's strategy paragraph. The
1500   stat strip mirrors .fund__stats so the two panels feel of a piece, with
1501   each value typed bigger than the eyebrow caption above it. */
1502.fund-about {
1503  padding: 16px 18px;
1504}
1505
1506.fund-about__stats {
1507  display: flex;
1508  flex-wrap: wrap;
1509  gap: 14px 36px;
1510}
1511
1512.fund-about__stat {
1513  display: flex;
1514  flex-direction: column;
1515  gap: 3px;
1516}
1517
1518.fund-about__cap {
1519  @include eyebrow;
1520  font-size: 0.58rem;
1521}
1522
1523.fund-about__val {
1524  font-size: 1.4rem;
1525  font-weight: 700;
1526  display: flex;
1527  flex-wrap: wrap;
1528  align-items: baseline;
1529  gap: 8px;
1530}
1531
1532/* The premium / discount caption that rides on the NAV cell — a small
1533   semantic-coloured pill that reads "+0.12% to price" in the right hue. */
1534.fund-about__sub {
1535  font-size: 0.72rem;
1536  font-weight: 600;
1537  padding: 2px 7px;
1538  border-radius: 999px;
1539  background: var(--well);
1540  color: var(--ink-dim);
1541
1542  &--good { color: var(--up); background: var(--up-soft); }
1543  &--ok   { color: var(--warn); background: var(--warn-soft); }
1544  &--bad  { color: var(--down); background: var(--down-soft); }
1545}
1546
1547.fund-about__summary {
1548  margin-top: 16px;
1549  padding-top: 16px;
1550  border-top: 1px solid var(--rule);
1551  color: var(--ink-dim);
1552  font-size: 0.88rem;
1553  line-height: 1.5;
1554}
1555
1556/* The fund-profile mix sections each got their own heading in Phase 28, so
1557   the Asset / Sector / Geography bars stack with a clear divider between. */
1558.fund__mix-label {
1559  @include eyebrow;
1560  font-size: 0.58rem;
1561  margin: 0 0 9px;
1562}
1563
1564/* Trailing-returns table (Phase 28). Tight, readable; the % column is the
1565   primary read, with annualised muted as a secondary cue. */
1566.ret-panel {
1567  padding: 12px 18px 14px;
1568}
1569
1570.ret-table {
1571  width: 100%;
1572  border-collapse: collapse;
1573}
1574
1575.ret-table th,
1576.ret-table td {
1577  text-align: left;
1578  padding: 8px 0;
1579  border-bottom: 1px solid var(--rule);
1580  font-size: 0.9rem;
1581}
1582
1583.ret-table th[scope="row"] {
1584  font-weight: 500;
1585  color: var(--ink-dim);
1586}
1587
1588.ret-table .num {
1589  text-align: right;
1590  font-weight: 600;
1591}
1592
1593.ret-table thead th {
1594  @include eyebrow;
1595  font-size: 0.58rem;
1596  color: var(--ink-faint);
1597  font-weight: 600;
1598  border-bottom: 1px solid var(--rule-strong);
1599}
1600
1601.ret-cell--up   { color: var(--up); }
1602.ret-cell--down { color: var(--down); }
1603.ret-cell--ann  { color: var(--ink-dim); font-weight: 500; }
1604
1605.ret-panel__src {
1606  margin-top: 11px;
1607  color: var(--ink-faint);
1608  font-size: 0.76rem;
1609  line-height: 1.5;
1610}
1611
1612/* Growth-of-$10k chart panel. Mirrors the price chart's panel framing, but
1613   shorter — the chart is a single-line area, not a multi-pane workspace. */
1614.growth-panel {
1615  padding: 16px 18px;
1616}
1617
1618#growth-chart {
1619  width: 100%;
1620  height: 240px;
1621}
1622
1623.growth-empty {
1624  color: var(--ink-dim);
1625  font-size: 0.88rem;
1626  padding: 28px 12px;
1627  text-align: center;
1628}
1629
1630.growth-panel__src {
1631  margin-top: 11px;
1632  color: var(--ink-faint);
1633  font-size: 0.76rem;
1634  line-height: 1.5;
1635}
1636
1637/* on a narrow phone the position value yields to the weight, the key figure */
1638@media (max-width: $bp-sm) {
1639  .hold {
1640    grid-template-columns: 2.6ch 1fr auto;
1641  }
1642
1643  .hold__val {
1644    display: none;
1645  }
1646}
1647
1648/* ---------- dividends (Phase 26) ---------- */
1649.div-panel {
1650  padding: 14px 16px;
1651}
1652
1653/* headline: inferred cadence over an on-track pill on the right */
1654.div-head {
1655  display: flex;
1656  align-items: center;
1657  justify-content: space-between;
1658  flex-wrap: wrap;
1659  gap: 8px 14px;
1660  margin-bottom: 14px;
1661}
1662
1663.div-cadence {
1664  font-size: 0.95rem;
1665  font-weight: 600;
1666  color: var(--ink);
1667}
1668
1669/* a 2- or 3-column row of prior-year / YTD / projected totals */
1670.div-pace {
1671  display: grid;
1672  grid-template-columns: 1fr;
1673  gap: 12px 18px;
1674  margin: 0 0 12px;
1675}
1676
1677@media (min-width: 520px) {
1678  .div-pace {
1679    grid-template-columns: repeat(3, 1fr);
1680  }
1681}
1682
1683.div-pace__item {
1684  display: flex;
1685  flex-direction: column;
1686  gap: 3px;
1687  padding: 10px 12px;
1688  border: 1px solid var(--rule);
1689  border-radius: 4px;
1690}
1691
1692.div-pace__cap {
1693  @include eyebrow;
1694  font-size: 0.6rem;
1695}
1696
1697.div-pace__val {
1698  font-size: 1.05rem;
1699  font-weight: 600;
1700  display: flex;
1701  align-items: baseline;
1702  flex-wrap: wrap;
1703  gap: 2px 8px;
1704}
1705
1706.div-pace__sub {
1707  @include eyebrow;
1708  font-size: 0.62rem;
1709  font-weight: 400;
1710}
1711
1712.div-pace__sub--good { color: var(--up); }
1713.div-pace__sub--bad  { color: var(--down); }
1714.div-pace__sub--ok   { color: var(--ink-dim); }
1715
1716.div-pace__src {
1717  margin: 0 0 16px;
1718  padding-top: 11px;
1719  border-top: 1px solid var(--rule);
1720  font-size: 0.74rem;
1721  color: var(--ink-faint);
1722}
1723
1724/* per-event history; mirrors the .roster row layout */
1725.div-hist__title {
1726  @include eyebrow;
1727  color: var(--ink);
1728  margin-bottom: 2px;
1729  padding-top: 14px;
1730  border-top: 1px solid var(--rule-strong);
1731}
1732
1733.div-hist {
1734  margin: 0;
1735  padding: 0;
1736  list-style: none;
1737}
1738
1739.div-row {
1740  display: flex;
1741  justify-content: space-between;
1742  align-items: baseline;
1743  gap: 16px;
1744  padding: 9px 0;
1745  border-top: 1px solid var(--rule);
1746}
1747
1748.div-row:first-child {
1749  border-top: none;
1750}
1751
1752.div-row__date {
1753  font-size: 0.85rem;
1754  color: var(--ink-dim);
1755}
1756
1757.div-row__amt {
1758  font-size: 0.95rem;
1759  font-weight: 600;
1760}
1761
1762/* ---------- desktop layering ---------- */
1763@media (min-width: $bp-md) {
1764  #chart {
1765    height: 460px;
1766  }
1767
1768  .sym-head__ticker {
1769    font-size: 2.2rem;
1770  }
1771
1772  .sym-head__price {
1773    font-size: 2.2rem;
1774  }
1775
1776  /* Desktop: the badge spans all three header rows so name + quote pull
1777     back to column 1; the badge then sits as a clean anchor on the right
1778     edge instead of crowding the ticker row. */
1779  .sym-head--has-health {
1780    .sym-head__name,
1781    .sym-head__quote {
1782      grid-column: 1;
1783    }
1784    .sym-head__health {
1785      display: block;
1786      grid-row: 1 / span 3;
1787      align-self: start;
1788    }
1789  }
1790
1791  /* Desktop carries the donut, so the flattened mobile verdict line hides. */
1792  .sym-verdict {
1793    display: none;
1794  }
1795
1796  .range-btn,
1797  .ind-btn {
1798    min-height: 34px;
1799  }
1800
1801  .fin__tab {
1802    min-height: 34px;
1803  }
1804}
1805
1806/* ---------- earnings (Phase 25) ---------- */
1807.earn {
1808  padding: 14px 16px;
1809}
1810
1811/* The two headline cells — most recent + next expected — stack on phone and
1812   sit side by side from ~520px, mirroring .div-pace. */
1813.earn__pair {
1814  display: grid;
1815  grid-template-columns: 1fr;
1816  gap: 12px 18px;
1817  margin: 0 0 12px;
1818}
1819
1820@media (min-width: 520px) {
1821  .earn__pair {
1822    grid-template-columns: 1fr 1fr;
1823  }
1824}
1825
1826.earn__cell {
1827  display: flex;
1828  flex-direction: column;
1829  gap: 3px;
1830  padding: 10px 12px;
1831  border: 1px solid var(--rule);
1832  border-radius: 4px;
1833}
1834
1835.earn__cap {
1836  @include eyebrow;
1837  font-size: 0.6rem;
1838}
1839
1840.earn__val {
1841  font-size: 1.05rem;
1842  font-weight: 600;
1843  display: flex;
1844  align-items: baseline;
1845  flex-wrap: wrap;
1846  gap: 2px 8px;
1847}
1848
1849.earn__sub {
1850  @include eyebrow;
1851  font-size: 0.62rem;
1852  font-weight: 400;
1853  color: var(--ink-faint);
1854}
1855
1856.earn__past-title {
1857  @include eyebrow;
1858  color: var(--ink);
1859  margin-bottom: 2px;
1860  padding-top: 14px;
1861  border-top: 1px solid var(--rule-strong);
1862}
1863
1864.earn__list {
1865  margin: 0;
1866  padding: 0;
1867  list-style: none;
1868}
1869
1870.earn-row {
1871  display: flex;
1872  justify-content: space-between;
1873  align-items: baseline;
1874  gap: 16px;
1875  padding: 9px 0;
1876  border-top: 1px solid var(--rule);
1877}
1878
1879.earn-row:first-child {
1880  border-top: none;
1881}
1882
1883.earn-row__date {
1884  font-size: 0.85rem;
1885  color: var(--ink-dim);
1886}
1887
1888.earn-row__ago {
1889  font-size: 0.78rem;
1890  color: var(--ink-faint);
1891}
1892
1893.earn__src {
1894  margin: 12px 0 0;
1895  padding-top: 11px;
1896  border-top: 1px solid var(--rule);
1897  font-size: 0.74rem;
1898  color: var(--ink-faint);
1899}
1900
1901/* ---------- on-demand refresh control (Phase B) ----------
1902   A button + live status + a thin progress bar that fills as the symbol's
1903   data is pulled. The fill is neutral ink, not semantic green: it is progress,
1904   not a "good" reading. The bar reserves its row height even when hidden so
1905   the header does not jump when a refresh starts. */
1906.refresh {
1907  display: flex;
1908  align-items: center;
1909  gap: var(--sp-3);
1910  margin: 0 0 var(--sp-3);
1911  min-height: 1.9rem;
1912}
1913
1914.refresh__btn {
1915  display: inline-flex;
1916  align-items: center;
1917  gap: 0.4em;
1918  padding: 0.3rem 0.7rem;
1919  font: 500 var(--fs-sm) / 1 inherit;
1920  color: var(--ink-dim);
1921  background: var(--surface);
1922  border: 1px solid var(--rule-strong);
1923  border-radius: var(--radius-sm);
1924  cursor: pointer;
1925  transition: color 0.12s, border-color 0.12s, background 0.12s;
1926
1927  svg {
1928    width: 1em;
1929    height: 1em;
1930    transition: transform 0.4s;
1931  }
1932
1933  &:hover:not(:disabled) {
1934    color: var(--ink);
1935    border-color: var(--ink);
1936  }
1937  &:disabled {
1938    cursor: default;
1939    opacity: 0.7;
1940  }
1941}
1942
1943/* Spin the icon while a refresh is in flight. */
1944.refresh.is-running .refresh__btn svg {
1945  animation: refresh-spin 0.9s linear infinite;
1946}
1947
1948@keyframes refresh-spin {
1949  to {
1950    transform: rotate(360deg);
1951  }
1952}
1953
1954.refresh__status {
1955  flex: 1 1 auto;
1956  min-width: 0;
1957  font-size: var(--fs-xs);
1958  color: var(--ink-faint);
1959  white-space: nowrap;
1960  overflow: hidden;
1961  text-overflow: ellipsis;
1962}
1963
1964.refresh__bar {
1965  flex: 0 0 30%;
1966  max-width: 12rem;
1967  height: 3px;
1968  background: var(--rule);
1969  border-radius: 999px;
1970  overflow: hidden;
1971}
1972
1973.refresh__fill {
1974  width: 0;
1975  height: 100%;
1976  background: var(--ink-dim);
1977  border-radius: inherit;
1978  transition: width 0.3s ease;
1979}
1980
1981@media (prefers-reduced-motion: reduce) {
1982  .refresh.is-running .refresh__btn svg {
1983    animation: none;
1984  }
1985  .refresh__fill {
1986    transition: none;
1987  }
1988}
1989
1990/* ---------- indicator read (under the chart) ----------
1991   A colour-coded read of the chart's indicators: an overall trend verdict, an
1992   RSI momentum gauge, and one signal tile per moving average. */
1993
1994/* Tone helper: each toned element exposes its semantic ink + soft wash as vars
1995   so the gauge / tiles can paint from one place. */
1996.ind-tone--up    { --tone: var(--up);   --tone-soft: var(--up-soft); }
1997.ind-tone--down  { --tone: var(--down); --tone-soft: var(--down-soft); }
1998.ind-tone--warn  { --tone: var(--warn); --tone-soft: var(--warn-soft); }
1999.ind-tone--steady { --tone: var(--ink-dim); --tone-soft: rgba(33, 31, 26, 0.07); }
2000
2001.ind-read {
2002  display: flex;
2003  flex-direction: column;
2004  gap: var(--sp-3);
2005}
2006.ind-read__head {
2007  display: flex;
2008  align-items: center;
2009  justify-content: space-between;
2010  flex-wrap: wrap;
2011  gap: var(--sp-1) var(--sp-3);
2012}
2013.ind-read__title {
2014  @include serif;
2015  font-size: var(--fs-lg);
2016  font-weight: 600;
2017}
2018
2019/* Overall verdict pill. */
2020.ind-verdict {
2021  display: inline-flex;
2022  align-items: center;
2023  gap: 8px;
2024  padding: 5px 12px 5px 10px;
2025  border-radius: 999px;
2026  font-weight: 700;
2027  font-size: var(--fs-sm);
2028  color: var(--tone);
2029  background: var(--tone-soft);
2030}
2031.ind-verdict--up { color: var(--up); background: var(--up-soft); }
2032.ind-verdict--down { color: var(--down); background: var(--down-soft); }
2033.ind-verdict--warn { color: var(--warn); background: var(--warn-soft); }
2034.ind-verdict--steady { color: var(--ink-dim); background: rgba(33, 31, 26, 0.07); }
2035.ind-verdict__dot {
2036  width: 8px;
2037  height: 8px;
2038  border-radius: 50%;
2039  background: currentColor;
2040}
2041.ind-verdict__note {
2042  font-weight: 500;
2043  font-size: var(--fs-xs);
2044  opacity: 0.85;
2045}
2046
2047/* RSI momentum gauge. */
2048.ind-rsi {
2049  border: 1px solid var(--rule);
2050  border-radius: var(--radius);
2051  background: var(--surface);
2052  padding: var(--sp-3);
2053  display: flex;
2054  flex-direction: column;
2055  gap: 4px;
2056}
2057.ind-rsi__top {
2058  display: flex;
2059  align-items: baseline;
2060  justify-content: space-between;
2061  gap: var(--sp-2);
2062}
2063.ind-rsi__label {
2064  @include eyebrow;
2065}
2066.ind-rsi__verdict {
2067  font-weight: 700;
2068  font-size: var(--fs-sm);
2069  color: var(--tone);
2070}
2071.ind-rsi__track {
2072  position: relative;
2073  height: 8px;
2074  margin-top: 26px;
2075  border-radius: 999px;
2076  background: var(--rule);
2077}
2078.ind-rsi__zone {
2079  position: absolute;
2080  top: 0;
2081  bottom: 0;
2082}
2083.ind-rsi__zone--under {
2084  left: 0;
2085  width: 30%;
2086  background: var(--up-soft);
2087  border-radius: 999px 0 0 999px;
2088}
2089.ind-rsi__zone--over {
2090  left: 70%;
2091  right: 0;
2092  background: var(--down-soft);
2093  border-radius: 0 999px 999px 0;
2094}
2095.ind-rsi__marker {
2096  position: absolute;
2097  top: -4px;
2098  bottom: -4px;
2099  width: 2px;
2100  background: var(--tone);
2101  border-radius: 1px;
2102}
2103.ind-rsi__bubble {
2104  position: absolute;
2105  bottom: 14px;
2106  left: 0;
2107  transform: translateX(-50%);
2108  padding: 2px 7px;
2109  border-radius: 7px;
2110  background: var(--tone);
2111  color: var(--surface);
2112  font-size: var(--fs-2xs);
2113  font-weight: 700;
2114}
2115.ind-rsi__scale {
2116  display: flex;
2117  justify-content: space-between;
2118  margin-top: 7px;
2119  font-size: var(--fs-2xs);
2120  color: var(--ink-faint);
2121}
2122.ind-rsi__note {
2123  margin-top: 4px;
2124  font-size: var(--fs-sm);
2125  color: var(--ink-dim);
2126  line-height: 1.4;
2127}
2128
2129/* Trend signal tiles. */
2130.ind-signals {
2131  display: grid;
2132  grid-template-columns: repeat(2, minmax(0, 1fr));
2133  gap: 10px;
2134}
2135@media (min-width: $bp-sm) {
2136  .ind-signals {
2137    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
2138  }
2139}
2140.ind-sig {
2141  border: 1px solid var(--rule);
2142  border-left: 3px solid var(--tone);
2143  border-radius: var(--radius-sm);
2144  background: var(--tone-soft);
2145  padding: 9px 11px;
2146  display: flex;
2147  flex-direction: column;
2148  gap: 3px;
2149}
2150.ind-sig__top {
2151  display: flex;
2152  align-items: baseline;
2153  justify-content: space-between;
2154  gap: 6px;
2155}
2156.ind-sig__label {
2157  @include eyebrow;
2158}
2159.ind-sig__value {
2160  @include mono;
2161  font-size: var(--fs-2xs);
2162  color: var(--ink-dim);
2163}
2164.ind-sig__status {
2165  @include mono;
2166  font-weight: 700;
2167  font-size: var(--fs-md);
2168  color: var(--tone);
2169  line-height: 1.1;
2170}
2171.ind-sig__note {
2172  font-size: var(--fs-2xs);
2173  color: var(--ink-dim);
2174}
2175
2176.ind-read__foot {
2177  font-size: var(--fs-xs);
2178  color: var(--ink-faint);
2179}