/* duratar webapp — "Signal" design system.
   Fintech-dashboard voice: teal accent, cool-slate neutrals, rounded cards,
   KPI tiles, soft depth. Fully themed via CSS custom properties; the in-app
   toggle stamps data-theme on <html> (see theme.js), with prefers-color-scheme
   as the no-JS fallback. Green/red are reserved strictly for P/L semantics —
   the teal accent is never one of them. */

:root {
  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;

  /* Density scale — see the block under the theme definitions. Declared here so
     --radius / --radius-sm below can already reference it. */
  --s: 1;

  /* light (default) */
  --bg: #f5f7fa;
  --surface: #ffffff;
  --surface-2: #eef2f7;
  --text: #0f1b2d;
  --dim: #5a6b80;
  --border: #dce3ec;
  --accent: #0e9488;
  --accent-ink: #ffffff;
  --pos: #15803d;
  --neg: #c2364a;
  /* Non-P/L chart accents (Section 2 monthly drill-downs), myfxbook-style. */
  --chart-blue: #3b82f6;
  --chart-purple: #8b5cf6;
  /* Categorical hues for the Portfolio Allocation pie — one per asset category,
     assets within a category rendered as tints of theirs. Kept off the reserved
     P/L green/red. */
  --cat-1: #0e9488;
  --cat-2: #3b82f6;
  --cat-3: #8b5cf6;
  --cat-4: #d97706;
  --cat-5: #db2777;
  /* Utility-action buttons (config download/upload). A blue distinct from the
     teal accent (the primary CTA) and from the reserved P/L green/red. Filled
     like the Run button; --util-ink is the on-fill text (white on light). */
  --util: #3b82f6;
  --util-ink: #ffffff;
  --shadow: 0 1px 2px rgba(16, 27, 45, .05), 0 8px 24px rgba(16, 27, 45, .06);
  --radius: calc(12px * var(--s));
  --radius-sm: calc(8px * var(--s));
}

:root[data-theme="dark"] {
  --bg: #0d1117;
  --surface: #151b24;
  --surface-2: #1c2530;
  --text: #e6edf5;
  --dim: #8b98a9;
  --border: #263140;
  --accent: #2dd4bf;
  --accent-ink: #06201d;
  --pos: #3fb950;
  --neg: #ff6b73;
  --chart-blue: #60a5fa;
  --chart-purple: #a78bfa;
  --cat-1: #2dd4bf;
  --cat-2: #60a5fa;
  --cat-3: #a78bfa;
  --cat-4: #fbbf24;
  --cat-5: #f472b6;
  --util: #60a5fa;
  --util-ink: #0d1117;
  --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 10px 30px rgba(0, 0, 0, .35);
}

/* no-JS fallback: honor the OS preference unless the toggle forced light */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0d1117;
    --surface: #151b24;
    --surface-2: #1c2530;
    --text: #e6edf5;
    --dim: #8b98a9;
    --border: #263140;
    --accent: #2dd4bf;
    --accent-ink: #06201d;
    --pos: #3fb950;
    --neg: #ff6b73;
    --chart-blue: #60a5fa;
    --chart-purple: #a78bfa;
    --cat-1: #2dd4bf;
    --cat-2: #60a5fa;
    --cat-3: #a78bfa;
    --cat-4: #fbbf24;
    --cat-5: #f472b6;
    --util: #60a5fa;
    --util-ink: #0d1117;
    --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 10px 30px rgba(0, 0, 0, .35);
  }
}

/* ---------- density scale ---------- */
/* One knob for the whole sheet. The "a A" control in the top bar stamps
   data-textsize on <html> (see fontscale.js), pre-paint like the theme. Large
   (--s: 1) is the default and the no-JS / first-visit state; only "small" is
   ever stamped.

   This works because nearly every size below is in rem: scaling the root font
   size scales fonts, padding, margins, gaps and the box header strips together,
   in one place. NEW CSS MUST BE WRITTEN IN rem or it silently won't scale — the
   few px literals that do need to follow are written calc(X * var(--s)).

   Deliberately NOT scaled: @media breakpoints (those are real viewport widths),
   focus rings (an accessibility affordance, constant by design), hairline
   1px borders, and the two boxes whose contents are drawn outside this sheet —
   the Allocation pie and the chart iframe (.chart-frame), which render at their
   own sizes and so only lose room, never density, when the frame shrinks.
   Density is independent of theme — the two never nest. */
:root[data-textsize="small"] { --s: .75; }
/* calc(100% * …), not calc(16px * …): 100% inherits the user's own browser
   font-size preference instead of overriding it. */
html { font-size: calc(100% * var(--s)); }

* { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--text);
  /* .9375rem == 15px at the default root size, but rides --s. */
  font: .9375rem/1.55 var(--font-sans);
  -webkit-font-smoothing: antialiased;
}

/* ---------- header ---------- */
/* One row that becomes two when it has to. The three groups (brand · nav ·
   actions) have a hard minimum width, and without `wrap` the actions cluster —
   rightmost, so the theme toggle first — simply ran off the right edge and gave
   the page a horizontal scrollbar on a phone. Wrapping needs no breakpoint and
   is the reason there isn't one: flex breaks the line on the items' unshrunk
   widths, so the actions drop below *before* the brand is squeezed enough to
   wrap its wordmark mid-air, and because every one of those widths is in rem the
   turn happens at a proportionally narrower viewport in the dense view — which
   is what a viewport @media could not have expressed. The gap doubles as the
   row gap once there are two rows. */
header {
  position: sticky; top: 0; z-index: 10;
  display: flex; flex-wrap: wrap; align-items: center; gap: .6rem;
  padding: .8rem 1.4rem;
  background: color-mix(in srgb, var(--surface) 82%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
}
.brand {
  color: var(--text); font-weight: 700; font-size: 1.12rem;
  letter-spacing: -.01em; text-decoration: none;
}
.brand .dot { color: inherit; }
.sub { color: var(--dim); font-size: .85rem; }
/* Top-bar nav (Portfolio / Assets / Backtest). Sits just after the brand, with a
   roomy gap separating it from the brand wordmark. */
.mainnav { display: flex; align-items: center; gap: .3rem; margin-left: 2.8rem; }
.mainnav a {
  color: var(--dim); text-decoration: none; font-weight: 600; font-size: .92rem;
  padding: .4rem .7rem; border-radius: var(--radius-sm); line-height: 1;
  transition: color .15s, background .15s;
}
.mainnav a:hover { color: var(--text); background: var(--surface-2); }
.mainnav a.active { color: var(--text); background: var(--surface-2); }
@media (max-width: 620px) { .mainnav { margin-left: .7rem; } }
/* Right-aligned cluster: auth control (when auth is enabled) + theme toggle. */
.header-actions { margin-left: auto; display: flex; align-items: center; gap: .6rem; }
.logout-form { margin: 0; display: contents; }
/* Compact top-bar button, sized to match .theme-toggle's height. */
.auth-btn {
  cursor: pointer; font: inherit; font-weight: 600; font-size: .9rem;
  /* Center content explicitly: the modal's `.primary` login button also matches
     the generic `button.primary` rule, whose taller line-height + vertical
     padding would otherwise push the label below center inside this fixed box. */
  display: inline-flex; align-items: center; justify-content: center;
  /* Floored: at --s .75 a flat scale gives 27px, which is a poor pointer target.
     max() keeps the control comfortably clickable while the label still shrinks. */
  height: max(30px, calc(36px * var(--s))); padding: 0 .9rem;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: calc(10px * var(--s)); color: var(--text); line-height: 1;
  transition: border-color .15s, background .15s, filter .15s, transform .08s ease;
}
.auth-btn:hover { border-color: var(--accent); }
.auth-btn:active { transform: translateY(1px) scale(.98); }
.auth-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.auth-btn.primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.auth-btn.primary:hover { filter: brightness(1.06); border-color: var(--accent); }
.auth-btn.danger { background: var(--neg); color: #fff; border-color: var(--neg); }
.auth-btn.danger:hover { filter: brightness(1.06); border-color: var(--neg); }
.auth-btn.danger:focus-visible { outline-color: var(--neg); }
.theme-toggle {
  cursor: pointer;
  /* Same 30px pointer-target floor as .auth-btn, so the top-bar cluster stays
     aligned at either density. */
  width: max(30px, calc(36px * var(--s))); height: max(30px, calc(36px * var(--s)));
  display: grid; place-items: center;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: calc(10px * var(--s)); color: var(--text); font-size: 1rem; line-height: 1;
  transition: border-color .15s, background .15s;
}
.theme-toggle:hover { border-color: var(--accent); }

/* ---------- text-size control ("a A") ---------- */
/* Segmented pill between the auth button and the theme toggle. The two glyphs
   are sized small/large so the control reads as what it does without a label;
   the active side is filled with the accent. fontscale.js drives aria-pressed. */
.fontscale {
  display: inline-flex; align-items: center; gap: .1rem;
  height: max(30px, calc(36px * var(--s)));
  padding: 0 calc(4px * var(--s));
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: calc(10px * var(--s));
}
.fs-btn {
  cursor: pointer; background: transparent; border: 0; color: var(--dim);
  font-family: var(--font-sans); font-weight: 700; line-height: 1;
  padding: 0 .45rem; height: 100%; border-radius: calc(7px * var(--s));
  transition: color .15s, background .15s;
}
.fs-btn:hover { color: var(--text); }
.fs-btn[aria-pressed="true"] { color: var(--accent-ink); background: var(--accent); }
.fs-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.fs-sm { font-size: .78rem; }
.fs-lg { font-size: 1.05rem; }

/* ---------- login modal ---------- */
.modal { display: none; position: fixed; inset: 0; z-index: 50; }
.modal--open { display: grid; place-items: center; }
.modal__backdrop {
  position: absolute; inset: 0; cursor: pointer;
  background: color-mix(in srgb, #000 55%, transparent);
  backdrop-filter: blur(2px);
}
.modal__panel {
  position: relative; width: min(92vw, calc(360px * var(--s)));
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 1.4rem;
}
.modal__panel h2 { margin: 0 0 1rem; font-size: 1.15rem; letter-spacing: -.01em; }
.modal__panel label { display: block; font-size: .85rem; color: var(--dim); margin-bottom: .8rem; }
.modal__panel input[type="text"],
.modal__panel input:not([type]),
.modal__panel input[type="password"],
.modal__panel select {
  width: 100%; margin-top: .3rem; font: inherit;
  padding: .5rem .6rem; border-radius: var(--radius-sm);
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
}
.modal__panel input:focus-visible,
.modal__panel select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.modal__error {
  margin: 0 0 .9rem; padding: .5rem .7rem; font-size: .85rem;
  color: var(--neg); background: color-mix(in srgb, var(--neg) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--neg) 35%, transparent); border-radius: var(--radius-sm);
}
.modal__text { margin: 0 0 1.2rem; font-size: .9rem; line-height: 1.5; color: var(--text); }
.modal__actions { display: flex; justify-content: flex-end; gap: .6rem; margin-top: .4rem; }

/* flow-root + zero top padding so the first box's 1.2rem top margin is contained
   and the header→first-box gap matches the 1.2rem gap between stacked boxes. */
main { max-width: none; margin: 0 auto; display: flow-root; padding: 0 clamp(.625rem, 1.25vw, 1.25rem) 1.5rem; }
h1 { font-size: 1.6rem; letter-spacing: -.02em; display: flex; align-items: center; gap: .7rem; }
a { color: var(--accent); }

/* ---------- main boxes (page-level cards) ---------- */
.box {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  margin: 1.2rem 0; overflow: hidden;
}
.box-hd {
  display: flex; align-items: center; gap: .6rem;
  padding: .85rem 1.25rem; border-bottom: 1px solid var(--border);
  background: color-mix(in srgb, var(--surface-2) 55%, var(--surface));
}
.box-hd h2 { margin: 0; font-size: 1.08rem; letter-spacing: -.01em; }
.box-hd .hint { color: var(--dim); font-size: .8rem; }
.box-hd .status { margin-left: .1rem; }
.box-bd { padding: 1.25rem; }

/* About box (Home page): centered, ~50% of the viewport with ~25% margin each
   side. Narrow screens fall back to full width so it stays readable. */
.about-box { width: 50%; margin-left: auto; margin-right: auto; }
@media (max-width: 900px) { .about-box { width: 100%; } }
/* The one box a pure scale hurts: its width is a share of the *viewport*, not a
   rem measure, so shrinking the type alone stretches the line to ~102 characters
   — well past a comfortable measure. Cap it in rem (which does scale) so prose
   holds ~78 characters at either size. Scoped to small so the default is
   untouched on wide screens. */
:root[data-textsize="small"] .about-box { max-width: 46rem; }
/* Comfortable measure + line-height for reading. */
.about-prose p { margin: 0 0 1.1rem; line-height: 1.7; color: var(--text); }
.about-prose p:last-child { margin-bottom: 0; }

/* ---------- Portfolio box: category columns of asset pills ---------- */
/* A full-width row of category columns (ETFs · Stocks · Commodities · Currencies ·
   Crypto) split by vertical hairline dividers; assets flow horizontally as pills
   that wrap within their column. Columns stay equal width and share the divider via
   a right border (dropped on the last). The track count is not written down: one
   auto column per rendered category (`grid-auto-flow: column`), so adding to
   `db.CATEGORIES` needs no CSS change. Stacks to a single column on narrow screens. */
.pf-cats {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
}
.pf-cat { padding: .15rem 1.15rem; border-right: 1px solid var(--border); }
.pf-cat:first-child { padding-left: 0; }
.pf-cat:last-child { padding-right: 0; border-right: 0; }
.pf-cat-hd {
  display: flex; align-items: baseline; gap: .4rem; margin-bottom: .7rem;
}
.pf-cat-name {
  font-size: .66rem; letter-spacing: .06em; text-transform: uppercase;
  color: var(--dim); font-weight: 700;
}
.pf-cat-count { font-size: .7rem; color: var(--dim); font-variant-numeric: tabular-nums; }
.pf-cat-empty { margin: 0; color: var(--dim); font-size: .85rem; }
.pf-chips { display: flex; flex-wrap: wrap; gap: .4rem; }
/* One asset pill: a mono ticker link, plus (console only) an inline delete ×. */
.pf-chip {
  display: inline-flex; align-items: center; gap: .15rem;
  border: 1px solid var(--border); border-radius: calc(20px * var(--s));
  background: var(--surface-2); transition: border-color .12s ease, background .12s ease;
}
.pf-chip:hover { border-color: var(--accent); }
.pf-chip-link {
  padding: .2rem .6rem; font-family: var(--font-mono); font-weight: 600;
  font-size: .78rem; color: var(--text); text-decoration: none; border-radius: calc(20px * var(--s));
}
.pf-chip-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pf-chip.sel {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.pf-chip.sel .pf-chip-link { color: var(--accent); }
.pf-chip-del {
  display: inline-flex; align-items: center; justify-content: center;
  /* 15px floor — the ✕ is the smallest target on the page. */
  width: max(15px, calc(18px * var(--s))); height: max(15px, calc(18px * var(--s)));
  margin-right: .25rem; padding: 0; cursor: pointer;
  color: var(--dim); background: transparent; border: 0; border-radius: 50%;
  line-height: 0; transition: color .12s ease, background .12s ease;
}
.pf-chip-del:hover { color: var(--neg); background: color-mix(in srgb, var(--neg) 15%, transparent); }
.pf-chip-del:focus-visible { outline: 2px solid var(--neg); outline-offset: 1px; }
.pf-del-form { margin: 0; display: inline-flex; }
@media (max-width: 720px) {
  .pf-cats { grid-auto-flow: row; grid-auto-columns: auto; }
  .pf-cat { padding: .6rem 0; border-right: 0; border-bottom: 1px solid var(--border); }
  .pf-cat:first-child { padding-top: 0; }
  .pf-cat:last-child { padding-bottom: 0; border-bottom: 0; }
}
/* Right-justified action in a box header (e.g. "Add to Portfolio"). */
.box-hd .hd-action { margin-left: auto; display: flex; align-items: center; gap: .5rem; }
.hd-btn {
  cursor: pointer; font: inherit; font-weight: 600; font-size: .82rem;
  color: var(--accent-ink); background: var(--accent); border: 0;
  padding: .4rem .7rem; border-radius: var(--radius-sm);
  transition: filter .15s, transform .08s ease;
}
.hd-btn:hover { filter: brightness(1.06); }
.hd-btn:active { transform: translateY(1px) scale(.98); filter: brightness(.92); }
.hd-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.hd-btn:disabled { opacity: .45; cursor: not-allowed; filter: none; transform: none; }
/* Destructive header action (Recent Jobs → Delete selected). */
.hd-btn.danger { background: var(--neg); color: #fff; }
.hd-btn.danger:focus-visible { outline-color: var(--neg); }
/* The bulk-delete control group sits beside "Add to Portfolio". Its checkboxes
   live in the rows below (form="jobs-bulk-del"), so the form itself only holds
   the select-all box and the Delete button. */
.jobs-bulk { margin: 0; display: flex; align-items: center; gap: .5rem; }
.jobs-all { display: inline-flex; align-items: center; cursor: pointer; }

/* ---------- inner panels (grouped items) ---------- */
/* The min track scales too, so the panel count per row is the same at either
   density — the layout stays recognisable, the panels just get tighter. */
.panel-grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(calc(260px * var(--s)), 1fr)); }
/* horizontal rule splitting the parameter panels into two sections */
.panel-divider { border: 0; border-top: 1px solid var(--border); margin: 1rem 0; }
.panel-grid.two { grid-template-columns: 1fr; }
@media (min-width: 620px) { .panel-grid.two { grid-template-columns: 1fr 1fr; } }
/* Summary / Long / Short side by side; collapse to fewer columns as width drops */
.panel-grid.three { grid-template-columns: 1fr; }
@media (min-width: 620px) { .panel-grid.three { grid-template-columns: 1fr 1fr; } }
@media (min-width: 900px) { .panel-grid.three { grid-template-columns: 1fr 1fr 1fr; } }
.panel {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); overflow: hidden; align-self: start;
}
.panel > .pt {
  display: flex; justify-content: space-between; align-items: baseline; gap: .5rem;
  padding: .5rem .85rem; background: var(--surface-2); border-bottom: 1px solid var(--border);
}
.panel > .pt h3 { margin: 0; font-size: .92rem; font-weight: 650; }
.panel > .pt .meta {
  font-size: .66rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--dim);
}
.panel-bd { padding: .55rem .85rem .7rem; }
.panel-bd .field:first-child { margin-top: .3rem; }

/* A sub-strategy panel's enable checkbox IS its title (see index.html's `subpanel`):
   the header is a label, so the whole title row toggles the strategy and the panel
   carries no separate on/off field. Unchecked, the body folds away — every parameter
   inside is `dep-*`-hidden by form.js at that point, so an open panel would just be
   an empty box. Driven by :has() rather than a JS-synced class: the checkbox is the
   only input to the state, so there is nothing to keep in sync. The inputs stay in
   the DOM and still POST, exactly as they did when only their fields were hidden —
   the server ignores a disabled sub-strategy's parameters. RSI period is the one
   exception and is NOT in here: the server reads it whatever the sub-strategy does,
   so it moves out to its own box rather than folding away (index.html's bo_stack). */
.subpanel > .pt { align-items: center; }
.pt-check { display: inline-flex; align-items: center; gap: .5rem; margin: 0; cursor: pointer; }
.pt-check input {
  margin: 0; accent-color: var(--accent);
  width: max(14px, calc(16px * var(--s))); height: max(14px, calc(16px * var(--s)));
}
.pt-check h3 { user-select: none; }
.subpanel:has(> .pt .pt-check input:not(:checked)) > .panel-bd { display: none; }
.subpanel:has(> .pt .pt-check input:not(:checked)) > .pt h3 { color: var(--dim); }

/* A Breakout panel and its "RSI Period" box share ONE grid cell, so the box sits
   directly under the panel — in the space the panel's own body vacates when the
   sub-strategy is switched off. As a plain grid sibling it would land in the next
   COLUMN instead, away from the panel it stands in for. Only one of the two ever
   holds the field (index.html's bo_stack), so the pair is never taller than the
   panel alone. */
.panel-stack { display: flex; flex-direction: column; gap: 1rem; align-self: start; }
.panel-stack > .panel { align-self: stretch; }
/* RSI period keeps its help line only in the standalone box, where it answers
   "why is this one field still here?". Back inside Breakout it is one parameter
   among five and the line is just noise, so it is hidden by LOCATION — the field
   is a single node that moves between the two boxes, so there is one <small> in
   the DOM and no second text to keep in step. */
.subpanel .field[data-name^="rsi_period_"] small { display: none; }

.placeholder { color: var(--dim); text-align: center; padding: 1.6rem 1rem; font-size: .92rem; }
.placeholder code { font-size: .85em; }
.rmeta { color: var(--dim); font-size: .83rem; margin: 0 0 1rem; }

/* run configuration disclosure */
.cfg { margin-top: 1rem; }
.cfg summary { cursor: pointer; color: var(--dim); font-size: .85rem; font-weight: 600; }
.cfg-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(calc(200px * var(--s)), 1fr)); gap: .3rem .9rem; margin-top: .7rem; }
.cfg-item { display: flex; justify-content: space-between; gap: .6rem; border-bottom: 1px solid var(--border); padding: .2rem 0; font-size: .8rem; }
.cfg-item .ck { color: var(--dim); }
.cfg-item .cv { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* ---------- cards & fields ---------- */
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 1.1rem 1.3rem; margin: 1.1rem 0;
}
.note { color: var(--dim); font-size: .85rem; margin: .1rem 0 .9rem; }
.field { margin: .75rem 0; }
.field > label, .field .label {
  display: block; font-weight: 600; font-size: .88rem; margin-bottom: .3rem;
}
.field small { display: block; color: var(--dim); font-size: .78rem; margin-top: .25rem; }
.field.hidden { display: none; }
/* The same `hidden` class on a whole panel — form.js puts it on the "RSI Period"
   box while that side's Breakout panel is holding the field itself. Kept scoped
   to .panel rather than a bare `.hidden`, so it cannot collide with the
   `is-hidden` / other visibility conventions used elsewhere on the page. */
.panel.hidden { display: none; }
/* a sub-option of the field above it (config_form's `indent=True` — the pullback
   zone's ending conditions, the strength exits' bars/percent pairs). Padding on
   the .field wrapper indents the control AND its help line together, rather than
   only the label. The amount is the parent checkbox's width plus its label gap,
   so the indent is derived from the thing it is indenting past instead of an
   arbitrary offset, and it tracks the text-size scale. Keep the two values in
   step with `.check input`'s size and `.check`'s gap below. (The sub-checkbox
   lands a couple of px right of the parent's label text — the UA's own margin on
   the input — which reads fine; zeroing that margin would change every checkbox.)

   PURELY POSITIONAL: it must not touch colour or size, so an indented field
   reads exactly like an un-indented one. Named `subfield`, NOT `sub` — `.sub`
   above is the top-bar subtitle style (dim, smaller), and since colour inherits,
   putting that class on the wrapper greyed out every label inside it. */
.field.subfield { padding-left: calc(max(14px, 16px * var(--s)) + .5rem); }
/* a short format hint sharing the label's row, right-aligned over the input
   (index.html's `field(help_right=true)` — the date fields). Width tracks the
   input's own max-width so the hint lands on the input's right edge, not the
   panel's. */
.field .lbl-row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: .5rem; max-width: calc(360px * var(--s));
}
.field .lbl-row > label { margin-bottom: .3rem; }
.field .lbl-row small { margin-top: 0; }

input[type=text], input[type=number], select {
  width: 100%; max-width: calc(360px * var(--s)); padding: .5rem .6rem;
  font: inherit; font-size: .92rem; color: var(--text);
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-sm); transition: border-color .15s, box-shadow .15s;
}
input:focus-visible, select:focus-visible {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);
}

/* checkboxes */
.check { display: inline-flex; align-items: center; gap: .5rem; font-weight: 500; cursor: pointer; }
.check input {
  width: max(14px, calc(16px * var(--s))); height: max(14px, calc(16px * var(--s)));
  accent-color: var(--accent);
}

/* radios rendered as a segmented control (.radios > label.radio > input) */
.radios { display: inline-flex; flex-wrap: wrap; gap: .35rem; }
.radio {
  position: relative; cursor: pointer; user-select: none;
  padding: .45rem .85rem; font-size: .88rem; font-weight: 600; color: var(--dim);
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
  transition: .15s;
}
.radio input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
.radio:hover { color: var(--text); }
.radio:has(input:checked) {
  background: var(--accent); color: var(--accent-ink); border-color: var(--accent);
}
.radio:has(input:focus-visible) {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent);
}

/* ---------- buttons ---------- */
button.primary {
  cursor: pointer; font: inherit; font-weight: 700; font-size: 1rem;
  background: var(--accent); color: var(--accent-ink); border: 0;
  padding: .72rem 1.4rem; border-radius: var(--radius-sm);
  position: relative;
  transition: filter .15s, transform .08s ease, padding .2s ease;
}
button.primary:hover { filter: brightness(1.06); }
/* A · depress-and-darken: tactile feedback the instant it's pressed */
button.primary:active { transform: translateY(1px) scale(.98); filter: brightness(.92); }
button.primary:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
/* C · loading state: on submit the button locks and grows a spinner, so the
   click→303-redirect gap reads as "working" and double-submits are blocked. */
button.primary .btn-spinner {
  position: absolute; left: 1.15rem; top: 50%;
  width: calc(15px * var(--s)); height: calc(15px * var(--s));
  margin-top: calc(-7.5px * var(--s));
  border: 2px solid color-mix(in srgb, var(--accent-ink) 40%, transparent);
  border-top-color: var(--accent-ink); border-radius: 50%;
  opacity: 0;
}
button.primary.is-busy {
  pointer-events: none; padding-left: 2.6rem; filter: brightness(.95);
}
button.primary.is-busy .btn-spinner { opacity: 1; animation: btn-spin .6s linear infinite; }
@keyframes btn-spin { to { transform: rotate(360deg); } }

/* ---------- status pills ---------- */
.status {
  font-size: .7rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  padding: .18rem .6rem; border-radius: 999px; border: 1px solid transparent;
}
.status-queued  { color: var(--dim);   background: color-mix(in srgb, var(--dim) 12%, transparent);   border-color: color-mix(in srgb, var(--dim) 30%, transparent); }
.status-running { color: var(--accent);background: color-mix(in srgb, var(--accent) 14%, transparent);border-color: color-mix(in srgb, var(--accent) 35%, transparent); }
.status-done    { color: var(--pos);   background: color-mix(in srgb, var(--pos) 14%, transparent);   border-color: color-mix(in srgb, var(--pos) 35%, transparent); }
.status-failed  { color: var(--neg);   background: color-mix(in srgb, var(--neg) 14%, transparent);   border-color: color-mix(in srgb, var(--neg) 35%, transparent); }

/* ---------- KPI tiles (results) ---------- */
/* No top margin: the box-bd padding alone sets the gap above the KPI row, so it
   matches the box's left/right padding instead of doubling up. */
.kpis { display: grid; grid-template-columns: repeat(2, 1fr); gap: .7rem; margin: 0 0 1.1rem; }
@media (min-width: 560px) { .kpis { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 800px) { .kpis { grid-template-columns: repeat(6, 1fr); } }
.kpi {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: .75rem .8rem .8rem; box-shadow: var(--shadow);
}
.kpi.hero { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.kpi .k {
  font-size: .66rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--dim);
}
.kpi .v {
  margin-top: .3rem; font-family: var(--font-mono); font-size: 1.12rem; font-weight: 600;
  letter-spacing: -.02em; font-variant-numeric: tabular-nums;
}
.v.pos { color: var(--pos); }
.v.neg { color: var(--neg); }

/* ---------- tables ---------- */
/* Recent Jobs: two side-by-side columns (10 + 10), stacked on narrow screens. */
.jobs-split { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; align-items: start; }
@media (max-width: 620px) { .jobs-split { grid-template-columns: 1fr; } }
table { border-collapse: collapse; width: 100%; }
table.jobs th, table.jobs td {
  text-align: left; padding: .45rem .6rem; border-bottom: 1px solid var(--border); font-size: .86rem;
}
table.jobs th {
  font-size: .66rem; letter-spacing: .06em; text-transform: uppercase; color: var(--dim); font-weight: 700;
}
table.jobs tr:last-child td { border-bottom: 0; }
table.jobs td a { font-family: var(--font-mono); font-weight: 600; text-decoration: none; }

table.kv td { padding: .32rem 0; font-size: .87rem; border-bottom: 1px solid var(--border); }
table.kv tr:last-child td { border-bottom: 0; }
table.kv td:first-child { color: var(--dim); }
table.kv td.num {
  text-align: right; font-family: var(--font-mono);
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
}
.num.pos { color: var(--pos); }
.num.neg { color: var(--neg); }
.null { color: var(--dim); }
.jobs tr.job-row { cursor: pointer; transition: background .12s ease; }
.jobs tr.job-row:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); }
.jobs tr.sel, .jobs tr.sel:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  box-shadow: inset calc(3px * var(--s)) 0 0 var(--accent);
}
/* Recent Jobs delete controls: the trash cell and the bulk-select cell. Both are
   shrink-to-fit and centred, so they take no width from the data columns. */
table.jobs th.jobs-c, table.jobs td.jobs-c {
  width: 1%; white-space: nowrap; text-align: center; padding-left: .2rem; padding-right: .2rem;
}
.job-del-form { margin: 0; display: inline-flex; }
.job-del {
  display: inline-flex; align-items: center; justify-content: center;
  /* Same pointer-target floor as the Portfolio chip's delete. */
  width: max(18px, calc(22px * var(--s))); height: max(18px, calc(22px * var(--s)));
  padding: 0; cursor: pointer;
  color: var(--dim); background: transparent; border: 0; border-radius: 50%;
  line-height: 0; transition: color .12s ease, background .12s ease;
}
.job-del:hover { color: var(--neg); background: color-mix(in srgb, var(--neg) 15%, transparent); }
.job-del:focus-visible { outline: 2px solid var(--neg); outline-offset: 1px; }
/* A queued/running job can't be deleted (the worker may still be writing its
   files) — the control stays visible but inert, and jobs.remove refuses it too. */
.job-del:disabled { opacity: .3; cursor: not-allowed; }
.job-del:disabled:hover { color: var(--dim); background: transparent; }
.job-check { cursor: pointer; vertical-align: middle; }
.job-check:disabled { cursor: not-allowed; opacity: .4; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* ---------- run + config-action row ---------- */
/* Run backtest (primary CTA) sits on one row with the config download/upload
   actions to its right. The config buttons are a violet outline (--util) so they
   read as secondary next to the solid teal Run button, and stay clear of the
   P/L-reserved green/red. */
.run-row {
  display: flex; flex-wrap: wrap; gap: .6rem; align-items: center;
  margin-top: 1.25rem;
}
.cfg-btn {
  display: inline-flex; align-items: center; gap: .4rem; cursor: pointer;
  font: inherit; font-weight: 600; font-size: 1rem;
  color: var(--util-ink); text-decoration: none;
  background: var(--util); border: 0;
  /* Vertical padding + font-size match button.primary so the height matches Run. */
  padding: .72rem 1.1rem; border-radius: var(--radius-sm);
  transition: filter .15s, transform .08s ease;
}
.cfg-btn:hover { filter: brightness(1.06); }
.cfg-btn:focus-visible { outline: 2px solid var(--util); outline-offset: 2px; }
/* Press animation: same depress-and-darken motion as the primary Run button. */
.cfg-btn:active { transform: translateY(1px) scale(.98); filter: brightness(.92); }
.cfg-error { flex-basis: 100%; margin: 0; color: var(--neg); font-size: .85rem; }

/* ---------- misc ---------- */
.pending { color: var(--dim); }
.error {
  background: color-mix(in srgb, var(--neg) 10%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--neg) 45%, transparent);
  color: var(--neg); padding: .8rem 1rem; border-radius: var(--radius); margin: 1rem 0;
}
.error-trace {
  white-space: pre-wrap; overflow-x: auto; font-family: var(--font-mono);
  font-size: .8rem; color: var(--neg);
}
code { font-family: var(--font-mono); background: var(--surface-2); padding: .1rem .35rem; border-radius: 5px; font-size: .85em; }

/* Embedded chart (box 4). The chart doc is theme-aware: theme.js loads it with
   the current theme in the URL hash and posts updates on toggle, so it follows
   the webapp's light/dark selector (see duratar/chart.py). The backdrop below is
   only visible while the iframe loads — var(--bg) keeps it matching the theme. */
.chart-frame {
  /* Deliberately NOT scaled by --s (see the density-scale note above). The
     iframe's document draws at its own font size, so scaling only the frame
     shrank the box without shrinking anything in it — and since the chart's
     panes are fixed heights inside a 100vh column, every pixel taken off the
     frame came out of the price chart alone (at --s .75 it was left ~90px).
     Same height at either density. */
  display: block; width: 100%; height: 900px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg);
}

/* ---------- Section 2 · myfxbook-style stats (results box) ---------- */
/* Stats table (narrow) beside the chart panel (wide); stack on narrow screens.
   Sits above the Summary/Long/Short grid, so space them apart. */
.s2-grid { display: grid; gap: 1rem; grid-template-columns: 1fr; align-items: start; margin-bottom: 1rem; }
@media (min-width: 780px) {
  .s2-grid { grid-template-columns: minmax(calc(260px * var(--s)), calc(340px * var(--s))) 1fr; }
  /* Side by side, the chart panel is exactly as tall as the Stats panel: both
     grid items stretch to the row (over .panel's own align-self:start), and the
     canvas wrap claims whatever is left under the tab strip. The row's own
     height is the Stats table's, because the wrap flexes from a 0 basis and so
     contributes nothing to the chart panel's natural height — no circular
     "canvas props up the row it is sized from". min-height is only a floor for a
     hypothetical very short Stats table. Two-column layout only: stacked, each
     panel is its own auto-height row with nothing to claim. */
  .s2-grid > .panel { align-self: stretch; }
  .s2-grid > .s2-charts { display: flex; flex-direction: column; }
  .s2-grid > .s2-charts > .panel-bd { flex: 1; min-height: 0; display: flex; }
  .s2-grid > .s2-charts > .panel-bd > .s2-canvas-wrap {
    flex: 1 1 0; height: auto; min-height: calc(160px * var(--s));
  }
}

/* Portfolio Performance (/portfolio): the Allocation pie beside the aggregated
   equity panel. The pie column is ~30% wider and taller than the equity panel's
   340px so the pie can keep a modest radius and still have room to explode.

   This is the one box the density scale deliberately does NOT shrink: the pie's
   canvas strip, its legend strip and the column width are plain px, so the
   Allocation panel — and with it the height-matched equity panel — is the same
   size at either text size. Only the two title strips (and the label text the
   pie draws itself, which reads --s) get denser, which is the point: same chart,
   more chart per pixel of chrome. */
.pf-perf-grid { display: grid; gap: 1rem; grid-template-columns: 1fr; align-items: start; }
/* Stacked (below the two-column breakpoint) the equity panel has no row height to
   match, so pin its canvas to the same unscaled strip the pie uses. */
.pf-perf-grid .s2-canvas-wrap { height: 340px; }
/* Side by side, the equity panel matches the Allocation panel's height exactly:
   stretching both grid items (over .panel's own align-self:start) makes them as
   tall as the row, and the canvas wrap then claims whatever is left under the tab
   strip. Deriving it from the row — rather than restating the pie's 380px + 62px
   legend — keeps the two in step if either strip is retuned. Two-column layout
   only: stacked, each panel is its own auto-height row with nothing to claim.

   The breakpoint is where the equity panel gets at least as much width as the
   Allocation panel it sits beside — 494 + 1rem gap + ~494, plus the page and box
   padding. Below it the equity chart was being squeezed into a sliver next to a
   full-width pie; now it takes its own full-width row instead. */
@media (min-width: 1080px) {
  .pf-perf-grid { grid-template-columns: minmax(364px, 494px) 1fr; }
  .pf-perf-grid > .panel { align-self: stretch; }
  .pf-perf-grid > .s2-charts { display: flex; flex-direction: column; }
  .pf-perf-grid > .s2-charts > .panel-bd { flex: 1; min-height: 0; }
  .pf-perf-grid > .s2-charts > .panel-bd > .s2-canvas-wrap { height: 100%; }
}
.pf-alloc canvas { cursor: pointer; }
/* Two fixed strips: the pie's canvas and the legend below it. Both heights are
   hard-set so neither can borrow space from the other — a wrapping legend must
   never shrink the pie (that resize is what killed the explode effect). Both are
   unscaled px — see .pf-perf-grid. */
.pf-alloc .pf-alloc-wrap { height: 380px; }  /* outranks the shared .s2-canvas-wrap */
.pf-legend {
  display: flex; flex-wrap: wrap; justify-content: center; align-content: center;
  gap: .25rem .8rem; height: 62px; overflow: hidden;
  font-size: .8rem; color: var(--dim);
}
.pf-legend span { display: inline-flex; align-items: center; gap: .35rem; }
.pf-legend i { width: .6rem; height: .6rem; border-radius: 50%; flex: none; }

.s2-charts .s2-pt { padding: .4rem .6rem; }
.s2-tabs { display: inline-flex; flex-wrap: wrap; gap: .3rem; }
.s2-tab {
  cursor: pointer; font: inherit; font-size: .82rem; font-weight: 600;
  color: var(--dim); background: var(--bg);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: .32rem .7rem; transition: .15s;
}
.s2-tab:hover { color: var(--text); border-color: var(--accent); }
.s2-tab.is-active {
  color: var(--accent-ink); background: var(--accent); border-color: var(--accent);
}
.s2-tab:focus-visible {
  outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent);
}
/* Fixed-height wrap so Chart.js (maintainAspectRatio:false) has a box to fill. */
.s2-canvas-wrap { position: relative; height: calc(340px * var(--s)); width: 100%; }

/* Advanced Statistics (7-tab panel, full width below the Stats/charts grid). */
.s2-adv { margin-top: 1rem; }
.adv-hd { flex-wrap: wrap; row-gap: .5rem; }
.adv-hd h3 { margin-right: .6rem; }
.adv-tabs { flex-wrap: wrap; }
.adv-panel.is-hidden { display: none; }
/* Trades tab: three grouped columns filled top-to-bottom (myfxbook layout). */
.stat-cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0 2rem; }
.stat-col { display: flex; flex-direction: column; }
@media (max-width: 860px) { .stat-cols { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .stat-cols { grid-template-columns: 1fr; } }
.stat {
  display: flex; justify-content: space-between; align-items: baseline; gap: .6rem;
  padding: .34rem 0; border-bottom: 1px solid var(--border); font-size: .87rem;
}
.stat .sk { color: var(--dim); }
.stat .sv { font-family: var(--font-mono); font-variant-numeric: tabular-nums; letter-spacing: -.01em; text-align: right; }
/* Summary / Risk-of-ruin tables reuse table.kv; give headers + numeric columns. */
table.adv-tbl th {
  font-size: .66rem; letter-spacing: .06em; text-transform: uppercase; color: var(--dim);
  font-weight: 700; padding: .3rem 0 .4rem; border-bottom: 1px solid var(--border);
}
table.adv-tbl th.num, table.adv-tbl td.num { text-align: right; }
table.adv-tbl td { padding: .34rem 0; }
table.adv-tbl td:not(:first-child), table.adv-tbl th:not(:first-child) { padding-left: 1rem; }
/* Risk of ruin is transposed (levels as columns) — let it scroll if too wide. */
.adv-tbl-scroll { overflow-x: auto; }
table.adv-ror { min-width: calc(640px * var(--s)); }
table.adv-ror td:first-child, table.adv-ror th:first-child { white-space: nowrap; }

/* Trading Activity — History: full-width table, all trades in a fixed-height
   scroll box with a sticky header. */
.s2-history { margin-top: 1rem; }
.s2-history-scroll { max-height: calc(420px * var(--s)); overflow-y: auto; }
table.s2-history-tbl { width: 100%; min-width: calc(720px * var(--s)); }
table.s2-history-tbl th {
  position: sticky; top: 0; z-index: 1; background: var(--surface);
}
table.s2-history-tbl td { white-space: nowrap; }
/* color:var(--text) overrides table.kv's dim first-child rule for the Open cell. */
table.s2-history-tbl td.ttime { color: var(--text); font-variant-numeric: tabular-nums; }
table.s2-history-tbl td.tsym { font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
/* Open · Close · Symbol · Side are centered (myfxbook layout); the price/P&L
   columns keep the numeric right-align from table.adv-tbl. */
table.s2-history-tbl th:nth-child(-n+4),
table.s2-history-tbl td:nth-child(-n+4) { text-align: center; }
.trade-side {
  display: inline-block; font-size: .68rem; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; padding: .08rem .4rem; border-radius: 4px;
  border: 1px solid var(--border); color: var(--dim);
}
.trade-side.pos { color: var(--pos); border-color: color-mix(in srgb, var(--pos) 35%, transparent); background: color-mix(in srgb, var(--pos) 12%, transparent); }
.trade-side.neg { color: var(--neg); border-color: color-mix(in srgb, var(--neg) 35%, transparent); background: color-mix(in srgb, var(--neg) 12%, transparent); }
.s2-history-meta { margin-top: .6rem; }

/* Monthly Analytics: full-width gain chart (the month picker) over three drill-downs. */
.s2-monthly { margin-top: 1rem; }
.month-cap {
  margin: .2rem 0 .5rem; font-size: .82rem; font-weight: 650; color: var(--text);
}
.month-cap[data-cap] { color: var(--dim); font-weight: 600; }
.s2-canvas-wrap.month-gain { height: calc(240px * var(--s)); margin-bottom: 1rem; cursor: pointer; }
/* The three drill-downs reflow to the panel's *own* width, not the viewport's:
   `auto-fit` drops to two columns (then one) when the panel is narrow — which it
   is whenever the dense view pairs Monthly analytics beside Trading activity, at
   any viewport width. A viewport breakpoint can't see that halving, which is why
   this is a track sizing rule and not an @media.
   Both bounds matter: the min() floor lets the track fall below the nominal
   column width rather than overflow a very narrow panel, and the explicit
   minimum replaces `1fr`'s implicit `auto` one — a canvas reports its current
   pixel width as min-content, so `auto` floored every column at the canvas's
   initial 300px, the panel overflowed, and Chart.js (which only ever reads the
   container) had nothing to shrink into. min-width:0 on the cell is the same
   guard one level in, for when the floor itself is what gives. */
.month-drill {
  display: grid; gap: 1.2rem;
  grid-template-columns: repeat(auto-fit, minmax(min(calc(300px * var(--s)), 100%), 1fr));
}
.month-cell { min-width: 0; }
.s2-canvas-wrap.sm { height: calc(200px * var(--s)); }
/* "Please select a month" placeholder, shown until a month bar is clicked. */
.month-empty { display: none; color: var(--dim); text-align: center; font-size: .9rem; padding: 4.6rem 1rem; }
.month-cell.is-empty .s2-canvas-wrap { display: none; }
.month-cell.is-empty .month-empty { display: block; }

/* ---------- small-view (dense) row pairing ---------- */
/* At --s .75 every panel is three quarters of its normal size, so a wide screen
   has room the large view doesn't: .s2-row-activity (a plain block wrapper around
   Monthly analytics + Trading activity) becomes a two-column grid there. The
   breakpoint is a viewport width — never scaled, like every other @media here —
   set at the narrowest the two stay readable in: 1080px gives each ~490px. Below
   it, and at the default density, the wrapper is inert and Trading activity takes
   its own full-width row, which is the layout the large view always uses. */
@media (min-width: 1080px) {
  :root[data-textsize="small"] .s2-row-activity {
    display: grid; gap: 1rem; align-items: start; margin-top: 1rem;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  /* The 1rem that separated the two when stacked is now the grid's column gap. */
  :root[data-textsize="small"] .s2-row-activity > .panel { margin-top: 0; }
}

@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
