/* ============================================================================
   Screen-scoped composition. Everything here is layout for one region that has
   no second use; anything reused twice belongs in components.css.
   ========================================================================== */

/* ------------------------------------------------------------- figure band */

/* Not seven equal cells. Equal cells promote everything and therefore nothing,
   and the seven counts on this screen are not equally important: the coverage
   span is what the whole data spine exists to establish, so it leads at a
   larger step and the rest support it at one size down. Hairline-separated
   cells in one band, not cards — same information, roughly 40% less vertical
   space, and no nested containers.

   Column count is fixed per breakpoint rather than auto-fit, because the cells
   carry their own separators and a separator has to know where a row ends.
   auto-fit wrapped unpredictably, and a wrapped row exposed that cells only had
   a right-hand rule, so two rows ran together. */
.figures {
  display: grid;
  grid-template-columns: 1.7fr repeat(4, minmax(0, 1fr));
  border: var(--hair) solid var(--rule);
  border-radius: var(--r);
  background: var(--surface);
  overflow: hidden;
  margin-bottom: var(--s-5);
}

.figure {
  min-width: 0;
  padding: var(--s-3) var(--s-4);
  border-right: var(--hair) solid var(--rule);
}

.figure:last-child { border-right: 0; }
.figure.lead { padding-left: var(--edge-x); background: var(--sunken); }

.figure-k {
  display: block;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--muted);
  /* Wraps rather than truncating: these labels are one or two words and fit on
     one line at desktop widths, and at narrow widths a second line costs a few
     pixels where "classificatio…" costs the reader the meaning. */
  text-wrap: balance;
}

.figure-v {
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
  margin-top: 2px;
  font-family: var(--mono);
  font-size: var(--t-figure);
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-figure);
}

.figure.lead .figure-v { font-size: var(--t-display); }

/* A zero here is a real state, not a broken number. */
.figure-v.zero { color: var(--faint); }
.figure-v.pos  { color: var(--pos); }
.figure-v.neg  { color: var(--neg); }

/* Inline, never stacked. As a third line this set the height of every cell in
   the grid, so one annotation on one figure bought a band of dead space across
   all five. */
.figure-note {
  font-family: var(--sans);
  font-size: var(--t-micro);
  font-weight: 400;
  letter-spacing: normal;
  color: var(--faint);
}

/* The word joining two dates. It is a connector, not data, so it recedes — but
   it recedes to --faint, which is still text. It was set to --rule-2 and measured
   1.31:1: legible only to someone who already knew what it said. */
.figure-sub .joiner { color: var(--faint); padding: 0 2px; }

.figure-sub {
  margin-top: 2px;
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 1180px) {
  .figures { grid-template-columns: 1.7fr repeat(2, minmax(0, 1fr)); }
  .figure:nth-child(3n) { border-right: 0; }
  .figure:not(:nth-last-child(-n+2)) { border-bottom: var(--hair) solid var(--rule); }
  .figure.lead { grid-column: 1 / -1; border-right: 0; }
}

@media (max-width: 560px) {
  .figures { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .figure { border-right: var(--hair) solid var(--rule); }
  .figure:nth-child(2n), .figure:last-child { border-right: 0; }
  .figure:not(:nth-last-child(-n+1)) { border-bottom: var(--hair) solid var(--rule); }
}

/* ---------------------------------------------------------- dashboard grid */

/* Wide region and a rail, not two halves. The left column holds the things you
   read across (a timeline, a table); the right holds the things you read down
   (what needs attention, one series). At 1240px the rail stops being wide
   enough for its own content and the two stack — which is a decision about
   content, not a device breakpoint. */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(336px, 1fr);
  gap: var(--s-5);
  margin-bottom: var(--s-6);
  align-items: start;
}

.split > * { min-width: 0; }

.stack { display: grid; gap: var(--s-5); }

@media (max-width: 1240px) {
  .split { grid-template-columns: minmax(0, 1fr); }
}

.grid-2 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-5);
}

@media (max-width: 900px) {
  .grid-2 { grid-template-columns: minmax(0, 1fr); }
}

/* ------------------------------------------------------- coverage timeline */

/* Per-security stored history as spans on one shared axis. This is the fact the
   data spine is for — that a portfolio can only honestly be measured over the
   window every holding covers — and it is unreadable as a column of two dates
   per row. Drawn with positioned elements against a percentage axis; no chart
   library, and it stays legible at any width because the axis is the container. */
.timeline {
  padding: var(--s-4) var(--edge-x) var(--s-3);
  /* One declaration of the column geometry, used by the lanes, the axis and the
     window overlay. Three copies is how an overlay ends up a few pixels off the
     axis it is supposed to be measured against. */
  --tl-cols: 58px minmax(0, 1fr) 52px;
}

/* The lanes and the band that crosses all of them.

   The shared window has to span every row, so it cannot live inside a row's
   track — as a child of the first `.tl-track` it rendered as a 22px dash beside
   the first ticker, which read as a stray mark rather than as a window. It is
   now an overlay over the whole lane block, laid out on the same grid so its
   percentage space is identical to each track's. */
.tl-lanes { position: relative; }

.tl-overlay {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: grid;
  grid-template-columns: var(--tl-cols);
  gap: var(--s-3);
  pointer-events: none;
}

.tl-window-host { position: relative; }

/* Above the overlay, so the spans read over the band rather than under it. */
.tl-row { position: relative; z-index: 1; }

.tl-row {
  display: grid;
  grid-template-columns: var(--tl-cols);
  align-items: center;
  gap: var(--s-3);
  height: 22px;
}

.tl-tick {
  font-family: var(--mono);
  font-size: var(--t-data);
  font-weight: 600;
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
}

.tl-track {
  position: relative;
  height: 100%;
  /* The axis reads as an axis because of the ruled grid behind it, and the grid
     is honest: one line per five years of the range it spans, drawn from the
     same rule token as everything else. */
  background:
    repeating-linear-gradient(to right,
      var(--rule-2) 0 1px,
      transparent 1px var(--tl-gridstep, 20%));
  background-position: left center;
  background-size: 100% 11px;
  background-repeat: no-repeat;
}

.tl-span {
  position: absolute;
  top: 50%;
  height: 9px;
  margin-top: -4.5px;
  border-radius: 1px;
  background: var(--accent-line);
  /* Enters by growing along the axis from its own start date. One authored
     moment on the screen, 320ms, and it is the region's own reveal rather than
     an entrance repeated on every section. */
  transform-origin: left center;
}

.tl-span.total-return { background: var(--accent); }
.tl-span.unclassified { background: var(--warn); }

/* The window every holding shares, marked on the same axis so the cost of a
   single recent listing is visible rather than stated. */
.tl-window {
  position: absolute;
  top: 0;
  bottom: 0;
  border-left: var(--hair) dashed var(--accent);
  border-right: var(--hair) dashed var(--accent);
  background: color-mix(in oklab, var(--accent) 8%, transparent);
  pointer-events: none;
}

.tl-axis {
  display: grid;
  grid-template-columns: var(--tl-cols);
  gap: var(--s-3);
  margin-top: var(--s-2);
  padding-top: var(--s-2);
  border-top: var(--hair) solid var(--rule);
}

.tl-axis-scale {
  display: flex;
  justify-content: space-between;
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--faint);
}

.tl-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  padding: var(--s-3) var(--edge-x);
  border-top: var(--hair) solid var(--rule);
  font-size: var(--t-micro);
  color: var(--muted);
}

.tl-legend > span { display: inline-flex; align-items: center; gap: var(--s-2); }

.swatch {
  width: 14px;
  height: 8px;
  border-radius: 1px;
  background: var(--accent-line);
  flex: 0 0 auto;
}

.swatch.total-return { background: var(--accent); }
.swatch.unclassified { background: var(--warn); }
.swatch.window {
  background: color-mix(in oklab, var(--accent) 12%, transparent);
  border-left: var(--hair) dashed var(--accent);
  border-right: var(--hair) dashed var(--accent);
}

@media (max-width: 560px) {
  .timeline { --tl-cols: 46px minmax(0, 1fr) 40px; }
}

/* --------------------------------------------------------------- series */

/* One line, real data, no axis furniture beyond the two labels that make it
   readable. A rate history is content here — the engine's risk-free input —
   not a decorative sparkline standing in for one. */
.series { padding: var(--s-4) var(--edge-x) var(--s-3); }

.series-head { display: flex; align-items: baseline; gap: var(--s-2); margin-bottom: var(--s-3); }

.series-now {
  font-family: var(--mono);
  font-size: var(--t-figure);
  letter-spacing: var(--tr-figure);
  color: var(--ink);
}

.series-when { font-family: var(--mono); font-size: var(--t-micro); color: var(--faint); }

.series svg { display: block; width: 100%; height: 84px; overflow: visible; }
.series .line { fill: none; stroke: var(--accent); stroke-width: 1.25; vector-effect: non-scaling-stroke; }
.series .area { fill: color-mix(in oklab, var(--accent) 11%, transparent); stroke: none; }
.series .base { stroke: var(--rule); stroke-width: 1; vector-effect: non-scaling-stroke; }

.series-scale {
  display: flex;
  justify-content: space-between;
  margin-top: var(--s-2);
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--faint);
}

/* An underwater curve is the same shape as a growth curve and means the
   opposite, so it does not borrow the accent. `.base` marks the zero line the
   depth is measured from, which is the only axis furniture it needs. */
.series.negative .line { stroke: var(--neg); }
.series.negative .area { fill: color-mix(in oklab, var(--neg) 12%, transparent); }
.series.negative .series-now { color: var(--neg); }

/* ---------------------------------------------------------- metric rows */

/* The methodology sentence under a metric's value. A metric that cannot be
   explained in one line is a metric nobody can defend, so the explanation sits
   with the number rather than behind a click.

   It is not `.figure-note`: that one lives inside the figures band, where the
   grid separates it from the value. In a key/value cell an inline span runs
   straight on from the number it annotates — "2070.97%Compounded daily returns"
   — so this one is a block with its own leading. */
/* The overlaid-series rules that used to live here are gone with the markup
   they styled. The Benchmarks comparison drew its own stretched 100x40 chart
   with a bespoke four-slot palette and its own legend; it now uses the shared
   interactive primitive at the end of this sheet, which brings its own line
   treatments, its own legend and a crosshair none of this had.

   The base single-line rules above are NOT dead -- the dashboard's risk-free
   rate history and the scenario path chart still use them.

   No selector is named in this comment. Several tests in this project locate a
   scope by plain substring search over the sheet, and a comment that quotes a
   selector moves that index into the wrong block. */

/* A comparison draws several lines on one pair of axes. `.series` styles exactly
   one, so an overlay needs its own slot per subject — and the slots have to be
   distinguishable without colour alone, because a reader with a colour-vision
   deficiency still has to be able to follow which line is theirs. So each slot
   pairs a token colour with a dash pattern, and the legend repeats both.

   Four slots. Beyond four subjects a line chart stops being readable whatever the
   palette does, and the screen offers three. */

.metric-why {
  display: block;
  margin-top: calc(var(--s-1) / 2);
  /* Prose in a data cell, like `.name`: `tbody td` is `nowrap` so a numeric
     column cannot break mid-figure, and a sentence inheriting that runs off the
     side of its own cell instead of wrapping. */
  white-space: normal;
  max-width: 62ch;
  color: var(--faint);
  font-family: var(--sans);
  font-size: var(--t-micro);
  font-weight: 400;
  letter-spacing: normal;
  line-height: var(--lh);
}

/* ------------------------------------------------------------- attention */

/* Open data-quality flags. A list rather than a table, because there are few of
   them, each needs a sentence, and the sentence is the point. */
.attn { list-style: none; }

.attn > li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s-1) var(--s-3);
  padding: var(--s-3) var(--edge-x);
  border-bottom: var(--hair) solid var(--rule);
}

.attn > li:last-child { border-bottom: 0; }

.attn .who {
  grid-column: 1;
  font-family: var(--mono);
  font-size: var(--t-data);
  font-weight: 600;
  color: var(--ink-2);
}

.attn .what { grid-column: 2; display: flex; align-items: baseline; gap: var(--s-2); flex-wrap: wrap; }
.attn .what strong { font-size: var(--t-data); font-weight: 600; color: var(--ink); }
.attn .when { font-family: var(--mono); font-size: var(--t-micro); color: var(--faint); }
.attn .why { grid-column: 2; color: var(--muted); font-size: var(--t-data); }

/* ------------------------------------------------------------ table toolbar */

/* Filter and scope, above the table it acts on and inside its frame, so it
   reads as part of the table rather than as a floating control that happens to
   sit nearby. */
.table-bar {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--edge-x);
  border: var(--hair) solid var(--rule);
  border-bottom: 0;
  border-radius: var(--r) var(--r) 0 0;
  background: var(--sunken);
}

.table-bar + .table-wrap { border-radius: 0 0 var(--r) var(--r); }
.table-bar .count { margin-left: auto; font-family: var(--mono); font-size: var(--t-micro); color: var(--muted); }

/* Rows hidden by the filter. A class rather than inline display, so the filter
   can be cleared without the script remembering what each row used to be. */
tr[data-filtered] { display: none; }

.filter-empty { padding: var(--s-5) var(--edge-x); color: var(--muted); font-size: var(--t-data); }

/* ---------------------------------------------------------- planned screen */

/* A section that does not exist yet. It says which phase owns it, what it will
   contain, and where the work that exists today actually lives — rather than
   rendering a convincing dashboard of numbers nobody computed. */
.planned { max-width: 78ch; }

.planned-phase {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  margin-bottom: var(--s-4);
}

.planned h2 { margin-bottom: var(--s-2); font-size: var(--t-lead); }

.planned-list { list-style: none; display: grid; gap: var(--s-2); margin-bottom: var(--s-5); }

.planned-list li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s-3);
  align-items: baseline;
  color: var(--ink-2);
  font-size: var(--t-body);
}

.planned-list .icon { color: var(--faint); margin-top: 2px; }

.planned-now {
  padding: var(--s-4) var(--edge-x);
  border-top: var(--hair) solid var(--rule-2);
}

/* -------------------------------------------------------------- settings */

.setting-group + .setting-group { margin-top: var(--s-6); }

/* Span reveal. The one authored moment on the dashboard: each span grows along
   the axis it is measured against, left to right, so the motion states what the
   chart is for rather than decorating it. Staggered by 26ms — short enough that
   the whole region resolves in under half a second, long enough to read as a
   sweep rather than as everything appearing at once.

   Gated on [data-revealed], which the script sets only when reduced motion is
   off. Without the script, or with the preference on, the spans are simply
   drawn in place: the default state is the finished state, never the hidden one. */
.timeline[data-revealed] .tl-span {
  animation: tl-span-in 320ms var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * 26ms);
}

@keyframes tl-span-in {
  from { transform: scaleX(0); opacity: 0.4; }
  to   { transform: scaleX(1); opacity: 1; }
}

.timeline[data-revealed] .tl-window {
  animation: tl-window-in 260ms var(--ease-out) 180ms both;
}

@keyframes tl-window-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  /* Belt and braces: the global block already clamps the duration, but a stagger
     delay it does not touch would leave the last span invisible for a beat. */
  .timeline[data-revealed] .tl-span,
  .timeline[data-revealed] .tl-window {
    animation-delay: 0ms !important;
  }
}

/* ============================================================ risk (Phase 4)

   Two new visual elements, both built from existing tokens.

   The heatmap shades a cell by opacity over a soft accent, rather than by
   picking a colour per bucket. That keeps it to one rule per direction instead
   of eleven themed tokens, and it means the shading is automatically correct in
   both themes because the underlying token already is.

   Every shaded cell also prints its number. Colour is an accelerant here, never
   the carrier: a correlation matrix a reader cannot read without distinguishing
   two blues is a matrix that excludes people for no gain. */

.matrix { font-variant-numeric: tabular-nums; }
.matrix th.tick,
.matrix td.tick { white-space: nowrap; }

.corr {
  position: relative;
  text-align: left;
  isolation: isolate;
}
.corr-fill {
  position: absolute;
  inset: 1px;
  border-radius: var(--r-sm);
  background: var(--accent-line);
  z-index: -1;
}
.corr-fill.negative { background: var(--pos); }
.corr-v { position: relative; }
.corr.self { color: var(--faint); }

/* The self-correlation diagonal is 1.0 by definition and carries no
   information, so it is dimmed rather than shaded like a finding. */
.corr.self .corr-fill { display: none; }

/* --- what-if sliders. Progressive enhancement: the range input posts inside a
   real form and works with scripting off; JS only removes the round trip. */

.sliders { display: grid; gap: var(--s-4); }

.slider-row {
  display: grid;
  grid-template-columns: minmax(88px, 1fr) 2fr minmax(96px, auto);
  align-items: center;
  gap: var(--s-4);
}

.slider-row input[type="range"] {
  width: 100%;
  accent-color: var(--accent);
  cursor: pointer;
}

.slider-row input[type="range"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.slider-v {
  font-family: var(--mono);
  font-size: var(--t-data);
  text-align: right;
  white-space: nowrap;
}

.slider-delta { color: var(--faint); }
.slider-delta.pos { color: var(--neg); }   /* more risk reads as a caution */
.slider-delta.neg { color: var(--pos); }   /* less risk reads as favourable */

/* --- the residual bucket. Given finding 04 this is not decoration: a
   look-through chart that quietly sums to 84% is worse than no chart, so the
   residual gets a row that looks deliberately unlike a holding. */

.residual-row {
  background: var(--raised);
  font-style: italic;
}
.residual-row td { color: var(--muted); }

/* ============================================================ alignment (Phase 5)

   Three things that must stay visually distinct: the investor, the portfolio, and
   the gap between them. A single combined gauge would hide exactly the separation
   the phase exists to make. */

.grid-3 {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-5);
  margin-bottom: var(--s-6);
  align-items: start;
}
.grid-3 > * { min-width: 0; }
@media (max-width: 1100px) { .grid-3 { grid-template-columns: minmax(0, 1fr); } }

/* A score, its band, and what it means in plain units. Three lines, because one
   big number on its own invites a reader to treat it as a measurement of something
   absolute. */
.scorecard { padding: var(--s-5); }
.scorecard .sc-k {
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--muted);
}
.scorecard .sc-v {
  display: block;
  font-size: var(--t-display);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tr-figure);
  line-height: var(--lh-tight);
  margin: var(--s-2) 0 var(--s-1);
}
.scorecard .sc-band {
  display: inline-block;
  font-size: var(--t-data);
  color: var(--ink-2);
}
.scorecard .sc-note {
  display: block;
  font-size: var(--t-data);
  color: var(--muted);
  line-height: var(--lh);
  margin-top: var(--s-3);
  max-width: 46ch;
}
.scorecard .sc-sub {
  display: flex;
  gap: var(--s-4);
  margin-top: var(--s-3);
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--faint);
}

/* Both scores on one track, so the gap is visible as a distance rather than only
   readable as a number. Every marker is also labelled in text — the track is an
   accelerant, never the only channel. */
.gapbar { margin: var(--s-4) 0 var(--s-3); }
.gapbar-track {
  position: relative;
  height: 10px;
  border-radius: var(--r-pill);
  background: linear-gradient(
    to right,
    var(--pos-soft) 0%,
    var(--raised) 45%,
    var(--neg-soft) 100%
  );
  border: var(--hair) solid var(--rule);
}
.gapbar-mark {
  position: absolute;
  top: -5px;
  width: 2px;
  height: 20px;
  background: var(--ink);
  transform: translateX(-1px);
}
/* Two neutral hues, and the same shade whichever way the gap runs.
   These were `--warn` for the portfolio mark and `--pos`/`--neg` for the span,
   which painted a verdict the prose beside it explicitly declines to give: a
   portfolio below tolerance came up green and one above it red. Under-risking is
   not virtue and over-risking is not danger — that judgment depends on the
   investor's alternatives, which this model does not know. Colour here encodes
   *which* score, never whether it is good. */
.gapbar-mark.tolerance { background: var(--accent); }
.gapbar-mark.portfolio { background: var(--ink); }
.gapbar-span {
  position: absolute;
  top: 3px;
  height: 4px;
  background: var(--muted);
  opacity: 0.45;
}
.gapbar-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4);
  margin-top: var(--s-4);
  font-size: var(--t-data);
  color: var(--muted);
}
.gapbar-legend .swatch-line {
  display: inline-block;
  width: 12px;
  height: 2px;
  vertical-align: middle;
  margin-right: var(--s-2);
  background: var(--ink);
}
.gapbar-legend .swatch-line.tolerance { background: var(--accent); }
.gapbar-legend .swatch-line.portfolio { background: var(--ink); }

/* Component contribution rows: raw input, what the model made of it, and the
   points. A component that was excluded keeps its row and says why. */
.excluded-row td { color: var(--muted); font-style: italic; }

/* ===================================================== scenarios (Phase 6) */

/* The evidence badge. This is the most important label on the Scenarios screen:
   it says whether a number was measured, assumed, or inferred by a model. It is
   deliberately a block that sits above the figures rather than a chip beside a
   control, because §55 requires the distinction to survive a reader who never
   hovers anything. Colour is the second channel; the words carry it. */
.evidence {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  border: var(--hair) solid transparent;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-weight: 600;
}
.evidence::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.evidence.pos   { background: var(--pos-soft);  color: var(--pos);  border-color: var(--pos); }
.evidence.info  { background: var(--info-soft); color: var(--info); border-color: var(--info); }
.evidence.warn  { background: var(--warn-soft); color: var(--warn); border-color: var(--warn); }
.evidence.quiet { background: var(--raised);    color: var(--muted); }

.evidence-note {
  margin: var(--s-3) 0 var(--s-4);
  max-width: 68ch;
  color: var(--muted);
  font-size: var(--t-body);
  line-height: var(--lh);
}
.evidence-note em { color: var(--ink-2); font-style: italic; }

/* The kind tabs. `.segmented` already styles the container and its children, but
   its active rule matches `[aria-current="true"]` and correct navigation
   semantics is `aria-current="page"` — so the class is carried too and styled
   here rather than the attribute being bent to fit the stylesheet. */
.seg-item.on {
  background: var(--surface);
  color: var(--ink);
  font-weight: 600;
  box-shadow: 0 1px 2px rgb(16 22 28 / 0.08);
}

/* The shipped historical windows. Cards rather than a select, because each one
   carries a sentence about why its dates are its dates and that sentence is the
   difference between a replay and a number. */
.scenario-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--s-3);
}
.scenario-card {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-4);
  border: var(--hair) solid var(--rule);
  border-radius: var(--r);
  background: var(--surface);
  text-decoration: none;
  color: inherit;
  transition: border-color var(--d-fast) ease, background-color var(--d-fast) ease;
}
.scenario-card:hover { border-color: var(--rule-2); background: var(--raised); }
.scenario-card.on { border-color: var(--accent); background: var(--raised); }
.scenario-card b { color: var(--ink); font-size: var(--t-body); }
.scenario-when {
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--muted);
}
.scenario-why {
  color: var(--muted);
  font-size: var(--t-body);
  line-height: var(--lh);
}

/* Shock entry. One numeric input per holding, laid out so the ticker, the field
   and its unit read as one line rather than three columns of a form. */
.shock-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: var(--s-3);
}
.shock-row {
  display: flex;
  align-items: center;
  gap: var(--s-2);
}
.shock-row label { min-width: 4.5rem; color: var(--ink-2); }
.shock-row input[type="number"] { width: 5.5rem; text-align: right; }
.shock-unit { color: var(--muted); font-size: var(--t-micro); }
.shock-toggle {
  display: flex;
  align-items: flex-start;
  gap: var(--s-2);
  margin-top: var(--s-4);
  max-width: 60ch;
  color: var(--ink-2);
  font-size: var(--t-body);
  line-height: var(--lh);
}
.shock-toggle input { margin-top: 0.2em; flex: none; }

.stress-row {
  display: grid;
  grid-template-columns: minmax(11rem, auto) 1fr 4rem;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-3);
}
.stress-row label { color: var(--ink-2); }
.stress-row input[type="range"] { width: 100%; }

/* Direct and propagated moves must not look alike. A left border rather than a
   background so the row still reads as part of the table, and the chip in the
   Source column carries the word — the stripe is the second channel, never the
   only one. */
.move-direct     { box-shadow: inset 3px 0 0 var(--info); }
.move-propagated { box-shadow: inset 3px 0 0 var(--warn); }
.move-unchanged td,
.move-unavailable td { color: var(--muted); }

.meta-dates {
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--muted);
}

/* `.findings` was emitted by the Alignment screen from Phase 5 and never styled,
   so it fell back to the browser's default bulleted list — disc markers and a
   40px indent in the middle of a design that uses neither. Scenarios emits it
   too, so it is styled once here for both. */
.findings {
  list-style: none;
  margin: var(--s-3) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}
.findings > li {
  padding-left: var(--s-4);
  border-left: 2px solid var(--rule-2);
  line-height: var(--lh);
}
.findings > li > b { display: block; color: var(--ink); font-weight: 600; }
.findings .metric-why { display: block; margin-top: var(--s-2); }

@media (max-width: 40rem) {
  .stress-row { grid-template-columns: 1fr; gap: var(--s-2); }
  .scenario-grid { grid-template-columns: 1fr; }
}

/* A field-level error, beside the control it is about. The page-level callout
   still lists every problem; this is what a reader who tabbed straight to the
   named field gets, and it is wired to the input through `aria-describedby`. */
.field-error {
  flex-basis: 100%;
  color: var(--neg);
  font-size: var(--t-micro);
  line-height: var(--lh);
}

/* ============================================ deliverables (Phase 7) */

/* The forms. Every Phase 7 screen opens with one, and they are all the same
   shape: a row of labelled controls that wraps. Declared once rather than four
   times, because four almost-identical form layouts is how the gaps between
   screens start disagreeing by two pixels. */
.report-form,
.rule-form,
.transition-form,
.goal-form {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--s-3) var(--s-4);
  align-items: start;
}
.report-form .field,
.rule-form .field,
.transition-form .field,
.goal-form .field {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  min-width: 0;
}
.report-form label,
.rule-form label,
.transition-form label,
.goal-form label {
  color: var(--ink-2);
  font-size: var(--t-micro);
  font-weight: 600;
}
.report-form .hint,
.rule-form .hint,
.transition-form .hint,
.goal-form .hint {
  color: var(--muted);
  font-size: var(--t-micro);
  line-height: var(--lh);
}
.report-form .field.actions,
.rule-form .field.actions,
.transition-form .field.actions,
.goal-form .field.actions {
  justify-content: end;
  align-self: end;
}

/* A form that is one button, sitting inline in a table cell or a section head. */
.inline-form { display: inline; }

/* The continuation row under a table row: a note that belongs to the row above
   rather than to the table. Indented and quiet, so scanning the numeric columns
   is not interrupted by prose. */
tr.sub > td {
  padding-top: 0;
  border-top: 0;
  color: var(--muted);
  font-size: var(--t-micro);
  line-height: var(--lh);
}

.u-sep {
  display: inline-block;
  width: var(--hair);
  height: 1em;
  margin: 0 var(--s-2);
  background: var(--rule);
  vertical-align: middle;
}

/* ---------------------------------------------------------------- the report */

/* A document, not a screen. Narrower measure than the rest of the application,
   because this is continuous prose and tables meant to be read in order rather
   than a dashboard meant to be scanned. */
.doc {
  max-width: 62rem;
  display: flex;
  flex-direction: column;
  gap: var(--s-6);
}
.doc-head { display: flex; flex-direction: column; gap: var(--s-2); }
.doc-head h1 { font-size: var(--t-h1); line-height: 1.2; margin: 0; }
.doc-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-5);
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--t-micro);
}
.doc-meta b { color: var(--ink); font-weight: 600; }
.doc-actions { display: flex; gap: var(--s-2); }

.doc-section { display: flex; flex-direction: column; gap: var(--s-3); }
.doc-section > h2 {
  font-size: var(--t-h3);
  margin: 0;
  padding-bottom: var(--s-2);
  border-bottom: var(--hair) solid var(--rule);
}
.doc-section > h3 {
  font-size: var(--t-body);
  color: var(--ink-2);
  margin: var(--s-2) 0 0;
}
.doc-note, .doc-list, .doc-findings {
  max-width: 68ch;
  color: var(--ink-2);
  font-size: var(--t-body);
  line-height: var(--lh);
}
.doc-list, .doc-findings { padding-left: var(--s-4); display: grid; gap: var(--s-2); }
.doc-findings { color: var(--ink); }
.disclosures ul {
  padding-left: var(--s-4);
  display: grid;
  gap: var(--s-2);
  max-width: 68ch;
  color: var(--muted);
  font-size: var(--t-micro);
  line-height: var(--lh);
}
.doc .table-wrap { max-width: 100%; }
.doc td.note {
  color: var(--muted);
  font-size: var(--t-micro);
  white-space: normal;
  line-height: var(--lh);
}

/* ------------------------------------------------------------- the feed */

.feed-day { display: flex; flex-direction: column; gap: var(--s-2); margin-bottom: var(--s-5); }
.feed { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.feed-item {
  display: grid;
  grid-template-columns: auto 6.5rem 1fr auto auto;
  align-items: baseline;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-3);
  border-bottom: var(--hair) solid var(--rule);
}
.feed-item:hover { background: var(--raised); }
.feed-icon { color: var(--muted); align-self: center; display: inline-flex; }
.feed-item.warn .feed-icon { color: var(--warn); }
.feed-kind {
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  color: var(--muted);
}
.feed-body { color: var(--ink); font-size: var(--t-body); line-height: var(--lh); }
.feed-where { color: var(--muted); margin-left: var(--s-2); }
.feed-time {
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.feed-link { font-size: var(--t-micro); }

/* A bare <figure> is the only element in this application that carries a user
   agent margin nothing resets — 1em 40px in both engines — so the chart sat
   visibly indented from every panel beside it. Reset here rather than on the
   element, because a global figure reset would reach markup this phase does not
   own. */
.chart { margin: 0; display: flex; flex-direction: column; gap: var(--s-2); }
.chart figcaption { max-width: 68ch; }

/* ------------------------------------------------------- the projection fan */

/* A band, not a line. A single median curve is read as a prediction however it
   is labelled; a shaded spread is read as a range, which is what it is. The
   median is dashed for the same reason — a solid line reads as the answer. */
.projection-chart svg { width: 100%; height: 16rem; display: block; }
.projection-chart .band { stroke: none; }
.projection-chart .band.outer { fill: var(--accent); opacity: 0.12; }
.projection-chart .band.inner { fill: var(--accent); opacity: 0.22; }
.projection-chart .median {
  fill: none;
  stroke: var(--ink);
  stroke-width: 1.5;
  stroke-dasharray: 5 3;
  vector-effect: non-scaling-stroke;
}
.projection-chart .goal {
  stroke: var(--warn);
  stroke-width: 1;
  stroke-dasharray: 2 3;
  vector-effect: non-scaling-stroke;
}

@media (max-width: 720px) {
  .feed-item { grid-template-columns: auto 1fr auto; }
  .feed-kind { grid-column: 2; }
  .feed-body { grid-column: 1 / -1; }
}

/* ================================================================ sign in */

/* The one screen an anonymous request can reach, and the only one served
   without the rail, the top bar or `shell.css`. It is therefore self-contained:
   every rule below reads from `tokens.css` and reuses `base.css` and
   `components.css` primitives, and nothing here leaks onto another screen.

   The composition is a two-column grid, not a card. A panel floating on a
   tinted field is what every sign-in screen defaults to, and it describes no
   product in particular. This one is built from the three things that actually
   identify this application: a hairline, a step between two surface layers, and
   mono labels over a sans task column. The identity column anchors to the top
   and the form sits centred in its own column, so the asymmetry reads as
   composed rather than as a centring bug. */

.signin {
  display: grid;
  /* Both, in this order: a browser without dvh drops that one declaration and
     keeps the vh it already applied, rather than losing full-height sizing. */
  min-height: 100vh;
  min-height: 100dvh;
  /* Clamped rather than fractional: a proportional identity column keeps
     growing on a wide monitor and turns into a field of empty frame. This one
     scales through the middle range and pins at both ends. */
  grid-template-columns: clamp(19rem, 30%, 27rem) minmax(0, 1fr);
  /* Row 1 takes the slack so the notice stays pinned to the bottom edge of the
     identity column and the void between them is the deliberate part. */
  grid-template-rows: 1fr auto;
  grid-template-areas:
    "id   task"
    "note task";
}

/* Both left-column regions carry the frame surface and the closing edge, so
   they read as one continuous column rather than as two stacked panels. The
   grid has no row gap, which is what keeps the seam invisible. */
.signin-id,
.signin-notice {
  /* --chrome-deep, not --chrome. The step between --chrome and --ground is
     tuned for a rail sitting beside dense content; across a full-height seam
     with nothing else on the screen it reads as an unpainted area rather than
     as a second surface. The deep step is unambiguous in both themes. */
  background: var(--chrome-deep);
  border-right: var(--hair) solid var(--rule-2);
}

.signin-id {
  grid-area: id;
  padding: var(--s-7);
}

.signin-lockup {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}

/* The sign-in lockup. This replaced `.signin-mark` (the initials "RA" on an
   inked plate) and `.signin-name` (the word "Ramos" beside it) — a placeholder
   pair, now that there is real artwork.

   BIGGER HERE THAN IN THE RAIL, ON PURPOSE. The identity column is the only
   thing on this screen besides the form, and it is the first impression of the
   product; the rail's 26px is a persistent chrome element and a different job.
   `clamp` scales it through the middle range and pins it at both ends, matching
   how the column itself is sized — so the lockup and the column it sits in grow
   and stop together, rather than the logo overflowing a column that stopped. */
.signin-logo {
  display: block;
  height: clamp(44px, 6vw, 64px);
  width: auto;
  /* The identity column is clamp(19rem, 30%, 27rem) with --s-7 padding on both
     sides. Height governs at every size this reaches, so this bound is the
     guard rather than the mechanism -- but without it a future taller logo
     would push into the padding instead of stopping at it. */
  max-width: 100%;
  object-fit: contain;
  /* The artwork is opaque -- see the note beside `.rail-logo` in shell.css for
     the measurement and for why this is a corner rather than a colour fix. */
  border-radius: var(--r-sm);
}

.signin-def {
  max-width: 30ch;
  margin-top: var(--s-4);
  color: var(--muted);
  font-size: var(--t-lead);
  line-height: var(--lh);
  text-wrap: balance;
}

.signin-notice {
  grid-area: note;
  padding: 0 var(--s-7) var(--s-7);
}

.signin-notice p {
  padding-top: var(--s-5);
  border-top: var(--hair) solid var(--rule-2);
  /* --muted rather than --faint: --faint clears 4.5:1 on --chrome-deep by four
     hundredths, which is a pass with no margin left for a display profile. */
  color: var(--muted);
  font-size: var(--t-data);
  line-height: var(--lh);
}

.signin-notice code {
  color: var(--ink-2);
  overflow-wrap: anywhere;
}

/* -------------------------------------------------------------- the task */

.signin-task {
  grid-area: task;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: var(--s-7);
  background: var(--ground);
}

/* One measure for the whole column, so the heading, any banner and the form
   share an edge. 23rem is wide enough for an email address at --t-body and
   narrow enough that the eye does not travel to read a two-field form. */
.signin-task > * {
  width: 100%;
  max-width: 23rem;
}

.signin-task h1 {
  margin-bottom: var(--s-5);
  color: var(--ink);
  font-size: var(--t-figure);
  letter-spacing: var(--tr-tight);
}

.signin-form {
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
}

.signin-form .field {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
}

/* The label margin from components.css would compound with the flex gap. */
.signin-form .field label {
  margin-bottom: 0;
  transition: color var(--d-fast) ease;
}

/* The field a person is typing in says so from its label as well as from its
   border. It is the smallest state on the screen and the one most often left
   out; two channels cost nothing and survive a reader who cannot separate the
   accent from the rule. */
.signin-form .field:focus-within label {
  color: var(--ink-2);
}

/* Declared rather than inherited: the generic control rule in components.css
   lists text, number, search and password, and NOT email, so the two fields on
   this screen would otherwise disagree with each other about their own height,
   border and type. --ctl-lg is this product's large control; a 48px field would
   be a size that exists nowhere else in the application. */
.signin-form .field input {
  width: 100%;
  min-width: 0;
  height: var(--ctl-lg);
  padding: 0 var(--s-3);
  border: var(--hair) solid var(--rule-2);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--t-body);
  transition: border-color var(--d-fast) ease;
}

/* Not redundant with the generic `input:hover` in components.css, which is the
   same specificity: the base rule above sits later in the cascade and wins, so
   without this the two fields on this screen would have no hover state at all.
   Verified in the browser by deleting this rule and watching hover stop. */
@media (hover: hover) and (pointer: fine) {
  .signin-form .field input:hover:not(:disabled) {
    border-color: color-mix(in oklab, var(--accent) 30%, var(--rule-2));
  }
}

/* The ring is the product's one ring, from base.css. What is restored here is
   the 2px offset: components.css pulls it to 1px for dense in-table controls,
   and on a 34px field at the door of the application it should have air. */
.signin-form .field input:focus-visible {
  border-color: var(--accent);
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.signin-form .hint {
  color: var(--faint);
  font-size: var(--t-micro);
}

/* Autofill is a browser surface, like the caret and the scrollbar. Left alone,
   Chrome paints its own near-white field and near-black text over the dark
   theme, which is the one place this product would ship a 2:1 contrast pair.
   The inset shadow is the only way to repaint that background.

   Two rules, not one selector list: a browser that does not understand
   `:autofill` drops the whole list, and that is the browser this is for. */
.signin-form .field input:-webkit-autofill,
.signin-form .field input:-webkit-autofill:hover,
.signin-form .field input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--ink);
  caret-color: var(--accent);
  -webkit-box-shadow: inset 0 0 0 100px var(--surface);
  box-shadow: inset 0 0 0 100px var(--surface);
}

.signin-form .field input:autofill,
.signin-form .field input:autofill:hover,
.signin-form .field input:autofill:focus {
  -webkit-text-fill-color: var(--ink);
  caret-color: var(--accent);
  box-shadow: inset 0 0 0 100px var(--surface);
}

.signin-submit {
  height: var(--ctl-lg);
  margin-top: var(--s-2);
  font-size: var(--t-body);
}

/* The chevron says which way this goes. Two pixels on hover is the whole of the
   motion, gated on a real pointer because :hover latches after a tap. The press
   itself is `.btn:active` from components.css, which scales the button and the
   glyph inside it together. */
.signin-go {
  transition: transform var(--d-fast) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .signin-submit:hover:not(:disabled) .signin-go {
    transform: translateX(2px);
  }
}

/* The one authored moment on this screen. A banner arrives on a fresh document,
   so it cannot be a transition and it cannot be interrupted; a one-shot keyframe
   is correct here and nowhere else in this product. Under a reduced-motion
   preference the movement goes and the fade stays: the point is that the message
   registers as having just arrived, and opacity carries that on its own. */
@keyframes signin-arrive-fade {
  from { opacity: 0; }
}

@keyframes signin-arrive {
  from { opacity: 0; transform: translateY(-4px); }
}

.signin-banner {
  margin-bottom: var(--s-5);
  animation: signin-arrive-fade var(--d) var(--ease-out) both;
}

@media (prefers-reduced-motion: no-preference) {
  .signin-banner {
    animation-name: signin-arrive;
  }
}

/* The plate this rule defended is gone — `.signin-mark` was two letters on an
   inked square, and Windows high contrast stripped the square's fill and left
   the letters floating, so it needed a border to stay a plate.

   A raster logo has no such failure mode: forced-colors leaves images alone,
   and `forced-color-adjust` is deliberately NOT set here. Forcing the artwork
   to keep its own colours in a mode somebody turned on for legibility is a
   decision for the person using it, not for this stylesheet. */


/* --- bulk selection on the people screen ---------------------------------- */

/* A control bar above the table it acts on. `flex-wrap` rather than a media
   query: the three things in it are a checkbox, a count and a button, and they
   reflow onto two lines on a narrow screen without needing a breakpoint of
   their own. */
/* ----------------------------------------------------- two-factor setup */

/* The QR needs a light plate whatever the theme: a scanner reads dark-on-light,
   and inverting it in dark mode produces a code half the apps refuse. So the
   plate is explicitly white and the module colour explicitly near-black, rather
   than inheriting tokens that flip. */
.mfa-qr {
  margin: var(--s4) 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s2);
}
.mfa-qr svg {
  background: #fff;
  padding: var(--s3);
  border-radius: var(--r2);
  border: 1px solid var(--line);
  width: 220px;
  height: 220px;
}
.mfa-steps {
  margin: var(--s3) 0;
  padding-left: var(--s5);
  line-height: 1.7;
}
.mfa-key-label { margin-bottom: var(--s1); }
/* The setup key is read aloud and typed by hand, so it is grouped in fours and
   set in the mono face at a size that survives a phone screen. */
.mfa-key code {
  font-family: var(--mono);
  font-size: 1.05rem;
  letter-spacing: 0.08em;
  word-break: break-all;
}
.mfa-recovery { margin-top: var(--s4); }
.mfa-recovery summary { cursor: pointer; }
.mfa-codes {
  list-style: none;
  padding: 0;
  margin: var(--s4) 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--s2);
}
.mfa-codes code {
  font-family: var(--mono);
  font-size: 1rem;
  letter-spacing: 0.06em;
}

.bulk-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-3);
}

.bulk-all {
  display: flex;
  align-items: center;
  gap: var(--s-2);
}

/* The count sits between the toggle and the button and should not be squeezed
   out when the row wraps. */
.bulk-bar .muted { margin-right: auto; }

/* The selection column. Narrow, and the checkbox is centred in it so a column
   of thirty does not read as a ragged edge. */
.bulk-select { margin: 0; }

/* ---------------------------------------------------------------- narrow */

/* One column, and the notice moves below the form rather than above it: the
   grid areas do the reordering, so the document order a screen reader follows
   is the same order a phone shows. */
@media (max-width: 900px) {
  .signin {
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr auto;
    grid-template-areas:
      "id"
      "task"
      "note";
  }

  .signin-id,
  .signin-notice {
    border-right: 0;
  }

  .signin-id {
    padding: var(--s-5);
    border-bottom: var(--hair) solid var(--rule-2);
  }

  .signin-def {
    max-width: 46ch;
    margin-top: var(--s-3);
    font-size: var(--t-body);
  }

  /* Top-anchored on a phone, not centred: the software keyboard takes half the
     viewport the moment the first field is focused, and a centred form is the
     one that gets pushed out from under the caret. */
  .signin-task {
    justify-content: flex-start;
    align-items: flex-start;
    padding: var(--s-6) var(--s-5);
  }

  .signin-notice {
    padding: var(--s-5);
    border-top: var(--hair) solid var(--rule-2);
  }

  /* The column edge already closed the region; a second line inside it would
     read as a nested boundary that is not there. */
  .signin-notice p {
    padding-top: 0;
    border-top: 0;
  }
}

@media (max-width: 400px) {
  .signin-id,
  .signin-notice { padding: var(--s-4); }
  .signin-task { padding: var(--s-5) var(--s-4); }
}


/* ================================================================== print */

/* The printable report is served without any application chrome at all
   (`design.page(chrome=False)`), so nothing here has to hide a rail. What is
   left to do is the things a screen stylesheet gets wrong on paper: dark
   backgrounds that waste ink, table headers that vanish after the first page,
   sections that break across a page boundary, and links whose destinations are
   invisible once the page is not clickable. */
@media print {
  :root {
    --surface: #fff;
    --raised: #fff;
    --ink: #000;
    --ink-2: #222;
    --muted: #555;
    --rule: #bbb;
    --rule-2: #999;
  }
  body.bare, body { background: #fff; }
  .content.document { padding: 0; max-width: none; }
  .doc { max-width: none; gap: 1.2rem; }

  /* Repeat the header on every page a long table spans. Without this, page two
     of a holdings table is a column of numbers with no labels. */
  thead { display: table-header-group; }
  tfoot { display: table-footer-group; }
  tr, .figure, .callout { break-inside: avoid; }
  .doc-section { break-inside: auto; }
  .doc-section > h2 { break-after: avoid; }

  /* A scroll container cannot scroll on paper; letting it size to content is
     the difference between a clipped table and a readable one. */
  .table-wrap { overflow: visible !important; max-height: none !important; }
  .table-wrap.tall { max-height: none !important; }

  /* Interactive controls have no meaning in a printed document. */
  .doc-actions, .btn, form, .segmented, .route-progress { display: none !important; }

  /* A link's destination is part of the document once the document is paper. */
  .doc a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 90%; }

  @page { margin: 16mm 14mm; }
}

/* ============================================================ models (Phase 9)
   The landing surface. Deliberately not a table: the job here is "find one and
   open it", and a seven-column sortable grid answers "compare them", which is
   the question the Benchmarks and Transition screens exist for. Each row is one
   anchor, so the whole row is the target, it is one tab stop, and it works with
   scripting off. */

.model-controls {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  flex-wrap: wrap;
  margin-bottom: var(--s-4);
}

.model-controls > .actions { margin-left: auto; }

.model-list {
  border: var(--hair) solid var(--rule-2);
  border-radius: var(--r);
  background: var(--surface);
  overflow: hidden;
}

.model-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-4) var(--s-5);
  color: var(--ink-2);
  text-decoration: none;
  /* Colour only. This row is clicked many times a day, so the feedback lands
     within a frame; nothing here has to play out. */
  transition: background-color var(--d-fast) ease, color var(--d-fast) ease;
}

/* The separator moved from the anchor to its wrapper. `.model-row` is no longer
   the row -- it is the *link inside* the row, because a button cannot be nested
   in an anchor and the row needed one. Adjacent anchors are therefore no longer
   adjacent rows, and the old selector would have drawn nothing. */
.model-item { display: flex; align-items: center; }
.model-item + .model-item { border-top: var(--hair) solid var(--rule); }

/* The anchor takes the room the actions do not. `min-width: 0` so the name can
   truncate rather than pushing the buttons off the end. */
.model-item > .model-row { flex: 1 1 auto; min-width: 0; }

/* Small, quiet, and separated by a hairline so the boundary between "open this
   model" and "act on this model" is visible without hovering to find it. */
.model-actions {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: 0 var(--s-5) 0 var(--s-4);
  border-left: var(--hair) solid var(--rule);
  align-self: stretch;
}

/* A form is a block by default and would add its own line box inside the flex
   row, which shifts the buttons a couple of pixels off centre. */
.model-actions form { display: contents; }

/* The same 2px accent edge the rail's current item uses, scaled in on hover.
   One idiom for "this is the one", drawn as a pseudo-element so no text shifts
   by two pixels between states. */
.model-row::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 2px;
  background: var(--accent);
  transform: scaleY(0);
  transform-origin: center;
  transition: transform var(--d-fast) var(--ease-out);
}

.model-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.model-name {
  color: var(--ink);
  font-size: var(--t-lead);
  font-weight: 600;
  letter-spacing: var(--tr-tight);
}

.model-desc {
  color: var(--muted);
  font-size: var(--t-data);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Every value on this line is a measurement or a date, so it is mono: that is
   what lets the eye run down the column and compare "8 holdings" against
   "31 holdings" without reading either word. */
.model-meta {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--t-micro);
  white-space: nowrap;
}

/* The Ramos Portfolio Risk Score on a model card. Mono for the figure, for the
   same reason `.model-meta` is mono: the point of putting it here is that a
   reader can run down the column and compare. Sits BEFORE the meta line in the
   markup so it reads immediately after the model's name, which is the order the
   question is asked in -- "which model, and how risky is it". */
.model-score {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: var(--s-1);
  margin-right: var(--s-3);
  font-family: var(--mono);
  font-size: var(--t-micro);
  white-space: nowrap;
  color: var(--ink);
}

/* The denominator is quieter than the figure but never hidden: `/ 32` is what
   stops the number being read on the wrong axis, so it may not be decoration
   that a narrow viewport drops. */
.model-score .rs-of { color: var(--muted); }

.model-score-band {
  color: var(--muted);
  font-family: var(--sans);
}

/* An absent score is muted but still occupies the column, so a row without one
   is visibly a row without one rather than a row that failed to render. */
.model-score.is-absent { color: var(--muted); }

/* Below the mid breakpoint the band WRAPS rather than disappearing.
   `display: none` removed it from the accessibility tree as well as the page,
   and 52rem is reached by an ordinary laptop at 175% zoom -- well inside what
   1.4.10 requires to reflow "without loss of information". The band is the one
   thing that makes "21.6 / 32" interpretable, so it is the last thing that
   should vanish. `white-space: normal` is required explicitly because the band
   inherits `nowrap` from `.model-score`. */
@media (max-width: 52rem) {
  .model-score { flex-wrap: wrap; }
  .model-score-band { flex: 1 1 100%; white-space: normal; }
}

.model-go { flex: 0 0 auto; color: var(--faint); transition: color var(--d-fast) ease; }

@media (hover: hover) and (pointer: fine) {
  .model-row:hover { background: var(--raised); color: var(--ink); }
  .model-row:hover::before { transform: scaleY(1); }
  .model-row:hover .model-go { color: var(--accent); }
}

/* Deliberately no transform on press. A 0.97 scale is right for a button and
   wrong for a row this wide — at 700px it reads as the layout flexing, not as a
   press. The surface stepping one layer deeper is the same signal without the
   distortion. */
.model-row:active { background: var(--sunken); }

.model-row:focus-visible { outline-offset: -2px; }

/* Below the point where the meta line and the name stop fitting on one row, the
   meta drops under the name rather than the name truncating to nothing. */
@media (max-width: 760px) {
  .model-row { flex-wrap: wrap; gap: var(--s-2) var(--s-4); }
  .model-main { flex: 1 1 100%; }
  .model-desc { white-space: normal; }
  .model-go { margin-left: auto; }

  /* Below this width the row already wraps its own contents, so the actions go
     under it rather than competing with the name for a line. The hairline turns
     horizontal for the same reason: it is separating stacked things now. */
  .model-item { flex-wrap: wrap; }
  .model-item > .model-row { flex: 1 1 100%; }
  .model-actions {
    flex: 1 1 100%;
    justify-content: flex-end;
    border-left: 0;
    border-top: var(--hair) solid var(--rule);
    padding: var(--s-2) var(--s-5);
  }
}

/* ---------------------------------------------------------------- discovery */

.discover-form {
  display: flex;
  align-items: flex-end;
  gap: var(--s-3);
  margin-bottom: var(--s-5);
}

.discover-form .field { flex: 1 1 auto; max-width: 32rem; }

@media (max-width: 620px) {
  .discover-form { flex-wrap: wrap; }
  .discover-form .field { max-width: none; flex: 1 1 100%; }
}

/* ============================================================== the cockpit
   One screen that answers a model in about ten seconds, over the Phase 2-7
   engines unchanged. Not a numbered phase: it adds no engine.

   Density is deliberately not uniform down the page: the focal object at the top
   is the airiest thing in the product, and the panels below it return to the
   table density everything else uses. That gradient is the progressive
   disclosure, expressed in spacing rather than in tabs. */

/* ------------------------------------------------------------- the risk scale
   The focal object. The score, the 1-99 scale it is a position on, and the range
   it implies are one picture, because they are one measurement read three ways.
   A number in a box beside a range in another box is two facts; this is one. */

.riskscale { display: grid; gap: var(--s-4); }

.rs-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--s-3); }

.rs-v {
  font-size: var(--t-display);
  font-weight: 600;
  letter-spacing: var(--tr-figure);
  line-height: 1;
  color: var(--ink);
}

.rs-of { color: var(--faint); font-size: var(--t-data); }

.rs-band {
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--ink-2);
}

.rs-chip { margin-left: auto; }

/* The track carries the fill and the mark. The mark is centred on its position
   and would be clipped at either end by an overflow rule, so there is not one. */
.rs-track {
  position: relative;
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--sunken);
}

/* Full width, scaled along X, origin at the left. The same convention as
   .meter-fill and .bar-fill, so the three read as one idiom, and it keeps the
   one animation on this screen on the compositor. */
.rs-fill {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--accent-line), var(--accent));
  transform: scaleX(var(--to, 0));
  transform-origin: left center;
  animation: rs-sweep var(--d-panel) var(--ease-out) both;
}

/* The one authored moment on this screen. It says "this is a position on a
   scale" in a way a static mark does not, it runs once on load, and it is a
   transform, so it costs no layout. */
@keyframes rs-sweep {
  from { transform: scaleX(0); }
  to   { transform: scaleX(var(--to, 0)); }
}

.rs-mark {
  position: absolute;
  top: 50%;
  width: 3px;
  height: 18px;
  margin-left: -1.5px;
  border-radius: var(--r-pill);
  background: var(--ink);
  transform: translateY(-50%);
  box-shadow: 0 0 0 3px var(--surface);
}

.rs-axis { position: relative; height: 2.6rem; }

.rs-tick {
  position: absolute;
  top: 0;
  width: var(--hair);
  height: 5px;
  background: var(--rule-2);
}

.rs-tick.major { background: var(--rule); height: 7px; }

.rs-tick-l,
.rs-tick-n {
  position: absolute;
  left: 0;
  transform: translateX(-50%);
  white-space: nowrap;
}

.rs-tick-l {
  top: 8px;
  color: var(--muted);
  font-size: var(--t-micro);
}

/* `--muted`, not `--faint`. At 11px the faint token measures 4.34:1 against the
   dark theme's surface, under the 4.5:1 that WCAG AA sets for small text; the
   tick labels beside these numerals already use --muted and pass. Measured in
   the browser rather than reasoned about. */
.rs-tick-n {
  top: 1.45rem;
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--t-micro);
}

/* The first and last labels would otherwise hang off the ends of the track. */
.rs-tick:first-child .rs-tick-l,
.rs-tick:first-child .rs-tick-n { transform: none; }
.rs-tick:last-child .rs-tick-l,
.rs-tick:last-child .rs-tick-n { transform: translateX(-100%); }

/* ------------------------------------------------------------ probable range */

.rs-range { display: grid; gap: var(--s-2); padding-top: var(--s-3); }

.rs-range-k {
  color: var(--muted);
  font-size: var(--t-micro);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
}

.rs-range-bar {
  position: relative;
  height: 8px;
  border-radius: var(--r-sm);
  background: var(--sunken);
  overflow: hidden;
}

/* Symmetric by construction, because the model assumes no drift. Drawing it
   asymmetrically would assert an expected return this system does not hold. */
.rs-range-fill {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    var(--neg-soft),
    var(--raised) 48%,
    var(--raised) 52%,
    var(--pos-soft)
  );
}

.rs-range-zero {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: var(--hair);
  background: var(--rule);
}

.rs-range-ends {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  font-size: var(--t-data);
}

.rs-range-mid { color: var(--faint); font-size: var(--t-micro); }

/* ----------------------------------------------------------------- coverage
   A compact sibling of .meter rather than a reuse of it: that component's
   readout is 22px because it was built for the builder's weight total, where the
   number is the point. Here it is a caption under a figure. */

.coverage {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-2);
}

.coverage .u-label { font-size: var(--t-micro); }

.coverage-track {
  flex: 0 1 72px;
  height: 3px;
  border-radius: var(--r-pill);
  background: var(--sunken);
  overflow: hidden;
}

.coverage-fill {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--warn);
  transform: scaleX(var(--fill, 0));
  transform-origin: left center;
}

.coverage.full .coverage-fill { background: var(--pos); }

.coverage-v { color: var(--ink-2); font-family: var(--mono); font-size: var(--t-micro); }

/* ------------------------------------------------------------ absent readings
   An absence has to look different from a small number, and different again from
   a failure. The word does that work; a dash alone never could. */

.figure-absent {
  color: var(--muted);
  font-size: var(--t-lead);
  font-weight: 500;
  letter-spacing: normal;
}

/* `.figure-sub` is `white-space: nowrap` with an ellipsis, because it was built
   for the one short line every earlier screen puts there. The cockpit puts three
   things under a value -- an evidence chip, a coverage bar, and for an absence
   the reason it is absent -- and the reason was being truncated mid-word: "This
   book holds no funds, s". Scoped with :has() so the single-line behaviour the
   other screens depend on is untouched. */
.figure-sub:has(.evidence),
.figure-sub:has(.figure-why),
.figure-sub:has(.coverage) {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}

.figure-why {
  margin: var(--s-2) 0 0;
  max-width: 100%;
  color: var(--faint);
  font-family: var(--sans);
  font-size: var(--t-micro);
  line-height: var(--lh);
  letter-spacing: normal;
  overflow-wrap: break-word;
}

/* ------------------------------------------------------------- stress cards */

.st-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--s-4);
}

/* Elevation declared once, as a border. A 1px border under a soft shadow is the
   ghost card, and this surface already sits on --surface. */
.st-card {
  display: grid;
  align-content: start;
  gap: var(--s-3);
  padding: var(--s-4);
  border: var(--hair) solid var(--rule);
  border-radius: var(--r);
  background: var(--raised);
}

.st-card h3 {
  margin: 0;
  font-size: var(--t-body);
  font-weight: 600;
  letter-spacing: var(--tr-tight);
}

.st-when {
  color: var(--faint);
  font-family: var(--mono);
  font-size: var(--t-micro);
}

.st-legs { display: grid; gap: var(--s-2); }

/* The comparison is the content of the card, so the two legs share one scale and
   the relationship between them is visible rather than left to arithmetic. */
.st-leg {
  display: grid;
  grid-template-columns: 1fr minmax(48px, 2fr) auto;
  align-items: center;
  gap: var(--s-2);
}

.st-leg-k {
  overflow: hidden;
  color: var(--ink-2);
  font-size: var(--t-micro);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.st-leg-bar {
  height: 6px;
  border-radius: var(--r-sm);
  background: var(--sunken);
  overflow: hidden;
}

.st-leg-fill { display: block; height: 100%; border-radius: var(--r-sm); }

/* `--muted`, not `--rule`. As a rule colour against a --sunken track this
   measured 1.06:1 in light and 1.27:1 in dark, under the 3:1 SC 1.4.11 sets for
   a graphical object -- so the benchmark leg was not a faint bar, it was an
   absent one, and the comparison the card exists to draw was half missing. The
   two legs are still told apart by their labels and their signed values; the
   colour difference is the secondary cue, not the only one. */
.st-leg-fill.bench { background: var(--muted); }
.st-leg-fill.model { background: var(--accent); }

.st-leg-v { font-size: var(--t-data); }

.st-rec {
  display: grid;
  gap: 2px;
  padding-top: var(--s-2);
  border-top: var(--hair) solid var(--rule-2);
}

.st-rec b { font-size: var(--t-data); font-weight: 600; }
.st-rec-alt { color: var(--faint); font-size: var(--t-micro); }
.st-rec.pending b { color: var(--muted); font-weight: 500; }

.st-absent { margin: 0; color: var(--muted); font-size: var(--t-data); }

/* `--ctl-sm` is the 24px floor SC 2.5.8 sets, and the token exists because that
   floor was already fixed once for the in-table controls. This is a standalone
   block link at the foot of a card, not text inside a sentence, so the inline
   exception does not apply to it. */
.st-go {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  min-height: var(--ctl-sm);
  margin-top: auto;
  font-size: var(--t-data);
}

/* The risk-and-reward rules that used to live here are gone with the markup
   they styled: a 9rem box with no axes, whose bubble rule gave the rebuilt
   bubble a second, conflicting definition further down this file. The
   replacement is the scatter block at the end of the sheet.

   No selector is spelled out in this comment on purpose. Several tests in this
   project locate a scope with a plain substring search over the sheet, so a
   comment that merely QUOTES a selector moves that index into the wrong block
   and can empty a whole scope -- which has happened in this repository before. */

/* ------------------------------------------------------------ reduced motion
   The sweep is the only animation this screen adds, and it carries meaning
   rather than decoration, so it is removed rather than shortened: the fill sits
   at its final width without it. */

@media (prefers-reduced-motion: reduce) {
  .rs-fill { animation: none; }
}

@media (max-width: 900px) {
  .rs-axis { height: 3rem; }
  .rs-chip { margin-left: 0; flex: 1 1 100%; }
}

/* ============================================================================
   The Stats screen and the correlation ribbon diagram.

   Two new component families and one new drawing. Every other section on that
   screen reuses `.figures`, `.bars`, `.table-wrap`, `.disclosure`, `.segmented`
   and the cockpit's own `.riskscale` and `.scatter` — the point of the screen is
   that it quotes existing components rather than acquiring parallel ones.
   ========================================================================== */

/* ------------------------------------------------------------- the page index
   Seventeen sections, and a way to reach one. Anchors rather than tabs: the
   targets are all on the page, so this accelerates a reader who uses it and
   costs nothing to one who scrolls past. */

.st-index {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1) var(--s-2);
  margin: 0 0 var(--s-5);
  padding-bottom: var(--s-4);
  border-bottom: var(--hair) solid var(--rule);
}

.st-index a {
  padding: 3px var(--s-2);
  min-height: var(--ctl-sm);
  display: inline-flex;
  align-items: center;
  border-radius: var(--r);
  font-size: var(--t-micro);
  color: var(--muted);
  text-decoration: none;
  transition: color var(--d-fast) ease, background var(--d-fast) ease;
}

.st-index a:hover { color: var(--ink); background: var(--raised); }

/* `scroll-margin-top` on the anchor, not padding: the topbar is sticky, so an
   un-offset jump lands the heading underneath it. */
.st-anchor { scroll-margin-top: calc(var(--topbar) + var(--s-4)); }

/* ------------------------------------------------------------- small blocks */

.st-assume { margin-bottom: var(--s-5); }
.st-assume .note { margin-top: var(--s-3); }

.st-limits {
  margin: var(--s-4) 0 0;
  padding-left: var(--s-5);
  color: var(--muted);
  font-size: var(--t-data);
}

.st-limits li { margin-bottom: var(--s-1); }

.st-class { margin-bottom: var(--s-5); }
.st-class:last-child { margin-bottom: 0; }
.st-class .u-label { margin-bottom: var(--s-3); }

.st-residual { margin-top: var(--s-5); }
.st-residual .u-label { margin-bottom: var(--s-3); }

/* --------------------------------------------------- the diversification effect
   A stated figure with its own sentence under it, not a card. The border is the
   only elevation and there is no shadow behind it: one declaration, per the
   craft floor. */

.st-effect {
  margin: var(--s-5) 0;
  padding: var(--s-4) var(--s-5);
  border: var(--hair) solid var(--rule-2);
  border-radius: var(--r-lg);
  background: var(--sunken);
}

.st-effect-v {
  margin: var(--s-2) 0 var(--s-3);
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
}

.st-effect-v b {
  font-size: var(--t-figure);
  letter-spacing: var(--tr-figure);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.st-effect-v span { color: var(--muted); font-size: var(--t-data); }

.st-effect-w { margin: 0 0 var(--s-3); color: var(--ink-2); font-size: var(--t-data); }
.st-effect-w b { font-variant-numeric: tabular-nums; }
.st-effect .note { margin: 0; }

/* ------------------------------------------------------------- extreme pairs */

.st-pair {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-3);
  padding: var(--s-3) 0;
  border-bottom: var(--hair) solid var(--rule);
  font-size: var(--t-data);
}

.st-pair .u-label { flex: 0 0 11rem; }
.st-pair .mono { color: var(--ink); }
.st-pair b { font-variant-numeric: tabular-nums; }
.st-pair b.pos { color: var(--pos); }
.st-pair b.neg { color: var(--neg); }
.st-pair .quiet { color: var(--faint); font-size: var(--t-micro); }

/* ------------------------------------------------------------------- clusters */

.st-clusters { margin-top: var(--s-5); }
.st-clusters ul { list-style: none; margin: var(--s-3) 0 0; padding: 0; }

.st-clusters li {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-1) var(--s-2);
  padding: var(--s-2) 0;
  font-size: var(--t-data);
}

.st-clusters li .mono {
  padding: 1px var(--s-2);
  border: var(--hair) solid var(--rule-2);
  border-radius: var(--r);
  background: var(--raised);
  color: var(--ink);
  font-size: var(--t-micro);
}

.st-clusters .quiet { color: var(--faint); font-size: var(--t-micro); }

/* -------------------------------------------------------------- withheld grid
   Not a card grid standing in for structure: these are four short statements of
   absence, and a row each with a rule between is the honest amount of furniture.
   Auto-fit rather than fixed columns so three of them do not stretch. */

/* 15rem, not 19rem. At a 320px viewport `.content` leaves 292px, and a
   `minmax()` track does not shrink below its own minimum — so a 19rem (304px)
   floor gave the whole page a horizontal scrollbar at exactly the width WCAG's
   reflow criterion tests. `min-width: 0` on the child stops a long country name
   pushing it back out. */
.st-exp-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--s-5) var(--s-6);
}

.st-exp { min-width: 0; }

.st-exp { border-top: var(--hair) solid var(--rule-2); padding-top: var(--s-3); }
.st-exp p { margin: 0 0 var(--s-2); font-size: var(--t-data); color: var(--ink-2); }
.st-exp p:last-of-type { margin-bottom: 0; color: var(--muted); }
.st-exp .coverage { margin: var(--s-2) 0 var(--s-3); }
.st-exp .disclosure { margin-top: var(--s-3); }
.st-exp .evidence { margin-bottom: var(--s-2); }

/* The measured value and the words that stand in its place occupy the same slot
   and are deliberately NOT the same size. A figure earns the lead; "Not
   available" is a label about the figure, and setting it in the figure's type
   made an absence look like a result — which is exactly the confusion this
   section exists to remove. */
.st-exp-v {
  margin: 0 0 var(--s-2) !important;
  color: var(--ink) !important;
  font-family: var(--mono);
  font-size: var(--t-lead);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tr-num);
}

.st-exp-s {
  color: var(--muted) !important;
  font-size: var(--t-micro) !important;
  text-transform: uppercase;
  letter-spacing: var(--tr-label);
}

/* ============================================================ share breakdown

   Label, proportional track, value. The track is the share itself rather than
   the share relative to the largest row, so a book that is 40% in one place
   fills 40% of the bar and the residual is visible without being listed. `bars`
   scales to its own maximum and is right for a ranking; this is right for a
   composition. */

.breakdown { display: grid; gap: var(--s-2); margin: 0 0 var(--s-3); }

.bd-row {
  display: grid;
  grid-template-columns: minmax(6rem, 1fr) minmax(3rem, 5rem) auto;
  align-items: center;
  gap: var(--s-3);
}

.bd-k {
  overflow: hidden;
  color: var(--ink-2);
  font-size: var(--t-data);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bd-track {
  position: relative;
  overflow: hidden;
  height: 4px;
  border-radius: 2px;
  background: var(--raised);
}

.bd-fill {
  display: block;
  height: 100%;
  border-radius: 2px;
  background: var(--accent);
  transform: scaleX(var(--fill, 0));
  transform-origin: left center;
}

/* A row that names what is NOT placed is drawn in muted ink with a fainter
   fill, so the eye reads composition first and residual second without either
   being hidden.

   Keyed on a class the emitter sets, NOT on `:last-child`. The positional version
   was wrong twice over: the size split appends no residual row at all, so it faded
   whichever real band sorted last, and a geography with one residual row faded the
   genuine country above it. A colour that asserts something false is worse than
   one that says nothing, and this one was asserting "not placed" about measured
   weight. */
.bd-row.is-residual .bd-fill {
  background: color-mix(in oklab, var(--muted) 45%, var(--rule-2));
}
.bd-row.is-residual .bd-k { color: var(--muted); font-style: italic; }

.bd-v {
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* ============================================================ ribbon diagram

   The drawing is `aria-hidden`; the pairwise table under the disclosure is the
   accessible reading of it, following the pattern the cockpit's risk/reward plot
   established.

   One hue, and a dash for the sign. The first draft gave offsetting pairs their
   own colour and reached for `--pos` to do it, which says "this is good" about a
   negative correlation in a product where that token means a gain on every other
   screen. There is no second non-semantic hue in the token set that reads clearly
   against `--accent-line`, so sign moved onto the stroke pattern instead:
   intensity is |correlation| and a dashed ribbon runs against rather than with.
   That borrows nothing from the P&L palette and stays readable for somebody who
   cannot separate two hues at all, which a two-colour encoding never is. */

.ch-wrap {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 17rem);
  gap: var(--s-6);
  align-items: start;
  margin: var(--s-5) 0;
}

.ch-figure { margin: 0; min-width: 0; }

.chord {
  display: block;
  width: 100%;
  max-width: 26rem;
  margin: 0 auto;
  height: auto;
  overflow: visible;
}

/* An arc is a SECURITY, so it wears that security's own colour, set inline per
   segment from the one resolver in `ramos.palette` and falling back to the old
   single hue if a segment ever arrives without one.

   This diagram used to draw every holding in the same blue, which made the one
   thing it is for -- following a named holding's relationships out of the
   bundle -- possible only by hovering. Identity is now carried by hue, and the
   relationship channels below are untouched: a ribbon is still the pair's
   correlation, drawn in a neutral so that it belongs to neither end. */
.ch-seg {
  fill: none;
  stroke: var(--seg-tone, var(--accent-line));
  stroke-width: 13;
  stroke-linecap: butt;
  transition: stroke var(--d-fast) ease, opacity var(--d-fast) ease;
}

/* A short leg keeps its own colour and is drawn THIN. An earlier note here said
   a short could not take a hue because a second colour would collide with the
   correlation-sign channel; that reasoning held while the arcs were monochrome
   and the ribbons carried two hues, and neither is true now. The ribbons encode
   sign by dash pattern, the arcs encode identity by hue, and short-ness has the
   width channel to itself -- three meanings on three channels, and none of them
   is colour alone. */
.ch-seg.short {
  stroke-width: 7;
}

.ch-rib {
  fill: none;
  stroke: var(--accent-line);
  stroke-linecap: round;
  /* `--s` is |correlation|, set inline per ribbon. The floor is not zero: a weak
     relationship is a real one and an invisible curve is an absent one. */
  opacity: calc(0.18 + 0.55 * var(--s, 0.5));
  transition: opacity var(--d-fast) ease, stroke var(--d-fast) ease;
}

/* Offsetting: same hue, dashed, and slightly stronger so the dashes read at a
   hairline width. `stroke-dasharray` is in user units, which is what the viewBox
   is in, so it does not change with the rendered size. */
.ch-rib.offset {
  stroke-dasharray: 5 3.5;
  opacity: calc(0.26 + 0.6 * var(--s, 0.5));
}

/* Focus. The whole point of the diagram is following one holding's curves out of
   the bundle, so the unrelated ones drop back hard rather than politely. 0.06 is
   still visible as texture, which keeps the shape of the book readable. */
.chord[data-focus] .ch-rib { opacity: 0.06; }
.chord[data-focus] .ch-rib[data-on] { opacity: calc(0.35 + 0.6 * var(--s, 0.5)); }
.chord[data-focus] .ch-seg { opacity: 0.25; }
/* Focusing a holding must not repaint it: the arc that lights up has to be
   recognisably the same arc the reader was already looking at, and swapping its
   hue for the accent threw away the identity the selection was made on. Opacity
   alone now, which is the channel the ribbons already use for the same job. */
.chord[data-focus] .ch-seg[data-on] { opacity: 1; }

.ch-figure figcaption {
  margin-top: var(--s-3);
  color: var(--faint);
  font-size: var(--t-micro);
  line-height: var(--lh);
}

/* The continuum, stated. Offsetting on the left, moving together on the right,
   which is the direction the ribbons' own colours run. */
.ch-scale {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-3);
  font-size: var(--t-micro);
  color: var(--muted);
}

.ch-ramp {
  flex: 1 1 auto;
  height: 4px;
  border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--rule-2), var(--accent-line));
}

/* The sign key, beside the magnitude ramp. Two short strokes rather than two
   words of legend text, because they are the actual marks used on the diagram. */
.ch-key {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  margin-top: var(--s-2);
  font-size: var(--t-micro);
  color: var(--muted);
}

.ch-key span { display: inline-flex; align-items: center; gap: var(--s-2); }

.ch-key i {
  width: 22px;
  height: 0;
  border-top: 2px solid var(--accent-line);
}

.ch-key i.offset { border-top-style: dashed; }

/* ------------------------------------------------------------------ the legend
   The label layer. Text around a circle is unreadable past a handful of arcs and
   needs rotation maths to be even that; a list of links is legible at any count,
   keyboard reachable, and is what makes selection work with scripting off. */

.ch-legend { min-width: 0; }
.ch-legend .u-label { margin-bottom: var(--s-2); }

.ch-leg {
  display: grid;
  grid-template-columns: 10px 4rem minmax(0, 1fr);
  grid-template-areas: "swatch ticker weight" ". near near";
  align-items: center;
  gap: 2px var(--s-2);
  padding: var(--s-2);
  min-height: var(--ctl-sm);
  border-radius: var(--r);
  border-bottom: var(--hair) solid var(--rule);
  color: var(--ink-2);
  text-decoration: none;
  font-size: var(--t-micro);
  transition: background var(--d-fast) ease, color var(--d-fast) ease;
}

.ch-leg:hover { background: var(--raised); color: var(--ink); }
.ch-leg.on { background: var(--accent-soft); color: var(--ink); }

/* `--faint` on `--accent-soft` measures 4.50:1 in dark — exactly on the AA line
   and below it once anti-aliasing is accounted for. The selected row raises the
   detail text a step. `--faint` stays for the unselected rows, where it sits on
   `--ground` and measures comfortably above. */
.ch-leg.on .ch-n { color: var(--ink-2); }

/* The same inline colour the arc took, so a legend row and its arc cannot
   disagree. Both read it from one map built once per render. */
.ch-swatch {
  grid-area: swatch;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  background: var(--seg-tone, var(--accent-line));
}

/* A short's swatch stays hollow. It is the legend's echo of the thin arc, and
   it keeps short-ness readable without depending on the hue -- which now says
   which holding this is rather than which side of the book it sits on. */
.ch-swatch.short {
  background: transparent;
  border: var(--hair) solid var(--seg-tone, var(--rule-2));
}

/* Reduced motion already drops the transitions further down; the dash pattern is
   static and needs no gate. */

.ch-t { grid-area: ticker; color: var(--ink); }
.ch-w { grid-area: weight; text-align: right; font-variant-numeric: tabular-nums; }
.ch-n { grid-area: near; color: var(--faint); }

/* A standalone action, so it carries the 24px floor rather than the inline
   exception — that exception is for a link inside a sentence, which this is not. */
.ch-clear {
  display: inline-flex;
  align-items: center;
  min-height: var(--ctl-sm);
  padding: 0 var(--s-2);
  margin-top: var(--s-2);
  margin-left: calc(var(--s-2) * -1);
  border-radius: var(--r);
  font-size: var(--t-micro);
  color: var(--accent);
}

.ch-clear:hover { background: var(--raised); }

@media (max-width: 1080px) {
  .ch-wrap { grid-template-columns: minmax(0, 1fr); }
  .ch-legend {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: 0 var(--s-4);
  }
  .ch-legend .u-label { grid-column: 1 / -1; }
}

@media (max-width: 720px) {
  .st-pair .u-label { flex: 1 1 100%; }
  .st-index { gap: 0 var(--s-1); }
}

/* ---------------------------------------------------------- reduced motion
   Every transition above is a colour or an opacity change on a hover state, and
   reduced motion means less movement rather than no feedback. They are removed
   anyway: at this density a page of simultaneously fading rows is exactly the
   ambient shimmer the preference is set to avoid. */

@media (prefers-reduced-motion: reduce) {
  .st-index a,
  .ch-seg,
  .ch-rib,
  .ch-leg { transition: none; }
}


/* The enrichment workflow's numbered instructions. Two of its four steps happen
   in Excel, where the application cannot put a hint, so the list is prose the
   reader is meant to follow rather than a caption -- it gets the body measure and
   real spacing between items instead of the browser's default tight list.

   A real `<ol>`, so the numbering and the "list, 4 items" announcement come from
   the element rather than from styling. */
.steps {
  margin: var(--s-3) 0 0;
  padding-left: var(--s-5);
  max-width: 78ch;
  color: var(--ink-2);
  font-size: var(--t-body);
  line-height: 1.65;
}

.steps li + li {
  margin-top: var(--s-2);
}

.steps b {
  color: var(--ink);
}


/* ============================================================================
   The large multi-series line chart (`api/charts.py`).

   Placed here rather than in `components.css` for the cascade: this file loads
   last, so the reduced-motion block at the end of this addition is the final
   word on its own transitions. `.ch-*` (the chord diagram) already sets that
   precedent a few hundred lines above.

   `lc-` prefix, verified free of collisions before use. Every one of these names
   is a three-file contract -- Python emits it, this file styles it, `chart.js`
   reads a subset -- so a rename is a change in all three. The names JS reads are
   marked below.
   ========================================================================== */

.lc {
  margin: 0;
  padding: var(--s-4) var(--edge-x) var(--s-3);
}

/* --- the legend, which carries the end labels ---------------------------- */

.lc-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  margin-bottom: var(--s-3);
}

/* A button rather than a span: clicking one emphasises its line. With scripting
   off it is an inert button that still reads as a label, which is why the text
   carries the whole meaning and the press state carries none of it. */
.lc-key {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 2px 6px;
  margin: -2px -6px;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  font: inherit;
  color: var(--ink-2);
  cursor: pointer;
  text-align: left;
}

.lc-key:hover { background: var(--sunken); }
.lc-key[aria-pressed="true"] { background: var(--sunken); color: var(--ink); }

.lc-swatch {
  width: 18px;
  height: 0;
  border-top: 2.5px solid var(--rule-2);
  flex: none;
}

.lc-name { font-size: var(--t-micro); }

.lc-end {
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--ink);
}

.lc-ref {
  font-size: var(--t-micro);
  color: var(--faint);
}

/* --- the drawing -------------------------------------------------------- */

/* No `preserveAspectRatio="none"`: the SVG carries its own text, and stretching
   it would distort every tick label. Height follows width from the viewBox. */
.lc-svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  /* Horizontal movement belongs to the chart, vertical to the page.

     Without this the browser claims a touch drag for panning after a few
     pixels and fires `pointercancel`, so scrubbing by finger never starts and
     only a tap works -- the drag path in `chart.js` would have been reachable
     from the test harness and nowhere else. `none` would have taken the page's
     own vertical scroll with it, which on a full-width chart means a reader
     cannot scroll past it. */
  touch-action: pan-y;
}

/* **These were not being drawn at all.** The stroke was `var(--hair)`, and
   `--hair` is a WIDTH token (1px) -- `stroke: 1px` is not a colour, so the
   declaration was invalid and every gridline rendered with no stroke. A reader
   had axis tick labels with nothing to read across from, on every line chart in
   the product. Found by review; the zero line was unaffected because it sets
   its own colour below. */
.lc-grid {
  stroke: var(--rule);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

/* The zero line is the one gridline that means something, so it is drawn as a
   rule rather than as a hair. */
.lc-grid[data-zero] {
  stroke: var(--rule-2);
}

/* Sized in USER UNITS against a 1000-wide viewBox. With the cap above the
   rendered size is at most the declared one, so 13u is a tick label of 11-13px
   on any desktop -- between `--t-micro` and `--t-body`, which is where a tick
   label belongs. The steps raise the units as the column narrows, because
   below the cap the scale factor falls and a fixed unit would shrink with it:
   at 11u in a 360px column it is about 4px, and page zoom narrows the column
   at the same rate so zooming does not help. */
.lc-ylab,
.lc-xlab {
  font-family: var(--mono);
  font-size: 13px;
  fill: var(--faint);
}

/* The axis TITLE. One step up from a tick and carrying weight instead of size,
   so it reads as the heading of the axis without competing with the figures on
   the page -- it must not land anywhere near `--t-figure`, which is what the
   20u frontier title was doing. Not the mono face: this is words, not data. */
.lc-axis {
  fill: var(--ink-2);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: var(--tr-label, 0);
}

@media (max-width: 1180px) {
  .lc-ylab,
  .lc-xlab { font-size: 16px; }
  .lc-axis { font-size: 17px; }
}

/* **Below here the axis TITLES go, and the labels get the room.**

   The left pad is baked into the viewBox by `charts._pad_left`, which sizes it
   for a label at `YLAB_DESIGN_SIZE` (16u) plus `Y_TITLE_ROOM`. CSS can raise
   the label size further but cannot widen the pad, so past that step a label
   grows left into the space the rotated title occupies -- and two overlapping
   strings are less readable than one. The title is the one to drop: the
   figcaption below the plot names the measure in words, and the data table
   carries every value, so nothing is lost but the duplicate.

   Measured at the step this replaces: a `-20.00%` label at 26u is 109u wide
   against 99u of usable pad, and it ran straight through the title. */
@media (max-width: 760px) {
  .lc-ylab,
  .lc-xlab { font-size: 19px; }
  .lc-axis,
  .sc-axis { display: none; }
}

@media (max-width: 560px) {
  .lc-ylab,
  .lc-xlab { font-size: 22px; }
}


/* ============================================= RC16.1.2: chart type, in scale

   **Every SVG chart here is sized in viewBox USER UNITS against a 1000-unit
   box, so a declared font-size renders at `declared x (css width / 1000)`.**
   `.content` carries no max-width -- a deliberate decision, because the tables
   are what the width is for -- so that scale factor is UNBOUNDED. Measured: on
   a 2560px window the plot column is about 2200px, which drew the frontier's
   20u axis title at roughly 44px and its 19u tick labels at about 42px, beside
   body text of 13px. That is the "giant 40, 60, 80 tick labels" this release
   was asked to fix, and it is also why the previous correction could not work:
   it raised the units at narrow VIEWPORTS, and the 900px column and the 2200px
   column both occur at wide ones.

   Breakpoints cannot bound a ratio whose denominator is unbounded, so the plot
   is capped at its own viewBox width instead. The scale factor is then at most
   1.0 and a declared unit is an upper bound in real pixels; the steps below
   raise the units as the column narrows, which is the half the old rules got
   right. The TABLE under each figure is not capped -- it keeps the full width
   the content area exists to give it. */
.lc-svg,
.sc-svg,
.fr-svg,
.br-svg,
/* The transition map was left out of this list when it was written and
   was therefore the one chart still drawing its 13u ticks at about 29px
   on a 2200px column -- the exact defect the paragraph above describes,
   surviving on the one figure nobody checked. It shares `.sc-tick` and
   `.sc-axis` with the scatter, so it also shares their ladder. */
.tr-svg {
  max-width: 1000px;
}

.lc-line {
  fill: none;
  stroke-width: 1.75;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

/* Five slots. Colour AND dash, never colour alone -- `test_frontend` already
   asserts this application does not encode meaning in hue for classification,
   and a line chart is the same problem.

   These four treatments were duplicated by the Benchmarks screen's own
   four-slot palette, and this comment used to point at it so a reader moving
   between the two screens would not have to relearn which line was the model.
   That duplicate is gone: Benchmarks draws with this primitive now, so the two
   screens match because they are the same code rather than because two
   palettes were kept in step by hand. */
.lc-line.q1 { stroke: var(--accent); }
.lc-line.q2 { stroke: var(--neg); stroke-dasharray: 5 3; }
.lc-line.q3 { stroke: var(--warn); stroke-dasharray: 2 2.5; }
.lc-line.q4 { stroke: var(--info); stroke-dasharray: 8 3 2 3; }
.lc-line.q5 { stroke: var(--ink-2); stroke-dasharray: 1 3; }

.lc-swatch.q1 { border-top-color: var(--accent); }
.lc-swatch.q2 { border-top-color: var(--neg); border-top-style: dashed; }
.lc-swatch.q3 { border-top-color: var(--warn); border-top-style: dotted; }
.lc-swatch.q4 { border-top-color: var(--info); border-top-style: dashed; }
.lc-swatch.q5 { border-top-color: var(--ink-2); border-top-style: dotted; }

/* Emphasis. `data-dim` is set by `chart.js` on the figure when one series is
   selected; every line except the selected one fades rather than disappearing,
   so the shape of the comparison is still visible. */
.lc[data-dim] .lc-line { opacity: 0.22; }
.lc[data-dim] .lc-line[data-on] { opacity: 1; stroke-width: 2.25; }

/* --- the crosshair (JS-driven; absent with scripting off) ---------------- */

.lc-cross-line {
  stroke: var(--ink-2);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  vector-effect: non-scaling-stroke;
}

.lc-dot {
  stroke: var(--surface);
  stroke-width: 1.5;
}

.lc-dot.q1 { fill: var(--accent); }
.lc-dot.q2 { fill: var(--neg); }
.lc-dot.q3 { fill: var(--warn); }
.lc-dot.q4 { fill: var(--info); }
.lc-dot.q5 { fill: var(--ink-2); }

/* --- the scrubber ------------------------------------------------------- */

.lc-scrub-row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-top: var(--s-2);
}

.lc-scrub-label {
  font-size: var(--t-micro);
  color: var(--faint);
  flex: none;
}

/* Native range input. Arrows, Home, End and Page keys work already, and the
   product's one focus ring reaches it through the `:focus-visible` rule in
   `base.css` -- `input[type=range]` is already covered there. */
.lc-scrub {
  flex: 1 1 auto;
  min-width: 0;
  /* A comfortable hit target on touch. WCAG 2.5.8 asks 24px; this is the
     thickness of the whole control, not just the thumb. */
  min-height: 24px;
}

.lc-readout {
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--ink);
  flex: none;
  min-width: 8.5em;
  text-align: right;
}

/* --- caption and absences ---------------------------------------------- */

.lc-cap {
  margin-top: var(--s-2);
  font-size: var(--t-micro);
  color: var(--faint);
  line-height: 1.6;
}

.lc-gone {
  margin: var(--s-3) var(--edge-x) 0;
  padding: var(--s-3);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  background: var(--sunken);
}

.lc-gone-head {
  margin: 0 0 var(--s-2);
  font-size: var(--t-micro);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--muted);
}

.lc-gone ul { margin: 0; padding: 0; list-style: none; }
.lc-gone li + li { margin-top: var(--s-1); }
.lc-gone-name { font-family: var(--mono); color: var(--ink); }
.lc-gone-why { color: var(--ink-2); font-size: var(--t-micro); }

@media (max-width: 720px) {
  .lc-scrub-row { flex-wrap: wrap; }
  .lc-readout { min-width: 0; text-align: left; }
}

/* ============================================================================
   The correlation heatmap.

   The table is `api/stats.py`'s existing `_matrix_table` -- a real `<table>` with
   real headers, already the accessible reading. All this adds is a fill computed
   from a custom property the cell carries, which is exactly how `.ch-rib` already
   takes its strength (`style="--s:0.921"`). Nothing about the markup's semantics
   changes, so every guarantee the table already had survives.
   ========================================================================== */

.corr-grid td {
  position: relative;
  /* The number stays above the fill and is the primary carrier of the value.
     Colour is the secondary encoding, never the only one. */
  z-index: 0;
}

/* `--v` is the coefficient, -1..1, written by Python. The fill is mixed from the
   ramp toward the surface by |v|, so zero is the page's own background and the
   ends are the strongest the ramp goes. `oklab` because a naive sRGB mix through
   grey goes muddy in the middle third. */
.corr-grid {
  /* How far the strongest cell mixes toward its hue. Per theme, because the
     contrast arithmetic is not symmetric: mixing a light hue into a near-black
     surface raises the background luminance toward the text's, while mixing the
     same hue into white lowers it away. At 62% both dark-theme hues measured
     about 4.3:1 against `--ink` for 11px text, which is below AA. */
  --corr-mix: 62%;
}

.corr-grid td[style*="--v"]::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: color-mix(
    in oklab,
    var(--corr-pos) calc(var(--v) * var(--corr-mix)),
    var(--surface)
  );
}

.corr-grid td[data-sign="neg"]::before {
  background: color-mix(
    in oklab,
    var(--corr-neg) calc(var(--v) * -1 * var(--corr-mix)),
    var(--surface)
  );
}

/* Both dark states, because "system dark" stamps no attribute on the root and an
   explicit choice stamps one -- the same pair `tokens.css` already writes for
   every token. 40% measures about 7:1 for both hues, well clear of the floor,
   and the ramp is still plainly a ramp: the difference between a 0.2 and a 0.9
   cell is what the reader needs, not the absolute saturation of the end. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .corr-grid { --corr-mix: 40%; }
}

:root[data-theme="dark"] .corr-grid { --corr-mix: 40%; }

/* The diagonal, and any pair that could not be measured. A hatch rather than an
   off-scale colour, because an unmeasurable cell and a genuine zero must not
   look alike -- which is the failure this project's own Stats notes name ("a
   zero correlation is not always a zero"). */
.corr-grid td[data-unmeasurable]::before {
  background: repeating-linear-gradient(
    45deg,
    var(--sunken),
    var(--sunken) 3px,
    var(--surface) 3px,
    var(--surface) 6px
  );
}

.corr-grid td[data-self] {
  color: var(--faint);
}

.corr-grid th[scope="row"] {
  position: sticky;
  left: 0;
  background: var(--raised);
  z-index: var(--z-sticky);
}

/* The ramp legend. Not decorative: it is the only place the mapping from colour
   to number is stated. */
.corr-key {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-bottom: var(--s-3);
  font-size: var(--t-micro);
  color: var(--faint);
}

.corr-key-ramp {
  flex: 0 1 220px;
  height: 10px;
  border-radius: var(--r-sm);
  border: 1px solid var(--rule);
  background: linear-gradient(
    90deg,
    var(--corr-neg),
    var(--corr-mid) 50%,
    var(--corr-pos)
  );
}

/* ============================================================================
   Reduced motion.

   There is deliberately no rule for the crosshair, the dots or the lines. Those
   are positioned by `transform` from `chart.js`, and `components.css`'s global
   reduced-motion allow-list already excludes `transform` -- so they snap by
   construction, and none of them carries a `transition` in this file for a rule
   here to turn off. A block naming them shipped for one cycle and disabled
   nothing; it read as a safety net over motion that does not exist, which is how
   a real omission later gets mistaken for a covered one.

   `.lc-tip`'s own `transition: none` further down is the one real rule in this
   component, because opacity is the one property that genuinely transitions.
   ========================================================================== */


/* The tooltip. Positioned by `chart.js` with `transform`, which is both the
   performant choice and the one the global reduced-motion allow-list excludes --
   so it snaps rather than sliding when the preference is set. */

/* **Capped with the SVG it contains, and that is load-bearing.**

   `chart.js` places the tooltip in CSS pixels as `x * (plot.width / 1000)`,
   where `x` is a viewBox coordinate. Capping `.lc-svg` alone left this box
   full-width, so on any column wider than 1000px the tooltip was scaled by
   the wrong number and drifted away from the crosshair it labels -- about
   600px away on a 2200px column. The flip-near-the-right-edge test read the
   wrong width too. One box, one scale. */
.lc-plot { position: relative; max-width: 1000px; }

.lc-tip {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  pointer-events: none;
  min-width: 10rem;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  background: var(--surface);
  box-shadow: var(--lift);
  font-size: var(--t-micro);
  line-height: 1.5;
  /* Only opacity transitions. The position must never lag a pointer: an eased
     translate on a crosshair reads as the chart being slow rather than as
     polish, which is the one thing motion must not do to a reading tool. */
  transition: opacity var(--d-fast) var(--ease-out);
}

.lc-tip[hidden] { display: none; }

.lc-tip-day {
  font-family: var(--mono);
  color: var(--ink);
  margin-bottom: 2px;
}

.lc-tip-row {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  justify-content: space-between;
  color: var(--ink-2);
}

.lc-tip-row .lc-tip-v {
  font-family: var(--mono);
  color: var(--ink);
}

.lc-tip-sw {
  display: inline-block;
  width: 12px;
  height: 0;
  border-top: 2.5px solid var(--rule-2);
  margin-right: var(--s-1);
}

/* Hue **and** dash, matching each line's own `stroke-dasharray` and the
   legend's swatch. The tooltip's were solid for every series, so the one place a
   reader compares five values at once was the one place the redundant encoding
   was missing. Each row also carries its series' name, so this was never
   colour-alone -- it was inconsistent, which is its own cost. */
.lc-tip-sw.q1 { border-top-color: var(--accent); border-top-style: solid; }
.lc-tip-sw.q2 { border-top-color: var(--neg); border-top-style: dashed; }
.lc-tip-sw.q3 { border-top-color: var(--warn); border-top-style: dotted; }
.lc-tip-sw.q4 { border-top-color: var(--info); border-top-style: dashed; }
.lc-tip-sw.q5 { border-top-color: var(--ink-2); border-top-style: dotted; }

/* A session this subject has no value on. Named rather than omitted -- see
   `chart.js` -- and quieted so it does not read as a figure. */
.lc-tip-v[data-absent] {
  color: var(--faint);
  font-variant-numeric: normal;
}

@media (prefers-reduced-motion: reduce) {
  .lc-tip { transition: none; }
}

/* ============================================================================
   The scenario comparison picker, and the unavailable-subject block.

   The picker is a row of links, one per offered benchmark, each a toggle. Styled
   on the existing `.chip` so it reads as the same kind of object as every other
   small label in the product; `.chip.on` is new and is the selected state.
   ========================================================================== */

.cmp-pick {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-3);
  padding: 0 var(--edge-x) var(--s-3);
}

.cmp-pick-label {
  font-size: var(--t-micro);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--muted);
  flex: none;
}

.cmp-pick-set {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}

/* A chip that is also a link. The underline is removed because the chip's own
   shape already reads as interactive, and kept on hover so the affordance is
   not carried by colour alone. */
a.chip {
  text-decoration: none;
  transition: background-color var(--d-fast) var(--ease-out),
    color var(--d-fast) var(--ease-out),
    transform 120ms var(--ease-out);
}

a.chip:hover { text-decoration: underline; }

/* Tactile feedback, and **one rule**. A second `a.chip:active` further down set
   `translateY(1px)` and, being later, won -- two ways of saying "this gave way"
   with only one in effect and nothing to say which. `scale(0.97)` is the one
   kept: it is what every other pressable in this file uses, it scales the chip's
   own text with it, and it does not shift a row of chips off its baseline the way
   a translate on one of them does. Subtle on purpose -- 0.97 is felt, not seen. */
a.chip:active { transform: scale(0.97); }

a.chip:focus-visible {
  /* `--focus` is not a token this project defines, so this declaration was
     invalid -- and because it outranks the base focus ring on specificity, the
     element ended up with NO visible focus indicator at all. The base ring uses
     the accent, so this does too. */
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  text-decoration: none;
}

/* The selected state. Both a fill and a leading tick, so selection is not
   carried by colour alone -- the rule this codebase already applies to
   classification values. */
.chip.on {
  background: var(--accent-soft);
  color: var(--accent-ink);
  box-shadow: inset 0 0 0 1px var(--accent-line);
}

.chip.on::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 5px;
  border-radius: 50%;
  background: currentColor;
  vertical-align: 1px;
}

/* --- a subject with no line ---------------------------------------------- */

/* A subject that has no line. Not a callout, so it does not wear a callout's
   3px left tab -- that border is the most recognisable tell of generated UI and
   `components.css` already spends it on the one thing that is genuinely an
   interruption. This is a stated absence sitting under a figure, which is a
   quieter thing: the tinted ground and the heading carry it. */
.cov-out {
  margin: 0 var(--edge-x) var(--s-3);
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  background: var(--warn-soft);
}

.cov-head {
  margin: 0 0 var(--s-2);
  font-family: var(--mono);
  color: var(--ink);
}

.cov-tag {
  font-family: var(--sans);
  font-size: var(--t-micro);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--warn);
}

.cov-line {
  margin: 0 0 var(--s-1);
  color: var(--ink-2);
  font-size: var(--t-body);
  line-height: 1.6;
}

.cov-line.quiet { color: var(--muted); font-size: var(--t-micro); }
.cov-line b { color: var(--ink); font-family: var(--mono); }

@media (prefers-reduced-motion: reduce) {
  a.chip { transition: none; }
  a.chip:active { transform: none; }
}

/* --- the heatmap's own heading block ------------------------------------- */

.corr-block { margin-top: var(--s-5); }

.corr-h {
  margin: 0 var(--edge-x) var(--s-1);
  font-size: var(--t-title);
  letter-spacing: var(--tr-tight);
  color: var(--ink);
}

.corr-sub {
  margin: 0 var(--edge-x) var(--s-3);
  max-width: 68ch;
  font-size: var(--t-micro);
  color: var(--ink-2);
  line-height: 1.6;
}

.corr-key { padding: 0 var(--edge-x); }
.corr-key-note { color: var(--muted); }

/* The grid's own cells run tighter than a normal table: 24 columns of a
   four-character number do not need row padding built for prose. */
.corr-grid td,
.corr-grid th { padding: 4px 8px; }
.corr-grid td { font-family: var(--mono); font-size: var(--t-micro); }

/* ============================================================================
   Modeled performance: the timeframe strip and the column picker.
   ========================================================================== */

/* The timeframe control's own links, and the chart legend's keys. Same press
   feedback as the chips, same reason. The legend key is a real `<button>`, so it
   would otherwise be the one control on the figure that felt inert. */
.perf-frames .segmented a,
.lc-key {
  transition: background-color var(--d-fast) var(--ease-out),
    color var(--d-fast) var(--ease-out),
    transform 120ms var(--ease-out);
}

.perf-frames .segmented a:active,
.lc-key:active { transform: scale(0.97); }

.perf-frames { padding: 0 var(--edge-x) var(--s-3); }

/* An unavailable timeframe is shown, disabled, with its reason -- never hidden.
   A reader who cannot find 5Y cannot tell whether the product lacks it or their
   model does. Styled to read as inert: no pointer, muted, and struck through so
   the state is not carried by colour alone. */
/* An unavailable timeframe. Struck through, and **focusable**, because the
   reason used to live only in a `title` (needs a hovering pointer) and in
   screen-reader-only text -- so a sighted keyboard-only reader saw the strike and
   had no route to why. It is in the tab order and states itself on focus. */
.seg-off {
  position: relative;
  padding: 5px 10px;
  border-radius: var(--r-sm);
  font-size: var(--t-micro);
  color: var(--faint);
  text-decoration: line-through;
  cursor: not-allowed;
}

.seg-off::after {
  content: attr(data-why);
  position: absolute;
  left: 50%;
  top: calc(100% + 6px);
  z-index: 20;
  width: max-content;
  max-width: 260px;
  transform: translateX(-50%);
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  background: var(--raised);
  /* `--shadow-2` is undefined here as well. The border carries the popup on
     its own, so the declaration is dropped rather than replaced with a shadow
     nobody chose. */
  color: var(--ink-2);
  font-size: var(--t-micro);
  line-height: 1.4;
  text-decoration: none;
  text-align: left;
  /* **From 0.97, never from 0.** Nothing in the world appears out of nothing, and
     an element scaling up from zero reads as arriving from somewhere else. 150ms
     ease-out is the tooltip band: fast enough to feel like a response to the
     focus rather than a delay after it.

     `visibility` is transitioned with a zero duration and `allow-discrete` so it
     flips at the right end of each direction -- present for the whole fade in,
     removed only once the fade out has finished. Without it the node stays in the
     accessibility tree while invisible. */
  transform-origin: top center;
  transform: translateX(-50%) scale(0.97);
  opacity: 0;
  visibility: hidden;
  transition: opacity 150ms var(--ease-out),
    transform 150ms var(--ease-out),
    visibility 0s linear 150ms;
}

.seg-off:focus-visible {
  /* `--focus` is not a token this project defines, so this declaration was
     invalid -- and because it outranks the base focus ring on specificity, the
     element ended up with NO visible focus indicator at all. The base ring uses
     the accent, so this does too. */
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.seg-off:focus-visible::after,
.seg-off:hover::after {
  transform: translateX(-50%) scale(1);
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

/* --- the column picker --------------------------------------------------- */

.pick-form { padding: var(--s-3) var(--edge-x) var(--s-4); }

/* Four named groups rather than one column of twenty checkboxes. Twenty in a
   list is the shape that makes a picker unusable. */
.pick-groups {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--s-4);
}

.pick-head {
  margin: 0 0 var(--s-2);
  font-size: var(--t-micro);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--muted);
}

.pick-box {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px var(--s-2);
  align-items: start;
  /* 24px minimum target height, WCAG 2.5.8. A dense picker is exactly where
     rows get squeezed below it. */
  min-height: 24px;
  padding: 4px 0;
  cursor: pointer;
}

.pick-box:hover .pick-name { color: var(--ink); }

.pick-box input {
  margin: 2px 0 0;
  grid-row: 1 / 3;
}

.pick-name { color: var(--ink-2); font-size: var(--t-body); }

.pick-why {
  grid-column: 2;
  color: var(--faint);
  font-size: var(--t-micro);
  line-height: 1.5;
}

.pick-actions {
  display: flex;
  gap: var(--s-2);
  align-items: center;
  margin-top: var(--s-4);
  padding-top: var(--s-3);
  border-top: 1px solid var(--hair);
}

@media (max-width: 720px) {
  .pick-groups { grid-template-columns: 1fr; }
}

/* ==========================================================================
   THE ADVISOR MODEL SHEET
   --------------------------------------------------------------------------
   Eighteen columns, which is wider than any other table in this application.
   The answer is a horizontal scroll with the row's own label pinned, not a
   narrower table: the column list is the product contract and dropping one to
   fit is not a layout decision, it is a change to what the screen says.

   Additive and screen-scoped, exactly as `.corr-grid` above is. Everything
   here is layered onto `d.table()`'s own output through one extra class, so
   `components.css`'s shared table rules are untouched and `app.js` -- which
   reads `data-sortable`, `data-filter-for` and `.table-wrap`, and no class on
   the table -- needs no change.
   ========================================================================== */

/* The band strip (Description & Allocation / Performance / Risk /
   Distribution) deliberately does NOT stick. Two stacked sticky header rows
   need the second one's `top` to equal the first one's rendered height, which
   is a pixel constant that silently breaks the first time the header font or
   the row padding moves -- and the failure is two header rows drawn on top of
   each other. The strip is orientation you read once; the eighteen headings
   are what you need while scrolling, so those are what stay. */
.advisor-sheet thead tr:first-child th {
  position: static;
  color: var(--faint);
  background: var(--sunken);
  border-bottom: 0;
}

/* Each band, separated from its neighbour so the grouping is visible without
   a second colour. `:first-child` keeps the leftmost flush with the frame. */
.advisor-sheet .sheet-band {
  border-left: var(--hair) solid var(--rule-2);
  text-align: left;
}

.advisor-sheet thead tr:first-child th:first-child { border-left: 0; }

/* The ticker column: the row's label, and the one thing that must stay
   readable at every scroll position. A `<th scope="row">` rather than a
   `<td>`, so every other cell announces as "STX, Last Price, 92.14" instead
   of as a loose number -- and so this keys on the attribute, the same way the
   correlation grid's pinned column does.

   `tbody td`'s padding and rule do not reach a `<th>`, so they are restated
   here rather than inherited by accident. */
.advisor-sheet tbody th[scope="row"] {
  position: sticky;
  left: 0;
  z-index: var(--z-sticky);
  padding: var(--row-pad) var(--cell-x) var(--row-pad) var(--edge-x);
  border-bottom: var(--hair) solid var(--rule);
  border-right: var(--hair) solid var(--rule-2);
  background: var(--surface);
  text-align: left;
  font-weight: 600;
  white-space: nowrap;
}

.advisor-sheet tbody tr:last-child th[scope="row"] { border-bottom: 0; }

/* The corner cell. It is sticky on both axes, so it has to outrank both the
   sticky header row and the sticky column -- otherwise the first heading
   slides under one of them at the one position where a reader is most likely
   to be looking for it. */
.advisor-sheet thead tr:last-child th:first-child {
  left: 0;
  z-index: calc(var(--z-sticky) + 2);
  border-right: var(--hair) solid var(--rule-2);
}

/* A wide table is read across, so the row under the pointer or the caret
   needs to be findable. Kept off the pinned cell's own background, which is
   opaque by necessity. */
.advisor-sheet tbody tr:hover th[scope="row"],
.advisor-sheet tbody tr:focus-within th[scope="row"] {
  background: var(--raised);
}

/* Names are the one column that may run long, and a fund's registered name
   runs very long. Bounded rather than wrapped: wrapping one cell makes the
   whole row two lines tall and costs the scan the table exists for. */
.advisor-sheet tbody td:nth-child(2) {
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --------------------------------------------------- provenance and controls */

/* The two freshness claims, side by side and separately labelled. They are
   never merged: a refreshed price must not vouch for a Bloomberg reference
   figure the owner last imported six weeks ago. */
.sheet-prov {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-6);
  margin: 0 0 var(--s-4);
  font-size: var(--t-data);
  color: var(--ink-2);
}

.sheet-prov-k {
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
}

.sheet-prov-k::after { content: ":"; }

.sheet-prov-n { color: var(--faint); }

/* Refresh and Download Excel sit in the page head's action row. Forms rather
   than links because both are POSTs, and `display: contents` keeps the form
   element from introducing a box the flex row would have to reason about. */
.sheet-action { display: contents; }

@media (max-width: 720px) {
  .sheet-prov { gap: var(--s-2); flex-direction: column; }
  /* The pinned column costs a third of a phone viewport. Below this width the
     table scrolls as one piece instead, which is the lesser loss: a frozen
     column that leaves four visible is not a frozen column, it is a
     two-column table with a scrollbar. */
  .advisor-sheet tbody th[scope="row"],
  .advisor-sheet thead tr:last-child th:first-child {
    position: static;
  }
}

/* ==========================================================================
   THE ADVISOR SHEET'S FOUR SECTIONS
   --------------------------------------------------------------------------
   Twenty-one columns is further than an eye tracks. The band strip names the
   four groups across the top, but a reader scrolled sideways has lost that
   strip and needs the column itself to say which group it belongs to.

   RESTRAINT IS THE WHOLE DESIGN. These are mixes toward the page's own
   surface at 4-7%, which is a shade rather than a colour -- enough to see a
   boundary when the eye travels down a column, not enough to read as
   highlighting. A financial sheet in four saturated blocks looks like a
   spreadsheet somebody formatted by hand, and this one is read for eight
   hours a day.

   Mixed inline with `color-mix` rather than declared as tokens: a token must
   be defined in `:root`, in the system-dark block and in the explicit-dark
   block, and `test_both_themes_define_every_token` rightly fails one that is
   not. Four tints needed in one screen's stylesheet do not earn three
   declarations each -- and mixing against `var(--surface)` follows the theme
   automatically, because `--surface` is already defined in all three.
   ========================================================================== */

/* The strength of a tint, per theme. A hue mixed into white lowers the
   background's luminance away from the text's; the same hue mixed into
   near-black raises it toward the text's. The dark theme therefore takes a
   weaker mix to keep `--ink` at the same contrast over it, which is the same
   asymmetry `.corr-grid` already measured and records above. */
.advisor-sheet { --sec-mix: 6%; --sec-edge: 22%; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .advisor-sheet { --sec-mix: 9%; --sec-edge: 30%; }
}

:root[data-theme="dark"] .advisor-sheet { --sec-mix: 9%; --sec-edge: 30%; }

/* Description & Allocation -- the identity block, and deliberately the
   quietest. It carries the ticker, the name and the sector: words, not
   figures, and the place the eye starts rather than a place it compares. A
   neutral drawn from the ink itself, so it reads as "unmarked" beside the
   three that follow. */
.advisor-sheet .sec-description {
  background: color-mix(in oklab, var(--ink-2) var(--sec-mix), var(--surface));
}

/* Performance. `--info` is this system's annotation blue and is the one
   semantic hue with no health meaning attached -- `--pos` and `--neg` say
   good and bad, and a return column must not be tinted by a colour that
   pre-judges the number in it. */
.advisor-sheet .sec-performance {
  background: color-mix(in oklab, var(--info) var(--sec-mix), var(--surface));
}

/* Risk. `--warn` at this strength is a warm neutral rather than a warning:
   the four figures here are beta, standard deviation, R-squared and Sharpe,
   and none of them is bad news by virtue of existing. Chosen because it is
   the warm end of the palette and reads as "attend to this", not because
   anything in the column is wrong. */
.advisor-sheet .sec-risk {
  background: color-mix(in oklab, var(--warn) var(--sec-mix), var(--surface));
}

/* Distribution -- the income block, and the one an advisor points at. */
.advisor-sheet .sec-distribution {
  background: color-mix(in oklab, var(--pos) var(--sec-mix), var(--surface));
}

/* The boundary between two sections, drawn once on the leading column rather
   than on every cell. Stronger than `--rule` so a section edge is visibly a
   different thing from a cell edge. */
.advisor-sheet .sheet-band + .sheet-band,
.advisor-sheet thead tr:last-child th.sec-performance:first-of-type,
.advisor-sheet tbody td.sec-performance:first-of-type,
.advisor-sheet thead tr:last-child th.sec-risk:first-of-type,
.advisor-sheet tbody td.sec-risk:first-of-type,
.advisor-sheet thead tr:last-child th.sec-distribution:first-of-type,
.advisor-sheet tbody td.sec-distribution:first-of-type {
  border-left: var(--hair) solid var(--rule-2);
}

/* The band strip takes the same tint at full strength, so the heading and the
   columns under it are visibly one thing. It sits on `--sunken` otherwise,
   and that stays the base the mix is made against. */
.advisor-sheet thead tr:first-child th.sec-description {
  background: color-mix(in oklab, var(--ink-2) var(--sec-edge), var(--sunken));
}

.advisor-sheet thead tr:first-child th.sec-performance {
  background: color-mix(in oklab, var(--info) var(--sec-edge), var(--sunken));
}

.advisor-sheet thead tr:first-child th.sec-risk {
  background: color-mix(in oklab, var(--warn) var(--sec-edge), var(--sunken));
}

.advisor-sheet thead tr:first-child th.sec-distribution {
  background: color-mix(in oklab, var(--pos) var(--sec-edge), var(--sunken));
}

/* The heading row keeps its own `--raised` from `components.css`: two tinted
   header rows stacked would leave the column names floating in colour with no
   plain surface to anchor them. Only the band strip above is tinted. */
.advisor-sheet thead tr:last-child th {
  background: var(--raised);
}

/* HOVER MUST SURVIVE THE TINT. `tbody tr:hover td` setting a background would
   erase the section colour for the row under the pointer, which is the one
   row a reader is studying. An inset shadow paints over the tint instead of
   replacing it, so the row lifts and the sections stay legible through it. */
.advisor-sheet tbody tr:hover td,
.advisor-sheet tbody tr:focus-within td {
  box-shadow: inset 0 0 0 999px color-mix(in oklab, var(--ink) 4%, transparent);
}

/* --------------------------------------------------- the scenario controls */

/* The two editable inputs sit in the figure band above the table. They reuse
   `.num-in` and `.mono-in` from `components.css` for their own appearance;
   what is added here is only the width, which a figure cell does not
   otherwise constrain. */
.sheet-scenario .sheet-in {
  width: 100%;
  max-width: 11ch;
  height: var(--ctl-sm);
}

.sheet-in-unit {
  margin-left: var(--s-1);
  color: var(--muted);
  font-size: var(--t-micro);
}

/* The submit is for a reader with scripting off; with scripting on the figures
   have already moved by the time anybody reaches it. Visible either way --
   a control that appears only when JavaScript fails is a control nobody has
   ever seen working. */
.sheet-scenario-go { margin: var(--s-3) 0 var(--s-4); }

@media (max-width: 720px) {
  /* A tinted column is the first thing to go when the table is scrolling as
     one piece: at this width the sections are already adjacent rather than
     spread, and four shades in a narrow viewport read as stripes. */
  .advisor-sheet .sec-description,
  .advisor-sheet .sec-performance,
  .advisor-sheet .sec-risk,
  .advisor-sheet .sec-distribution {
    background: var(--surface);
  }
}

/* ============================================================ allocation ring

   The fund drill-down's donut. Arcs are stroked circular segments rather than
   filled wedges, which is what makes the ring a ring: one radius, one
   stroke-width, no inner circle painted over an outer one. It also means an
   arc's colour is its `stroke`, not its `fill`, and `fill: none` is
   load-bearing rather than tidy -- an unfilled path would otherwise close
   itself across the chord and paint the middle.

   Every tone arrives as `--ring-tone` on the element, mixed in Python from
   `--accent` and `--surface`. Nothing here declares a colour token, because a
   token has to exist in `:root` and in both dark blocks and
   `test_both_themes_define_every_token` rightly fails one that does not -- and
   a ramp of eleven would be eleven declarations, three times over. */

.ring-fig {
  margin: 0;
}

.ring-body {
  display: flex;
  align-items: center;
  gap: var(--s-6);
  flex-wrap: wrap;
}

.ring {
  flex: 0 0 auto;
  width: 240px;
  max-width: 100%;
  height: auto;
}

/* The track is the whole circle, always drawn, always underneath. It is what
   makes an under-disclosed fund legible: the arcs cover what the snapshot
   accounts for and the track shows, in the same stroke, how much they do not.
   A fund publishing 43.87% of itself should look like one. */
.ring-track {
  fill: none;
  stroke: var(--sunken);
  stroke-width: var(--ring-thickness, 34);
}

.ring-arc {
  fill: none;
  stroke: var(--ring-tone, var(--accent));
  stroke-width: var(--ring-thickness, 34);
  stroke-linecap: butt;
  transition: opacity var(--d-fast) ease;
}

/* Emphasis dims the rest rather than brightening the one, so the ring keeps
   its total weight on screen and the eye is drawn by contrast instead of by a
   colour that would collide with the ramp. */
.ring-fig[data-focus] .ring-arc { opacity: 0.25; }
.ring-fig[data-focus] .ring-arc[data-on] { opacity: 1; }

.ring-keys {
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 1 1 15rem;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
}

.ring-key {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: var(--s-2);
  padding: var(--s-1) var(--s-2);
  border-radius: var(--r-sm);
  font-size: var(--t-micro);
  transition: background var(--d-fast) ease;
}

.ring-fig[data-focus] .ring-key { opacity: 0.45; }
.ring-fig[data-focus] .ring-key[data-on] { opacity: 1; background: var(--sunken); }

.ring-swatch {
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 2px;
  background: var(--ring-tone, var(--accent));
  align-self: center;
}

/* The remainder is an absence, so it reads as the track it represents rather
   than as a twelfth holding: the background nothing was drawn on, outlined. */
.ring-swatch-rest {
  background: var(--sunken);
  border: var(--hair) solid var(--rule-2);
}

.ring-rest .ring-label,
.ring-rest .ring-weight {
  color: var(--ink-2);
}

.ring-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ring-weight {
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}

/* Below this the ring and a three-column legend cannot share a row without the
   labels truncating to nothing. The ring keeps its meaning at any size; the
   legend needs the width more, so it takes the whole of it. */
@media (max-width: 720px) {
  .ring-body {
    flex-direction: column;
    align-items: stretch;
  }

  .ring {
    align-self: center;
    width: 200px;
  }
}

/* ---------------------------------------------------- Strategies (RC16 P2) */

/* The objective chooser, RC16.1.

   It was fifteen `.radio-card` labels in a `.radio-row` grid -- every method in
   the catalog expanded, with its own paragraph, permanently, above the
   constraints. Those two classes were emitted by this screen and by nothing
   else, so they are gone with the markup rather than left as rules describing
   an element the application no longer produces.

   What replaces them needs almost no styling: a `<select>` is a native control
   this stylesheet already dresses through `.field select`, it is keyboard
   operable and screen-reader labelled for free, and `<optgroup>` gives the four
   catalog families their headings without a second grouping convention. */

/* The chosen method's own sentence. Every objective's is in the document and
   exactly one is visible, so `strategies.js` swaps `hidden` rather than writing
   text -- which is what keeps strings out of that file. `[hidden]` already
   means display:none; this only stops a later rule on `.hint` overriding it,
   which would show fifteen descriptions at once and undo the whole change. */
.method-describes[hidden] { display: none; }
.method-describes { display: block; }

/* The full catalogue, behind "What each method does". A definition list
   because that is what it is: fifteen names and what each one means. */
.method-list { margin: 0; }
.method-list dt { font-weight: 600; margin-top: var(--s-3); }
.method-list dt:first-child { margin-top: 0; }
.method-list dd {
  margin: var(--s-1) 0 0;
  color: var(--ink-2);
  font-size: var(--t-micro);
  max-width: 68ch;
}

/* The two scenario methods, both described. The live one is marked with a rule
   and a weight rather than with colour alone, and carries a visually hidden
   "(showing)" so the distinction survives in a screen reader. */
.method-pairs {
  display: grid;
  gap: var(--s-3);
  margin-top: var(--s-3);
}

.method-pair {
  padding-left: var(--s-3);
  border-left: 2px solid var(--rule);
}

.method-pair.on { border-left-color: var(--accent); }
.method-pair .hint { display: block; margin-top: var(--s-1); max-width: 68ch; }

/* The caveat row, RC16.1.

   Every warning on this screen used to be one `<ul>` inside one highlighted
   callout: the unaligned benchmark, the absent inflation series, the in-sample
   disclosure and a solver that stopped without settling, four unrelated
   concepts under one icon and several hundred words, opening every successful
   analysis. A reader learns to skip a block that size, and what they then skip
   is the in-sample disclosure.

   So severity is the chip and methodology is behind it. Each condition is one
   short line; `<details>` carries the paragraph. Nothing was deleted -- every
   sentence is still in the document, which is what makes this a presentation
   change rather than a reduction in disclosure. */
.caveat-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin: var(--s-3) 0;
  padding: 0;
  list-style: none;
}

/* The row is a `<ul>` so a screen reader announces how many caveats there are.
   Its items carry no marker and no width of their own -- the `.caveat` inside
   each one is the box. */
.caveat-row > li { display: contents; }

/* Each one is its own box so an open disclosure pushes the row down rather
   than stretching its neighbours. `100%` once open, because a paragraph of
   methodology wrapped into a third of the row is unreadable. */
.caveat {
  flex: 0 1 auto;
  max-width: 100%;
  padding: var(--s-2) var(--s-3);
  border: var(--hair) solid var(--rule);
  border-radius: var(--r-sm);
  background: var(--surface);
}

/* **Full width always, rather than only once open.**
   `flex-basis: 100%` applied on open moved the control a reader had just
   activated: it wrapped onto its own row, its summary jumped down a line and
   its siblings jumped up. At magnification that is losing the thing you just
   clicked. Four of these fit on one line at the cost of a reflow on every
   interaction, which is the wrong trade for a set that is usually one or two
   items and is read rather than scanned. */
.caveat { flex-basis: 100%; }

.caveat > summary { font-size: var(--t-micro); }

/* Tone is carried by the left border AND by the words in the summary, never by
   colour alone -- "Benchmark comparison unavailable" says what it is with the
   stylesheet switched off. */
.caveat-warn  { border-left: 2px solid var(--warn); }
.caveat-quiet { border-left: 2px solid var(--rule); }

.caveat .disclosure-body {
  font-size: var(--t-micro);
  color: var(--ink-2);
  max-width: 68ch;
}

.caveat .disclosure-body ul { margin: 0; padding-left: var(--s-4); }
.caveat .disclosure-body li + li { margin-top: var(--s-2); }

@media (max-width: 720px) {
  /* Four chips across a phone is four truncated lines. */
  .caveat { flex-basis: 100%; }
}

/* The two allocations, side by side and then stacked. `1fr 1fr` rather than
   `auto-fit`: these two cards are a comparison, and a grid that reflowed one
   of them to full width at an intermediate size would put the provided
   portfolio above the optimised one at a width where both still fit. */
.alloc-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-4);
  align-items: start;
}

.alloc-card {
  border: var(--hair) solid var(--rule);
  border-radius: var(--r);
  padding: var(--s-4);
  background: var(--surface);
  min-width: 0;
}

.alloc-head {
  margin: 0 0 var(--s-3);
  font-size: var(--t-body);
  font-weight: 600;
}

/* A panel for a constraint the chosen objective does not take is `hidden` by
   the server and unhidden by `strategies.js`. `[hidden]` already means
   display:none, and this only stops a later flex/grid rule overriding it --
   a visible control the domain will refuse is worse than no control. */
.method-panel[hidden] { display: none; }

.method-panel {
  padding: var(--s-3);
  border-radius: var(--r-sm);
  background: var(--sunken);
}

/* The metric name and its one-line meaning share a cell, because a Sharpe
   ratio that does not say whether it is ex-ante or ex-post is a number two
   readers will read two ways. */
.perf-table th[scope="row"] {
  text-align: left;
  font-weight: 500;
}

.perf-table th[scope="row"] .hint {
  display: block;
  font-weight: 400;
  color: var(--ink-2);
  font-size: var(--t-micro);
}

.perf-table .u-sub {
  color: var(--ink-2);
  font-size: var(--t-micro);
}

@media (max-width: 720px) {
  .alloc-pair { grid-template-columns: 1fr; }
}


/* ===================================================== risk and reward

   A real scatter, replacing a 100x40 box that stretched to its container and
   carried no axes at all. The SVG keeps its aspect ratio and scales with the
   column, so every tick label stays the size it was drawn at. */
.sc-figure {
  position: relative;
  margin: 0;
}

.sc-svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

/* Recessive by design: the grid is a reading aid and the bubbles are the
   content. A gridline as strong as a mark competes with it. */
.sc-grid {
  stroke: var(--rule);
  stroke-width: 1;
}

/* The zero-return reference. Stronger than a gridline because crossing back
   through zero is the one comparison every reader of this chart makes, and it
   is a solid line rather than a colour so it survives any theme. */
.sc-zero {
  stroke: var(--rule-2);
  stroke-width: 1.5;
}

.sc-axis-line {
  stroke: var(--rule-2);
  stroke-width: 1;
}

/* Sized in USER UNITS against a 1000-unit viewBox, and capped above so the
   rendered size is at most the declared one.

   **These were 19u and 20u and that was the complaint.** The sizes were chosen
   against a 450px column -- the width this chart gets beside the rail between
   about 720px and 1000px, where 15u renders under 7px -- which is a real case
   and not the only one. Uncapped, the same declarations drew a 44px axis title
   on a wide monitor. The cap fixes the top end and the steps below fix the
   bottom, which is the pair the old rules were trying to do with steps alone. */
.sc-tick {
  fill: var(--muted);
  font-family: var(--mono);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

.sc-axis {
  fill: var(--ink-2);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: var(--tr-label, 0);
}

/* One holding. The hue is the security's identity, set inline from the one
   resolver, and the stroke is the surface rather than a colour so overlapping
   bubbles read as separate marks instead of merging into a blob. */
/* `fill-opacity` is 0.88 rather than the 0.72 this shipped with first.
   Review measured the blended result and it was the problem: a hue documented
   at 2.3:1 against the surface lands near 1.85:1 once it is 72% opaque, and the
   published contrast figures for these tokens are for SOLID fills. 0.88 keeps
   almost all of the declared contrast while leaving just enough transparency
   that two overlapping holdings are both visible -- which is why this is not
   simply 1.

   None of it is load-bearing for identity. Every bubble is also labelled where
   the label fits, and every holding is in the table with its ticker, both
   figures and its weight. */
.sc-dot {
  fill: var(--sc-tone, var(--accent));
  fill-opacity: 0.88;
  stroke: var(--surface);
  stroke-width: 2;
  transition: fill-opacity var(--d-fast) ease;
}

.sc-figure[data-focus] .sc-dot { fill-opacity: 0.22; }
.sc-figure[data-focus] .sc-dot[data-on] { fill-opacity: 0.95; }

/* The ticker beside its bubble. Drawn in the ink colour, not the bubble's hue:
   a label is text and has to clear the contrast floor against the surface,
   which a mark's fill is not required to do. */
.sc-label {
  fill: var(--ink-2);
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 600;
  paint-order: stroke;
  stroke: var(--surface);
  stroke-width: 3;
}

/* The legend dot in the table, so a row and its bubble share a colour. */
.sc-key {
  display: inline-block;
  width: 9px;
  height: 9px;
  margin-right: var(--s-2);
  border-radius: 50%;
  background: var(--sc-tone, var(--accent));
}

.sc-tip {
  position: absolute;
  z-index: var(--z-sticky);
  transform: translate(-50%, calc(-100% - 10px));
  min-width: 11rem;
  padding: var(--s-2) var(--s-3);
  border: var(--hair) solid var(--rule-2);
  border-radius: var(--r-2);
  background: var(--raised);
  font-size: var(--t-micro);
  pointer-events: none;
}

.sc-tip[hidden] { display: none; }

.sc-tip-head {
  margin-bottom: var(--s-2);
  font-family: var(--mono);
  font-weight: 600;
  color: var(--ink);
}

.sc-tip-row {
  display: flex;
  gap: var(--s-3);
  justify-content: space-between;
}

.sc-tip-k { color: var(--muted); }

.sc-tip-v {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}

/* On a phone the tick and label type would be drawn at a size that renders
   below the legible floor once the SVG is scaled to a narrow column, so they
   grow in user units to compensate. The chart is still the same chart; the
   table below carries every figure either way. */
/* Two steps rather than one. The single 720px breakpoint left the 721-1000px
   range -- where the rail takes most of the width and the plot column is
   narrowest relative to the viewport -- at the unscaled size, which is the one
   band review found illegible. */
@media (max-width: 1180px) {
  .sc-tick { font-size: 16px; }
  .sc-axis { font-size: 17px; }
  .sc-label { font-size: 16px; }
  .fr-dot-label { font-size: 16px; }
}

/* `.sc-axis` is hidden below 760px by the rule above, for the reason given
   there -- the pad cannot grow with the font, so the title is what gives way. */
@media (max-width: 760px) {
  .sc-tick { font-size: 19px; }
  .sc-label { font-size: 19px; }
  .fr-dot-label { font-size: 19px; }
}

@media (max-width: 560px) {
  .sc-tick { font-size: 22px; }
  .sc-label { font-size: 22px; }
  .fr-dot-label { font-size: 22px; }
}

/* The dim-others emphasis is a transition on `fill-opacity`. It carries no
   information a reader would lose without it -- the tooltip carries the
   figures -- so it is dropped outright rather than shortened. */
@media (prefers-reduced-motion: reduce) {
  .sc-dot { transition: none; }
}

/* The tooltip is clamped inside the figure by the script, but a bubble at the
   very top would still put it over the section above. Keeping it inside the
   figure's own box is enough, and the figure is never the first thing on the
   screen. */
.sc-tip { max-width: min(18rem, 90vw); }

/* ===================================== the stateless risk-profile questionnaire

   Deliberately small. The question itself reuses `.qset` / `.qopts` / `.qopt`
   from the Phase 5 investor questionnaire, which already carries the radio
   sizing, the hover, the focus ring and the `:has(input:checked)` selected
   state in both themes -- so the selected answer is a border and a background
   change rather than a colour alone, and this file adds none of it again.

   Every colour below is an existing token. Nothing here declares a new one,
   which is what keeps `test_both_themes_define_every_token` honest: a token
   must be declared in `:root` and in both dark scopes, and a questionnaire is
   not a reason to add a third place for a colour to live. */

.rp-count {
  margin: 0 0 var(--s-2);
  color: var(--ink-2);
  font-size: var(--t-micro);
}

/* The progress row. Numbered, so the position is legible without colour and
   without counting pips -- and each pip carries its own visually-hidden
   sentence, so a screen reader is told "Question 3, current" rather than being
   handed eight list items that all read as "3". */
.rp-pips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1);
  margin: 0 0 var(--s-4);
  padding: 0;
  list-style: none;
}

.rp-pip {
  display: grid;
  place-items: center;
  min-width: 1.75rem;
  min-height: 1.75rem;
  padding: 0 var(--s-1);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  color: var(--ink-2);
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
}

/* Three states, and none of them is only a colour: the current step also gains
   a heavier border and bolder numeral, and a completed one a filled surface. */
/* A completed step is a filled chip with a rule-2 border, not a faint tint.
   The first version used --raised on --surface with --ink text, which measured
   as a difference a sighted reader could not see while a screen reader said
   "answered" -- the visible state weaker than the spoken one. */
.rp-pip[data-state="done"] {
  background: var(--raised);
  border-color: var(--rule-2);
  color: var(--ink);
  font-weight: 600;
}

/* Back sits left of Continue visually while CONTINUE is first in the DOM, so
   that pressing Enter on a radio submits Continue rather than silently going
   backwards. See `_question_form` in api/riskprofile.py. */
.rp-back { order: -1; }

/* The refusal is focused on load so it is announced; it must therefore show a
   focus ring like anything else that can hold focus. */
.rp-error:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The whole row is the target, so the focus ring belongs to the row rather
   than to a 13px dot inside it. */
.qopt:has(input[type="radio"]:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.rp-pip[data-state="now"] {
  border-color: var(--accent);
  border-width: 2px;
  color: var(--ink);
  font-weight: 650;
}

.rp-form { display: block; }

.rp-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-5);
}

/* Back and Continue sit in one row, and each form is its own block, so the
   restart control on the result does not inherit the row's spacing twice. */
.rp-actions form { margin: 0; }

.rp-result {
  padding: var(--s-5);
  border: 1px solid var(--rule);
  border-radius: var(--r);
  background: var(--raised);
}

.rp-eyebrow {
  margin: 0;
  color: var(--ink-2);
  font-size: var(--t-micro);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.rp-model {
  margin: var(--s-1) 0 0;
  font-size: var(--t-display);
  font-weight: 700;
  letter-spacing: var(--tr-figure);
  line-height: 1.1;
}

.rp-profile {
  margin: var(--s-1) 0 0;
  color: var(--ink-2);
  font-size: var(--t-figure);
}

.rp-figures {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-5);
  margin: var(--s-5) 0 0;
}

.rp-figures dt {
  margin: 0;
  color: var(--ink-2);
  font-size: var(--t-micro);
}

.rp-figures dd {
  margin: var(--s-1) 0 0;
  font-size: var(--t-figure);
  font-variant-numeric: tabular-nums;
}

/* At phone width the figures stack rather than wrapping into a ragged row, and
   the two actions become full-width targets instead of two small ones sharing a
   line. */
@media (max-width: 560px) {
  .rp-figures { flex-direction: column; gap: var(--s-3); }
  .rp-actions > .btn,
  .rp-actions form,
  .rp-actions form > .btn { width: 100%; }
}

/* ---- RC16 Part 3: the efficient frontier -------------------------------- */
/* The SVG is aria-hidden and the table below it is the authoritative reading,
   which is the arrangement every other chart here uses. No transition and no
   animation anywhere in this block, so there is nothing for a reduced-motion
   preference to have to suppress. */
.fr { margin: 0; }
.fr-svg { width: 100%; height: auto; display: block; }
.fr-curve {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linejoin: round;
}
/* MARKERS CARRY A SHAPE, NOT ONLY A COLOUR. Provided is a square, Proposed a
   diamond, minimum variance a triangle and maximum Sharpe a cross -- drawn by
   `charts._fr_mark`. Two portfolios sitting on one curve is exactly the
   comparison colour-blindness makes hardest, and the legend repeats the shape
   beside its name so the pairing is readable away from the plot. */
.fr-mark { fill: var(--ink); stroke: var(--surface); stroke-width: 1.5; }
.fr-mark path { fill: none; stroke: var(--ink); stroke-width: 2.5; }
.fr-provided { fill: var(--ink-2); }
.fr-proposed { fill: var(--accent); }
.fr-min_variance { fill: var(--pos); }
.fr-max_sharpe path { stroke: var(--warn); }

.fr-legend {
  list-style: none;
  margin: var(--s-2) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  font-size: var(--t-micro);
  color: var(--muted);
}
.fr-key { display: flex; align-items: center; gap: var(--s-1); }
/* The legend glyph is an inline SVG drawn by `charts._fr_mark` -- the SAME
   function the plot uses, so the two cannot drift. The CSS boxes this
   replaced could only approximate three of the four shapes (a rotate for the
   diamond, a clip-path for the triangle) and gave Maximum Sharpe a plain
   square where the plot draws a cross. It also survives forced-colors mode,
   where a background-only swatch can render as Canvas and vanish. */
.fr-swatch { flex: none; }
.fr-outside { color: var(--warn); }
.fr-table { margin-top: var(--s-3); overflow-x: auto; }

@media (max-width: 720px) {
  .fr-legend { gap: var(--s-1) var(--s-3); }
}


/* ======================================================= RC16 Part 6
   THE FRONTIER LABORATORY.

   Three additions to the frontier figure above and one new figure. No
   animation and no transition anywhere in this block, so a reduced-motion
   preference has nothing to suppress. */

/* A vertex per SOLVED frontier portfolio. Small, hollow and in the curve's own
   hue: it has to be landable by a pointer without becoming a second visual
   layer competing with the four named markers, which are the things a reader
   is looking for. `cursor: help` is the same invitation `design.absent` uses
   for a figure that carries an explanation. */
.fr-vertex {
  fill: var(--surface);
  stroke: var(--accent);
  stroke-width: 1.5;
  cursor: help;
}

/* SECURITY DOTS. The colour is the security's canonical hue, handed in as
   `--dot` by `charts.frontier_chart` -- one map for the whole page, so a dot
   here and a band in the transition map are the same holding.

   The ring is `--surface` rather than none: dots overlap, and without a gap
   between two adjacent hues the pair reads as one larger mark. The same 2px
   surface ring the allocation rings already use for the same reason. Colour is
   never the only carrier -- every dot has its ticker in its own tooltip and a
   row of its own in the table beneath. */
.fr-dot circle {
  fill: var(--dot, var(--ink-2));
  stroke: var(--surface);
  stroke-width: 2;
  cursor: help;
}

/* ------------------------------------------------------- transition map */
.tr { margin: 0; }
.tr-svg { width: 100%; height: auto; display: block; }

/* One rect per non-zero weight. `--band` is the security's canonical hue; the
   hairline stroke in the surface colour is what keeps two adjacent bands of
   similar hue from reading as one, which at a hundred columns matters more
   than it does on a ring. `shape-rendering: crispEdges` stops a column a
   fraction of a pixel wide being antialiased into a smear. */
.tr-band {
  fill: var(--band, var(--ink-2));
  stroke: var(--surface);
  stroke-width: 0.4;
  shape-rendering: crispEdges;
  cursor: help;
}
/* Cash is not a security and takes no categorical hue -- it reads as the
   absence of a holding, which is what a recessive surface says. Matches
   `palette.CASH_TONE`.

   It carries a visible stroke because `--sunken` against `--surface` measures
   about 1.1:1 in both themes: filled alone the band is indistinguishable from
   a gap in the column, which is the one thing this chart must not say by
   accident. The stroke is the band's own edge, not a second fill, so cash
   still reads as quieter than every holding beside it. */
.tr-cash { fill: var(--sunken); stroke: var(--ink-2); stroke-width: 0.6; }

/* A withheld modeled-stress reason code: one unbroken 36-character token
   the reader is asked to quote back. At 320px -- a 400% zoom of a phone
   viewport -- it is wider than the callout it sits in, and nothing in
   this stylesheet wraps unbroken text globally. Breaking anywhere is
   right for an identifier whose shape is already carried by the
   underscores. */
.reason-code {
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* The legend that names a band or a dot. A swatch plus the ticker, because a
   colour on its own identifies nothing -- and the ticker is the accessible
   name, with the swatch `aria-hidden` so it is not announced as an image. */
.fl-legend {
  list-style: none;
  margin: var(--s-2) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1) var(--s-3);
  font-size: var(--t-micro);
  color: var(--muted);
}
.fl-key { display: flex; align-items: center; gap: var(--s-1); }
.fl-swatch {
  flex: none;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  background: var(--tone, var(--ink-2));
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--ink) 18%, transparent);
}
/* Cash takes no categorical hue -- it reads as the absence of a holding, which
   is what `palette.CASH_TONE` says. Its swatch needs a visible edge for the
   same reason its BAND does: `--sunken` against `--surface` is about 1.1:1, so
   without a border the chip is invisible and the legend row reads as a label
   with nothing beside it. */
.fl-cash {
  background: var(--sunken);
  box-shadow: inset 0 0 0 1px var(--ink-2);
}

/* The ticker beside a security dot. Drawn rather than left to the tooltip,
   because a tooltip is a mouse affordance: without it a sighted keyboard or
   touch reader identified a security by hue alone, and three of the ten
   validated hues sit below 3:1 against the surface -- which the palette is
   allowed to do precisely BECAUSE every mark is also labelled. Placed by
   `charts._sc_labels`, so labels that would collide are dropped; the dot and
   its table row never are. */
/* A security's ticker beside its dot on the frontier. On the same ladder as
   every other label in an SVG here -- it was a bare 11u, which is 24px on a
   wide monitor and 5px in a narrow column. */
.fr-dot-label {
  font-size: 13px;
  fill: var(--muted);
  paint-order: stroke;
  stroke: var(--surface);
  stroke-width: 3px;
  stroke-linejoin: round;
}

/* The hundred-row table scrolls in both directions on anything narrow, so it
   is a focusable region -- without `tabindex` a keyboard-only reader cannot
   scroll it, and it is the authoritative reading of three of this page's five
   sections. */
/* `.fl-wide` is applied TO `.table-wrap`, not around it -- see
   `frontier_ui.portfolios_table`. Wrapped around it, the outer div never
   overflowed (the inner one has the smaller max-height), so it was a tab stop
   where the arrow keys did nothing while the element that actually scrolls
   had none. So no `overflow` or `max-height` here: `.table-wrap` owns both. */
.fl-wide table { font-size: var(--t-micro); }
.fl-wide thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface);
}

@media (max-width: 720px) {
  .fl-legend { gap: var(--s-1) var(--s-2); }
}


/* ======================================================= RC16 Part 7
   MONTHLY RETURNS PAGINATION.

   `.btn.sm` and the `[aria-disabled="true"]` appearance are already declared
   in components.css; this is layout only. A control that cannot act is a
   `<span aria-disabled="true">` rather than a dead link -- a disabled anchor
   is still focusable and still announces as a link, which invites a reader to
   press something that will not move. No animation, so a reduced-motion
   preference has nothing to suppress. */
.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-3);
}
.pg-at {
  font-size: var(--t-micro);
  color: var(--muted);
  margin: 0 var(--s-2);
}

/* A disabled pager control is a span, so `:disabled` never matches it and the
   base `.btn:hover:not(:disabled)` and `:active:not(:disabled)` rules still
   fire -- a control that cannot act was lighting up and playing the press
   scale. Specificity beats both. */
.pager .btn[aria-disabled="true"]:hover,
.pager .btn[aria-disabled="true"]:active {
  background: var(--surface);
  border-color: var(--line);
  transform: none;
}

/* `.u-sub` is declared only under `.perf-table`, so the partial markers, the
   unit suffixes in the drawdown headers and the stress table's measured-dates
   line rendered at full body size and colour. Scoped to the Part 7 tables
   rather than made global, which would restyle every screen that has one. */
#hist-annual .u-sub,
#hist-monthly .u-sub,
#hist-stress .u-sub,
[id^="hist-dd-"] .u-sub {
  font-size: var(--t-micro);
  color: var(--muted);
}

@media (max-width: 720px) {
  .pager { gap: var(--s-1); }
  .pg-at { flex-basis: 100%; margin: 0; order: -1; }
}

/* ============================================================ RC16 Part 8
   Assets: the grouped annual bars and the monthly correlation heatmap.

   The bar chart reuses `.lc-grid`, `.lc-ylab`, `.lc-xlab`, `.lc-legend` and
   `.lc-name` from the line chart rather than declaring its own: a tick label
   is a tick label, and two sets of rules for one is how two charts on one
   screen come to disagree about type size. Only what is genuinely new to a
   bar is below. */

.br-fig { margin: 0; }

.br-svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.br-bar {
  /* A hairline of surface between a bar and whatever is behind it, so two
     adjacent bars of similar tone still read as two. `non-scaling-stroke`
     because the viewBox is scaled to the column width and a 1-unit stroke
     would otherwise thicken on a narrow screen and eat the bar. */
  stroke: var(--surface);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

/* The zero line is not a gridline. A bar chart of returns is read against it
   -- above is a gain and below is a loss -- so it carries more weight than
   the intervals either side. */
.br-zero {
  stroke: var(--rule-2);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
}

/* A category with no measured value for this year. It keeps its slot and says
   so; the alternative is a zero-height bar sitting on the axis, which reads
   as a year the security went nowhere. */
.br-absent {
  fill: var(--muted);
  /* 13u rather than `--t-micro`: this is a viewBox unit, not a CSS pixel, and
     a token sized for HTML text means something else inside a scaled SVG. */
  font-size: 13px;
  font-style: italic;
}

/* The same ladder `.lc-ylab` and `.lc-xlab` are on. Without it this renders
   at about 4px on a phone -- and it is the ONLY cue distinguishing a bar that
   is missing from one that is a genuine 0.00%. */
@media (max-width: 1180px) {
  .br-absent { font-size: 16px; }
}

@media (max-width: 760px) {
  .br-absent { font-size: 19px; }
}

@media (max-width: 560px) {
  .br-absent { font-size: 22px; }
}

/* A filled block rather than `.lc-swatch`'s 2.5px rule: that one is a line's
   swatch and a bar is an area. */
.br-swatch {
  width: 12px;
  height: 12px;
  border-radius: 2px;
  flex: none;
}

/* The legend is `aria-hidden` and inert -- the table names every bar -- so the
   button affordances `.lc-key` carries must not survive onto it. */
.br-legend { margin-top: var(--s-2); }

.br-legend .lc-key {
  cursor: default;
  background: none;
}

.br-legend .lc-key:hover { background: none; }
.br-legend .lc-key:active { transform: none; }

/* No correlation-cell rules here: Monthly Correlations reuses `.corr-grid`,
   which already owns the per-theme mix fraction, the hatched unmeasurable
   cell, the `data-self` diagonal and the sticky row header. A second ramp
   would have meant positive correlation reading orange on Stats and blue on
   Assets. */

/* `.u-sub` is declared per surface (see the note above the Part 7 block), so
   the Part 8 tables need their own. Without it the ticker beside a name, the
   Portfolio/Asset kind word and the year beside a best-year figure all render
   at body size and colour, and read as content rather than as annotation. */
/* RC16.1: the modeled stress table's Evidence column, which says what each
   row's basis actually rests on -- session counts, a resolved share, the
   benchmark a beta was fitted against, or the reason there is nothing. It is
   annotation beside a figure rather than a figure, so it takes the same
   treatment every other `.u-sub` does. Added here because this stylesheet
   declares `.u-sub` per surface by deliberate convention; without its own rule
   the column would render at body weight and read as a measurement. */
#stress-holdings .u-sub,
#assets-correlations .u-sub,
.asset-table .u-sub {
  display: block;
  font-size: var(--t-micro);
  color: var(--muted);
  font-weight: 400;
}

/* A total row is a summary of the rows above it and is separated from them,
   rather than looking like one more security. */
.asset-table tr.total > *,
.asset-table tr.total th {
  border-top: 2px solid var(--rule-2);
  font-weight: 600;
}

/* The per-portfolio sub-heading inside a section. `h3` alone falls back to the
   base scale, which is nearly the section's own h2. */
h3.sub {
  font-size: var(--t-body);
  font-weight: 600;
  margin: var(--s-4) 0 var(--s-2);
}

@media (forced-colors: active) {
  .br-bar { forced-color-adjust: none; }
  .br-swatch { forced-color-adjust: none; }
}

/* Advisor presentation: the firm's disclosure, the methodology notes and the
   marker on a pay date that has gone by. Subordinate to the figures -- quiet
   enough not to compete with the sheet, readable enough to be a disclosure. */
.sheet-disclosure { margin-top: var(--s-5); }
.sheet-disclosure-h { font-size: 0.8125rem; letter-spacing: 0.02em;
  text-transform: uppercase; color: var(--ink-2); margin: 0 0 var(--s-2); }
.sheet-disclosure-b { font-size: 0.75rem; line-height: 1.55; color: var(--ink-2);
  max-width: 80ch; margin: 0; }
.sheet-disclosure-v { font-size: 0.6875rem; color: var(--ink-3);
  margin: var(--s-2) 0 0; }
.sheet-method { font-size: 0.75rem; line-height: 1.5; color: var(--ink-2);
  max-width: 76ch; margin: var(--s-2) 0 0; }
.sheet-past { font-size: 0.6875rem; color: var(--ink-3); white-space: nowrap; }

/* The read-only Important Disclosures page. */
.doc-page { max-width: 80ch; }
.doc-body { font-size: 0.875rem; line-height: 1.7; color: var(--ink-1); margin: 0; }
.doc-meta { font-size: 0.75rem; color: var(--ink-3); margin: var(--s-4) 0 0; }
