/* ==========================================================================
   Cards & surfaces
   ========================================================================== */

.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-sm), inset 0 1px 0 var(--surface-highlight);
  padding: var(--card-pad);
  transition: box-shadow var(--dur-base) var(--ease-standard),
    transform var(--dur-base) var(--ease-standard),
    border-color var(--dur-base) var(--ease-standard);
}

.card--interactive:hover {
  box-shadow: var(--shadow-md), inset 0 1px 0 var(--surface-highlight);
  border-color: var(--color-border-strong);
  transform: translateY(-2px);
}

.card--flat {
  box-shadow: inset 0 1px 0 var(--surface-highlight);
}

/* Narrow viewports drive the density token rather than .card's padding
   directly, so the Compact preference and the mobile breakpoint can't fight
   each other. */
@media (max-width: 560px) {
  :root {
    --card-pad: var(--space-5);
  }

  :root[data-density="compact"] {
    --card-pad: var(--space-4);
  }
}

.card--glass {
  background: var(--glass-bg);
  border-color: var(--glass-border);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
}

.card__label {
  font-size: var(--fs-footnote);
  color: var(--color-text-secondary);
  font-weight: var(--fw-medium);
}

/* ---- KPI tile ---- */

.kpi {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.kpi__value {
  font-size: var(--fs-title1);
  font-weight: var(--fw-semibold);
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.kpi__delta {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  width: fit-content;
}

.kpi__delta--up {
  color: var(--color-profit);
  background: var(--color-profit-soft);
}

.kpi__delta--down {
  color: var(--color-loss);
  background: var(--color-loss-soft);
}

/* ---- Metric grid ----
   One card, N metrics, hairlines instead of a bordered card per number.
   Replaces the "one card = one number" pattern on Dashboard, Analytics and
   Growth Simulator.

   Separators are drawn as *leading* edges - border-left on every cell that
   isn't in column 1, border-top on every cell below row 1. That is exact
   for any item count, including a partially filled last row, which a
   trailing right/bottom border plus :last-child cannot do. */

.metric-grid {
  display: grid;
}

.metric-grid__item {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-4);
  min-width: 0;
}

.metric-grid__label {
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  color: var(--color-text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.metric-grid__value {
  font-size: var(--fs-title3);
  font-weight: var(--fw-semibold);
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.metric-grid__hint {
  font-size: var(--fs-caption);
  color: var(--color-text-tertiary);
}

.metric-grid--cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.metric-grid--cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.metric-grid--cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

.metric-grid--cols-2 .metric-grid__item:not(:nth-child(2n + 1)),
.metric-grid--cols-3 .metric-grid__item:not(:nth-child(3n + 1)),
.metric-grid--cols-4 .metric-grid__item:not(:nth-child(4n + 1)) {
  border-left: 1px solid var(--color-border);
}

.metric-grid--cols-2 .metric-grid__item:nth-child(n + 3),
.metric-grid--cols-3 .metric-grid__item:nth-child(n + 4),
.metric-grid--cols-4 .metric-grid__item:nth-child(n + 5) {
  border-top: 1px solid var(--color-border);
}

/* 2-column fallback: reset the wider layouts' separators, then re-derive
   them for two columns with the same leading-edge rule. */
@media (max-width: 900px) {
  .metric-grid--cols-3,
  .metric-grid--cols-4 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .metric-grid--cols-3 .metric-grid__item,
  .metric-grid--cols-4 .metric-grid__item {
    border-left: none;
    border-top: none;
  }

  .metric-grid--cols-3 .metric-grid__item:not(:nth-child(2n + 1)),
  .metric-grid--cols-4 .metric-grid__item:not(:nth-child(2n + 1)) {
    border-left: 1px solid var(--color-border);
  }

  .metric-grid--cols-3 .metric-grid__item:nth-child(n + 3),
  .metric-grid--cols-4 .metric-grid__item:nth-child(n + 3) {
    border-top: 1px solid var(--color-border);
  }
}

/* A metric grid fills its card edge-to-edge - the cell padding replaces the
   card's own, so the hairlines run the full width. */
.card--metrics {
  padding: 0;
  overflow: hidden;
}

/* ---- KPI cards (icon tile + label + value) ----
   Used where only a handful of metrics are shown, so the card chrome reads
   as hierarchy rather than waste. The dense .metric-grid above is still the
   right choice past ~6 metrics. */

.kpi-cards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--stack-gap);
}

@media (min-width: 1100px) {
  .kpi-cards {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

.kpi-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.kpi-card__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
}

.kpi-card__icon {
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
}

.kpi-card__icon--profit { color: var(--color-profit); background: var(--color-profit-soft); }
.kpi-card__icon--loss { color: var(--color-loss); background: var(--color-loss-soft); }
.kpi-card__icon--accent { color: var(--color-accent); background: var(--color-accent-soft); }
.kpi-card__icon--neutral { color: var(--color-text-secondary); background: var(--color-surface-hover); }

.kpi-card__label {
  font-size: var(--fs-footnote);
  color: var(--color-text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.kpi-card__value {
  font-size: var(--fs-title1);
  font-weight: var(--fw-bold);
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

/* ---- Pill toggle ----
   The single segmented-control family: unit ($/R), time range, chart mode
   and breakdown dimension all render through TJ.Components.UI.pillToggle()
   and therefore share shape, height, radius, spacing and active state.
   Deliberately quieter than the older .segmented (which stays for the
   Journal tab bar and the in-form Buy/Sell style switches) - these steer
   content, they are not navigation. */

.pill-toggle {
  display: inline-flex;
  flex-shrink: 0;
  max-width: 100%;
  padding: 2px;
  gap: 2px;
  border-radius: var(--radius-pill);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
}

.pill-toggle__opt {
  flex-shrink: 0;
  min-width: 28px;
  height: 24px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-pill);
  background: none;
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  white-space: nowrap;
  color: var(--color-text-tertiary);
  transition: color var(--dur-fast) var(--ease-standard),
    background-color var(--dur-fast) var(--ease-standard);
}

.pill-toggle__opt:hover {
  color: var(--color-text-secondary);
}

.pill-toggle__opt.is-active {
  background: var(--color-surface-hover);
  color: var(--color-accent);
}

/* Touch sizing without growing the visual chrome. */
@media (max-width: 880px), (pointer: coarse) {
  .pill-toggle__opt {
    min-width: 34px;
    height: 30px;
  }
}

/* A control with many options (the 5-step time range) may need to scroll
   on the narrowest phones rather than force its row wider. */
.pill-toggle--scroll {
  overflow-x: auto;
  scrollbar-width: none;
}

.pill-toggle--scroll::-webkit-scrollbar {
  display: none;
}

/* ---- Collapsible variant ----
   Rests as a single pill showing only the current selection, and fans out
   to the full option row on hover, keyboard focus, or tap. Same visual
   family as every other pill toggle - only the resting footprint changes.

   The inactive options are collapsed with max-width/padding/opacity rather
   than `display: none` so the expansion can animate and so they stay in the
   accessibility tree. `.is-open` is set by JS for touch, where there is no
   hover to rely on. */
.pill-toggle--collapsible .pill-toggle__opt:not(.is-active) {
  max-width: 0;
  min-width: 0;
  padding-left: 0;
  padding-right: 0;
  opacity: 0;
  pointer-events: none;
  overflow: hidden;
  transition: max-width var(--dur-base) var(--ease-standard),
    padding var(--dur-base) var(--ease-standard),
    opacity var(--dur-fast) var(--ease-standard),
    color var(--dur-fast) var(--ease-standard);
}

.pill-toggle--collapsible:hover .pill-toggle__opt:not(.is-active),
.pill-toggle--collapsible:focus-within .pill-toggle__opt:not(.is-active),
.pill-toggle--collapsible.is-open .pill-toggle__opt:not(.is-active) {
  max-width: 8rem;
  padding-left: var(--space-3);
  padding-right: var(--space-3);
  opacity: 1;
  pointer-events: auto;
}

/* No affordance glyph: the pill's own shape and the accent-coloured active
   option are enough, and a chevron read as a separate control sitting next
   to the value. */

/* ---- Hero equity card ---- */

.hero-card {
  background: linear-gradient(160deg, var(--color-surface) 0%, var(--color-surface-raised) 100%);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-2xl);
  padding: var(--space-8);
  box-shadow: var(--shadow-md);
  position: relative;
  overflow: hidden;
}

.hero-card::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(560px 200px at 85% -10%, rgba(var(--color-accent-rgb), 0.16), transparent 70%),
    radial-gradient(420px 180px at 5% 120%, rgba(var(--color-accent-rgb), 0.08), transparent 70%);
  pointer-events: none;
}

.hero-card__figure {
  font-size: var(--fs-hero);
  font-weight: var(--fw-bold);
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* ==========================================================================
   Chart card - shared wrapper for any card hosting TJ.Components.Charts
   .lineChart() (Dashboard's equity curve, Growth Simulator's projection).
   ========================================================================== */

.chart-card {
  position: relative;
  overflow: hidden;
}

.chart-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(90deg, transparent, rgba(var(--color-accent-rgb), 0.5), transparent);
}

.chart-card__canvas-wrap {
  position: relative;
  height: 240px;
  margin-top: var(--space-4);
}

/* Placeholder state (mixed currencies / no data). Same box, so swapping
   between a chart and a message never changes the card's height. */
.chart-card__canvas-wrap--message {
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ==========================================================================
   Chart tooltip (opt-in `tooltip` config on TJ.Components.Charts.lineChart)
   ========================================================================== */

.chart-tooltip {
  position: absolute;
  top: 4px;
  left: 0;
  transform: translateX(-50%);
  background: var(--glass-bg);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  box-shadow: var(--shadow-md);
  font-size: var(--fs-caption);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-standard);
  white-space: nowrap;
  z-index: 2;
}

.chart-tooltip.is-visible {
  opacity: 1;
}

.chart-tooltip__value {
  font-weight: var(--fw-semibold);
  color: var(--color-text-primary);
  font-variant-numeric: tabular-nums;
}

.chart-tooltip__date {
  color: var(--color-text-tertiary);
  font-size: 10px;
  margin-top: 2px;
}

/* ==========================================================================
   Buttons
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-md);
  font-size: var(--fs-footnote);
  font-weight: var(--fw-semibold);
  white-space: nowrap;
  transition: transform var(--dur-fast) var(--ease-spring-snappy),
    background-color var(--dur-fast) var(--ease-standard),
    box-shadow var(--dur-fast) var(--ease-standard),
    border-color var(--dur-fast) var(--ease-standard);
}

.btn:active {
  transform: scale(0.96);
}

.btn--primary {
  background: var(--color-accent);
  color: var(--color-on-accent);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(var(--color-accent-rgb), 0.32);
}

.btn--primary:hover {
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15), 0 6px 18px rgba(var(--color-accent-rgb), 0.42);
}

.btn--secondary {
  background: var(--color-surface-raised);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border-strong);
}

.btn--secondary:hover {
  background: var(--color-surface-hover);
}

.btn--ghost {
  background: transparent;
  color: var(--color-text-secondary);
}

.btn--ghost:hover {
  background: var(--color-surface-hover);
  color: var(--color-text-primary);
}

/* Ghost icon button in a persistent "on" state (e.g. the Minimal colour
   scheme being active). */
.btn--ghost.is-active {
  background: var(--color-surface-hover);
  color: var(--color-accent);
}

.btn--danger {
  background: var(--color-loss-soft);
  color: var(--color-loss);
}

.btn--danger:hover {
  background: rgba(255, 69, 58, 0.22);
}

.btn--sm {
  padding: var(--space-2) var(--space-4);
  font-size: var(--fs-caption);
}

.btn--icon {
  padding: var(--space-2);
  width: 36px;
  height: 36px;
  border-radius: var(--radius-md);
}

.btn--icon svg {
  width: 18px;
  height: 18px;
}

.btn:disabled {
  opacity: 0.4;
  pointer-events: none;
}

/* ==========================================================================
   Forms
   ========================================================================== */

/* min-width: 0 is load-bearing, not defensive styling. A flex/grid item's
   automatic minimum size is its content's min-content size, and form
   controls (notably input[type=date] and input[type=number]) carry a large
   intrinsic minimum that `width: 100%` does not override in WebKit. Without
   this the field refuses to shrink, pushes past the dialog, and - because
   .modal__body scrolls - the whole panel becomes pannable sideways. */
.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

.field__label {
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  color: var(--color-text-secondary);
}

.field__hint {
  font-size: var(--fs-caption);
  color: var(--color-text-tertiary);
}

.input,
.select {
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  font-size: var(--fs-body);
  color: var(--color-text-primary);
  transition: border-color var(--dur-fast) var(--ease-standard),
    box-shadow var(--dur-fast) var(--ease-standard);
  width: 100%;
  /* Controls must be allowed to shrink below their intrinsic minimum;
     see the note on .field above. box-sizing is already border-box from
     the reset, so max-width: 100% cannot be defeated by padding. */
  min-width: 0;
  max-width: 100%;
}

/* WebKit gives date/time/number controls an intrinsic width that ignores
   `width: 100%`; these declarations are what actually let them fit a
   narrow dialog on iOS. */
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="number"] {
  min-width: 0;
  max-width: 100%;
}

/* ---- Date/time controls on WebKit ----
   `min-width: 0` alone is not enough. While the control keeps its native
   appearance, iOS Safari sizes it from the *shadow* content - the
   ::-webkit-date-and-time-value box - and that box has its own intrinsic
   width plus a centring margin, neither of which the host element's width
   can override. The result is a field that renders wider than its
   container and pushes past the dialog edge.

   Dropping the native appearance is what hands sizing back to CSS; the
   shadow-part rules then stop the inner value box from re-inflating it.
   Everything else about the control (the native picker, the value format,
   the locale) is unaffected - only its box sizing changes. */
input[type="date"],
input[type="time"],
input[type="datetime-local"] {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
}

input[type="date"]::-webkit-date-and-time-value,
input[type="time"]::-webkit-date-and-time-value,
input[type="datetime-local"]::-webkit-date-and-time-value {
  /* iOS centres this box and lets it set the control's width. Left-align
     it, let it shrink, and it stops driving the outer size. */
  text-align: left;
  min-width: 0;
  margin: 0;
}

input[type="date"]::-webkit-datetime-edit,
input[type="time"]::-webkit-datetime-edit,
input[type="datetime-local"]::-webkit-datetime-edit {
  min-width: 0;
  overflow: hidden;
}

/* Removing the native appearance also removes the calendar affordance on
   WebKit, so restore a visible one that stays inside the field. */
input[type="date"]::-webkit-calendar-picker-indicator {
  margin-left: auto;
  flex-shrink: 0;
  opacity: 0.5;
  cursor: pointer;
}

.input:focus,
.select:focus {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

/* iOS Safari auto-zooms the page on focusing any text input/textarea/select
   under 16px, which is what made New Trade / Add Challenge / Growth
   Simulator's number fields feel like a "moving", mis-scaled panel on
   phones - this is the shared root cause, fixed once here rather than
   per-view. Scoped to mobile widths only so desktop's 15px stays as-is. */
@media (max-width: 880px) {
  .input,
  .select,
  textarea.input {
    font-size: 16px;
  }
}

.input.is-invalid {
  border-color: var(--color-loss);
}

.input.is-invalid:focus {
  border-color: var(--color-loss);
  box-shadow: 0 0 0 3px var(--color-loss-soft);
}

.input::placeholder {
  color: var(--color-text-tertiary);
}

.input:hover:not(:focus) {
  border-color: var(--color-border-strong);
}

.select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 20px) center, calc(100% - 15px) center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  color: var(--color-text-primary);
  cursor: pointer;
}

/* ---- Custom select (enhances native .select; see js/components/select.js) ---- */

.cselect {
  position: relative;
  width: 100%;
}

/* Native <select> kept as the source of truth, visually removed but still
   focusable-by-name / present for FormData. */
.cselect__native {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  border: 0;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  overflow: hidden;
  pointer-events: none;
}

.cselect__trigger {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  font-size: var(--fs-body);
  font-weight: var(--fw-medium);
  color: var(--color-text-primary);
  cursor: pointer;
  text-align: left;
  transition: border-color var(--dur-fast) var(--ease-standard),
    background-color var(--dur-fast) var(--ease-standard),
    box-shadow var(--dur-fast) var(--ease-standard);
}

.cselect__trigger:hover {
  border-color: var(--color-border-strong);
  background: var(--color-surface-hover);
}

.cselect.is-open .cselect__trigger,
.cselect__trigger:focus-visible {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

.cselect__trigger[disabled] {
  opacity: 0.5;
  pointer-events: none;
}

.cselect__value {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cselect__chevron {
  flex-shrink: 0;
  color: var(--color-text-tertiary);
  transition: transform var(--dur-fast) var(--ease-standard);
}

.cselect.is-open .cselect__chevron {
  transform: rotate(180deg);
  color: var(--color-text-secondary);
}

.cselect__panel {
  /* Portaled to <body> and positioned via inline left/top/width when open
     (see select.js) - this deliberately escapes any ancestor's stacking
     context or overflow clipping, which `position: absolute` nested inside
     .cselect could not (e.g. .stagger-item's persistent transform from its
     entrance animation creates a new stacking context that trapped a
     nested z-index). */
  position: fixed;
  z-index: var(--z-dropdown);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  padding: var(--space-2);
  display: none;
  flex-direction: column;
  gap: 2px;
  max-height: 264px;
  overflow-y: auto;
  animation: cselect-pop var(--dur-fast) var(--ease-decel);
}

.cselect__panel.is-open {
  display: flex;
}

@keyframes cselect-pop {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}

.cselect__option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  font-size: var(--fs-footnote);
  color: var(--color-text-secondary);
  text-align: left;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color var(--dur-fast) var(--ease-standard),
    color var(--dur-fast) var(--ease-standard);
}

.cselect__option:hover {
  background: var(--color-surface-hover);
  color: var(--color-text-primary);
}

.cselect__option.is-active {
  color: var(--color-accent);
  font-weight: var(--fw-medium);
  background: var(--color-accent-soft);
}

/* minmax(0, 1fr), never a bare 1fr: a bare `1fr` track floors at its
   content's min-content size, which for a form control is large enough to
   blow the dialog out sideways. Same fix, same reason as .grid-cols-N in
   layout.css. */
.form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4);
  min-width: 0;
}

@media (max-width: 700px) {
  .form-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ---- Segmented control (tabs) ---- */

.segmented {
  display: inline-flex;
  padding: 3px;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  position: relative;
  gap: 2px;
}

.segmented__option {
  position: relative;
  z-index: 1;
  padding: var(--space-2) var(--space-5);
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  color: var(--color-text-secondary);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-standard);
}

.segmented__option:hover {
  color: var(--color-text-primary);
}

.segmented__option.is-active {
  color: var(--color-text-primary);
}

.segmented__thumb {
  position: absolute;
  top: 3px;
  bottom: 3px;
  /* The thumb's geometry is written inline in JS from a measurement taken
     at render time. It is a decoration that by definition can never be
     wider than the track it slides in, so this invariant is stated here
     rather than trusted to every call site re-measuring on resize. Without
     it a stale width (dialog measured wide, then narrowed) overflows its
     container and makes the whole panel pannable. */
  max-width: 100%;
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur-base) var(--ease-spring),
    width var(--dur-base) var(--ease-spring);
}

/* ---- Touch target sizing ----
   Bumps tap targets toward the ~44px recommendation on phones/tablets
   and genuine touch input, without changing the tighter desktop-mouse
   sizing. */
@media (max-width: 880px), (pointer: coarse) {
  .btn--icon {
    width: 44px;
    height: 44px;
  }

  .segmented__option {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 38px;
    padding: var(--space-3) var(--space-5);
  }
}

/* ---- Toggle switch ---- */

.switch {
  width: 44px;
  height: 26px;
  border-radius: var(--radius-pill);
  background: var(--color-border-strong);
  position: relative;
  transition: background-color var(--dur-base) var(--ease-standard);
  flex-shrink: 0;
}

.switch.is-on {
  background: var(--color-accent);
}

.switch__thumb {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  transition: transform var(--dur-base) var(--ease-spring-snappy);
}

.switch.is-on .switch__thumb {
  transform: translateX(18px);
}

/* ---- Slider ---- */

.slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--color-border-strong);
  outline: none;
}

.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
  border: 2px solid var(--color-accent);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-spring-snappy);
}

.slider::-webkit-slider-thumb:hover {
  transform: scale(1.15);
}

.slider::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
  border: 2px solid var(--color-accent);
  cursor: pointer;
}

/* ==========================================================================
   Badges
   ========================================================================== */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
}

.badge--profit {
  color: var(--color-profit);
  background: var(--color-profit-soft);
}

.badge--loss {
  color: var(--color-loss);
  background: var(--color-loss-soft);
}

.badge--neutral {
  color: var(--color-text-secondary);
  background: var(--color-surface-hover);
}

.badge--accent {
  color: var(--color-accent);
  background: var(--color-accent-soft);
}

.badge-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

/* ==========================================================================
   Table
   ========================================================================== */

.data-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
}

.data-table th {
  text-align: left;
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  color: var(--color-text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
}

.data-table td {
  height: var(--row-h);
  padding: var(--space-2) var(--space-4);
  border-bottom: 1px solid var(--color-border);
  font-size: var(--fs-footnote);
  vertical-align: middle;
}

.data-table tbody tr {
  transition: background-color var(--dur-fast) var(--ease-standard);
}

.data-table tbody tr:hover {
  background: var(--color-surface-hover);
}

.data-table tbody tr:last-child td {
  border-bottom: none;
}

.row-actions {
  display: flex;
  gap: var(--space-1);
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-standard);
}

.data-table tbody tr:hover .row-actions,
.data-table tbody tr:focus-within .row-actions {
  opacity: 1;
}

/* ---- Mobile: card-per-row ----
   Below this width a <table class="data-table"> loses its grid shape
   entirely and each <tr> becomes a stacked card; each <td> that carries
   a data-label attribute (set by the view's row-render function, mirroring
   its <th> text) gets that label rendered via ::before as a left-aligned
   caption, value right-aligned. The one label-less td per row (row actions)
   right-aligns on its own with no caption. Avoids horizontal scroll without
   a second, parallel rendering path per table. */
@media (max-width: 700px) {
  .data-table thead {
    display: none;
  }

  .data-table,
  .data-table tbody,
  .data-table tr {
    display: block;
    width: 100%;
  }

  .data-table tbody tr {
    padding: var(--space-4);
    border-bottom: 1px solid var(--color-border);
  }

  .data-table tbody tr:last-child {
    border-bottom: none;
  }

  .data-table td {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    height: auto; /* the desktop --row-h rule doesn't apply once rows stack */
    padding: var(--space-1) 0;
    border-bottom: none;
  }

  .data-table td[data-label]::before {
    content: attr(data-label);
    font-size: var(--fs-caption);
    font-weight: var(--fw-medium);
    color: var(--color-text-tertiary);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    flex-shrink: 0;
  }

  .data-table td:not([data-label]) {
    justify-content: flex-end;
    padding-top: var(--space-2);
  }

  .row-actions {
    opacity: 1;
  }
}

/* ---- Mobile: compact two-line trade row ----
   Opt-in modifier for trade lists (Journal List, Dashboard Recent Trades).
   The generic card-per-row treatment above turns one trade into ~8 stacked
   "label: value" lines; this lays the same <tr> out as a two-line row -
   line 1: symbol + direction + result, line 2: date · session · R - which
   fits roughly six trades per screen instead of one.

   Deliberately CSS-only, driven by the data-label attributes the row
   renderers already emit: one rendering path per table, no parallel mobile
   markup. Cells that don't belong in the compact row are hidden, not
   re-rendered. */

/* The session cell and the inline direction label exist only to feed the
   compact row; the desktop table has its own columns for both. */
.data-table--compact-rows .cell-session,
.data-table--compact-rows .cell-dir-inline {
  display: none;
}

@media (max-width: 700px) {
  /* Every meta value on line 2 gets its own explicit track: grid items are
     blockified, so several cells cannot share one cell of the grid without
     stacking on top of each other. Four auto tracks + a 1fr spacer + the
     actions track. Unused tracks (e.g. Account outside All Accounts mode)
     collapse to zero width. */
  .data-table--compact-rows tbody tr {
    display: grid;
    grid-template-columns: auto auto auto auto 1fr auto;
    align-items: center;
    column-gap: var(--space-1);
    row-gap: 2px;
    padding: var(--space-3) var(--space-4);
  }

  .data-table--compact-rows td {
    display: block;
    height: auto;
    padding: 0;
    min-width: 0;
  }

  /* Labels are redundant once the row reads as a sentence. */
  .data-table--compact-rows td[data-label]::before {
    content: none;
  }

  /* Line 1: symbol + direction, result pinned right. Symbol *spans* the
     meta tracks rather than sitting in track 1, so line 1's width never
     stretches the tracks line 2's date/session/R are laid out in. */
  .data-table--compact-rows td[data-label="Symbol"] {
    grid-column: 1 / 6;
    grid-row: 1;
    font-size: var(--fs-body);
  }

  .data-table--compact-rows .cell-dir-inline {
    display: inline;
    margin-left: var(--space-2);
    font-size: var(--fs-caption);
    font-weight: var(--fw-medium);
    color: var(--color-text-tertiary);
  }

  .data-table--compact-rows td[data-label="Result"] {
    grid-column: 6;
    grid-row: 1;
    text-align: right;
  }

  /* Line 2: date · session · account · R */
  .data-table--compact-rows td[data-label="Date"] { grid-column: 1; }
  .data-table--compact-rows td[data-label="Session"] { grid-column: 2; }
  .data-table--compact-rows td[data-label="Account"] { grid-column: 3; }
  .data-table--compact-rows td[data-label="R:R"],
  .data-table--compact-rows td[data-label="RR"] { grid-column: 4; }

  .data-table--compact-rows td[data-label="Date"],
  .data-table--compact-rows td[data-label="Session"],
  .data-table--compact-rows td[data-label="Account"],
  .data-table--compact-rows td[data-label="R:R"],
  .data-table--compact-rows td[data-label="RR"] {
    grid-row: 2;
    font-size: var(--fs-caption);
    color: var(--color-text-tertiary);
    white-space: nowrap;
  }

  .data-table--compact-rows .cell-session {
    display: block;
  }

  .data-table--compact-rows td[data-label="Date"]::after,
  .data-table--compact-rows td[data-label="Session"]::after,
  .data-table--compact-rows td[data-label="Account"]::after {
    content: " ·";
  }

  /* Everything the compact row deliberately drops - the detail that belongs
     to reading one trade, not scanning a list. Dir. goes because line 1
     already carries .cell-dir-inline. */
  .data-table--compact-rows td[data-label="Dir."],
  .data-table--compact-rows td[data-label="Entry → Exit"],
  .data-table--compact-rows td[data-label="Risk %"] {
    display: none;
  }

  /* Row actions sit on line 2, right-aligned opposite the meta line. */
  .data-table--compact-rows td:not([data-label]) {
    grid-column: 6;
    grid-row: 2;
    padding-top: 0;
  }

  .data-table--compact-rows .row-actions {
    justify-content: flex-end;
  }
}

/* ==========================================================================
   Empty state
   ========================================================================== */

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: var(--space-3);
  padding: var(--space-16) var(--space-6);
  color: var(--color-text-secondary);
}

.empty-state__icon {
  width: 44px;
  height: 44px;
  color: var(--color-text-tertiary);
}

/* ==========================================================================
   Modal
   ========================================================================== */

/* Extra iOS defense-in-depth alongside the .view-scroll lock in
   layout.css: stops the document itself from rubber-band bouncing while
   a modal is open, in case a gesture reaches past the backdrop. */
html.modal-open,
html.drawer-open {
  overscroll-behavior: none;
}

.modal-backdrop {
  position: fixed;
  /* Plain inset:0 - no explicit height. The dialog is centered (not
     edge/bottom anchored - see the mobile override below), so the
     backdrop only needs to dim + block touch across the screen; it
     doesn't need to be pixel-precise against the real visible viewport
     the way an edge-anchored sheet would. That precision requirement is
     exactly what earlier attempts (100dvh, then a JS-measured
     window.visualViewport custom property) were fighting to satisfy -
     removed in favor of just not depending on it. */
  inset: 0;
  background: rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  z-index: var(--z-modal-backdrop);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-standard);
  /* Stops a touch-drag started on the dimmed background itself from
     panning/scrolling anything underneath (iOS routes an unhandled drag
     on a fixed overlay to the nearest scrollable ancestor otherwise).
     .modal below resets this so scrolling the dialog's own content still
     works normally. */
  touch-action: none;
}

.modal-backdrop.is-open {
  opacity: 1;
}

.modal {
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-2xl);
  box-shadow: var(--shadow-xl);
  width: 100%;
  max-width: 520px;
  max-height: 88vh;
  display: flex;
  flex-direction: column;
  transform: scale(0.94) translateY(12px);
  opacity: 0;
  transition: transform var(--dur-slow) var(--ease-spring),
    opacity var(--dur-base) var(--ease-standard);
  z-index: var(--z-modal);
  touch-action: auto; /* re-enable normal touch scrolling within the dialog, undoing .modal-backdrop's touch-action:none */
}

.modal-backdrop.is-open .modal {
  transform: scale(1) translateY(0);
  opacity: 1;
}

.modal__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-6) var(--space-6) var(--space-4);
  border-bottom: 1px solid var(--color-border);
}

.modal__title {
  font-size: var(--fs-title3);
  font-weight: var(--fw-semibold);
}

.modal__body {
  padding: var(--space-6);
  min-width: 0;
  /* Both axes stated explicitly. Setting overflow-y alone does NOT leave
     overflow-x as `visible`: per the overflow spec, when one axis is not
     visible/clip the other computes to `auto`, which silently turned this
     panel into a horizontal scroller. The width fixes above are what stop
     content from overflowing in the first place; this declares the intent
     - vertical scrolling only - so no sub-pixel rounding can reintroduce
     a sideways drag on touch. */
  overflow-x: hidden;
  overflow-y: auto;
  /* Same reservation as .view-scroll (see css/layout.css): adding a partial
     exit row can push the dialog past its max height, and without a stable
     gutter every field inside it would shift sideways at that moment. */
  scrollbar-gutter: stable;
  /* Stops scrolling the dialog's own content from "chaining" into the
     background once it hits the top/bottom edge (iOS rubber-band scroll
     otherwise bleeds through to whatever's behind the modal). */
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.modal__footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-6) var(--space-6);
}

/* ---- Mobile: smaller, capped-height dialog ----
   Simplest stable fix: instead of trying to precisely anchor a full-bleed
   sheet to the real edge of the screen (which needs exact viewport
   math - 100dvh, then a JS-measured window.visualViewport var, both
   proved unreliable on real devices), the mobile dialog just stays the
   same *kind* of box as desktop - centered, all corners rounded, same
   scale+fade entrance - only smaller and margined so it comfortably
   fits with room to spare. Content taller than the cap scrolls inside
   .modal__body (overflow-y: auto, already set above); the dialog box
   itself never needs to know the exact pixel height of the viewport.
   Breakpoint matches the mobile shell's own (880px, see layout.css). */
@media (max-width: 880px) {
  .modal-backdrop {
    padding: var(--space-4);
  }

  .modal {
    max-width: 100%;
    max-height: 70vh;
    min-width: 0; /* without this, a flex item's implicit min-content width can win over max-width:100% and force the dialog wider than the viewport */
  }

  .modal__header {
    padding: var(--space-5) var(--space-5) var(--space-3);
  }

  .modal__body {
    padding: var(--space-4) var(--space-5);
  }

  .modal__footer {
    padding: var(--space-3) var(--space-5) calc(var(--space-5) + env(safe-area-inset-bottom));
  }
}

/* ==========================================================================
   Trade Detail drawer

   Sibling of .modal, not a variant of it: right-edge panel on desktop,
   full-screen sheet on mobile, and it stays open while a modal (Edit /
   Delete confirm) sits on top - hence the lower z-index pair and its own
   `drawer-open` scroll-lock class.
   ========================================================================== */

.detail-drawer-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  z-index: var(--z-drawer-backdrop);
  display: flex;
  justify-content: flex-end;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-standard);
  touch-action: none;
}

.detail-drawer-backdrop.is-open {
  opacity: 1;
}

.detail-drawer {
  position: relative;
  z-index: var(--z-drawer);
  width: 100%;
  max-width: 460px;
  height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--color-surface-raised);
  border-left: 1px solid var(--color-border);
  box-shadow: var(--shadow-lg);
  transform: translateX(100%);
  transition: transform var(--dur-slow) var(--ease-spring);
  touch-action: auto;
}

.detail-drawer-backdrop.is-open .detail-drawer {
  transform: translateX(0);
}

.detail-drawer__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-5) var(--space-5) var(--space-4);
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
}

.detail-drawer__title {
  font-size: var(--fs-footnote);
  font-weight: var(--fw-medium);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}

.detail-drawer__body {
  flex: 1;
  min-height: 0;
  padding: var(--space-5);
  overflow-x: hidden;
  overflow-y: auto;
  /* Screenshots load in asynchronously and change the panel's height; the
     gutter is reserved so that arrival doesn't nudge the whole panel. */
  scrollbar-gutter: stable;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.detail-drawer__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5) var(--space-5);
  border-top: 1px solid var(--color-border);
  flex-shrink: 0;
}

/* ---- Header block ---- */

.detail-head__top {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.detail-head__symbol {
  font-size: var(--fs-title3);
  font-weight: var(--fw-semibold);
}

.detail-head__pnl {
  font-size: var(--fs-title1);
  font-weight: var(--fw-semibold);
  line-height: 1.1;
  margin-top: var(--space-2);
  overflow-wrap: anywhere;
}

.detail-head__meta {
  margin-top: 2px;
  font-size: var(--fs-footnote);
  color: var(--color-text-secondary);
}

/* ---- Sections ---- */

.detail-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding-top: var(--space-5);
  border-top: 1px solid var(--color-border);
}

.detail-section__title {
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}

.detail-rows {
  display: flex;
  flex-direction: column;
}

.detail-rows__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  padding: 7px 0;
  border-bottom: 1px solid var(--color-border);
  font-size: var(--fs-footnote);
}

.detail-rows__row:last-child {
  border-bottom: none;
}

.detail-rows__row dt {
  color: var(--color-text-secondary);
  flex-shrink: 0;
}

.detail-rows__row dd {
  text-align: right;
  font-weight: var(--fw-medium);
  min-width: 0;
  overflow-wrap: anywhere;
}

.detail-partials {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-surface-hover);
  font-size: var(--fs-caption);
}

.detail-partials__row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
}

.detail-partials__row--rest {
  color: var(--color-text-tertiary);
}

.detail-notes {
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--color-surface-hover);
  font-size: var(--fs-footnote);
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* ---- Planned vs realized bar ----
   A price axis, so left-to-right is price order rather than profit order.
   Labels alternate above/below the track so two close markers can't
   overprint each other. */

.level-bar {
  /* Top padding clears the Stop/TP label row (labels are absolutely
     positioned inside the track). The two lower rows are cleared by the
     caption's margin-top instead, so the block ends right under the text. */
  padding: 32px 6px 4px;
}

.level-bar__track {
  position: relative;
  height: 6px;
  border-radius: 999px;
  background: var(--color-surface-hover);
}

.level-bar__zone {
  position: absolute;
  top: 0;
  bottom: 0;
  border-radius: 999px;
}

.level-bar__zone--risk {
  background: var(--color-loss);
  opacity: 0.35;
}

.level-bar__zone--reward {
  background: var(--color-profit);
  opacity: 0.35;
}

.level-bar__partial {
  position: absolute;
  top: 50%;
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  border-radius: 50%;
  background: var(--color-text-secondary);
}

.level-bar__marker {
  position: absolute;
  top: 50%;
  width: 0;
}

.level-bar__tick {
  position: absolute;
  left: -1px;
  top: -8px;
  width: 2px;
  height: 16px;
  border-radius: 1px;
  background: var(--color-text-tertiary);
}

.level-bar__label {
  position: absolute;
  left: 0;
  transform: translateX(-50%);
  font-size: var(--fs-caption);
  color: var(--color-text-tertiary);
  white-space: nowrap;
}

.level-bar__marker--above .level-bar__label {
  bottom: 13px;
}

.level-bar__marker--below .level-bar__label {
  top: 13px;
}

/* Second lower row - Exit only, so it can never overprint Entry. The 18px
   step is one label line-box, so the two rows clear each other exactly. */
.level-bar__marker--below-far .level-bar__label {
  top: 31px;
}

.level-bar__marker--stop .level-bar__tick {
  background: var(--color-loss);
}

.level-bar__marker--tp .level-bar__tick {
  background: var(--color-profit);
}

.level-bar__marker--entry .level-bar__tick {
  background: var(--color-text-primary);
}

/* The one marker that is fact rather than plan - drawn as a filled dot so
   it reads differently from the three planned ticks. */
.level-bar__marker--exit .level-bar__tick {
  left: -5px;
  top: -5px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--color-accent);
  box-shadow: 0 0 0 2px var(--color-surface-raised);
}

.level-bar__marker--exit .level-bar__label {
  color: var(--color-accent);
  font-weight: var(--fw-medium);
}

.level-bar__caption {
  margin-top: 48px;
  font-size: var(--fs-caption);
  color: var(--color-text-tertiary);
}

/* ---- Screenshots ---- */

.shot-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.shot-tile {
  position: relative;
  width: 84px;
  height: 62px;
  border-radius: var(--radius-md);
  overflow: hidden;
  border: 1px solid var(--color-border);
  background: var(--color-surface-hover);
}

.shot-tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  cursor: zoom-in;
  display: block;
}

.shot-tile__remove {
  position: absolute;
  top: 3px;
  right: 3px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  cursor: pointer;
}

.shot-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.shot-actions .btn.is-disabled {
  opacity: 0.5;
  pointer-events: none;
}

.shot-lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-toast);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6);
  background: rgba(0, 0, 0, 0.82);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-standard);
  cursor: zoom-out;
}

.shot-lightbox.is-open {
  opacity: 1;
}

.shot-lightbox img {
  max-width: 100%;
  max-height: 100%;
  border-radius: var(--radius-lg);
  object-fit: contain;
}

/* ---- Mobile: full-screen sheet ----
   Same breakpoint as the rest of the mobile shell (880px, see layout.css). */
@media (max-width: 880px) {
  .detail-drawer {
    max-width: 100%;
    border-left: none;
  }

  .detail-drawer__footer {
    padding-bottom: calc(var(--space-5) + env(safe-area-inset-bottom));
  }

  .shot-tile {
    width: 76px;
    height: 56px;
  }
}

/* ==========================================================================
   Toasts
   ========================================================================== */

.toast-stack {
  position: fixed;
  top: var(--space-6);
  right: var(--space-6);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-radius: var(--radius-lg);
  background: var(--glass-bg);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid var(--glass-border);
  box-shadow: var(--shadow-lg);
  min-width: 280px;
  max-width: 360px;
  font-size: var(--fs-footnote);
  font-weight: var(--fw-medium);
  pointer-events: auto;
  transform: translateX(24px) scale(0.98);
  opacity: 0;
  transition: transform var(--dur-base) var(--ease-spring),
    opacity var(--dur-base) var(--ease-standard);
}

.toast.is-visible {
  transform: translateX(0) scale(1);
  opacity: 1;
}

.toast__icon {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}

.toast--success .toast__icon {
  color: var(--color-profit);
}

.toast--error .toast__icon {
  color: var(--color-loss);
}

.toast--info .toast__icon {
  color: var(--color-accent);
}

/* ==========================================================================
   Progress / meter
   ========================================================================== */

.meter {
  height: 8px;
  border-radius: var(--radius-pill);
  background: var(--color-surface-hover);
  overflow: hidden;
  position: relative;
}

.meter__fill {
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--color-accent);
  transition: width var(--dur-slower) var(--ease-spring), background-color var(--dur-base) var(--ease-standard);
}

.meter__fill--warning {
  background: var(--color-warning);
}

.meter__fill--danger {
  background: var(--color-loss);
}

/* ==========================================================================
   Tooltip (simple title-based)
   ========================================================================== */

[data-tooltip] {
  position: relative;
}

/* Centred on its host by default. That is only safe when the host has at
   least half the tooltip's width of room on both sides - which a 36px icon
   button sitting ~20px from the edge of the screen does not. Hosts near an
   edge therefore opt into edge anchoring via data-tooltip-align, below,
   instead of being centred into the void. */
[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  background: var(--color-surface-raised);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border-strong);
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  font-size: var(--fs-caption);
  white-space: nowrap;
  box-shadow: var(--shadow-md);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease-standard),
    transform var(--dur-fast) var(--ease-standard);
  z-index: 1000;
}

[data-tooltip]:hover::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* Anchored to the host's left edge and growing rightwards - for controls
   near the left edge of the window (the sidebar's appearance buttons). */
[data-tooltip][data-tooltip-align="start"]::after {
  left: 0;
  transform: translateX(0) translateY(4px);
}

[data-tooltip][data-tooltip-align="start"]:hover::after {
  transform: translateX(0) translateY(0);
}

/* Mirror image, for controls near the right edge (table row actions). */
[data-tooltip][data-tooltip-align="end"]::after {
  left: auto;
  right: 0;
  transform: translateX(0) translateY(4px);
}

[data-tooltip][data-tooltip-align="end"]:hover::after {
  transform: translateX(0) translateY(0);
}

/* The sidebar is the left edge of the window, and its footer buttons are
   the narrowest hosts in the app - always anchor those. */
.sidebar__footer [data-tooltip]::after {
  left: 0;
  transform: translateX(0) translateY(4px);
}

.sidebar__footer [data-tooltip]:hover::after {
  transform: translateX(0) translateY(0);
}

/* Row actions live at the right edge of a table; a centred tooltip on the
   last column runs past the card. */
.row-actions [data-tooltip]::after {
  left: auto;
  right: 0;
  transform: translateX(0) translateY(4px);
}

.row-actions [data-tooltip]:hover::after {
  transform: translateX(0) translateY(0);
}

/* =====================================================================
   Chip picker - tap-to-select values from a dictionary.
   Used by the trade form (setup, mindset tags) and the Dashboard's Today
   check-in. One look for both, so a dictionary edit reads identically
   wherever it surfaces.
   ================================================================== */
.chip-picker {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 34px;
  padding: 0 var(--space-4);
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-border);
  background: var(--color-surface-raised);
  color: var(--color-text-secondary);
  font-size: var(--fs-footnote);
  font-weight: var(--fw-medium);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-standard),
    border-color var(--dur-fast) var(--ease-standard),
    color var(--dur-fast) var(--ease-standard);
}

.chip:hover {
  border-color: var(--color-border-strong);
  color: var(--color-text-primary);
}

.chip.is-active {
  background: var(--color-accent-soft);
  border-color: rgba(var(--color-accent-rgb), 0.45);
  color: var(--color-accent);
}

.chip-picker--sm .chip {
  min-height: 30px;
  padding: 0 var(--space-3);
  font-size: var(--fs-caption);
}

/* A value stored on a record that is no longer on its dictionary. Marked,
   never hidden or rewritten - see the dictionary hard rule in CLAUDE.md. */
.chip__offlist-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--color-text-tertiary);
}

.chip--offlist {
  border-style: dashed;
}

/* =====================================================================
   Dictionary editor (Settings -> Journaling)
   ================================================================== */
.dictionary-editor {
  list-style: none;
  margin: 0 0 var(--space-4);
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.dict-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--row-h);
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
}

.dict-item + .dict-item {
  border-top: 1px solid var(--color-border);
}

.dict-item__label {
  min-width: 0;
  overflow-wrap: anywhere;
  font-size: var(--fs-footnote);
  color: var(--color-text-primary);
}

.dict-item__actions {
  display: flex;
  flex-shrink: 0;
  gap: 2px;
}

.dict-item__actions .btn[disabled] {
  opacity: 0.3;
  pointer-events: none;
}

.dictionary-editor__add {
  flex-wrap: wrap;
}

.dictionary-editor__add .input {
  flex: 1 1 0;
  min-width: 0;
  max-width: 280px;
}

/* A stored value that is no longer on its dictionary, shown as a badge
   rather than a chip (read-only surfaces: trade detail, breakdown tables). */
.badge--offlist {
  border: 1px dashed var(--color-border-strong);
}

/* Tick-box variant of the chip picker: a multi-select list of ACTIONS reads
   as a set of tags when drawn as pills. Same data, same bind() contract. */
.chip-picker--check {
  flex-direction: column;
  gap: 2px;
  width: 100%;
}

.check-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 40px;
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-md);
  background: none;
  text-align: left;
  cursor: pointer;
  color: var(--color-text-secondary);
  font-size: var(--fs-footnote);
  transition: background-color var(--dur-fast) var(--ease-standard),
    color var(--dur-fast) var(--ease-standard);
}

.check-item:hover {
  background: var(--color-surface-hover);
  color: var(--color-text-primary);
}

.check-item__box {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  border-radius: 6px;
  border: 1.5px solid var(--color-border-strong);
  color: transparent;
  transition: background-color var(--dur-fast) var(--ease-standard),
    border-color var(--dur-fast) var(--ease-standard),
    color var(--dur-fast) var(--ease-standard);
}

.check-item__label {
  min-width: 0;
  overflow-wrap: anywhere;
  transition: color var(--dur-fast) var(--ease-standard);
}

.check-item.is-active {
  color: var(--color-text-primary);
}

.check-item.is-active .check-item__box {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-on-accent);
}

/* The classic "done" look: struck through and dimmed, once ticked. */
.check-item.is-active .check-item__label {
  color: var(--color-text-tertiary);
  text-decoration: line-through;
  text-decoration-color: var(--color-border-strong);
}

/* =====================================================================
   Collapsible card - shared by Today's check-in, Insights and Edge.

   The collapsed/expanded state is one class, `.is-collapsed`, on the card
   element itself. Because that element is the one node a data refresh never
   replaces (only [data-*-body]/[data-*-list] inside it is patched - see hard
   rule 11), the collapsed state survives every such refresh for free and
   only resets on a genuine full re-render.

   The animation runs on the Web Animations API
   (TJ.Components.UI.bindCollapsible/animateCollapse in ui-kit.js), not a CSS
   `transition`. Two CSS-only approaches were tried and both failed in this
   app's test environment for the same underlying reason - re-triggering an
   animated size change on an element that has already completed one layout
   pass got permanently stuck at the collapsed size on the very next attempt
   to re-expand: first `grid-template-rows: 1fr -> 0fr` (no effect at all,
   even set as `!important` inline), then a `transition: max-height`
   (collapsed correctly, but going back to expanded stayed stuck at 0 even
   with `!important`). WAAPI's `element.animate()` sidesteps CSS transition
   state entirely - each call is an independent, disposable Animation
   object - so it does not accumulate whatever state the transition-based
   attempt got stuck in. `.is-collapsed` alone still renders the correct
   collapsed state on the very first paint with no JS needed for that part.
   ================================================================== */
.collapsible-body {
  overflow: hidden;
}

.is-collapsed .collapsible-body {
  height: 0;
}

.collapsible-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  text-align: left;
  cursor: pointer;
  color: inherit;
  font: inherit;
}

.collapsible-toggle__title {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--fs-title3);
  font-weight: var(--fw-semibold);
  color: var(--color-text-primary);
}

.collapsible-toggle__chevron {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: var(--color-text-tertiary);
  transition: transform var(--dur-base) var(--ease-standard);
}

.is-collapsed .collapsible-toggle__chevron {
  transform: rotate(-90deg);
}

/* =====================================================================
   Today's check-in (Dashboard).

   Design intent: a couple of quick questions, not a form - no numbered
   rail, no per-step marker, no connector line. A question's own control
   (a highlighted mood icon, a ticked box, filled-in text) is what shows it
   was answered; a thin progress line at the top says how far along the
   user is without adding chrome to every question.
   ================================================================== */
.checkin-head__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-md);
  background: var(--color-accent-soft);
  color: var(--color-accent);
}

.checkin-head-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: var(--space-4) 0 var(--space-5);
}

.checkin-head__date {
  font-size: var(--fs-caption);
  color: var(--color-text-tertiary);
}

.checkin-note {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-4);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-surface-hover);
  font-size: var(--fs-caption);
  color: var(--color-text-tertiary);
}

.checkin-note svg {
  flex-shrink: 0;
}

.checkin-progress {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}

.checkin-progress__label {
  flex-shrink: 0;
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-tertiary);
}

.mini-bar {
  flex: 1 1 auto;
  min-width: 48px;
  max-width: 100%;
  height: 4px;
  border-radius: 999px;
  background: var(--color-surface-hover);
  overflow: hidden;
}

.checkin-step__meta .mini-bar {
  flex: 0 0 auto;
  width: 48px;
}

.mini-bar__fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--color-accent);
  transition: width var(--dur-base) var(--ease-decel);
}

.checkin-steps {
  display: flex;
  flex-direction: column;
}

/* A plain divider between questions - the only structure kept once the
   rail/marker/connector were removed. */
.checkin-step {
  padding: var(--space-5) 0;
}

.checkin-step:first-child {
  padding-top: 0;
}

.checkin-step:last-child {
  padding-bottom: 0;
}

.checkin-step + .checkin-step {
  border-top: 1px solid var(--color-border);
}

.checkin-step__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
}

.checkin-step__q {
  font-size: var(--fs-body);
  font-weight: var(--fw-semibold);
  color: var(--color-text-primary);
}

.checkin-step__meta {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-shrink: 0;
  font-size: var(--fs-caption);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-tertiary);
}

.checkin-step__opt {
  font-size: var(--fs-caption);
  font-weight: var(--fw-regular);
  color: var(--color-text-tertiary);
}

.checkin-step textarea.input {
  resize: vertical;
}

/* ---- Mood scale ----
   Equal-width tiles in a fixed row: an ordered scale has to look like an
   axis, which pills wrapping onto a second line actively hide. Deliberately
   no card chrome (no border/background) - just the glyph and its label, so
   the row reads as five quick taps rather than five little cards; the
   selected state is carried by colour and a small lift alone. */
.chip-picker--scale {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--space-1);
  width: 100%;
}

.scale-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: var(--space-2) 2px;
  border: 0;
  background: none;
  color: var(--color-text-tertiary);
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-spring),
    color var(--dur-fast) var(--ease-standard);
}

.scale-item:hover {
  color: var(--color-text-secondary);
}

.scale-item:active {
  transform: scale(0.94);
}

.scale-item__icon {
  display: flex;
  align-items: center;
  justify-content: center;
}

.scale-item__label {
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  line-height: 1;
  text-align: center;
  overflow-wrap: anywhere;
}

.scale-item.is-active {
  color: var(--color-accent);
  transform: scale(1.06);
}

@media (max-width: 560px) {
  .scale-item__label {
    font-size: 10px;
  }
}

/* A stored value that is no longer on its dictionary, shown as a badge
   rather than a chip (read-only surfaces: trade detail, breakdown tables). */
.badge--offlist {
  border: 1px dashed var(--color-border-strong);
}



/* Result column: a fixed track so the layout does not shift when the global
   unit flips. With table-layout:auto the column sized itself from whichever
   string was widest, and "+$239.08" is wider than "+2.00R" - so every column
   to its left moved on each R/$ toggle. Tabular figures keep the digits
   themselves aligned inside the badge.

   Scoped to >=701px on purpose: below that, components.css turns .data-table
   into stacked cards with flex cells, where a percentage/fixed width shrinks
   the row and the ellipsis truncates the value instead of the label. */
@media (min-width: 701px) {
  .data-table th.col-result,
  .data-table td[data-label="Result"] {
    width: 116px;
    text-align: right;
  }

  .data-table td[data-label="Result"] .badge {
    min-width: 96px;
    justify-content: flex-end;
    font-variant-numeric: tabular-nums;
  }
}
