Single-binary self-hosted market watcher for stocks, ETFs, indexes, and futures: live charts, key stats, fundamentals, SEC filings, and SSE streaming.
axumdockerfinancerustself-hostedsqlitestocksvite
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}