45.6 KB
raw
@use "../../base/styles/variables" as *;
@use "../../base/styles/mixins" as *;
/* ---------- header ----------
Identity (ticker + tag chips), name, and live quote stack as three
rows. When a stock has a synthesised health read, a circular % badge
anchors the top-right and spans those three rows so id / name / quote
read as one column on the left and the badge sits alone on the right
at desktop. On phone it slides under the quote so the ticker keeps
the full width of the screen. The double-rule under the header echoes
the new .section-title ledger underline so the page reads as a single
accounting register. */
.sym-head {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--sp-3) var(--sp-5);
margin: var(--sp-1) 0 var(--sp-5);
padding-bottom: var(--sp-4);
border-bottom: 1px solid var(--rule-strong);
box-shadow: 0 3px 0 -2px var(--rule);
}
/* With a health badge, the header becomes a 2-column grid at every width:
left column flexes, right column is auto-sized to the badge. Stacking
on phone is handled inside .sym-head__health (it justifies left and
the badge shrinks). */
.sym-head--has-health {
grid-template-columns: minmax(0, 1fr) auto;
}
.sym-head__id {
display: flex;
align-items: center;
gap: var(--sp-3);
flex-wrap: wrap;
min-width: 0;
grid-column: 1;
}
.sym-head__ticker {
@include mono;
font-weight: 700;
font-size: var(--fs-2xl);
letter-spacing: -0.02em;
}
.sym-head__tag {
@include eyebrow;
font-size: var(--eye-chip);
color: var(--ink-dim);
padding: 3px 8px;
border: 1px solid var(--rule-strong);
border-radius: 4px;
}
// Phase 15: sector / industry tag, linked to its /industries page. Same shape
// as a plain `.sym-head__tag`, with a hover ramp that lifts to ink.
.sym-head__tag--link {
color: var(--ink-dim);
text-decoration: none;
transition: color 0.1s, border-color 0.1s, background 0.1s;
&:hover {
color: var(--ink);
border-color: var(--ink);
background: var(--well);
}
}
/* Name + quote span the full header width by default; the desktop rule
below pulls them back to column 1 when the health badge is present
(the badge then anchors the right column across all three rows). */
.sym-head__name {
grid-column: 1 / -1;
color: var(--ink-dim);
font-size: var(--fs-md);
margin-top: -2px;
}
.sym-head__quote {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 4px var(--sp-3);
grid-column: 1 / -1;
}
.sym-head__price {
@include mono;
font-size: var(--fs-2xl);
font-weight: 700;
letter-spacing: -0.01em;
}
.sym-head__chg {
@include mono;
font-size: var(--fs-md);
font-weight: 700;
}
.sym-head__asof {
font-size: var(--fs-xs);
color: var(--ink-faint);
letter-spacing: 0;
text-transform: none;
/* a hairline divider before the asof so the "At close · quoted Nov 14"
reads as a separate annotation, not part of the % change figure */
padding-left: var(--sp-3);
border-left: 1px solid var(--rule);
margin-left: 2px;
}
/* ---------- mobile health verdict line (Phase 6) ----------
The donut's read, flattened to a single "● Verdict · NN% · trajectory ↑"
row sitting directly under the price, where a phone reads it first. Hidden
at $bp-md, where the donut (below) is the desktop treatment. Coloured by the
overall grade; works for a stock (with a trajectory clause) or an ETF. */
.sym-verdict {
grid-column: 1 / -1;
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 4px var(--sp-3);
margin-top: -2px;
font-size: var(--fs-sm);
}
.sym-verdict__dot {
align-self: center;
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--ink-faint);
}
.sym-verdict--good .sym-verdict__dot { background: var(--up); }
.sym-verdict--ok .sym-verdict__dot { background: var(--warn); }
.sym-verdict--bad .sym-verdict__dot { background: var(--down); }
.sym-verdict__text {
@include eyebrow;
font-size: var(--fs-xs);
font-weight: 700;
color: var(--ink-dim);
}
.sym-verdict--good .sym-verdict__text { color: var(--up); }
.sym-verdict--ok .sym-verdict__text { color: var(--warn); }
.sym-verdict--bad .sym-verdict__text { color: var(--down); }
.sym-verdict__pct {
@include mono;
font-weight: 700;
color: var(--ink-dim);
}
/* trajectory clause: a quiet annotation, divided off like .sym-head__asof */
.sym-verdict__traj {
font-size: var(--fs-xs);
color: var(--ink-faint);
padding-left: var(--sp-3);
border-left: 1px solid var(--rule);
}
.sym-verdict__traj--good { color: var(--up); }
.sym-verdict__traj--bad { color: var(--down); }
.sym-verdict__arrow {
@include mono;
margin-right: 3px;
font-weight: 700;
}
/* ---------- header health badge (Phase 17 redesign) ----------
A circular % donut anchors the header's top-right. The previous
full-width Stock health section is gone; the three sub-readings
(fundamentals / trajectory / leadership) live in a popover that
shows on hover and on keyboard / touch focus.
The donut uses the well-known stroke-dasharray trick: r = 50/π so the
circle's circumference is exactly 100, and "stroke-dasharray: N, 100"
then fills exactly N% of the ring with no maths in the template. */
.sym-head__health {
grid-column: 2;
grid-row: 1;
align-self: center;
justify-self: end;
position: relative;
/* Phase 6: on phones the flattened .sym-verdict line carries the read; the
donut is the desktop treatment, switched back on at $bp-md below. */
display: none;
}
.health-badge {
appearance: none;
display: grid;
grid-template-columns: auto auto;
align-items: center;
gap: 0 var(--sp-3);
padding: 6px 14px 6px 6px;
background: var(--paper);
border: 1px solid var(--rule-strong);
border-radius: 999px;
color: var(--ink);
cursor: help;
font: inherit;
text-align: left;
transition: border-color 0.12s, box-shadow 0.12s, transform 0.12s;
&:hover,
&:focus-visible {
border-color: var(--ink);
box-shadow: 0 1px 0 0 var(--rule);
}
}
.health-badge__ring {
grid-row: 1 / span 2;
width: 52px;
height: 52px;
display: block;
/* rotate -90deg so the 0% mark sits at the top */
transform: rotate(-90deg);
}
.health-badge__track {
stroke: var(--rule);
stroke-width: 3.2;
}
.health-badge__fill {
stroke-width: 3.2;
stroke-linecap: round;
transition: stroke-dasharray 0.4s ease;
}
.health-badge--good .health-badge__fill { stroke: var(--up); }
.health-badge--ok .health-badge__fill { stroke: var(--warn); }
.health-badge--bad .health-badge__fill { stroke: var(--down); }
.health-badge--unknown .health-badge__fill { stroke: var(--ink-faint); }
.health-badge__num {
@include mono;
font-weight: 700;
font-size: var(--fs-lg);
line-height: 1;
}
.health-badge--good .health-badge__num { color: var(--up); }
.health-badge--ok .health-badge__num { color: var(--warn); }
.health-badge--bad .health-badge__num { color: var(--down); }
.health-badge__unit {
font-size: 0.65em;
margin-left: 1px;
color: var(--ink-faint);
font-weight: 600;
}
.health-badge__caption {
@include eyebrow;
font-size: var(--eye-chip);
color: var(--ink-dim);
letter-spacing: 0.06em;
}
/* Hover / focus popover. Pure CSS — opens on the badge's :hover, on
keyboard focus (:focus-visible bubbles through :focus-within), and
stays open while the pointer is on the popover itself so a user
moving the cursor in to read it does not flicker the popover shut. */
.health-pop {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 30;
width: min(320px, calc(100vw - 32px));
padding: var(--sp-3) var(--sp-3) var(--sp-2);
background: var(--paper);
border: 1px solid var(--rule-strong);
border-radius: var(--radius-sm);
box-shadow: 0 8px 24px -8px rgba(33, 31, 26, 0.18);
opacity: 0;
pointer-events: none;
transform: translateY(-4px);
transition: opacity 0.12s, transform 0.12s;
}
.sym-head__health:hover .health-pop,
.sym-head__health:focus-within .health-pop {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.health-pop__head {
margin: 0 0 var(--sp-2);
padding-bottom: var(--sp-2);
border-bottom: 1px solid var(--rule);
font-size: var(--fs-sm);
color: var(--ink-dim);
}
.health-pop__verdict--good { color: var(--up); }
.health-pop__verdict--ok { color: var(--warn); }
.health-pop__verdict--bad { color: var(--down); }
.health-pop__verdict--unknown { color: var(--ink-faint); }
.health-pop__rows {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 4px;
}
.health-pop__row {
display: grid;
grid-template-columns: 1fr auto;
align-items: baseline;
gap: var(--sp-3);
padding: 6px var(--sp-2);
border-left: 3px solid var(--rule);
background: var(--well);
border-radius: var(--radius-sm);
}
.health-pop__row--good { border-left-color: var(--up); }
.health-pop__row--ok { border-left-color: var(--warn); }
.health-pop__row--bad { border-left-color: var(--down); }
.health-pop__row--unknown { border-left-color: var(--ink-faint); }
.health-pop__label {
@include eyebrow;
color: var(--ink);
}
.health-pop__value {
@include mono;
font-weight: 700;
font-size: var(--fs-sm);
}
.health-pop__row--good .health-pop__value { color: var(--up); }
.health-pop__row--ok .health-pop__value { color: var(--warn); }
.health-pop__row--bad .health-pop__value { color: var(--down); }
.health-pop__row--unknown .health-pop__value { color: var(--ink-faint); }
.health-pop__src {
margin: var(--sp-2) 0 0;
font-size: var(--fs-2xs);
color: var(--ink-faint);
line-height: 1.45;
font-style: italic;
}
/* ---------- chart panel ---------- */
.chart-panel {
padding: var(--sp-3) var(--sp-3) var(--sp-2);
}
/* Phase 31: a stricter two-row toolbar. Row 1 is the time-range bar with
the range-summary chip on the right; row 2 is the indicator toggle row
on a quieter hairline. Splitting them stops the crammed-into-one-row
look that earlier crowded the chart-bar at narrow widths. */
.chart-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--sp-2) var(--sp-4);
margin-bottom: var(--sp-2);
}
.range-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--sp-1);
}
/* Phase 6: a hairline after 1W sets the intraday ranges (1D / 1W) apart from
the daily ones without a heavier divider element. */
.range-btn--group-end {
margin-right: var(--sp-2);
position: relative;
}
.range-btn--group-end::after {
content: "";
position: absolute;
right: calc(var(--sp-1) * -1);
top: 50%;
transform: translateY(-50%);
height: 60%;
border-right: 1px solid var(--rule-strong);
}
/* the range-change chip: % and absolute move over the whole visible range */
.range-summary {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 1px;
margin-left: auto;
line-height: 1.25;
text-align: right;
}
/* `display: flex` would override the [hidden] attribute, as on .chart-readout */
.range-summary[hidden] {
display: none;
}
.range-summary__chg {
font-size: 1rem;
font-weight: 700;
}
.range-summary[data-dir="up"] .range-summary__chg {
color: var(--up);
}
.range-summary[data-dir="down"] .range-summary__chg {
color: var(--down);
}
.range-summary__abs {
margin-left: 8px;
color: var(--ink-dim);
font-weight: 600;
}
.range-summary__cap {
@include eyebrow;
font-size: 0.56rem;
color: var(--ink-faint);
}
/* ---------- indicator toggle row ---------- */
.ind-bar {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 14px;
padding-top: 10px;
border-top: 1px solid var(--rule);
}
.ind-btn {
@include mono;
display: inline-flex;
align-items: center;
gap: 7px;
min-height: 44px;
padding: 0 12px;
border-radius: var(--radius-sm);
border: 1px solid var(--rule);
background: var(--surface);
color: var(--ink-faint);
font-size: 0.76rem;
font-weight: 500;
cursor: pointer;
transition: color 0.13s, border-color 0.13s;
}
/* `display: inline-flex` above would override the [hidden] attribute's
display:none, so re-assert it. Without this the benchmark toggle (hidden on
symbols with no benchmark) and the daily-only toggles (hidden on the 1D / 1W
intraday ranges, Phase 6) would still show. */
.ind-btn[hidden] {
display: none;
}
.ind-btn:hover {
color: var(--ink-dim);
border-color: var(--rule-strong);
}
.ind-btn.is-active {
color: var(--ink);
border-color: var(--ink);
}
/* the swatch echoes the indicator's line color on the chart (chart.js paints
it); dimmed while the indicator is toggled off */
.ind-btn__dot {
width: 18px;
height: 3px;
border-radius: 2px;
background: var(--ink-faint);
flex: none;
opacity: 0.4;
transition: opacity 0.13s;
}
.ind-btn.is-active .ind-btn__dot {
opacity: 1;
}
.range-btn {
@include mono;
min-height: 44px;
min-width: 52px;
padding: 0 13px;
border-radius: var(--radius-sm);
border: 1px solid var(--rule-strong);
background: var(--surface);
color: var(--ink-dim);
font-size: 0.78rem;
font-weight: 500;
cursor: pointer;
transition: color 0.13s, background 0.13s, border-color 0.13s;
}
.range-btn:hover {
color: var(--ink);
border-color: var(--ink);
}
.range-btn.is-active {
color: var(--paper);
background: var(--ink);
border-color: var(--ink);
font-weight: 700;
}
#chart {
position: relative;
width: 100%;
height: 360px;
transition: opacity 0.15s;
/* horizontal drags drive the measure tool; vertical still scrolls the page */
touch-action: pan-y;
}
#chart.is-loading {
opacity: 0.45;
}
/* ---------- chart measure tool ---------- */
/* Shaded band spanning a click-dragged interval; bottom inset clears the
time axis. Pointer-transparent so the drag stays bound to #chart. */
.chart-band {
position: absolute;
top: 0;
bottom: 26px;
z-index: 3;
background: rgba(33, 31, 26, 0.07);
border-left: 1px solid var(--rule-strong);
border-right: 1px solid var(--rule-strong);
pointer-events: none;
}
.chart-readout {
position: absolute;
top: 8px;
z-index: 4;
display: flex;
flex-direction: column;
gap: 1px;
padding: 5px 9px;
background: var(--surface);
border: 1px solid var(--rule-strong);
border-radius: var(--radius-sm);
box-shadow: var(--lift-hover);
white-space: nowrap;
pointer-events: none;
}
/* `display: flex` above would override the [hidden] attribute's display:none,
leaving the readout chip stuck on screen after a selection is cleared, so
re-assert it (same fix as `.health-banner[hidden]`). */
.chart-readout[hidden] {
display: none;
}
.chart-readout__pct {
@include mono;
font-size: 0.92rem;
font-weight: 700;
}
.chart-readout[data-dir="up"] .chart-readout__pct {
color: var(--up);
}
.chart-readout[data-dir="down"] .chart-readout__pct {
color: var(--down);
}
.chart-readout__sub {
@include mono;
font-size: 0.66rem;
color: var(--ink-faint);
}
/* ---------- key stats: gauges ---------- */
.keystats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
gap: 12px;
}
.gauge {
@include card;
padding: 16px 16px 8px;
}
.gauge__row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.gauge__label {
@include eyebrow;
color: var(--ink);
}
.gauge__meta {
font-size: 0.74rem;
color: var(--ink-faint);
text-align: right;
}
/* the rail: pips need vertical room above the track for their dot heads */
.gauge__track {
height: 8px;
margin: 24px 0 10px;
}
.gauge__ends {
display: flex;
justify-content: space-between;
gap: 8px;
font-size: 0.82rem;
color: var(--ink-dim);
}
.gauge__ends-r {
text-align: right;
}
.gauge__ends-c {
text-align: center;
}
.gauge__cap {
@include eyebrow;
font-size: 0.56rem;
margin-right: 3px;
}
/* ---------- gauge legend ---------- */
.legend {
margin-top: 12px;
}
.legend__item {
display: grid;
grid-template-columns: 1fr auto auto;
align-items: baseline;
gap: 10px;
padding: 8px 0;
border-top: 1px solid var(--rule);
}
.legend__item dt {
display: flex;
align-items: center;
gap: 7px;
font-size: 0.85rem;
color: var(--ink-dim);
}
.legend__item dd {
font-size: 0.9rem;
font-weight: 600;
}
.legend__delta {
font-size: 0.78rem;
font-weight: 600;
min-width: 4.5ch;
text-align: right;
}
/* legend dots echo the markers on the rail */
.legend__dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--ink-dim);
flex: none;
}
.legend__dot--ghost {
background: transparent;
border: 2px solid var(--ink-faint);
}
.legend__dot--up {
background: var(--up);
}
.legend__dot--down {
background: var(--down);
}
/* ---------- fundamentals: graded ratio cards ---------- */
.fund-basis {
margin: 2px 0 14px;
color: var(--ink-faint);
font-size: 0.82rem;
}
/* Stale-earnings caveat: the latest SEC quarter is old enough that the
price-based ratios may lag. Amber, sparing — a confidence flag, not an alarm. */
.fund-stale {
margin: -8px 0 16px;
padding-left: 10px;
border-left: 2px solid var(--warn);
color: var(--warn);
font-size: 0.82rem;
}
/* "no recent trading data" banner for a dormant / delisted / renamed symbol,
sitting above the header so it is read first (see STALE_DATA_DAYS). */
.stale-banner {
margin: 4px 0 16px;
padding: 10px 14px;
border: 1px solid var(--warn);
border-left-width: 3px;
border-radius: 4px;
background: var(--warn-soft);
color: var(--ink);
font-size: 0.86rem;
line-height: 1.45;
strong { color: var(--warn); }
}
/* the rolled-up strong / fair / weak standing badge with a one-line reading,
sitting above the per-ratio grid (Phase 20) */
.fund-standing {
display: flex;
align-items: flex-start;
gap: 12px;
margin: 2px 0 16px;
}
/* the badge reads larger here than the inline app-wide pill */
.fund-standing .vbadge {
font-size: 0.72rem;
padding: 6px 13px;
}
.fund-standing__text {
max-width: 70ch;
color: var(--ink-dim);
font-size: 0.86rem;
}
.ratios {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(248px, 1fr));
gap: var(--sp-3);
}
.ratio {
@include card;
display: flex;
flex-direction: column;
padding: var(--sp-3) var(--sp-4) var(--sp-3);
}
.ratio__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-2);
}
.ratio__label {
@include eyebrow;
color: var(--ink);
}
/* the verdict badge: a quiet pill that carries the semantic color */
.ratio__badge {
@include eyebrow;
flex: none;
padding: 2px 8px;
border-radius: 999px;
font-size: var(--eye-chip);
}
.ratio__badge--good { color: var(--up); background: var(--up-soft); }
.ratio__badge--ok { color: var(--warn); background: var(--warn-soft); }
.ratio__badge--bad { color: var(--down); background: var(--down-soft); }
.ratio__badge--unknown { color: var(--ink-faint); background: var(--well); }
/* the figure itself, colored to match the grade */
.ratio__value {
@include mono;
font-size: var(--fs-xl);
font-weight: 700;
letter-spacing: -0.02em;
margin: var(--sp-2) 0 2px;
line-height: 1.05;
}
.ratio--good .ratio__value { color: var(--up); }
.ratio--ok .ratio__value { color: var(--warn); }
.ratio--bad .ratio__value { color: var(--down); }
.ratio--unknown .ratio__value { color: var(--ink-faint); }
.ratio__reading {
font-size: var(--fs-xs);
color: var(--ink-dim);
line-height: 1.45;
}
/* the static "how to read it" note, set off below a hairline. Phase 31
muted it further (smaller + tighter) — 9 ratios × the old explainer
block dominated the page; the value + verdict + reading carry the
primary signal and the explainer reads as a footnote. */
.ratio__explain {
margin-top: var(--sp-2);
padding-top: var(--sp-2);
border-top: 1px solid var(--rule);
font-size: var(--fs-2xs);
color: var(--ink-faint);
line-height: 1.4;
}
/* shown while the SEC sweep has not yet reached this symbol */
.fund-pending {
@include card;
padding: 18px 20px;
color: var(--ink-dim);
font-size: 0.88rem;
}
.fund-pending a {
text-decoration: underline;
text-underline-offset: 2px;
}
/* ---------- financials table + period toggle ---------- */
.fin {
padding: 14px 16px;
}
.fin__toggle {
display: flex;
gap: 6px;
margin-bottom: 14px;
}
.fin__tab {
@include mono;
min-height: 40px;
padding: 0 15px;
border-radius: var(--radius-sm);
border: 1px solid var(--rule-strong);
background: var(--surface);
color: var(--ink-dim);
font-size: 0.78rem;
font-weight: 500;
cursor: pointer;
transition: color 0.13s, background 0.13s, border-color 0.13s;
}
.fin__tab:hover {
color: var(--ink);
border-color: var(--ink);
}
.fin__tab.is-active {
color: var(--paper);
background: var(--ink);
border-color: var(--ink);
font-weight: 700;
}
/* wide tables scroll within the card rather than the page on a phone */
.fin__scroll {
overflow-x: auto;
}
.fin__table {
width: 100%;
border-collapse: collapse;
white-space: nowrap;
}
.fin__table th,
.fin__table td {
padding: 9px 10px;
text-align: right;
font-size: 0.85rem;
}
.fin__table th:first-child,
.fin__table td:first-child {
padding-left: 0;
}
.fin__table th:last-child,
.fin__table td:last-child {
padding-right: 0;
}
.fin__table thead th {
@include eyebrow;
font-size: 0.58rem;
color: var(--ink-faint);
border-bottom: 1px solid var(--rule-strong);
}
.fin__table tbody tr {
border-top: 1px solid var(--rule);
}
.fin__table tbody tr:first-child {
border-top: none;
}
.fin__table tbody th {
text-align: left;
font-weight: 500;
color: var(--ink-dim);
}
.fin__table tbody td {
font-weight: 600;
}
.fin__empty {
padding: 8px 0;
color: var(--ink-dim);
font-size: 0.86rem;
}
/* period-over-period growth cue (Phase 24): a small arrow on every changed
figure, plus a semantic colour where a rise has a clear good/bad reading */
.fin__table td.fin-cell--good {
color: var(--up);
}
.fin__table td.fin-cell--bad {
color: var(--down);
}
.fin-cell__dir {
margin-left: 4px;
font-size: 0.62rem;
vertical-align: 0.06em;
/* faint by default; a graded cell re-colours it to match the figure */
color: var(--ink-faint);
}
.fin__table td.fin-cell--good .fin-cell__dir,
.fin__table td.fin-cell--bad .fin-cell__dir {
color: inherit;
}
.fin-cell__dir--up::before {
content: "\25B4";
}
.fin-cell__dir--down::before {
content: "\25BE";
}
/* ---------- recent SEC filings ----------
The .filing / .lead-change / .anomaly rows all share the shape
"left-anchored tag · body · meta · external icon". The styles here
are the canonical version; .lead-change and .anomaly only override
what differs from a filing row. */
.filings {
padding: 2px var(--sp-4);
}
.filing-list {
margin: 0;
padding: 0;
list-style: none;
}
.filing {
border-top: 1px solid var(--rule);
}
.filing:first-child {
border-top: none;
}
.filing__link {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: 10px 0;
color: var(--ink);
transition: color 0.1s;
}
.filing__link:hover {
color: var(--ink-dim);
}
.filing__form {
@include mono;
flex: none;
min-width: 7ch;
text-align: center;
padding: 4px 7px;
border: 1px solid var(--rule-strong);
border-radius: 4px;
font-size: var(--fs-2xs);
font-weight: 700;
letter-spacing: 0.02em;
color: var(--ink);
}
.filing__body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 1px;
}
.filing__desc {
font-size: var(--fs-sm);
font-weight: 500;
}
.filing__meta {
font-size: var(--fs-xs);
color: var(--ink-faint);
}
.filing__ext {
flex: none;
width: 14px;
height: 14px;
color: var(--ink-faint);
transition: color 0.1s, transform 0.1s;
}
.filing__link:hover .filing__ext {
color: var(--ink-dim);
transform: translate(2px, -2px);
}
/* ---------- leadership: roster + change feed ---------- */
.leadership {
padding: var(--sp-3) var(--sp-4);
}
.roster {
margin: 0;
padding: 0;
list-style: none;
}
.roster__row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: 2px var(--sp-4);
padding: var(--sp-2) 0;
border-top: 1px solid var(--rule);
}
.roster__row:first-child {
border-top: none;
}
.roster__name {
font-size: var(--fs-md);
font-weight: 600;
}
.roster__role {
font-size: var(--fs-xs);
color: var(--ink-dim);
text-align: right;
}
/* the provenance note, set off below a hairline like .ratio__explain */
.roster__src {
margin-top: var(--sp-3);
padding-top: var(--sp-3);
border-top: 1px solid var(--rule);
font-size: var(--fs-xs);
color: var(--ink-faint);
line-height: 1.45;
}
/* recent officer/director changes, from 8-K item 5.02 */
.lead-changes {
margin-top: var(--sp-4);
padding-top: var(--sp-3);
border-top: 1px solid var(--rule-strong);
}
.lead-changes__title {
font-family: var(--font-serif);
font-weight: 600;
font-size: var(--fs-md);
letter-spacing: -0.01em;
color: var(--ink);
margin-bottom: var(--sp-1);
}
.lead-changes__list {
margin: 0;
padding: 0;
list-style: none;
}
.lead-change {
border-top: 1px solid var(--rule);
}
.lead-change:first-child {
border-top: none;
}
.lead-change__link {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: 10px 0;
color: var(--ink);
transition: color 0.1s;
}
.lead-change__link:hover {
color: var(--ink-dim);
}
.lead-change__body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 1px;
}
.lead-change__desc {
font-size: var(--fs-sm);
font-weight: 500;
}
.lead-change__meta {
font-size: var(--fs-xs);
color: var(--ink-faint);
}
.lead-change__link:hover .filing__ext {
color: var(--ink-dim);
transform: translate(2px, -2px);
}
/* ---------- per-ticker anomaly feed (Phase 16, redesigned) ----------
Every row uses the same 4-column grid so the date, the glyph, the
headline body, and the external-link icon land in the same x at
every row. The previous flex layout let each column flow to its
content width, which made dates of different widths (e.g. May 1 vs
May 14) push the glyph and body around — defeating the "scan
straight down" purpose of the feed.
A faint background tint on each row carries the event's polarity
(good / bad / neutral) so a quick scan reveals the recent trend
without reading any words. */
.anomalies {
padding: var(--sp-2) var(--sp-3);
}
.anomaly-list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 2px;
}
.anomaly {
border-radius: var(--radius-sm);
border-left: 3px solid transparent;
}
.anomaly--good {
background: color-mix(in oklab, var(--up) 8%, transparent);
border-left-color: color-mix(in oklab, var(--up) 65%, transparent);
}
.anomaly--bad {
background: color-mix(in oklab, var(--down) 9%, transparent);
border-left-color: color-mix(in oklab, var(--down) 65%, transparent);
}
.anomaly--neutral {
background: var(--well);
border-left-color: var(--rule-strong);
}
.anomaly__link {
display: grid;
/* date | glyph | body | ext-icon — all fixed widths except body,
so every row's glyph and body line up exactly under each other. */
/* date col fits "Jul 25, 2025" (the shortdate filter adds the year for
prior calendar years), glyph col fixed, body flexes, ext-icon fixed. */
grid-template-columns: 6.25rem 1.5rem minmax(0, 1fr) 1.25rem;
align-items: center;
gap: var(--sp-3);
padding: 9px var(--sp-3);
color: var(--ink);
text-decoration: none;
transition: color 0.1s;
}
a.anomaly__link:hover .anomaly__body {
color: var(--ink-dim);
}
.anomaly__date {
@include mono;
font-size: var(--fs-xs);
color: var(--ink-faint);
font-variant-numeric: tabular-nums;
}
.anomaly__glyph {
text-align: center;
font-weight: 700;
font-size: var(--fs-md);
color: var(--ink-faint);
line-height: 1;
}
.anomaly__glyph--up { color: var(--up); }
.anomaly__glyph--down { color: var(--down); }
.anomaly__glyph--drawdown { color: var(--down); }
.anomaly__glyph--fund-up { color: var(--up); }
.anomaly__glyph--fund-down { color: var(--down); }
.anomaly__glyph--leader { color: var(--ink-dim); }
.anomaly__body {
min-width: 0;
font-size: var(--fs-sm);
line-height: 1.4;
}
/* ext icon slot is always rendered so the body column stays the same
width whether or not a row has an outbound link. Empty span is fine. */
.anomaly__ext {
display: flex;
align-items: center;
justify-content: center;
color: var(--ink-faint);
}
.anomaly__ext .filing__ext {
width: 14px;
height: 14px;
}
.anomaly-list__src {
margin: var(--sp-3) 0 0;
padding-top: var(--sp-2);
border-top: 1px solid var(--rule);
font-size: var(--fs-xs);
color: var(--ink-faint);
font-style: italic;
line-height: 1.45;
}
/* ---------- ETF fund profile ---------- */
.fund {
padding: 16px 18px;
}
.fund__stats {
display: flex;
flex-wrap: wrap;
gap: 12px 36px;
}
.fund__stat {
display: flex;
flex-direction: column;
gap: 3px;
}
.fund__stat-cap {
@include eyebrow;
font-size: 0.58rem;
}
.fund__stat-val {
font-size: 1.4rem;
font-weight: 700;
}
/* the physical-bullion note for a commodity-trust ETF (GLD, SLV) */
.fund__note {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid var(--rule);
color: var(--ink-dim);
font-size: 0.86rem;
}
/* asset-class mix: a thin segmented bar plus a compact legend. Segments are
ink shades, not semantic green/amber/red — a fund's composition is not a
good/ok/bad judgement (same reasoning as the chart indicator palette). */
.fund__mix {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--rule);
}
.mixbar {
display: flex;
height: 10px;
border-radius: 999px;
overflow: hidden;
background: var(--well);
}
.mixbar__seg {
height: 100%;
border-right: 1px solid var(--surface);
}
.mixbar__seg:last-child {
border-right: none;
}
.mixbar__seg:nth-child(1) {
background: var(--ink);
}
.mixbar__seg:nth-child(2) {
background: var(--ink-dim);
}
.mixbar__seg:nth-child(3) {
background: var(--ink-faint);
}
.mixbar__seg:nth-child(n + 4) {
background: var(--rule-strong);
}
.mixlegend {
display: flex;
flex-wrap: wrap;
gap: 6px 22px;
margin-top: 13px;
}
.mixlegend__item {
display: flex;
align-items: baseline;
gap: 9px;
}
.mixlegend__item dt {
display: flex;
align-items: center;
gap: 7px;
font-size: 0.83rem;
color: var(--ink-dim);
}
.mixlegend__item dd {
font-size: 0.83rem;
font-weight: 600;
}
.mixlegend__dot {
width: 9px;
height: 9px;
border-radius: 2px;
flex: none;
background: var(--ink-faint);
}
.mixlegend__item:nth-child(1) .mixlegend__dot {
background: var(--ink);
}
.mixlegend__item:nth-child(2) .mixlegend__dot {
background: var(--ink-dim);
}
.mixlegend__item:nth-child(3) .mixlegend__dot {
background: var(--ink-faint);
}
.mixlegend__item:nth-child(n + 4) .mixlegend__dot {
background: var(--rule-strong);
}
/* ---------- top holdings ---------- */
.holdings {
padding: 2px var(--sp-4);
}
/* Phase 31: 2-up on desktop so a 25-row holdings list doesn't reach off
the page when the panel has the width to spare. The CSS columns layout
keeps the existing single-list markup intact (no template change) and
preserves reading order down-then-across. The break-inside avoids a
row splitting across a column boundary. */
.hold-list {
margin: 0;
padding: 0;
list-style: none;
}
@media (min-width: $bp-md) {
.hold-list {
columns: 2;
column-gap: var(--sp-6);
}
}
/* each row carries a faint left-anchored fill sized to the holding's weight,
scaled so the largest holding shown fills the rail */
.hold {
position: relative;
display: grid;
grid-template-columns: 2.6ch 1fr auto auto;
align-items: baseline;
gap: 2px var(--sp-3);
padding: 9px 7px;
border-top: 1px solid var(--rule);
break-inside: avoid;
}
.hold:first-child {
border-top: none;
}
/* with the 2-column layout the visual "first child" rule needs to apply
to whichever row starts each column — easiest via CSS column-rule */
@media (min-width: $bp-md) {
.hold-list {
column-rule: 1px solid var(--rule);
}
}
.hold__fill {
position: absolute;
left: 0;
top: 0;
bottom: 0;
z-index: 0;
background: var(--well);
border-radius: 3px;
}
/* the row text rides above the weight fill */
.hold > :not(.hold__fill) {
position: relative;
z-index: 1;
}
.hold__rank {
font-size: 0.74rem;
color: var(--ink-faint);
}
.hold__name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.88rem;
font-weight: 500;
}
.hold__wt {
font-size: 0.88rem;
font-weight: 700;
}
.hold__val {
min-width: 6.5ch;
text-align: right;
font-size: 0.82rem;
color: var(--ink-dim);
}
/* ── Phase 28: ETF "About this fund" + returns + growth-of-$10k ────────── */
/* About-this-fund panel: the headline ETF figures (expense / yield / NAV /
inception / family / category) over the issuer's strategy paragraph. The
stat strip mirrors .fund__stats so the two panels feel of a piece, with
each value typed bigger than the eyebrow caption above it. */
.fund-about {
padding: 16px 18px;
}
.fund-about__stats {
display: flex;
flex-wrap: wrap;
gap: 14px 36px;
}
.fund-about__stat {
display: flex;
flex-direction: column;
gap: 3px;
}
.fund-about__cap {
@include eyebrow;
font-size: 0.58rem;
}
.fund-about__val {
font-size: 1.4rem;
font-weight: 700;
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 8px;
}
/* The premium / discount caption that rides on the NAV cell — a small
semantic-coloured pill that reads "+0.12% to price" in the right hue. */
.fund-about__sub {
font-size: 0.72rem;
font-weight: 600;
padding: 2px 7px;
border-radius: 999px;
background: var(--well);
color: var(--ink-dim);
&--good { color: var(--up); background: var(--up-soft); }
&--ok { color: var(--warn); background: var(--warn-soft); }
&--bad { color: var(--down); background: var(--down-soft); }
}
.fund-about__summary {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--rule);
color: var(--ink-dim);
font-size: 0.88rem;
line-height: 1.5;
}
/* The fund-profile mix sections each got their own heading in Phase 28, so
the Asset / Sector / Geography bars stack with a clear divider between. */
.fund__mix-label {
@include eyebrow;
font-size: 0.58rem;
margin: 0 0 9px;
}
/* Trailing-returns table (Phase 28). Tight, readable; the % column is the
primary read, with annualised muted as a secondary cue. */
.ret-panel {
padding: 12px 18px 14px;
}
.ret-table {
width: 100%;
border-collapse: collapse;
}
.ret-table th,
.ret-table td {
text-align: left;
padding: 8px 0;
border-bottom: 1px solid var(--rule);
font-size: 0.9rem;
}
.ret-table th[scope="row"] {
font-weight: 500;
color: var(--ink-dim);
}
.ret-table .num {
text-align: right;
font-weight: 600;
}
.ret-table thead th {
@include eyebrow;
font-size: 0.58rem;
color: var(--ink-faint);
font-weight: 600;
border-bottom: 1px solid var(--rule-strong);
}
.ret-cell--up { color: var(--up); }
.ret-cell--down { color: var(--down); }
.ret-cell--ann { color: var(--ink-dim); font-weight: 500; }
.ret-panel__src {
margin-top: 11px;
color: var(--ink-faint);
font-size: 0.76rem;
line-height: 1.5;
}
/* Growth-of-$10k chart panel. Mirrors the price chart's panel framing, but
shorter — the chart is a single-line area, not a multi-pane workspace. */
.growth-panel {
padding: 16px 18px;
}
#growth-chart {
width: 100%;
height: 240px;
}
.growth-empty {
color: var(--ink-dim);
font-size: 0.88rem;
padding: 28px 12px;
text-align: center;
}
.growth-panel__src {
margin-top: 11px;
color: var(--ink-faint);
font-size: 0.76rem;
line-height: 1.5;
}
/* on a narrow phone the position value yields to the weight, the key figure */
@media (max-width: $bp-sm) {
.hold {
grid-template-columns: 2.6ch 1fr auto;
}
.hold__val {
display: none;
}
}
/* ---------- dividends (Phase 26) ---------- */
.div-panel {
padding: 14px 16px;
}
/* headline: inferred cadence over an on-track pill on the right */
.div-head {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px 14px;
margin-bottom: 14px;
}
.div-cadence {
font-size: 0.95rem;
font-weight: 600;
color: var(--ink);
}
/* a 2- or 3-column row of prior-year / YTD / projected totals */
.div-pace {
display: grid;
grid-template-columns: 1fr;
gap: 12px 18px;
margin: 0 0 12px;
}
@media (min-width: 520px) {
.div-pace {
grid-template-columns: repeat(3, 1fr);
}
}
.div-pace__item {
display: flex;
flex-direction: column;
gap: 3px;
padding: 10px 12px;
border: 1px solid var(--rule);
border-radius: 4px;
}
.div-pace__cap {
@include eyebrow;
font-size: 0.6rem;
}
.div-pace__val {
font-size: 1.05rem;
font-weight: 600;
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 2px 8px;
}
.div-pace__sub {
@include eyebrow;
font-size: 0.62rem;
font-weight: 400;
}
.div-pace__sub--good { color: var(--up); }
.div-pace__sub--bad { color: var(--down); }
.div-pace__sub--ok { color: var(--ink-dim); }
.div-pace__src {
margin: 0 0 16px;
padding-top: 11px;
border-top: 1px solid var(--rule);
font-size: 0.74rem;
color: var(--ink-faint);
}
/* per-event history; mirrors the .roster row layout */
.div-hist__title {
@include eyebrow;
color: var(--ink);
margin-bottom: 2px;
padding-top: 14px;
border-top: 1px solid var(--rule-strong);
}
.div-hist {
margin: 0;
padding: 0;
list-style: none;
}
.div-row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 16px;
padding: 9px 0;
border-top: 1px solid var(--rule);
}
.div-row:first-child {
border-top: none;
}
.div-row__date {
font-size: 0.85rem;
color: var(--ink-dim);
}
.div-row__amt {
font-size: 0.95rem;
font-weight: 600;
}
/* ---------- desktop layering ---------- */
@media (min-width: $bp-md) {
#chart {
height: 460px;
}
.sym-head__ticker {
font-size: 2.2rem;
}
.sym-head__price {
font-size: 2.2rem;
}
/* Desktop: the badge spans all three header rows so name + quote pull
back to column 1; the badge then sits as a clean anchor on the right
edge instead of crowding the ticker row. */
.sym-head--has-health {
.sym-head__name,
.sym-head__quote {
grid-column: 1;
}
.sym-head__health {
display: block;
grid-row: 1 / span 3;
align-self: start;
}
}
/* Desktop carries the donut, so the flattened mobile verdict line hides. */
.sym-verdict {
display: none;
}
.range-btn,
.ind-btn {
min-height: 34px;
}
.fin__tab {
min-height: 34px;
}
}
/* ---------- earnings (Phase 25) ---------- */
.earn {
padding: 14px 16px;
}
/* The two headline cells — most recent + next expected — stack on phone and
sit side by side from ~520px, mirroring .div-pace. */
.earn__pair {
display: grid;
grid-template-columns: 1fr;
gap: 12px 18px;
margin: 0 0 12px;
}
@media (min-width: 520px) {
.earn__pair {
grid-template-columns: 1fr 1fr;
}
}
.earn__cell {
display: flex;
flex-direction: column;
gap: 3px;
padding: 10px 12px;
border: 1px solid var(--rule);
border-radius: 4px;
}
.earn__cap {
@include eyebrow;
font-size: 0.6rem;
}
.earn__val {
font-size: 1.05rem;
font-weight: 600;
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 2px 8px;
}
.earn__sub {
@include eyebrow;
font-size: 0.62rem;
font-weight: 400;
color: var(--ink-faint);
}
.earn__past-title {
@include eyebrow;
color: var(--ink);
margin-bottom: 2px;
padding-top: 14px;
border-top: 1px solid var(--rule-strong);
}
.earn__list {
margin: 0;
padding: 0;
list-style: none;
}
.earn-row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 16px;
padding: 9px 0;
border-top: 1px solid var(--rule);
}
.earn-row:first-child {
border-top: none;
}
.earn-row__date {
font-size: 0.85rem;
color: var(--ink-dim);
}
.earn-row__ago {
font-size: 0.78rem;
color: var(--ink-faint);
}
.earn__src {
margin: 12px 0 0;
padding-top: 11px;
border-top: 1px solid var(--rule);
font-size: 0.74rem;
color: var(--ink-faint);
}
/* ---------- on-demand refresh control (Phase B) ----------
A button + live status + a thin progress bar that fills as the symbol's
data is pulled. The fill is neutral ink, not semantic green: it is progress,
not a "good" reading. The bar reserves its row height even when hidden so
the header does not jump when a refresh starts. */
.refresh {
display: flex;
align-items: center;
gap: var(--sp-3);
margin: 0 0 var(--sp-3);
min-height: 1.9rem;
}
.refresh__btn {
display: inline-flex;
align-items: center;
gap: 0.4em;
padding: 0.3rem 0.7rem;
font: 500 var(--fs-sm) / 1 inherit;
color: var(--ink-dim);
background: var(--surface);
border: 1px solid var(--rule-strong);
border-radius: var(--radius-sm);
cursor: pointer;
transition: color 0.12s, border-color 0.12s, background 0.12s;
svg {
width: 1em;
height: 1em;
transition: transform 0.4s;
}
&:hover:not(:disabled) {
color: var(--ink);
border-color: var(--ink);
}
&:disabled {
cursor: default;
opacity: 0.7;
}
}
/* Spin the icon while a refresh is in flight. */
.refresh.is-running .refresh__btn svg {
animation: refresh-spin 0.9s linear infinite;
}
@keyframes refresh-spin {
to {
transform: rotate(360deg);
}
}
.refresh__status {
flex: 1 1 auto;
min-width: 0;
font-size: var(--fs-xs);
color: var(--ink-faint);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.refresh__bar {
flex: 0 0 30%;
max-width: 12rem;
height: 3px;
background: var(--rule);
border-radius: 999px;
overflow: hidden;
}
.refresh__fill {
width: 0;
height: 100%;
background: var(--ink-dim);
border-radius: inherit;
transition: width 0.3s ease;
}
@media (prefers-reduced-motion: reduce) {
.refresh.is-running .refresh__btn svg {
animation: none;
}
.refresh__fill {
transition: none;
}
}
/* ---------- indicator read (under the chart) ----------
A colour-coded read of the chart's indicators: an overall trend verdict, an
RSI momentum gauge, and one signal tile per moving average. */
/* Tone helper: each toned element exposes its semantic ink + soft wash as vars
so the gauge / tiles can paint from one place. */
.ind-tone--up { --tone: var(--up); --tone-soft: var(--up-soft); }
.ind-tone--down { --tone: var(--down); --tone-soft: var(--down-soft); }
.ind-tone--warn { --tone: var(--warn); --tone-soft: var(--warn-soft); }
.ind-tone--steady { --tone: var(--ink-dim); --tone-soft: rgba(33, 31, 26, 0.07); }
.ind-read {
display: flex;
flex-direction: column;
gap: var(--sp-3);
}
.ind-read__head {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--sp-1) var(--sp-3);
}
.ind-read__title {
@include serif;
font-size: var(--fs-lg);
font-weight: 600;
}
/* Overall verdict pill. */
.ind-verdict {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 5px 12px 5px 10px;
border-radius: 999px;
font-weight: 700;
font-size: var(--fs-sm);
color: var(--tone);
background: var(--tone-soft);
}
.ind-verdict--up { color: var(--up); background: var(--up-soft); }
.ind-verdict--down { color: var(--down); background: var(--down-soft); }
.ind-verdict--warn { color: var(--warn); background: var(--warn-soft); }
.ind-verdict--steady { color: var(--ink-dim); background: rgba(33, 31, 26, 0.07); }
.ind-verdict__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.ind-verdict__note {
font-weight: 500;
font-size: var(--fs-xs);
opacity: 0.85;
}
/* RSI momentum gauge. */
.ind-rsi {
border: 1px solid var(--rule);
border-radius: var(--radius);
background: var(--surface);
padding: var(--sp-3);
display: flex;
flex-direction: column;
gap: 4px;
}
.ind-rsi__top {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--sp-2);
}
.ind-rsi__label {
@include eyebrow;
}
.ind-rsi__verdict {
font-weight: 700;
font-size: var(--fs-sm);
color: var(--tone);
}
.ind-rsi__track {
position: relative;
height: 8px;
margin-top: 26px;
border-radius: 999px;
background: var(--rule);
}
.ind-rsi__zone {
position: absolute;
top: 0;
bottom: 0;
}
.ind-rsi__zone--under {
left: 0;
width: 30%;
background: var(--up-soft);
border-radius: 999px 0 0 999px;
}
.ind-rsi__zone--over {
left: 70%;
right: 0;
background: var(--down-soft);
border-radius: 0 999px 999px 0;
}
.ind-rsi__marker {
position: absolute;
top: -4px;
bottom: -4px;
width: 2px;
background: var(--tone);
border-radius: 1px;
}
.ind-rsi__bubble {
position: absolute;
bottom: 14px;
left: 0;
transform: translateX(-50%);
padding: 2px 7px;
border-radius: 7px;
background: var(--tone);
color: var(--surface);
font-size: var(--fs-2xs);
font-weight: 700;
}
.ind-rsi__scale {
display: flex;
justify-content: space-between;
margin-top: 7px;
font-size: var(--fs-2xs);
color: var(--ink-faint);
}
.ind-rsi__note {
margin-top: 4px;
font-size: var(--fs-sm);
color: var(--ink-dim);
line-height: 1.4;
}
/* Trend signal tiles. */
.ind-signals {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
@media (min-width: $bp-sm) {
.ind-signals {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
}
.ind-sig {
border: 1px solid var(--rule);
border-left: 3px solid var(--tone);
border-radius: var(--radius-sm);
background: var(--tone-soft);
padding: 9px 11px;
display: flex;
flex-direction: column;
gap: 3px;
}
.ind-sig__top {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 6px;
}
.ind-sig__label {
@include eyebrow;
}
.ind-sig__value {
@include mono;
font-size: var(--fs-2xs);
color: var(--ink-dim);
}
.ind-sig__status {
@include mono;
font-weight: 700;
font-size: var(--fs-md);
color: var(--tone);
line-height: 1.1;
}
.ind-sig__note {
font-size: var(--fs-2xs);
color: var(--ink-dim);
}
.ind-read__foot {
font-size: var(--fs-xs);
color: var(--ink-faint);
}