/* ============================================================================
   Components.

   Every interactive component here ships default, hover, focus, active,
   disabled — and loading or selected where the component has that state. Half
   a state set is how two screens end up disagreeing about what "pressed" looks
   like.

   Hover is gated on `(hover: hover) and (pointer: fine)` throughout. On touch,
   :hover latches after a tap and leaves rows and controls looking permanently
   active. Press feedback is NOT gated, because a finger deserves the same
   confirmation a cursor gets.
   ========================================================================== */

/* ------------------------------------------------------------------- icons */

/* One drawn set, one stroke weight, one size. Icons are 16px on a 16px grid
   with a 1.5px stroke and round joins; nothing here is a glyph or an emoji
   standing in for a drawing. */
.icon {
  display: inline-block;
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
  vertical-align: -3px;
}

.icon-sm { width: 13px; height: 13px; vertical-align: -2px; }

/* ----------------------------------------------------------------- buttons */

/* One filled primary per view; everything else is quiet. A screen where every
   action is emphasised has no emphasis. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  height: var(--ctl);
  padding: 0 var(--s-3);
  border: var(--hair) solid var(--rule-2);
  border-radius: var(--r);
  background: var(--surface);
  color: var(--ink-2);
  font-family: var(--sans);
  font-size: var(--t-data);
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  /* Named properties, never `all`: `all` animates things you did not choose,
     including the ones that cost a layout pass. */
  transition: transform var(--d-press) var(--ease-out),
              background-color var(--d-fast) ease,
              border-color var(--d-fast) ease,
              color var(--d-fast) ease;
}

/* The press. 0.97 is felt rather than seen, and it is the single highest-value
   animation in the product: it is the interface confirming it heard the click.
   Faster than the release on purpose — the user's own action registers
   immediately, the system's return to rest may settle. */
.btn:active:not(:disabled) {
  transform: scale(0.97);
  transition-duration: 60ms;
}

.btn.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-on);
  font-weight: 600;
}

.btn.danger {
  color: var(--neg);
  border-color: color-mix(in oklab, var(--neg) 35%, var(--rule-2));
}

.btn.ghost {
  background: transparent;
  border-color: transparent;
  color: var(--muted);
}

.btn.icon-only {
  width: var(--ctl);
  padding: 0;
}

.btn.sm { height: var(--ctl-sm); padding: 0 var(--s-2); font-size: var(--t-micro); }
.btn.lg { height: var(--ctl-lg); padding: 0 var(--s-4); font-size: var(--t-body); }

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

@media (hover: hover) and (pointer: fine) {
  .btn:hover:not(:disabled) { background: var(--raised); border-color: var(--rule-2); color: var(--ink); }
  /* Mixed toward ink rather than set to it: in the dark theme --accent-ink is
     LIGHTER than --accent, so hovering there would move the surface and the
     label in the same direction and quietly flatten the contrast the token was
     introduced to protect. An 80/20 mix shifts perceptibly and keeps the pair. */
  .btn.primary:hover:not(:disabled) {
    background: color-mix(in oklab, var(--accent) 82%, var(--ink));
    border-color: color-mix(in oklab, var(--accent) 82%, var(--ink));
  }
  .btn.danger:hover:not(:disabled) { background: var(--neg-soft); border-color: var(--neg); }
  .btn.ghost:hover:not(:disabled) { background: var(--raised); color: var(--ink); border-color: transparent; }
}

/* Pending. The button keeps its width so the row does not reflow mid-request,
   swaps its icon for a spinner and states what it is doing. Used on the two
   actions here that genuinely take seconds — fetching a security's twenty-year
   history, and saving a version. */
.btn[data-pending] {
  pointer-events: none;
  color: var(--muted);
}

.btn[data-pending] .icon { display: none; }

.btn[data-pending]::before {
  content: "";
  width: 12px;
  height: 12px;
  border: 1.5px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  /* Fast. A quicker spinner makes the same wait feel shorter. */
  animation: spin 620ms linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* A button that reads as a link: for destructive-but-minor row actions, where a
   bordered control in every row would out-shout the data. */
.linkish {
  border: 0;
  background: none;
  padding: 0;
  color: var(--accent);
  font-family: var(--sans);
  font-size: var(--t-data);
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: color-mix(in oklab, var(--accent) 30%, transparent);
  text-underline-offset: 2px;
  transition: color var(--d-fast) ease, text-decoration-color var(--d-fast) ease;
}

@media (hover: hover) and (pointer: fine) {
  .linkish:hover { color: var(--accent-ink); text-decoration-color: currentColor; }
}

.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
}

.actions .spacer { margin-left: auto; }

/* Segmented control: two or three mutually exclusive views of the same data.
   A group of buttons, not tabs — tabs imply separate content. */
.segmented {
  display: inline-flex;
  padding: 2px;
  border: var(--hair) solid var(--rule-2);
  border-radius: var(--r);
  background: var(--sunken);
}

/* A segmented control with many options — the audit log's action filter grows one
   entry per distinct action recorded — wraps rather than pushing the whole content
   column past the viewport. `inline-flex` with no wrap is right for the two- and
   three-option cases every earlier phase used it for, and wrong the moment the set
   is data-driven. */
.segmented.wrap {
  flex-wrap: wrap;
  max-width: 100%;
  gap: 2px;
}

.segmented > * {
  /* `--ctl-sm`, not `calc(var(--ctl) - 6px)`. That computed to 22px, under the
     24px minimum WCAG 2.2 SC 2.5.8 sets for a pointer target — and `--ctl-sm`
     exists precisely because that floor was already fixed once, at the token, for
     in-table controls. These items sit gapless inside a 2px track, so neither the
     essential nor the spacing exception applies. */
  height: var(--ctl-sm);
  padding: 0 var(--s-3);
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.03em;
  text-transform: uppercase;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  transition: background-color var(--d-fast) ease, color var(--d-fast) ease;
}

.segmented > [aria-pressed="true"],
.segmented > [aria-current="true"] {
  background: var(--surface);
  color: var(--ink);
  font-weight: 600;
  box-shadow: 0 1px 2px rgb(16 22 28 / 0.08);
}

/* ------------------------------------------------------------------- forms */

input[type="text"],
input[type="number"],
input[type="search"],
input[type="password"],
select,
textarea {
  width: 100%;
  min-width: 0;
  height: var(--ctl);
  padding: 0 var(--s-2);
  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, background-color var(--d-fast) ease;
}

/* Figures are typed and read in columns, so they use the mono face and align
   right like every other number on the screen. */
input[type="number"],
input.mono-in { font-family: var(--mono); font-size: var(--t-data); }
input.num-in { font-family: var(--mono); font-size: var(--t-data); text-align: left; }

textarea {
  height: auto;
  min-height: 132px;
  padding: var(--s-2);
  line-height: var(--lh);
  resize: vertical;
}

select {
  padding-right: var(--s-2);
  cursor: pointer;
}

input::placeholder, textarea::placeholder { color: var(--faint); }

input:disabled, select:disabled, textarea:disabled {
  background: var(--sunken);
  color: var(--muted);
  cursor: not-allowed;
}

input[aria-invalid="true"],
select[aria-invalid="true"] {
  border-color: var(--neg);
  background: color-mix(in oklab, var(--neg-soft) 45%, var(--surface));
}

@media (hover: hover) and (pointer: fine) {
  input:hover:not(:disabled), select:hover:not(:disabled), textarea:hover:not(:disabled) {
    border-color: color-mix(in oklab, var(--accent) 30%, var(--rule-2));
  }
}

input:focus-visible, select:focus-visible, textarea:focus-visible {
  border-color: var(--accent);
  outline-offset: 1px;
}

label {
  display: block;
  margin-bottom: 3px;
  color: var(--muted);
  font-size: var(--t-micro);
}

fieldset { min-width: 0; margin: 0; padding: 0; border: 0; }

/* Controls inside a data table size to their CONTENT, not to their column.
   Table layout gives a column whatever width its header needs, and a stretched
   input turns a six-character ticker into a 250px box with the text stranded at
   one end — and puts the input's left edge nowhere near the right-aligned header
   above it. These caps are the difference between a form and a spreadsheet. */
td .mono-in { min-width: 74px; max-width: 108px; }
td .num-in  { min-width: 70px; max-width: 96px; }
td select   { min-width: 78px; max-width: 96px; }
td .mono-in, td .num-in, td select { height: var(--ctl-sm); }

.field-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: var(--s-3) var(--s-4);
}

.field-row.tight { grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); }

.check {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  margin: 0;
  color: var(--ink-2);
  font-size: var(--t-data);
  cursor: pointer;
}

.check input { width: auto; height: auto; margin: 0; }

/* Search field with the icon inside it. The icon is decoration for a control
   that already has a label, so it is aria-hidden and the input keeps its own
   accessible name. */
.search {
  position: relative;
  flex: 0 1 300px;
  min-width: 148px;
}

.search .icon {
  position: absolute;
  top: 50%;
  left: var(--s-2);
  transform: translateY(-50%);
  color: var(--faint);
  pointer-events: none;
  transition: color var(--d-fast) ease;
}

.search input { padding-left: 28px; }
.search:focus-within .icon { color: var(--accent); }

/* The keyboard hint, shown only where a keyboard exists. */
.search kbd {
  position: absolute;
  top: 50%;
  right: var(--s-2);
  transform: translateY(-50%);
  padding: 1px 4px;
  border: var(--hair) solid var(--rule-2);
  border-radius: var(--r-sm);
  background: var(--raised);
  color: var(--faint);
  font-size: var(--t-micro);
  line-height: 1.1;
  pointer-events: none;
  transition: opacity var(--d-fast) ease;
}

.search:focus-within kbd { opacity: 0; }

@media (pointer: coarse) { .search kbd { display: none; } }

/* ------------------------------------------------------------------ panels */

/* Elevation declared once: a hairline border and no shadow. Panels do not nest
   — a panel inside a panel is the tell that a layout was assembled rather than
   composed, and it costs 2px of rule and 28px of padding for nothing. */
.panel {
  border: var(--hair) solid var(--rule);
  border-radius: var(--r);
  background: var(--surface);
}

.panel-head {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  padding: var(--s-3) var(--edge-x);
  border-bottom: var(--hair) solid var(--rule);
}

.panel-head h2, .panel-head h3 {
  font-size: var(--t-lead);
  font-weight: 620;
}

.panel-head .meta {
  margin-left: auto;
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--muted);
  white-space: nowrap;
}

.panel-head .actions { margin-left: auto; }

.panel-body { padding: var(--s-4) var(--edge-x); }
.panel-body.flush { padding: 0; }

.panel-foot {
  padding: var(--s-3) var(--edge-x);
  border-top: var(--hair) solid var(--rule);
  font-size: var(--t-data);
  color: var(--muted);
}

/* A section is a region of the page, not a box. The heading sits on a rule
   that also carries the region's own count or state, which is how a heading
   earns its line. */
.section { margin-bottom: var(--s-6); }

.section-head {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  padding-bottom: var(--s-2);
  border-bottom: var(--hair) solid var(--rule-2);
  margin-bottom: var(--s-3);
}

.section-head h2 { font-size: var(--t-lead); font-weight: 620; }

.section-head .meta {
  margin-left: auto;
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--muted);
}

.section-head .actions { margin-left: auto; }

/* ------------------------------------------------------------------ tables */

/* The most important component in the product. Density, alignment and a header
   that stays put outrank every visual flourish available. */
.table-wrap {
  position: relative;
  border: var(--hair) solid var(--rule);
  border-radius: var(--r);
  background: var(--surface);
  overflow: auto;
  max-height: 66vh;
  overscroll-behavior-x: contain;
}

.table-wrap.tall { max-height: none; }

/* A shadow at the scrolled edge, so a table that continues sideways says so
   rather than looking as though the last column is the last column. Driven by a
   data attribute the script sets, not by :hover. */
.table-wrap[data-scroll-x="right"]::after,
.table-wrap[data-scroll-x="both"]::after {
  content: "";
  position: sticky;
  z-index: calc(var(--z-sticky) + 1);
  float: right;
  right: 0;
  top: 0;
  width: 18px;
  height: 100%;
  margin-left: -18px;
  background: linear-gradient(to right, transparent, rgb(16 22 28 / 0.09));
  pointer-events: none;
}

table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
}

thead th {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  padding: var(--row-pad) var(--cell-x);
  background: var(--raised);
  border-bottom: var(--hair) solid var(--rule-2);
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  text-align: left;
  white-space: nowrap;
}

/* Body-cell classes carry their own size, and on a header cell they would win
   on specificity and put two type sizes in one header row. */
thead th.num, thead th.mono, thead th.tick {
  font-size: var(--t-micro);
  letter-spacing: var(--tr-label);
}

/* Figures are LEFT aligned across the analytical product, by request of the
   portfolio managers who read these tables all day. The column is still a
   column: the mono face and the inherited lining figures keep digits in
   register, so the decimal points line up without the ragged left edge that
   right alignment gives a column of mixed-width values.

   The advisor's model sheet is the declared exception and keeps its figures on
   the right -- it reproduces a spreadsheet the firm already uses, and a
   spreadsheet right-aligns numbers. That exception is appended at the end of
   this file so the two rules read together. */
thead th.num { text-align: left; }

tbody td {
  padding: var(--row-pad) var(--cell-x);
  border-bottom: var(--hair) solid var(--rule);
  vertical-align: baseline;
  white-space: nowrap;
}

tbody tr:last-child td { border-bottom: 0; }

/* Content should not touch the frame. Cell padding carries the interior
   rhythm; the outer columns take a wider inset so the table reads as inset
   content rather than as edge-to-edge fill. */
thead th:first-child, tbody td:first-child { padding-left: var(--edge-x); }
thead th:last-child, tbody td:last-child { padding-right: var(--edge-x); }

.num { text-align: left; font-family: var(--mono); font-size: var(--t-data); }
.mono { font-family: var(--mono); font-size: var(--t-data); }

.tick {
  font-family: var(--mono);
  font-size: var(--t-data);
  font-weight: 600;
  letter-spacing: 0.01em;
}

/* A security name is prose in a data column: it wraps rather than truncating,
   because "iShares Russell 2000 E…" costs the reader the meaning to save the
   layout nothing. */
.name {
  white-space: normal;
  min-width: 18ch;
  max-width: 34ch;
  color: var(--ink-2);
}

.wrap { white-space: normal; }

/* Row hover is deliberately NOT transitioned. Sweeping down a dense table with
   a 140ms fade leaves a trailing smear of half-lit rows; an instant change
   tracks the cursor exactly and reads crisper. */
@media (hover: hover) and (pointer: fine) {
  tbody tr:hover td { background: var(--raised); }
  tbody tr[data-href]:hover { cursor: pointer; }
}

tbody tr[aria-selected="true"] td {
  background: var(--accent-soft);
  box-shadow: inset 2px 0 0 var(--accent);
}

/* Sortable headers. The affordance exists only where the sort actually works,
   so it is applied by the script rather than by the markup. */
th[data-sort] {
  cursor: pointer;
  user-select: none;
}

/* The header's control. Styled here rather than with inline attributes: as an
   inline `style` it could not restate `text-transform`, which is set on the `th`
   and reset to `none` by the button's own UA rules — so the sortable headers
   rendered in sentence case while every other column label was uppercase. Two
   header styles in one table. */
th[data-sort] .th-sort {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: color var(--d-fast) ease;
}

th[data-sort] .sort-mark {
  width: 11px;
  height: 11px;
  stroke-width: 2;
  /* --faint, not --rule-2. This is the affordance that says the column sorts, and
     at --rule-2 it measured 1.35:1 against the header band — present in the DOM
     and invisible on screen, which is the same as not having it. A rule colour is
     for rules. */
  color: var(--faint);
  transition: color var(--d-fast) ease, transform var(--d-fast) var(--ease-out);
}

th[data-sort][aria-sort] .sort-mark { color: var(--accent); }
th[data-sort][aria-sort="descending"] .sort-mark { transform: rotate(180deg); }

@media (hover: hover) and (pointer: fine) {
  th[data-sort]:hover .th-sort { color: var(--ink-2); }
  th[data-sort]:hover .sort-mark { color: var(--ink-2); }
}

/* A key/value table has no thead, so `thead th` never matches it and the cells
   would fall back to the browser default — 13px bold sans, the heaviest text
   on the page, labelling values that are lighter than their own label. A row
   label is the same role as a column label and gets the same treatment. */
.kv { width: 100%; }

/* The label treatment is restated in full, not inherited.
   `thead th` carries the mono face, the micro step, the tracking and the case —
   and a .kv table has no <thead>, so none of it applies. Relying on inheritance
   here left these cells on the browser default: 13px bold sans, the heaviest
   text on the page, labelling values lighter than their own label, with
   letter-spacing applied to lowercase sans. A row label is the same role as a
   column label and gets the same treatment. */
.kv th {
  position: static;
  width: 210px;
  padding: var(--row-pad) var(--cell-x);
  background: transparent;
  border-bottom: var(--hair) solid var(--rule);
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  /* Row-header cells default to centre alignment, which left them visibly out of
     line with every other column on the surface. */
  text-align: left;
  vertical-align: baseline;
  white-space: nowrap;
}

.kv th:first-child { padding-left: var(--edge-x); }

.kv tbody tr:last-child th { border-bottom: 0; }
.kv td { color: var(--ink-2); }

@media (max-width: 720px) {
  .kv th { width: auto; white-space: nowrap; }
}

/* -------------------------------------------------------- state vocabulary */

/* Health only, and rationed. Anything ordinal is expressed as a ramp so it
   reads as a scale rather than as three unrelated words. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 6px;
  border-radius: var(--r-pill);
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.03em;
  white-space: nowrap;
}

.chip.pos  { background: var(--pos-soft);  color: var(--pos); }
.chip.neg  { background: var(--neg-soft);  color: var(--neg); }
.chip.warn { background: var(--warn-soft); color: var(--warn); }
.chip.info { background: var(--info-soft); color: var(--info); }
.chip.quiet { background: var(--raised); color: var(--muted); }

/* The top bar's own chip. `quiet` on --raised is all but invisible against
   --chrome, so this one is transparent BY DECISION rather than by accident: the
   dot carries the state and the word carries the detail. */
.chip.bare { background: transparent; color: var(--ink-2); padding-inline: 0; }

.chip.outline {
  background: transparent;
  border: var(--hair) solid currentColor;
  font-weight: 500;
}

/* Severity, in the table. Squared rather than pill: it labels a row, and a pill
   in a data cell reads as a button. */
.sev {
  display: inline-block;
  padding: 1px 5px;
  border-radius: var(--r-sm);
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.sev-critical { background: var(--neg-soft);  color: var(--neg); }
.sev-warning  { background: var(--warn-soft); color: var(--warn); }
.sev-info     { background: var(--raised);    color: var(--muted); }

/* The exception earns the weight; the norm stays quiet — and a border is not
   quiet. Eight bordered boxes down one column read as a stack of disabled
   inputs, which is a false affordance beside the real controls in the same
   table. So the norm is plain text and only the exceptional states get a box. */
.tag {
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.03em;
  color: var(--muted);
  white-space: nowrap;
}

/* A CHIP THAT IS A LINK HAS TO LOOK LIKE ONE, and until RC16 Part 2.6.1 it
   did not have to. Every chip carrying a product label used to be a link, so
   the column was uniform and the hairline underline inherited from `a` was
   never the only thing distinguishing two cells. Eligibility is structural
   now, so the SAME WORDS are a link on one row and plain text on the next --
   and `.tag`'s own `color` beats the element rule on `a`, which left that
   30%-opacity hairline as the single cue. Colour alone would not have been
   enough either; here there was not even a colour difference.

   An underline at full strength in the text's own colour, rather than
   recolouring the chip: the muted tone is what keeps eight of these down one
   column from reading as a stack of controls, which is the note above, and
   that reasoning did not stop being true. Screen-reader users were never
   affected -- the link role and the `Fund detail for TICKER:` prefix always
   distinguished them. This is for everybody reading the column by eye. */
a.tag {
  text-decoration-color: currentColor;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

.tag.exception, .tag.unknown {
  display: inline-block;
  padding: 1px 5px;
  border: var(--hair) solid;
  border-radius: var(--r-sm);
  font-weight: 600;
}

.tag.exception {
  border-color: color-mix(in oklab, var(--accent) 40%, var(--rule-2));
  background: var(--accent-soft);
  color: var(--accent-ink);
}

.tag.unknown {
  border-color: color-mix(in oklab, var(--neg) 40%, var(--rule-2));
  background: var(--neg-soft);
  color: var(--neg);
}

/* An ordinal ramp, not three colours. */
.conf { font-family: var(--mono); font-size: var(--t-data); }
.conf-high     { color: var(--ink); font-weight: 600; }
.conf-moderate { color: var(--muted); }
.conf-low      { color: var(--warn); }
.conf-none     { color: var(--neg); font-weight: 600; }

/* A data gap is worth seeing, not worth shouting about. */
.gap {
  color: var(--warn);
  text-decoration: underline dotted color-mix(in oklab, var(--warn) 55%, transparent);
  text-underline-offset: 2px;
  cursor: help;
}

.dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--pos);
  flex: 0 0 auto;
}

.dot.bad  { background: var(--neg); }
.dot.warn { background: var(--warn); }
.dot.off  { background: var(--rule-2); }

/* ----------------------------------------------------------- notifications */

/* Errors block the save; warnings describe something the user may still want.
   The two must not look alike, or the warning list trains people to ignore
   both. The rule is a hairline plus a 2px accent edge — 2px is the ceiling, so
   the callout reads as annotated rather than as a coloured slab. */
/* Severity is carried by the icon and by a tinted surface, NOT by a thick edge.
   A 2px coloured border-left on a callout is the most recognisable tell of a
   generated interface, and it also reads as a nesting cue this component does not
   have. One hairline border tinted from the same hue does the same job at a
   quarter of the weight, and the two channels — glyph and ground — survive a
   reader who cannot separate the hues. */
.callout {
  display: flex;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border: var(--hair) solid color-mix(in oklab, var(--accent) 32%, var(--rule-2));
  border-radius: var(--r-sm);
  background: color-mix(in oklab, var(--accent-soft) 55%, var(--surface));
  color: var(--ink-2);
  font-size: var(--t-data);
}

.callout > .icon { margin-top: 1px; color: var(--accent); }

.callout.warn {
  border-color: color-mix(in oklab, var(--warn) 32%, var(--rule-2));
  background: color-mix(in oklab, var(--warn-soft) 60%, var(--surface));
}
.callout.warn > .icon { color: var(--warn); }

.callout.crit {
  border-color: color-mix(in oklab, var(--neg) 34%, var(--rule-2));
  background: color-mix(in oklab, var(--neg-soft) 70%, var(--surface));
}
.callout.crit > .icon { color: var(--neg); }

.callout b { color: var(--ink); }
.callout .body { min-width: 0; }

.banner {
  padding: var(--s-4);
  border: var(--hair) solid color-mix(in oklab, var(--neg) 35%, var(--rule-2));
  border-radius: var(--r);
  background: var(--neg-soft);
}

.banner h2 { margin-bottom: var(--s-1); font-size: var(--t-lead); color: var(--neg); }
.banner p { font-size: var(--t-data); color: var(--ink-2); }

/* Validation list. The severity word is a fixed-width mono column so the
   messages start on one left edge and the list can be scanned by shape. */
.issues { list-style: none; display: grid; gap: var(--s-1); margin: var(--s-3) 0 0; }

.issues li {
  display: flex;
  gap: var(--s-3);
  align-items: baseline;
  font-size: var(--t-data);
  color: var(--ink-2);
}

.issues .lvl {
  flex: 0 0 62px;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.issues .lvl-error { color: var(--neg); }
.issues .lvl-warning { color: var(--warn); }
.issues .lvl-info { color: var(--muted); }

/* ------------------------------------------------------------ empty states */

/* Teaches the interface rather than reporting its absence. Left-aligned, with
   the action that resolves it — a centred "Nothing here" with an illustration
   is decoration where an instruction belongs. */
.empty {
  padding: var(--s-6) var(--edge-x);
  color: var(--muted);
  font-size: var(--t-data);
}

.empty b {
  display: block;
  margin-bottom: 3px;
  color: var(--ink-2);
  font-size: var(--t-body);
}

.empty .actions { margin-top: var(--s-4); }
.empty code { color: var(--ink-2); }

/* ---------------------------------------------------------------- skeleton */

/* For the one thing on this surface that is genuinely asynchronous: the
   security lookup. A shimmering block where content will land, not a spinner
   floating in the middle of a panel. */
.skeleton {
  border-radius: var(--r-sm);
  background: linear-gradient(90deg, var(--raised) 25%, var(--sunken) 50%, var(--raised) 75%);
  background-size: 220% 100%;
  animation: sheen 1.1s linear infinite;
}

@keyframes sheen {
  from { background-position: 120% 0; }
  to   { background-position: -20% 0; }
}

.skeleton-line { height: 9px; margin: var(--s-2) 0; }

/* -------------------------------------------------------------------- menu */

/* An anchored popover. It scales from its trigger rather than from its own
   centre, because a menu that grows out of the middle of itself is not
   attached to anything the user pointed at. Never from scale(0) — nothing in
   the world appears out of nothing. */
.menu {
  position: absolute;
  z-index: var(--z-menu);
  min-width: 190px;
  padding: var(--s-1);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--lift);
  transform-origin: var(--menu-origin, top right);
  opacity: 0;
  transform: scale(0.97) translateY(-2px);
  pointer-events: none;
  transition: opacity var(--d) var(--ease-out), transform var(--d) var(--ease-out);
}

.menu[data-open] {
  opacity: 1;
  transform: scale(1) translateY(0);
  pointer-events: auto;
}

.menu-item {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  width: 100%;
  height: var(--ctl);
  padding: 0 var(--s-3);
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--ink-2);
  font-family: var(--sans);
  font-size: var(--t-data);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--d-fast) ease, color var(--d-fast) ease;
}

.menu-item .icon { color: var(--muted); }
.menu-item:hover, .menu-item[data-active] { background: var(--raised); color: var(--ink); }
.menu-item:hover .icon { color: var(--accent); }
.menu-item .hint { margin-left: auto; color: var(--faint); font-family: var(--mono); font-size: var(--t-micro); }
.menu-sep { height: var(--hair); margin: var(--s-1) var(--s-2); background: var(--rule); }

.menu-empty { padding: var(--s-3); color: var(--muted); font-size: var(--t-data); }

/* A full-width row that navigates. Distinct from .menu-item because it lives in
   page flow rather than in a popover, and its trailing chevron sits at the row's
   end — inside a menu it would sit against the label. */
.link-row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  color: var(--ink-2);
  font-size: var(--t-body);
  text-decoration: none;
  transition: background-color var(--d-fast) ease, color var(--d-fast) ease;
}

.link-row > .icon:first-child { color: var(--accent); }
.link-row .go { margin-left: auto; color: var(--faint); }

@media (hover: hover) and (pointer: fine) {
  .link-row:hover { background: var(--raised); color: var(--ink); }
  .link-row:hover .go { color: var(--accent); }
}

/* Phase 9. A stack of `.link-row`s divided by hairlines rather than spaced
   apart: these are a set to be read down, and the rule is what makes them read
   as one list. */
.link-rows { display: flex; flex-direction: column; }
.link-rows > .link-row + .link-row { border-top: var(--hair) solid var(--rule); }
.link-rows > .link-row { border-radius: 0; padding: var(--s-3); }

/* Two lines inside one row: the destination, and why you would go there. The
   second line is the reason these are rows and not a list of bare links — a
   navigation menu that only names its destinations makes the reader guess. */
.link-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.link-main > b { color: var(--ink); font-weight: 600; }
.link-main > span { color: var(--muted); font-size: var(--t-data); }
.link-row .go { flex: 0 0 auto; margin-left: auto; color: var(--faint); }

/* A form field whose value is not the reader's to change here, because it was
   decided upstream — the open model on a model screen. Stated, not disabled: a
   greyed-out select still invites a click and still reads as broken. */
.field-fixed {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  justify-content: flex-end;
}

.field-fixed > b {
  height: var(--ctl);
  display: flex;
  align-items: center;
  padding: 0 var(--s-3);
  /* No accent edge. A 2px coloured border on the left of a field is the shape
     this design system reserves for nothing, and the sunken surface plus the
     weight already say "decided elsewhere". */
  border: var(--hair) solid var(--rule);
  background: var(--sunken);
  color: var(--ink);
  font-size: var(--t-body);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The count beside a tab label. Mono and quiet: it is a quantity, and it must
   not compete with the word it qualifies. */
.seg-count {
  margin-left: var(--s-1);
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--faint);
}

.segmented > [aria-current="true"] .seg-count { color: var(--muted); }

/* -------------------------------------------------------- meter / progress */

/* The number that decides whether a portfolio can be saved, so it is stated as
   text first and the bar is support. A bar alone cannot distinguish 99.4% from
   100%, and that is exactly the difference that matters. */
.meter { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--s-3); }

.meter-v {
  font-family: var(--mono);
  font-size: var(--t-figure);
  letter-spacing: var(--tr-figure);
  /* The value changes as the user types. A colour transition here is legible
     where a position transition would be noise. */
  transition: color var(--d-fast) ease;
}

.meter.ok .meter-v { color: var(--pos); }
.meter.off .meter-v { color: var(--neg); }

.meter-track {
  flex: 0 1 280px;
  min-width: 130px;
  height: 4px;
  border-radius: var(--r-pill);
  background: var(--sunken);
  overflow: hidden;
}

/* display:block is load-bearing: these fills are spans, and an inline box
   ignores width and height, which renders the bar as an empty track that looks
   like a broken component rather than like a zero value.

   The fill is full-width and scaled along X rather than resized. It updates on
   every keystroke in the builder, and `width` is a layout property: animating it
   put a layout pass and a paint in the middle of typing. scaleX runs on the
   compositor, and `transform-origin: left` is what makes it grow from the track's
   start instead of from its middle. */
.meter-fill {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--pos);
  transform: scaleX(var(--fill, 0));
  transform-origin: left center;
  transition: transform var(--d) var(--ease-out), background-color var(--d-fast) ease;
}

.meter.off .meter-fill { background: var(--neg); }
.meter-note { color: var(--muted); font-size: var(--t-data); }

/* Top-of-window progress for a full navigation. Two pixels, accent, and it
   only exists while something is actually in flight. */
.route-progress {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 200;
  height: 2px;
  background: var(--accent);
  transform-origin: left center;
  transform: scaleX(0);
  opacity: 0;
  transition: transform 400ms var(--ease-out), opacity var(--d-fast) ease;
}

.route-progress[data-active] { opacity: 1; transform: scaleX(0.75); }

/* --------------------------------------------------------------- bar chart */

/* A proportion is read faster as a length than as a figure, so sector exposure
   is bars. Drawn with two elements and no library. */
.bars { display: grid; gap: var(--s-2); }

.bar-row {
  display: grid;
  grid-template-columns: minmax(96px, 168px) minmax(60px, 1fr) 62px;
  align-items: center;
  gap: var(--s-3);
}

.bar-k {
  overflow: hidden;
  color: var(--ink-2);
  font-size: var(--t-data);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bar-track { height: 8px; border-radius: var(--r-sm); background: var(--sunken); overflow: hidden; }

.bar-fill {
  display: block;
  height: 100%;
  background: var(--accent);
  border-radius: var(--r-sm);
}

/* A short position reduces its sector's exposure, so the bar changes colour
   rather than pretending a negative weight is a small positive one. */
.bar-fill.negative { background: var(--neg); }
.bar-v { text-align: right; color: var(--ink-2); font-family: var(--mono); font-size: var(--t-data); }

/* ------------------------------------------------------------- disclosure */

/* Progressive detail without a modal. A modal for "show me the rest of this
   row" interrupts a task that was not asking to be interrupted. */
.disclosure > summary {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  cursor: pointer;
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  list-style: none;
}

.disclosure > summary::-webkit-details-marker { display: none; }
.disclosure > summary .icon { transition: transform var(--d) var(--ease-out); }
.disclosure[open] > summary .icon { transform: rotate(90deg); }

@media (hover: hover) and (pointer: fine) {
  .disclosure > summary:hover { color: var(--ink-2); }
}

.disclosure-body { padding-top: var(--s-3); }

/* -------------------------------------------------------------- stat rows */

/* Paired key and value on one line. Used where four or five facts belong
   together but none of them leads — the alternative is five boxes, which
   promotes all five and therefore none. */
.stat-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-2) var(--s-5);
}

.stat { display: flex; align-items: baseline; gap: var(--s-2); }
.stat-k {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--muted);
}
.stat-v { font-family: var(--mono); font-size: var(--t-body); color: var(--ink); }

/* --------------------------------------------------------- reduced motion */

/* Gentler, not zero. Colour feedback still has to read, so only movement is
   dropped: collapsing every transition to 1ms removes useful feedback along
   with the motion, and a user who asked for less motion did not ask for less
   information.

   Last in the cascade, and !important on the movement properties, because
   these selectors tie on specificity with the component rules they override.
   When an equivalent block sat higher in a single stylesheet, the later
   component rule won and the preference was declared and silently ignored. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-property: background-color, border-color, color, opacity,
                         fill, stroke, box-shadow !important;
    transition-duration: var(--d-fast) !important;
  }

  /* Anything whose purpose IS the movement stops moving rather than moving
     fast. A 1ms slide is a flash, which is worse than an instant change. */
  .btn:active:not(:disabled) { transform: none !important; }
  .rail { transition: none !important; }
  .menu { transform: none !important; }
  .menu:not([data-open]) { transform: none !important; }
  .route-progress { transition-property: opacity !important; }
  .disclosure[open] > summary .icon { transition: none !important; }
  .skeleton { animation: none !important; background: var(--raised); }
  .btn[data-pending]::before { animation-duration: 900ms !important; animation-iteration-count: infinite !important; }
}

/* ============================================================ questionnaire
   (Phase 5)

   The app's first real radio-group form. The risk screen's funding control was
   hand-rolled with inline styles because it was one group of two; a seven-question
   questionnaire needs a component, not seven copies of that.

   No option is pre-selected anywhere in the markup — an unanswered question must
   stay unanswered, and a checked default would silently become an answer the
   investor never gave. The unanswered state is styled so it reads as incomplete
   rather than as neutral. */

.qset {
  border: var(--hair) solid var(--rule);
  border-radius: var(--r);
  padding: var(--s-4) var(--s-5);
  margin: 0 0 var(--s-4);
  background: var(--surface);
}

.qset[data-answered="no"] {
  border-left: 3px solid var(--warn);
}

.qset > legend {
  padding: 0 var(--s-2);
  font-size: var(--t-body);
  font-weight: 600;
  color: var(--ink);
  line-height: var(--lh);
}

.qset .qwhy {
  display: block;
  font-size: var(--t-data);
  color: var(--muted);
  line-height: var(--lh);
  margin: var(--s-2) 0 var(--s-4);
  max-width: 68ch;
}

.qopts { display: grid; gap: var(--s-1); }

/* The whole row is the hit target, not just the dot. A radio is small and a
   questionnaire is long. */
.qopt {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  cursor: pointer;
}

.qopt:hover { background: var(--raised); }

.qopt input[type="radio"] { accent-color: var(--accent); margin: 0; }

.qopt input[type="radio"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Selected rows are marked by weight and a rule as well as by background, so the
   selection survives a reader who cannot distinguish the two greys. */
.qopt:has(input:checked) {
  background: var(--accent-soft);
  box-shadow: inset 2px 0 0 var(--accent);
}
.qopt:has(input:checked) .qopt-t { font-weight: 600; }

.qopt-t { line-height: var(--lh); }

/* What this answer implies, in the units the score is built from. Shown per option
   so the instrument is legible rather than a black box — a reader can see that
   "about 20%" means a limit near 20% and not a mystery number of points. */
.qopt-v {
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--faint);
  white-space: nowrap;
}

.qopt:has(input:checked) .qopt-v { color: var(--accent-ink); }

/* Table cells default to `nowrap` so numeric columns stay aligned and a long
   ticker cannot reflow a row. That is right for data and wrong for prose: a cell
   holding a sentence forces the whole table wider than its container, and the
   sentence — which is usually the point of that column — ends up behind a
   horizontal scrollbar.

   `.wrap` opts one column back into wrapping. It carries a max-width because a
   sentence allowed to fill 1100px is no more readable than one that is clipped. */
th.wrap,
td.wrap {
  white-space: normal;
  max-width: 46ch;
  line-height: var(--lh);
}

/* The batch export sits in a page head's action row beside an ordinary link
   button. A form is a block, so without this it takes the whole row and pushes
   `New model` onto a second line — the same lesson as `.fr-xl` in the topbar,
   which is that a form standing in for a button has to be told it is one. */
.batch-export {
  display: contents;
}

/* A label for a GROUP of fields rather than for one input -- the builder's Risk
   Score Range is a Minimum and a Maximum that mean nothing apart. It spans the
   whole grid row so it sits above its two inputs instead of becoming a third
   column beside them, which is what `auto-fit` would otherwise do with it.

   The group is a `role="group"` with `aria-labelledby` pointing here rather
   than a `<fieldset><legend>`: every other control in this row is already a
   `<fieldset>` wrapping one input, and nesting fieldsets to group fieldsets
   reads as two levels of grouping to a screen reader where there is one. */
.field-legend {
  grid-column: 1 / -1;
  margin: 0 0 calc(var(--s-2) * -1) 0;
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: var(--tr-label, 0);
  color: var(--ink-2);
}


/* ------------------------------------------------------- figure alignment

   Two consequences of the left-aligned figure rule above.

   A body row header had no alignment rule of its own anywhere in this
   application, so it fell through to the browser default -- centre -- in every
   table that was not a key/value pair, an advisor sheet or a strategies
   comparison. Those three each fixed it locally and the general case stayed
   broken. One rule, and the local ones become redundant rather than wrong. */
tbody th { text-align: left; }

/* The advisor's model sheet reproduces the firm's own spreadsheet, so its
   figures stay where a spreadsheet puts them. Scoped to that one surface: it is
   a different product for a different role, and the portfolio managers' request
   was about the analytical screens they read. */
.advisor-sheet .num,
.advisor-sheet thead th.num { text-align: right; }

/* ------------------------------------------------------- time provenance

   The caption that says when the figures beneath it were measured. It carries
   dates and a session count, so it takes the mono face and lining figures like
   any other data, but it is chrome rather than content and stays muted.

   It wraps below the tablet breakpoint. A window caption is the longest string
   in a section heading and holding it on one line is what pushes a heading row
   into a horizontal scroll on a phone. */
.prov {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  white-space: nowrap;
}

@media (max-width: 720px) {
  .prov { white-space: normal; }
}


/* A provenance caption sitting under a band of figures or inside a figure's own
   sub-block, rather than in a section heading. Same type as the heading form;
   only the spacing differs. */
.band-when {
  margin: calc(var(--s-3) * -1) 0 var(--s-4) 0;
}

.figure-when {
  margin: var(--s-1) 0 0 0;
}
