/* DeskPilot — palettes
   ===========================================================================
   Three palettes, four choices. `lib/theme.php` puts the choice on <html> as
   data-theme, from the user's `users.theme` column cached in the session.

     dark    N1 Brass      warm near-black, cockpit amber   (also the default)
     light   N3 Parchment  warm paper, ink text, deep teal
     auto    Brass, or Parchment when the OS asks for light
     deuter  N5 Beacon     deuteranopia-safe

   Four rules hold in here, and every component below depends on them:

   1. NOTHING outside this block names a colour. A literal hex or rgb() further
      down the file is a colour that cannot follow the theme — that is what made
      light mode a real job rather than a token swap, since tints computed
      against a dark card wash out to nothing on parchment.

   2. Colours needed at partial alpha are ALSO published as `--x-rgb` channel
      triplets, used as `rgba(var(--up-rgb), .3)`. One variable then covers every
      alpha a component wants, instead of a --up-dim-1/-2/-3 ladder.

   3. A FILL and the LABEL ON IT are separate tokens. --accent and --down are
      tuned to be readable AS text on a card, which necessarily makes them too
      light to sit BEHIND white text: `#fff` on the old --accent measured 3.16:1
      and on --down 2.99:1, both failing AA on the two most important buttons in
      the app. Hence --accent-fill/--on-accent and --down-fill/--on-down — and
      --on-* is a token because Brass and Beacon have LIGHT fills that need a
      dark label, so a hard-coded #fff is wrong in two of the three palettes.

   4. Every text role in every palette is verified at >= 4.5:1 against the
      surface it sits on. brand/pal2.py computes them; brand/palettes.css records
      the measured ratio beside each one.
   =========================================================================== */

/* Theme-independent: geometry and type. */
:root {
    --radius:    14px;
    /* Chrome geometry, shared by the top bar and the rail that hangs below it. */
    --topbar-h:  62px;
    --rail-w:    212px;
    --font:      -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --mono:      ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

/* ---- N1 Brass — dark, and the default for anyone with no preference -------
   `auto` starts here and is overridden by the media query further down, so an
   OS asking for dark (or not asking at all) needs no second copy of this. */
:root,
[data-theme="dark"],
[data-theme="auto"] {
    color-scheme: dark;
    --bg:          #0A0907;
    --bg-2:        #11100C;
    --card:        #191712;
    --card-2:      #22201A;
    --line:        #373227;
    --line-soft:   #241F18;
    --text:        #F2EDE2;
    --muted:       #A79E8C;
    --faint:       #887F6C;
    --accent:      #E0A33C;
    --accent-fill: #E0A33C;
    --on-accent:   #12100B;
    --accent-2:    #C9822B;
    --up:          #4FD08A;
    --down:        #FF5F6B;
    --down-fill:   #D32348;
    --on-down:     #FFFFFF;
    --warn:        #FFD84D;
    --knob:        #F2EDE2;
    --text-rgb:     242,237,226;
    --accent-rgb:   224,163,60;
    --accent-2-rgb: 201,130,43;
    --up-rgb:       79,208,138;
    --down-rgb:     255,95,107;
    --warn-rgb:     255,216,77;
    --bg-rgb:       10,9,7;
    --chrome-rgb:   17,16,12;
    --scrim-rgb:    6,5,4;
    --shadow:      0 12px 40px rgba(0,0,0,.45);
    --up-dim:      rgba(var(--up-rgb),   .14);
    --down-dim:    rgba(var(--down-rgb), .14);
    --warn-dim:    rgba(var(--warn-rgb), .12);
}

/* ---- N3 Parchment — light ------------------------------------------------- */
[data-theme="light"] {
    color-scheme: light;
    --bg:          #F7F3EA;
    --bg-2:        #F1ECE0;
    --card:        #FFFDF8;
    --card-2:      #F5F1E7;
    --line:        #DCD5C6;
    --line-soft:   #E9E3D6;
    --text:        #1E1B16;
    --muted:       #5C554A;
    --faint:       #7C7466;
    --accent:      #0F6E63;
    --accent-fill: #0F6E63;
    --on-accent:   #FFFFFF;
    --accent-2:    #16857A;
    --up:          #0A6B4A;
    --down:        #B3243C;
    --down-fill:   #B3243C;
    --on-down:     #FFFFFF;
    --warn:        #8A5A08;
    --knob:        #FFFDF8;
    --text-rgb:     30,27,22;
    --accent-rgb:   15,110,99;
    --accent-2-rgb: 22,133,122;
    --up-rgb:       10,107,74;
    --down-rgb:     179,36,60;
    --warn-rgb:     138,90,8;
    --bg-rgb:       247,243,234;
    --chrome-rgb:   241,236,224;
    --scrim-rgb:    30,27,22;
    /* A 40px black shadow that reads as depth on near-black reads as dirt on
       paper, so the light theme gets a shorter, weaker one. */
    --shadow:      0 6px 18px rgba(30,27,22,.10);
    --up-dim:      rgba(var(--up-rgb),   .10);
    --down-dim:    rgba(var(--down-rgb), .09);
    --warn-dim:    rgba(var(--warn-rgb), .10);
}

/* `auto` = follow the OS, which already does the sunrise/sunset switch itself.
   THIS IS THE ONE DUPLICATED BLOCK IN THIS FILE: plain CSS has no way to alias a
   whole custom-property set into a second selector, and light-dark() would leave
   a browser that lacks it with no colours at all rather than the wrong ones.
   Keep it identical to [data-theme="light"] above — tests/http.php asserts the
   two agree, so drift fails the suite rather than shipping. */
@media (prefers-color-scheme: light) {
    [data-theme="auto"] {
        color-scheme: light;
        --bg:          #F7F3EA;
        --bg-2:        #F1ECE0;
        --card:        #FFFDF8;
        --card-2:      #F5F1E7;
        --line:        #DCD5C6;
        --line-soft:   #E9E3D6;
        --text:        #1E1B16;
        --muted:       #5C554A;
        --faint:       #7C7466;
        --accent:      #0F6E63;
        --accent-fill: #0F6E63;
        --on-accent:   #FFFFFF;
        --accent-2:    #16857A;
        --up:          #0A6B4A;
        --down:        #B3243C;
        --down-fill:   #B3243C;
        --on-down:     #FFFFFF;
        --warn:        #8A5A08;
        --knob:        #FFFDF8;
        --text-rgb:     30,27,22;
        --accent-rgb:   15,110,99;
        --accent-2-rgb: 22,133,122;
        --up-rgb:       10,107,74;
        --down-rgb:     179,36,60;
        --warn-rgb:     138,90,8;
        --bg-rgb:       247,243,234;
        --chrome-rgb:   241,236,224;
        --scrim-rgb:    30,27,22;
        --shadow:      0 6px 18px rgba(30,27,22,.10);
        --up-dim:      rgba(var(--up-rgb),   .10);
        --down-dim:    rgba(var(--down-rgb), .09);
        --warn-dim:    rgba(var(--warn-rgb), .10);
    }
}

/* ---- N5 Beacon — deuteranopia-safe ---------------------------------------
   The whole palette follows one rule: HUE IS RESERVED FOR MEANING. The accent
   is neutral bone, so the only two hues on the page are the Okabe-Ito pair —
   sky blue for profit, vermillion for loss. Simulated (brand/brand_deuter.py),
   the conventional green/red pair collapses to the SAME olive-yellow, i.e. the
   two numbers that matter most become one colour.

   `--warn` cannot be a third hue anyone with deuteranopia can name, so it is
   carried by LIGHTNESS (a much lighter yellow) plus the word in the label. That
   is the ceiling of what three states and two usable hue channels allow, and it
   is why nothing here may become colour-only. */
[data-theme="deuter"] {
    color-scheme: dark;
    --bg:          #0A0C0E;
    --bg-2:        #101317;
    --card:        #171B20;
    --card-2:      #1F242A;
    --line:        #333A43;
    --line-soft:   #232830;
    --text:        #ECEFF2;
    --muted:       #9BA3AC;
    --faint:       #7C848C;
    --accent:      #E9E4D8;
    --accent-fill: #E9E4D8;
    --on-accent:   #0A0C0E;
    --accent-2:    #C8C3B6;
    --up:          #56B4E9;
    --down:        #F0743A;
    --down-fill:   #BF4A16;
    --on-down:     #FFFFFF;
    --warn:        #F0E442;
    --knob:        #ECEFF2;
    --text-rgb:     236,239,242;
    --accent-rgb:   233,228,216;
    --accent-2-rgb: 200,195,182;
    --up-rgb:       86,180,233;
    --down-rgb:     240,116,58;
    --warn-rgb:     240,228,66;
    --bg-rgb:       10,12,14;
    --chrome-rgb:   16,19,23;
    --scrim-rgb:    4,6,8;
    --shadow:      0 12px 40px rgba(0,0,0,.45);
    --up-dim:      rgba(var(--up-rgb),   .14);
    --down-dim:    rgba(var(--down-rgb), .14);
    --warn-dim:    rgba(var(--warn-rgb), .12);
}

* { box-sizing: border-box; }
html, body { margin: 0; }
body {
    background:
        radial-gradient(1200px 600px at 80% -10%, rgba(var(--accent-rgb), .10), transparent 60%),
        radial-gradient(900px 500px at -10% 10%, rgba(var(--accent-2-rgb), .08), transparent 55%),
        var(--bg);
    color: var(--text);
    font-family: var(--font);
    -webkit-font-smoothing: antialiased;
    min-height: 100vh;
}

/* Links are the body text colour, not the browser's blue. There was no `a` rule
   at all, so every anchor that no component class happened to cover — the login
   card's "Forgot your password?", the terms/privacy cross-links, help.php's
   prose — rendered in #0000EE and, once followed, purple, on a dark card.
   Consequence to keep: colour is now no channel at all, so the UNDERLINE is the
   only thing marking a link in prose. Every component that turns it off
   (.btn, .menu-item, .rail-item, .brand, .badge-exp) replaces it with a border,
   a background or a pill; a new one must do the same rather than just dropping
   it. `:visited` is pinned for the same reason the base is: the UA sheet colours
   it separately, so leaving it out brings the purple back on exactly the links
   someone uses most. A link that carries a SAFETY signal keeps its own colour —
   .menu-verify's amber says "this needs attention", which is not a link colour.

   AND THE TRAP THAT COST: `a:visited` is specificity (0,1,1) and beats any single-class rule
   (0,1,0). So a component that sets its OWN text colour loses it the moment the
   link has been followed — `<a class="btn btn-primary">` rendered its label in
   --text over the accent gradient, which in Parchment is near-black ink on deep
   teal. It looked like a broken palette and was in fact "you have visited this
   page": Create a free account, Start free and Dashboard were all affected, and
   the account menu's red "Delete account" lost the red that made it a safety
   signal (that entry has since moved to account.php, where an irreversible
   cascade belongs — but the lesson is the reason this rule exists).
   Every component colour below therefore claims `:visited` as well, and
   tests/http.php pins the rule so the next one cannot forget. */
a, a:visited { color: var(--text); }
a:hover { text-decoration-color: var(--accent); }

/* ---- top bar ---- */
.topbar {
    display: flex; align-items: center; justify-content: space-between;
    /* A fixed height rather than padding, because --topbar-h is where the rail
       starts: derived from the content it would be a number two files have to agree
       on by luck. The tallest thing in here is the two-line brand stack (18px + 10px
       at line-height 1.1 = 31px), so 62px leaves the same ~15px above and below the
       old padding gave. */
    height: var(--topbar-h); padding: 0 22px;
    border-bottom: 1px solid var(--line-soft);
    background: rgba(var(--chrome-rgb), .7);
    backdrop-filter: blur(10px);
    position: sticky; top: 0; z-index: 20;
}
.brand { font-weight: 700; font-size: 18px; letter-spacing: .3px; display: flex; gap: 10px; align-items: center; }
/* The mark, inlined by dp_logo_svg() so it can inherit `currentColor` — which is
   what makes it follow the palette, including a live prefers-color-scheme flip
   under `auto`. An <img> is a separate document and cannot see --accent, so it
   would be stuck at one colour and, half the day, at the wrong one.
   `flex: none` because .brand is a flex row: without it the SVG is the
   shrinkable item next to the wordmark and squashes instead of the text
   wrapping — the same trap .switch-inline hit. */
.brand-logo {
    flex: none;
    width: 34px; height: 28px;
    color: var(--accent);
}
/* The wordmark is Space Grotesk 700, self-hosted rather than pulled from
   fonts.gstatic.com: a font CDN sees every visitor's IP, which is a data
   transfer the privacy policy would have to declare, and it would also make
   the brand depend on the network. 12.8 KB, latin only, loaded on demand
   because nothing else on the page uses the family. */
@font-face {
    font-family: "Space Grotesk";
    src: url("fonts/space-grotesk-700-latin.woff2") format("woff2");
    font-weight: 700; font-style: normal; font-display: swap;
}
.brand-name { font-family: "Space Grotesk", var(--font); letter-spacing: .2px; }
/* Two lines, right edges flush: the tier reads as a subtitle of the wordmark,
   which is why the stack is align-items:flex-end and not the usual start. */
.brand-stack { display: flex; flex-direction: column; align-items: flex-end; line-height: 1.1; }
.brand-tier {
    font-size: 10px; font-weight: 700; letter-spacing: 1.6px;
    text-transform: uppercase; color: var(--muted);
    margin-right: -1.6px;   /* letter-spacing trails the last glyph; pull it back so the right edges line up */
}
.brand-tier.tier-warn { color: var(--warn); }
.brand-tier.tier-dead { color: var(--down); }
.topbar-right { display: flex; align-items: center; gap: 14px; }
.clock { font-family: var(--mono); color: var(--muted); font-size: 13px; white-space: nowrap; }
/* The VPS's clock, in the timezone NinjaTrader is set to — see tickClock(). The
   zone is the load-bearing part (a bare 09:23 could be anyone's clock), so it
   keeps full contrast while the digits stay muted. */
.clock-tz { color: var(--text); font-weight: 700; }

/* ---- account menu (top bar dropdown) ---- */
.menu { position: relative; }
.menu-toggle { display: inline-flex; align-items: center; gap: 8px; }
.menu-avatar {
    display: inline-flex; align-items: center; justify-content: center;
    width: 22px; height: 22px; border-radius: 50%; font-size: 11px; font-weight: 700;
    background: linear-gradient(135deg, var(--accent-fill), var(--accent-2)); color: var(--on-accent);
}
.menu-label { max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.menu-caret { color: var(--muted); font-size: 10px; }
.menu.is-open .menu-toggle { border-color: var(--accent); }
.menu-panel {
    position: absolute; right: 0; top: calc(100% + 8px); z-index: 40;
    min-width: 250px; padding: 6px;
    background: var(--card); border: 1px solid var(--line);
    border-radius: 12px; box-shadow: var(--shadow);
}
.menu-panel[hidden] { display: none; }
.menu-head {
    padding: 10px 12px 12px; color: var(--faint); font-size: 12px; line-height: 1.5;
    border-bottom: 1px solid var(--line-soft); margin-bottom: 6px;
}
.menu-head strong { color: var(--text); font-size: 13px; word-break: break-all; }
.menu-item, .menu-item:visited {
    display: flex; align-items: center; gap: 10px;
    padding: 9px 12px; border-radius: 8px;
    color: var(--text); text-decoration: none; font-size: 13px; font-weight: 500;
}
.menu-item:hover { background: var(--card-2); }
.menu-item.is-on { color: var(--accent); }
.menu-ico { width: 16px; text-align: center; color: var(--muted); font-size: 13px; flex: none; }
.menu-item:hover .menu-ico { color: inherit; }
/* Two entries draw their icon as inline SVG in currentColor rather than as a
   glyph, because a colour-emoji glyph ignores `color` — see dp_icon_key() in
   lib/ui.php. The SVG is block-level so the 16px slot centres it the same way
   text-align centres the others. */
.menu-ico svg { display: block; margin: 0 auto; }

/* A menu entry that submits a form. .menu-item is written for anchors, so a
   <button> needs the UA's own button chrome taken off it before the two sit on
   the same baseline with the same box — the same argument as `a.btn` needing the
   button treatment on the class rather than per container. */
.menu-item-btn {
    width: 100%; background: none; border: 0; font: inherit; text-align: left;
    cursor: pointer;
}
.menu-ico-warn { color: var(--warn); font-weight: 700; }
.menu-verify-form { margin: 0; }
/* The menu's ONLY email-confirmation state, and the loudest thing in the panel on
   purpose. Amber rather than red: an unconfirmed address is something to attend
   to, not a failure or a danger — nothing is switched off by it. There is
   deliberately no confirmed counterpart here; that fact lives on account.php
   beside the address and the date, rather than as a permanent line in shared
   chrome saying nothing needs doing. */
.menu-verify, .menu-verify:visited { color: var(--warn); }
.menu-verify:hover { background: rgba(var(--warn-rgb), .12); }
.menu-verify:hover .menu-ico-warn { color: var(--warn); }
.menu-verify-text { display: flex; flex-direction: column; line-height: 1.35; }
/* The second line is what stops this reading as a passive badge: it names the
   action the click performs. */
.menu-verify-text small { color: var(--muted); font-size: 11px; font-weight: 500; }
.menu-verify:hover .menu-verify-text small { color: var(--text); }
.menu-sep { height: 1px; background: var(--line-soft); margin: 6px 4px; }

/* The same fact on account.php's Profile card, inline beside the address. The
   tick is a token, not a literal green: in Beacon it is the Okabe-Ito sky blue,
   because that palette reserves hue for meaning and a deuteranope cannot name
   green anyway. */
.verify-tag {
    display: inline-flex; align-items: center; gap: 6px;
    font-size: 12px; font-weight: 600; white-space: nowrap;
}
.verify-tag-ok { color: var(--up); }
.verify-tick { color: var(--up); font-weight: 700; }
/* The resend control on account.php: a real button, but a quiet one — the card
   already carries the explanation, so it must not outshout Save or Delete. */
.verify-resend {
    background: rgba(var(--warn-rgb), .12); border: 1px solid rgba(var(--warn-rgb), .35);
    color: var(--warn); border-radius: 999px; cursor: pointer;
    font: inherit; font-size: 12px; font-weight: 600; padding: 3px 10px;
}
.verify-resend:hover { background: rgba(var(--warn-rgb), .22); }
.verify-line { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

.pill { font-size: 12px; padding: 4px 10px; border-radius: 999px; font-weight: 600; border: 1px solid transparent; }
.pill-live  { color: var(--up);   background: var(--up-dim);   border-color: rgba(var(--up-rgb), .3); }
.pill-stale { color: var(--warn); background: rgba(var(--warn-rgb), .12); border-color: rgba(var(--warn-rgb), .3); }
.pill-dead  { color: var(--down); background: var(--down-dim); border-color: rgba(var(--down-rgb), .3); }
.pill-muted { color: var(--muted); background: var(--card-2); }

/* ---- layout ---- */
.wrap { max-width: 1240px; margin: 0 auto; padding: 24px 22px 60px; }
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; margin-bottom: 22px; }
.toolbar { display: flex; justify-content: flex-end; margin: -6px 0 16px; }
.switch-inline { display: inline-flex; align-items: center; gap: 10px; color: var(--muted); font-size: 13px; cursor: pointer; }

.kpi {
    background: linear-gradient(180deg, var(--card-2), var(--card));
    border: 1px solid var(--line-soft);
    border-radius: var(--radius);
    padding: 16px 18px;
    position: relative; overflow: hidden;
}
.kpi::after {
    content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
    background: linear-gradient(90deg, var(--accent), var(--accent-2));
    opacity: .5;
}
.kpi .label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .6px; }
.kpi .value { font-size: 26px; font-weight: 700; margin-top: 6px; font-family: var(--mono);
    font-variant-numeric: tabular-nums; }
.kpi .sub   { color: var(--faint); font-size: 12px; margin-top: 4px; }

/* ---- cards ---- */
.card {
    background: var(--card);
    border: 1px solid var(--line-soft);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    margin-bottom: 22px;
    overflow: hidden;
}
.card-head {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 16px 18px; border-bottom: 1px solid var(--line-soft);
}
.card-head h2 { margin: 0; font-size: 16px; font-weight: 700; }
.card-head .hint { color: var(--faint); font-size: 12px; }
.card-body { padding: 18px; }

.acct-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.acct-name { font-size: 17px; font-weight: 700; }
/* Fixed column widths (not content-sized) so the PnL / Unreal. / Positions /
   Net Liq columns land on the same x-position in EVERY account card — with a
   flex/auto layout each card sized its own columns and the numbers zig-zagged. */
.acct-metrics {
    display: grid;
    grid-template-columns: 112px 112px 78px 140px;
    gap: 14px; margin-left: auto;
}
.metric { text-align: right; min-width: 0; }
.metric .m-label { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .5px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.metric .m-value { font-family: var(--mono); font-size: 17px; font-weight: 700;
    font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.chart-wrap { position: relative; height: 220px; padding: 10px 6px 0; }

/* plan-limit banner (e.g. accounts withheld by the tier) */
.notice {
    display: block; margin-bottom: 18px; padding: 12px 16px;
    border: 1px solid rgba(var(--warn-rgb), .35); border-left: 3px solid var(--warn);
    border-radius: var(--radius); background: rgba(var(--warn-rgb), .08);
    color: var(--text); font-size: 13px; line-height: 1.5;
}
.notice-err { border-color: rgba(var(--down-rgb), .45); background: var(--down-dim); }
.notice strong { color: var(--warn); }
/* Honor the [hidden] attribute — display:block above otherwise overrides it. */
.notice[hidden] { display: none; }
.notice > div + div { margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(var(--warn-rgb), .2); }

/* An agent that has gone quiet is not a warning, it is a failure: no snapshots
   means the risk limits are not being enforced either. Red, not amber. */
.notice.notice-bad { border-color: rgba(var(--down-rgb), .35); border-left-color: var(--down);
    background: rgba(var(--down-rgb), .08); }
.notice.notice-bad strong { color: var(--down); }
.notice.notice-bad > div + div { border-top-color: rgba(var(--down-rgb), .2); }

.head-right { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.hint.strong { font-weight: 700; font-family: var(--mono); font-variant-numeric: tabular-nums; }
.hint.pos { color: var(--up); }
.hint.neg { color: var(--down); }
.pad-sm { padding: 0 18px 14px; }
.card.pad { padding: 18px 20px 20px; margin-bottom: 16px; }
.card.pad h2 { font-size: 16px; margin: 0 0 10px; }
.card.pad ul { margin: 8px 0 12px; padding-left: 20px; }
.card.pad li { margin-bottom: 6px; }
.card.pad p { margin: 0 0 10px; line-height: 1.55; }
.card.pad code {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
    background: var(--card-2); padding: 1px 5px; border-radius: 4px;
}
/* "experimental" marker on a section head, linking to the explanation */
.badge-exp, .badge-exp:visited {
    font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
    color: var(--warn); background: rgba(var(--warn-rgb), .12);
    border: 1px solid rgba(var(--warn-rgb), .4); border-radius: 999px;
    padding: 2px 8px; text-decoration: none; white-space: nowrap;
}
.badge-exp:hover { background: rgba(var(--warn-rgb), .22); }

/* ---- collapsible account cards ---- */
.acct-card { }
.acct-summary {
    display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
    padding: 14px 18px; cursor: pointer; user-select: none;
    transition: background .12s ease;
}
.acct-summary:hover { background: rgba(var(--text-rgb), .02); }
/* The chevron and the status dot, as ONE fixed-width group leading the row: the
   disclosure control first, then the account's state, then the name. Their combined
   width does not depend on the account, so both markers form columns that can be
   scanned down 88 cards and every name starts at the same x — which was the whole
   point of moving the dot off the end of the name.

   The chevron is 15px rather than 12: at 12 it read as a decorative bullet, which is
   the opposite of what it is. It is the only thing on a touch screen that says the
   row opens (no pointer cursor, no hover tint), so it has to look like a control. */
.acct-marks { display: flex; align-items: center; gap: 8px; flex: none; }
.acct-summary .chevron {
    color: var(--faint); font-size: 15px; line-height: 1;
    transition: transform .15s ease; flex: none;
}
.acct-card.open .acct-summary { border-bottom: 1px solid var(--line-soft); }
.acct-card.open .acct-summary .chevron { transform: rotate(90deg); }
/* Just the name now — the dot moved to .acct-marks at the head of the row. Kept as
   a flex row so anything added beside the name later lines up with it. */
.acct-id { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* Which VPS the account is on, under its name — rendered only when the tenant has
   more than one machine (see app.js). The column direction is a MODIFIER rather
   than the default, so a single-VPS card is laid out exactly as it was. Same shape
   as .brand-stack, which puts the plan code under the wordmark. */
.acct-id.has-vps { flex-direction: column; align-items: flex-start; gap: 1px; }
.acct-vps { display: block; font-size: 11px; color: var(--muted); line-height: 1.2;
            max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-toolbar { display: flex; align-items: center; gap: 12px; padding: 14px 18px 0; flex-wrap: wrap; }
.acct-toolbar .spacer { flex: 1 1 auto; }

/* segmented control (chart metric picker) */
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 9px; overflow: hidden; }
.seg-btn {
    font: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
    padding: 5px 11px; border: none; border-right: 1px solid var(--line);
    background: transparent; color: var(--muted); transition: .15s ease;
}
.seg-btn:last-child { border-right: none; }
.seg-btn:hover { color: var(--text); background: var(--card-2); }
.seg-btn.is-on { background: var(--card-2); color: var(--accent); }
.acct-body > .chart-wrap { padding-top: 14px; }

/* ---- tables ---- */
.table-wrap { overflow-x: auto; }
.tbl { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.tbl th {
    text-align: left; color: var(--muted); font-weight: 600; font-size: 11px;
    text-transform: uppercase; letter-spacing: .5px;
    padding: 10px 14px; border-bottom: 1px solid var(--line);
    white-space: nowrap;
}
.tbl td { padding: 11px 14px; border-bottom: 1px solid var(--line-soft); white-space: nowrap; }
.tbl tbody tr:hover { background: rgba(var(--text-rgb), .02); }
.tbl .num { font-family: var(--mono); text-align: right; font-variant-numeric: tabular-nums;
    white-space: nowrap; }
.empty { color: var(--faint); padding: 18px; text-align: center; font-size: 13px; }

.tag { font-size: 11px; padding: 2px 8px; border-radius: 6px; font-weight: 700; letter-spacing: .3px; }
.tag-long  { color: var(--up);   background: var(--up-dim); }
.tag-short { color: var(--down); background: var(--down-dim); }
.tag-flat  { color: var(--muted); background: var(--card-2); }
/* a toggle came back failed — the reason is in the title attribute */
.tag-fail { color: var(--down); background: var(--down-dim); cursor: help; }
/* a toggle is queued: the NT state is stale until it runs, so say that instead */
.tag-pending {
    color: var(--warn); background: rgba(var(--warn-rgb), .14);
    border: 1px solid rgba(var(--warn-rgb), .45); cursor: help;
}
/* Running but not placing live orders: NinjaTrader's own orange row. Deliberately
   the same --warn amber the Control Center uses, so the dashboard and the platform
   agree at a glance. Sits NEXT TO the state pill, hence the left margin. */
.tag-waitflat {
    color: var(--warn); background: rgba(var(--warn-rgb), .14);
    border: 1px solid rgba(var(--warn-rgb), .45); cursor: help; margin-left: 6px;
}
/* on/off could not be determined — dashed, so it does not read as a known state */
.tag-unknown {
    color: var(--warn); background: rgba(var(--warn-rgb), .10);
    border: 1px dashed rgba(var(--warn-rgb), .45); cursor: help;
}

/* Details is the one free-text column in the Activity table; capped, so a long
   value can never push Status and Result off the card (full text in the title). */
.tbl td.cmd-details { max-width: 280px; overflow: hidden; text-overflow: ellipsis; }
/* ...and Result wraps rather than widening the table: it is prose from the Add-On
   and can run to a hundred characters. Out-specifies `.tbl td`'s nowrap. */
.tbl td.cmd-result { white-space: normal; min-width: 220px; }
.st { font-size: 11px; padding: 2px 9px; border-radius: 999px; font-weight: 700; }
.st-pending    { color: var(--warn);   background: rgba(var(--warn-rgb), .12); }
.st-dispatched { color: var(--accent); background: rgba(var(--accent-rgb), .14); }
.st-done       { color: var(--up);     background: var(--up-dim); }
.st-failed     { color: var(--down);   background: var(--down-dim); }

.pos { color: var(--up); }
.neg { color: var(--down); }

/* ---- buttons ---- */
.btn, .btn:visited {
    font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
    border-radius: 9px; padding: 8px 13px; border: 1px solid var(--line);
    background: var(--card-2); color: var(--text); transition: .15s ease;
}
.btn:hover { border-color: var(--accent); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
/* The same state for an <a class="btn"> that the plan does not allow. An anchor
   cannot be :disabled — the attribute is meaningless on it — so gate() in app.js
   strips the href and adds this instead, and without the rule the link would look
   entirely available while doing nothing. */
.btn.is-disabled { opacity: .5; cursor: not-allowed; }
/* An <a class="btn"> has to render identically to a <button class="btn">. It did
   not: an anchor is inline and underlined, so it sat on a different baseline to
   its siblings and carried a rule through the middle of its label — visible in
   the landing page's header CTAs and in any modal that puts a link beside two
   buttons. landing.css had been patching this one container at a time
   (.lp-hero-cta, .lp-tier, .lp-split, .lp-cta) and still missed .lp-nav-cta,
   which is the argument for fixing it on the class instead. */
a.btn { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; }
a.btn:hover { text-decoration: none; }
/* A TRANSPARENT border, never `border: none`. .btn carries a 1px border, so
   dropping it made every primary button 2px shorter than the default button
   beside it — visible wherever the two sit in a row, and on the billing grid it
   read as one card's button being misaligned. The gradient paints under the
   border (background-clip is border-box), so this looks identical and measures
   the same. Any new .btn variant has to keep the 1px, transparent or not. */
.btn-primary, .btn-primary:visited {
    background: linear-gradient(135deg, var(--accent-fill), var(--accent-2));
    border: 1px solid transparent; color: var(--on-accent);
}
.btn-primary:hover { filter: brightness(1.08); }
.btn-danger, .btn-danger:visited { background: var(--down-dim); border-color: rgba(var(--down-rgb), .35); color: var(--down); }
.btn-danger:hover { background: rgba(var(--down-rgb), .22); border-color: var(--down); }
.btn-ghost { background: transparent; }
.btn-sm { padding: 5px 10px; font-size: 12px; }
.btn-block { width: 100%; padding: 11px; }
.btn-flatten, .btn-flatten:visited { background: var(--down-fill); color: var(--on-down); border: none; font-weight: 700; }
.btn-flatten:hover { filter: brightness(1.08); }
.row-actions { display: flex; gap: 6px; justify-content: flex-end; }

/* ---- settings pages ---- */
.page-title { margin: 6px 0 18px; font-size: 22px; }
.sub-title { margin: 4px 0 2px; font-size: 14px; font-weight: 700; }
.rule { border: none; border-top: 1px solid var(--line-soft); margin: 22px 0 18px; }
.stack { display: block; }
.form-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 10px; }
.form-row .input { width: auto; min-width: 210px; flex: 0 1 auto; }

/* definition list used for the profile block */
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 8px 22px; margin: 0 0 18px; }
.kv dt { color: var(--muted); font-size: 13px; }
.kv dd { margin: 0; font-size: 13px; }

.mono-sm { font-family: var(--mono); font-size: 12px; color: var(--muted); }
.code-block {
    font-family: var(--mono); font-size: 12.5px; line-height: 1.55;
    background: var(--bg-2); border: 1px solid var(--line);
    border-radius: 10px; padding: 14px 16px; margin: 0 0 14px;
    overflow-x: auto; white-space: pre; color: var(--text);
}
/* a card whose contents are a credential, and one that deletes things */
.secret-card { border-color: rgba(var(--warn-rgb), .45); }
.danger-card { border-color: rgba(var(--down-rgb), .35); }
.danger-card .card-head h2 { color: var(--down); }

/* ---- risk box ---- */
.risk {
    display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
    padding: 14px 18px; border-top: 1px solid var(--line-soft);
    background: var(--bg-2);
}
.risk .risk-title { font-weight: 700; font-size: 13px; }
.risk .hint { color: var(--faint); font-size: 12px; }
/* `flex: none` is not decoration. The track is a fixed 44x24 box whose knob is
   positioned inside it, but .switch-inline is a flex container: beside a long
   label the switch is the shrinkable item, and it shrank to 21px — under half —
   while the knob kept its 18px and its 20px checked travel, so a ticked box slid
   the knob clean outside its own track. Short labels hid it; the acceptance form
   on terms.php, whose label is a paragraph, did not. */
.switch { position: relative; display: inline-block; flex: none; width: 44px; height: 24px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; inset: 0; background: var(--line); border-radius: 999px; transition: .2s; }
/* THE KNOB NEEDS ITS OWN OUTLINE, because in two of the three palettes it has
   almost none against the track it sits on: measured knob-versus-track, Parchment
   OFF is 1.44:1, Brass ON 1.68:1 and Beacon ON 2.02:1 — all below the 3:1 a
   non-text control needs. Parchment is the one that looks broken rather than
   merely faint, and for the worst possible reason: `--knob` (#FFFDF8) is byte-
   identical to `--card`, so an unticked switch on any card is a pale capsule with
   no knob visible in it at all. Login escapes it only by accident (an anonymous
   visitor gets the default theme), so it showed up on the dashboard toolbar, the
   risk box and both modals instead.
   A ring rather than a darker `--knob`: the knob has to read on the grey OFF track
   AND on the saturated ON track in three palettes, and one token cannot be both.
   It is derived from `--text-rgb`, so it follows the theme like everything else
   here — a literal grey would be the thing the token block exists to prevent.
   `box-sizing` is declared because the reset is `* { box-sizing: border-box }` and
   the universal selector does NOT match pseudo-elements (see .plan-ticks li::before
   below): without it the 1px border makes this 20px inside a 24px track, and the
   20px checked travel then pushes it past the far edge. */
.slider::before {
    content: ""; position: absolute; height: 18px; width: 18px; left: 3px; top: 3px;
    box-sizing: border-box; background: var(--knob); border: 1px solid rgba(var(--text-rgb), .22);
    border-radius: 50%; transition: .2s;
}
.switch input:checked + .slider { background: var(--up); }
.switch input:checked + .slider::before { transform: translateX(20px); }
/* The input is `opacity: 0` and 0x0, so there is nothing for the browser's own
   focus ring to draw around — it has to be moved onto the slider by hand, or these
   five switches are the only controls in the app a keyboard cannot see it reach
   (.strat-sort, .shot-zoom and .tl-cell all carry one). */
.switch input:focus-visible + .slider { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Latent today — the one disabled toggle on schedules.php is a native checkbox —
   but a `.switch` whose input is disabled is otherwise indistinguishable from a
   live one, which is the read-only-tier failure the gated buttons avoid. */
.switch input:disabled + .slider { opacity: .45; cursor: not-allowed; }

/* Freshness as a colour instead of a word, for the per-account dot in a card header.
   Down a list of 88 cards the words were the noisiest thing on the page and repeated
   the same three states over and over; the colour carries all of it, and the duration
   lives in the title/aria-label that paintAgePill() rewrites each second.
   `flex: none` because .acct-marks is a flex row — the same trap as .switch below.
   The ring is a box-shadow rather than a border so it costs no layout space, and it is
   what stops a 9px dot vanishing against a dark card. */
.dot {
    flex: none; display: inline-block; width: 9px; height: 9px; border-radius: 50%;
    background: var(--muted); cursor: help; transition: background .2s, box-shadow .2s;
}
.dot-live  { background: var(--up);   box-shadow: 0 0 0 3px rgba(var(--up-rgb), .16); }
.dot-stale { background: var(--warn); box-shadow: 0 0 0 3px rgba(var(--warn-rgb), .18); }
.dot-dead  { background: var(--down); box-shadow: 0 0 0 3px rgba(var(--down-rgb), .18); }

/* A real checkbox, for a consent rather than a setting. A switch says "a mode is
   on" and is the right shape for "Hide disconnected accounts"; ticking a box is
   what someone recognises as accepting a document, and it is the affordance the
   acceptance record has to be able to claim was presented. It is also a native
   input rather than an `opacity: 0` one behind a styled span, so keyboard focus,
   a screen reader and the browser's own form restore all see what the user sees.
   Top-aligned: these labels are paragraphs, and a box centred against four lines
   of text reads as belonging to none of them. */
.checkbox-inline {
    display: flex; align-items: flex-start; gap: 10px;
    color: var(--muted); font-size: 13px; line-height: 1.65; cursor: pointer;
}
.signup-terms { margin-bottom: 12px; }
.checkbox-inline input[type="checkbox"] {
    flex: none; width: 17px; height: 17px; margin: 2px 0 0;
    /* No `color-scheme` here. It predates the palettes and was right when the
       only theme was dark; now each palette declares its own in the token block,
       and a local `color-scheme: dark` OVERRIDES the inherited one — so the tick
       and the box render for the wrong theme on Parchment while every other
       colour on the card follows it. Native form widgets are themed by
       inheriting `color-scheme`, which is the whole reason it is a root token. */
    accent-color: var(--accent); cursor: pointer;
}

.input {
    font: inherit; background: var(--bg); border: 1px solid var(--line);
    color: var(--text); border-radius: 9px; padding: 9px 11px; width: 100%;
}
.input:focus { outline: none; border-color: var(--accent); }
.input-sm { width: 130px; padding: 7px 10px; }

/* ---- modal ---- */
.modal-backdrop {
    position: fixed; inset: 0; background: rgba(var(--scrim-rgb), .72);
    display: flex; align-items: center; justify-content: center; z-index: 50;
    backdrop-filter: blur(4px);
}
/* Honor the [hidden] attribute — display:flex above otherwise overrides it. */
.modal-backdrop[hidden] { display: none; }
.modal {
    background: var(--card); border: 1px solid var(--line);
    border-radius: var(--radius); padding: 22px; width: min(420px, 92vw);
    box-shadow: var(--shadow);
}
.modal h3 { margin: 0 0 4px; }
.modal label { display: block; margin: 14px 0 4px; font-size: 13px; color: var(--muted); }
.modal-actions { display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin-top: 20px; }
/* "Read more" is a link, so it looks like one. It used to wear .btn and read as a
   third equal choice next to Cancel and Continue, when it is neither — it goes
   somewhere else entirely rather than answering the dialog. `margin-right: auto`
   pushes it to the far left so the two real buttons stay grouped on the right,
   which is the other half of saying it is not one of them. */
.modal-link {
    margin-right: auto; font-size: 13px;
    text-decoration: underline; text-underline-offset: 2px;
}
.modal-link:hover { text-decoration-color: var(--accent); }

/* ---- toast ---- */
.toast {
    position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
    background: var(--card-2); border: 1px solid var(--line); color: var(--text);
    padding: 12px 18px; border-radius: 10px; box-shadow: var(--shadow);
    z-index: 60; font-size: 14px; max-width: 90vw;
}
.toast.ok  { border-color: rgba(var(--up-rgb), .5); }
.toast.err { border-color: rgba(var(--down-rgb), .5); }

.foot { text-align: center; color: var(--faint); font-size: 12px; margin-top: 10px; }

/* ---- billing ---- */
.banner { padding: 12px 16px; border-radius: 10px; margin-bottom: 16px; font-size: 14px; border: 1px solid var(--line); }
.banner-ok   { border-color: rgba(var(--up-rgb), .4); background: var(--up-dim); color: var(--text); }
.banner-warn { border-color: rgba(var(--warn-rgb), .4); background: rgba(var(--warn-rgb), .10); color: var(--text); }
.banner-err  { border-color: rgba(var(--down-rgb), .45); background: var(--down-dim); color: var(--text); }
.plan-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; }
.plan-card {
    background: linear-gradient(180deg, var(--card-2), var(--card));
    border: 1px solid var(--line-soft); border-radius: var(--radius);
    padding: 20px 18px; display: flex; flex-direction: column; gap: 8px;
}
.plan-card.plan-current { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.plan-name  { font-size: 16px; font-weight: 700; }
/* Tabular figures and a touch of negative tracking: the mono face defaults to
   wide-set digits, so "0.00" and "29.99" read as spaced-out characters rather
   than as one number, and the three cards' prices did not line up column to
   column. The landing page's .lp-price strong tightens for the same reason. */
.plan-price {
    font-size: 26px; font-weight: 700; font-family: var(--mono);
    letter-spacing: -.5px; font-variant-numeric: tabular-nums;
}
.plan-price span {
    font-size: 13px; color: var(--muted); font-family: var(--font);
    font-weight: 500; letter-spacing: 0;
}
/* ---- the tier tick list, shared by index.php and billing.php ----
   Emitted by dp_plan_ticks_html() (lib/plans.php), so the two pricing surfaces
   show one list from one source. It is in app.css rather than landing.css for
   exactly that reason: landing.css is loaded by index.php alone, so styling it
   there is how the billing page came to compress the same nine lines into a
   single muted row.
   It replaced `.plan-feat`, which was that row. */
/* The tick list is the part that absorbs a card's spare height (`flex: 1 1 auto`),
   and `align-content: start` is what keeps that height as blank space *below* the
   last tick. Without it the grid's auto rows share the slack out between them, and
   a short card's ticks drift off the lines its neighbours' ticks sit on — the rows
   stop reading as a comparison of the same features. */
.plan-ticks {
    list-style: none; margin: 0 0 26px; padding: 0;
    display: grid; gap: 12px; align-content: start; flex: 1 1 auto;
}
.plan-ticks li { position: relative; padding-left: 30px; font-size: 14.5px; line-height: 1.5; }
/* box-sizing is declared HERE because app.css's reset is `* { box-sizing: border-box }`
   and the universal selector does NOT match pseudo-elements — the usual reset spells
   out `*, *::before, *::after` for exactly this reason. So this disc, which carries a
   1px border, was rendering 21px while every number placed against it was computed
   from the 19px the file said. That is what put the glyphs inside it off-centre. The
   declared size is 21 so nothing moves; the point is that the file now states what it
   draws, and everything positioned against it can be trusted to agree. */
.plan-ticks li::before {
    content: ""; position: absolute; left: 0; top: 1px; box-sizing: border-box;
    width: 21px; height: 21px; border-radius: 50%;
}
/* included: a drawn check inside a green disc.
   The check is an L of two borders, so its INK is nowhere near the middle of the box
   it is drawn in — it hugs one corner, and rotating that box about its own centre
   carries the ink further still. Centring the BOX therefore leaves the tick visibly
   off inside the disc, which is what it was doing (measurably ~1.9px left).
   The offsets below were arrived at by MEASURING a render rather than by deriving
   them: a mitred border corner does not have the centroid the rectangles suggest, so
   the arithmetic that looks right here is not. At these values the ink centroid sits
   0.13px right of and 0.02px below the disc's centre, i.e. centred to within a fifth
   of a pixel. Changing the disc size, the 5x9 box or the 42deg angle invalidates all of
   it, and the way to re-derive it is another screenshot, not another calculation. */
.plan-ticks li.yes::before { background: var(--up-dim); border: 1px solid rgba(var(--up-rgb), .32); }
.plan-ticks li.yes::after {
    content: ""; position: absolute; left: 6.8px; top: 4.3px;
    width: 5px; height: 9px; border-right: 2px solid var(--up); border-bottom: 2px solid var(--up);
    transform: rotate(42deg);
}
/* not included: a muted dash, and the text steps back.
   The dash is drawn as a centred BACKGROUND inside a box identical to the disc,
   rather than as a hand-placed 9x2 rule. Hand-placing it means two sets of
   coordinates that have to agree, and they did not — it sat a pixel off the
   disc's centre line. `center / 9px 2px` cannot: the box IS the disc's box, so
   the bar is centred in it by construction, at any disc size. */
.plan-ticks li.no { color: var(--faint); }
.plan-ticks li.no::before { background: var(--card-2); border: 1px solid var(--line-soft); }
.plan-ticks li.no::after {
    content: ""; position: absolute; left: 0; top: 1px; width: 21px; height: 21px;
    background: linear-gradient(var(--faint), var(--faint)) center / 9px 2px no-repeat;
}
/* The trial is the answer to "what does clicking this cost me", so it sits above
   the tick list and is the one thing on the card allowed to use the accent —
   the buttons below use --accent-fill, which is a different token for a reason
   (a fill and the label on it are tuned separately; see the palette rules). */
.plan-trial { color: var(--accent); font-size: 13px; font-weight: 700; margin-bottom: 4px; }
/* The action area, anchored to the bottom of the card, button last.
   This replaced `.plan-card .btn { margin-top: auto }`, which only ever worked on
   a button that is a DIRECT flex child of the card: the disabled "Current plan"
   is one and sat on the floor, while the two wrapped in a <form> resolved the
   auto margin against the form and stopped wherever the text above them ended.
   Three cards side by side, three different button heights, and the eye reads
   that as a broken grid before it reads any of the prices.
   Since the button is the LAST child here, a note above it can be any number of
   lines without moving it — which is why .plan-note is not under the button. */
.plan-act { margin-top: auto; display: flex; flex-direction: column; gap: 8px; }
.plan-act form { margin: 0; }
/* One class, not `class="hint" style="text-align:center;margin-top:6px"` three
   times over: an inline style cannot follow a theme and cannot be changed once. */
.plan-note {
    color: var(--muted); font-size: 12.5px; line-height: 1.45; text-align: center;
}
.plan-save  { font-size: 12.5px; color: var(--up); font-weight: 600; margin-top: -4px; }
/* "Monthly only" is a note, not a saving — it must not be green beside tiers that
   genuinely save something, or a tier with no annual price reads as the best deal. */
.plan-save-none { color: var(--faint); font-weight: 500; }

/* ---- monthly / annual switch ----
   Still links, and the period is still in the query string: that is what a
   shared link carries and what a visitor with no JavaScript gets. assets/period.js
   intercepts the click and swaps the view in place, because a whole page load to
   read a second price is a long wait for one number.
   What it does NOT do is resolve a price in the browser. The server renders both
   periods of every period-dependent fragment (dp_period_view()) and the rule
   below hides one; the toggle only moves `data-period-view`. `is-on` is carried
   by the filled background as well as the colour, and aria-current says it out
   loud. */
.period-switch { display: flex; align-items: center; gap: 6px; margin: 0 0 18px; flex-wrap: wrap; }
.period-opt, .period-opt:visited {
    padding: 7px 15px; border-radius: 999px; text-decoration: none;
    font-size: 13px; font-weight: 600; color: var(--muted);
    border: 1px solid var(--line-soft); background: var(--card);
}
.period-opt:hover { border-color: var(--accent); color: var(--text); }
.period-opt.is-on { background: var(--card-2); border-color: var(--accent); color: var(--text); }
.period-note {
    margin-left: 4px; padding: 4px 10px; border-radius: 999px;
    font-size: 11.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
    color: var(--up); background: var(--up-dim); border: 1px solid rgba(var(--up-rgb), .3);
}

/* One period on screen, both in the document.
   `display: contents` so the wrapper is invisible to layout — every existing
   rule about a card's flex children (.plan-act { margin-top: auto }, the grid
   gaps) keeps applying to the fragments themselves rather than to a box that
   appeared around them — .plan-act is what anchors the button to the card's
   floor, and it only reaches the card's flex context because of this. The hiding rule outranks it on specificity, which
   is what makes one attribute on an ancestor switch the whole page. */
.period-only { display: contents; }
[data-period-view="month"] .period-only[data-period="year"],
[data-period-view="year"]  .period-only[data-period="month"] { display: none; }

/* The saving on the MONTHLY view is an offer, not something already got, so it
   is stated quietly — the green .plan-save belongs to the view where the money
   is actually being saved. It is the line that makes the toggle worth clicking,
   and it only earns its place now that clicking costs nothing. */
.plan-save-hint { color: var(--muted); font-weight: 500; }

/* ---- utilities ---- */
/* A label that only a screen reader reads. Here rather than in landing.css
   because the waitlist form on index.php and the launch page's field share it,
   and lib/launch.php's page loads app.css alone. */
.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;
}

/* ---- login ---- */
/* OAuth "or" divider and provider button (login.php / signup.php). */
.oauth-divider {
    display: flex; align-items: center; gap: 12px;
    color: var(--muted); font-size: 12px; margin: 16px 0;
}
.oauth-divider::before, .oauth-divider::after {
    content: ''; flex: 1; border-top: 1px solid var(--line-soft);
}
.btn-google, .btn-google:visited {
    background: var(--card-2); border-color: var(--line);
    color: var(--text); gap: 8px; font-weight: 500;
}
.btn-google:hover { border-color: var(--accent); }

.login-body { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.login-card {
    background: var(--card); border: 1px solid var(--line-soft);
    border-radius: 18px; padding: 34px 30px; width: min(360px, 92vw);
    box-shadow: var(--shadow); text-align: center;
}
.login-card .brand { justify-content: center; font-size: 22px; }
.login-sub { color: var(--muted); margin: 6px 0 22px; font-size: 13px; }
.login-card .input { margin-bottom: 14px; text-align: center; }
.login-remember { justify-content: center; margin: 4px 0 16px; }
.login-error { color: var(--down); font-size: 13px; margin-bottom: 12px; }
.login-note { color: var(--muted); font-size: 13px; margin-bottom: 12px; }

@media (max-width: 640px) {
    .acct-metrics { margin-left: 0; width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .metric { text-align: left; }

    /* A narrow top bar has room for the wordmark, the connection state and a way
       into the menu — nothing else. The email's local part goes (the avatar letter
       and the button's aria-label/title still identify the account).
       The clock used to go too, on the grounds that the phone's status bar shows
       one. It does not show THIS one: the top bar's clock is the VPS's, in the
       timezone NinjaTrader is set to, which is the single thing up there the reader
       cannot work out for themselves — and a phone is exactly where that gets asked.
       Only its seconds go now.
       The logo goes too: below the rail breakpoint the bar already carries the
       hamburger, and mark + hamburger + wordmark leaves the freshness pill fighting
       for the same row. Scoped to .topbar deliberately — the login card centres the
       same mark at 22px and is the one place with room for it on a phone. */
    .topbar { padding: 0 14px; gap: 10px; }
    .topbar-right { gap: 10px; }
    .menu-label, .topbar .brand-logo { display: none; }
    /* The clock STAYS on a phone — it is the one element here that answers a
       question the reader cannot answer themselves, and a phone in Singapore is
       exactly where that question gets asked. Only the seconds go, which is what
       makes it fit beside the freshness pill and the hamburger. */
    .topbar .clock-sec { display: none; }
    .menu-toggle { padding-left: 8px; padding-right: 8px; }
}

/* ---- Dashboard settings page ---- */
.settings-toolbar {
    display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 4px 0 14px;
}

/* ---- Appearance picker ----
   A native radio group, for the same reason terms.php uses a native checkbox
   rather than the styled switch: this is an exclusive choice among four, and an
   opacity:0 input behind a decorated span is the wrong thing to have to prove was
   operable from a keyboard. */
.theme-form { display: grid; gap: 10px; }
.theme-option {
    display: flex; align-items: flex-start; gap: 12px;
    padding: 12px 14px; border: 1px solid var(--line-soft); border-radius: 10px;
    background: var(--card-2); cursor: pointer;
}
.theme-option:hover { border-color: var(--line); }
.theme-option:has(input:checked) { border-color: var(--accent); }
.theme-option input { flex: none; margin-top: 2px; accent-color: var(--accent); }
.theme-option input:disabled { cursor: not-allowed; }
.theme-option:has(input:disabled) { opacity: .6; cursor: not-allowed; }
/* The swatch strip. Its own data-theme makes these five chips resolve through
   that palette's custom properties, so the preview IS the theme rather than a
   copy of it. `flex: none` for the same reason .brand-logo needs it. */
.theme-preview { flex: none; display: inline-flex; gap: 3px; padding-top: 1px; }
.theme-preview i {
    display: block; width: 15px; height: 22px; border-radius: 3px;
    border: 1px solid var(--line);
}
.theme-preview .p-bg   { background: var(--bg); }
.theme-preview .p-card { background: var(--card); }
.theme-preview .p-acc  { background: var(--accent); }
.theme-preview .p-up   { background: var(--up); }
.theme-preview .p-down { background: var(--down); }
.theme-text { display: grid; gap: 2px; }
.theme-text .hint { margin: 0; }
.theme-palette {
    font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
    color: var(--faint);
}
@media (max-width: 640px) {
    /* Stacked, so the strip does not squeeze the sentence into one word a line. */
    .theme-option { flex-wrap: wrap; }
    .theme-text { flex-basis: 100%; }
}
.settings-toolbar .input { flex: 1 1 220px; min-width: 180px; }
#acct-table input[type="checkbox"] { width: auto; margin: 0; accent-color: var(--accent); }
#acct-table tbody tr[hidden] { display: none; }

/* ---- strategies section ---- */
.btn.has-sched { color: var(--accent); border-color: var(--accent); }
/* A notice that can be closed. Only the lapse is: it describes an EVENT, and
   news that cannot be acknowledged becomes furniture. The standing facts next to
   it ("you are on the free tier", "3 accounts are frozen") stay, because
   re-stating a condition that is still true is honest.
   The × is a real <button> with an aria-label, not a styled glyph — it is a
   control, and colour is not its only channel either. */
.notice-dismissable { display: block; padding-right: 26px; position: relative; }
.notice-x {
    position: absolute; top: -2px; right: 0;
    background: none; border: 0; padding: 0 4px; cursor: pointer;
    font: inherit; font-size: 18px; line-height: 1; color: var(--faint);
}
.notice-x:hover { color: var(--text); }

/* ---------------------------------------------------------------
   Accounts the plan no longer covers (the frozen group)

   Colour is the WEAKEST of four channels here on purpose. Grey alone is
   already spoken for — `pill-muted` is "no data yet", `--faint` is
   de-emphasised — and frozen is a deliberate state, not an unknown one.
   Drawing it like an unknown would make a frozen account look broken,
   which is the confusion this codebase keeps fixing elsewhere. The other
   three channels are the collapsed group itself, the word on every row,
   and the dates; every colour below is a token, and `filter: grayscale()`
   is deliberately not used — it was rejected once already for pinning a
   colour that follows no palette.
   --------------------------------------------------------------- */
.frozen-group { margin-top: 14px; }
/* The whole header is the control, so the hit area matches what looks
   clickable — and it is a <button>, because a div with a handler is not
   reachable from a keyboard and aria-expanded has nothing to sit on. */
.frozen-head {
    display: flex; align-items: center; gap: 8px; width: 100%;
    background: none; border: 0; padding: 0; cursor: pointer;
    font: inherit; font-weight: 700; color: var(--muted); text-align: left;
}
.frozen-head:hover { color: var(--text); }
.frozen-chev { display: inline-block; transition: transform .12s ease; }
.frozen-group.is-open .frozen-chev { transform: rotate(90deg); }
.frozen-group #frozen-why { margin: 10px 0 4px; max-width: 82ch; }
/* Dashed, because dashed already means "not a live fact" here (.tag-unknown),
   so it is an idiom the page has rather than a new one to learn. */
.tag-frozen {
    color: var(--faint); background: var(--card-2);
    border: 1px dashed var(--line); cursor: help; margin-left: 6px;
}
/* The numbers stay legible — they are the last true ones and someone may
   want to read them — but nothing in the row competes with a live card. */
.frozen-row td { color: var(--muted); }
.frozen-row td:first-child { color: var(--text); }

/* ---------------------------------------------------------------
   Strategy schedules (schedules.php)

   Every colour here is a token. A literal would not follow a theme, and a
   tint mixed against a dark card washes out to nothing on Parchment — the
   rule tests/http.php pins statically, because nothing server-side can see
   a chart drawn in the wrong palette.
   --------------------------------------------------------------- */

/* The clock line under the title. Not decoration: every time on the page is the
   VPS's local clock, and without this the reader has every reason to assume it is
   their own. */
.sched-clock { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin: 0 0 14px; }
.sched-clock .hint { max-width: 74ch; }

/* One rule, in a table cell. Muted rules are dimmed rather than hidden: a rule
   that is kept but never fires is something the schedule holds, and nothing else
   on the row would reveal it. */
.sched-line { font-size: 12px; white-space: nowrap; }
.sched-line.is-muted { color: var(--faint); text-decoration: line-through; }

/* Weekday chips. A button per day, so it is operable from a keyboard — a div with
   a click handler is not, and aria-pressed is what carries the state to a screen
   reader. Seven of these plus two quick-sets is the whole day picker. */
.sched-days { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.chip {
    font: inherit; font-size: 11px; font-weight: 700; letter-spacing: .2px;
    padding: 4px 8px; border-radius: 6px; cursor: pointer;
    background: var(--card-2); color: var(--muted); border: 1px solid var(--line);
}
.chip:hover { color: var(--text); }
/* The fill and the label on it are separate tokens on purpose: --accent is tuned to
   be readable AS TEXT on a card, which necessarily makes it too light to sit behind
   white text. */
.chip.is-on {
    background: var(--accent-fill); color: var(--on-accent);
    border-color: var(--accent-fill);
}
.chip-quick { font-weight: 400; margin-left: 6px; border-style: dashed; }
.sched-quick { display: inline-flex; gap: 4px; }

/* The week preview: seven day rows, each a 24h bar with the ON spans drawn on it.
   This is the consistency check made visible — a strategy left running over a
   weekend exists only in the sequence and cannot be seen by comparing rules. */
.sched-week { margin: 16px 0 6px; }
.sched-week-row { display: flex; align-items: center; gap: 8px; margin: 3px 0; }
.sched-week-day { width: 30px; font-size: 11px; color: var(--muted); flex: none; }
.sched-week-bar {
    position: relative; display: block; flex: 1; height: 12px;
    background: var(--card-2); border: 1px solid var(--line-soft); border-radius: 3px;
}
.sched-week-on {
    position: absolute; top: 0; bottom: 0; background: var(--accent-fill);
    border-radius: 2px; min-width: 2px;
}

.sched-add { display: flex; align-items: center; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.sched-add .hint { margin-left: auto; }
.sched-rules td { vertical-align: top; white-space: normal; }
/* The "Rule active" tick and the session close's "On" tick. Both were missed by
   the pass that gave .checkbox-inline / .theme-option / #acct-table / .strat-tbl /
   .strat-pick-all their accent, so they drew in the user agent's blue on all three
   palettes — see .checkbox-inline for why the fix is `accent-color` and never a
   local `color-scheme`. */
.sched-rules input[type="checkbox"] { width: auto; margin: 0; accent-color: var(--accent); }
/* A muted rule is kept and never fires, so it is dimmed rather than removed —
   the row has to stay editable and re-tickable. */
.sched-rules tr.is-muted { opacity: .55; }
/* The editor is wider than a confirmation dialog because it holds a table and a
   week preview. It is still a panel over the list rather than part of it: the list
   is rebuilt by every poll, and unsaved rules must not be. */
.modal-wide { width: min(720px, 94vw); }
/* Same job as .modal-link — pushed to the far left so the two real answers stay
   grouped on the right — but it is a destructive button rather than a link, so it
   keeps its .btn-danger colour. */
.modal-link-left { margin-right: auto; }

.sched-copy { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; }
.sched-copy label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--muted); }
.sched-copy select { min-width: 220px; max-width: 100%; }

/* The copy card's destination list. A copy is strategy -> strategy and every
   destination is ticked by hand, so this is a list of every other strategy the
   tenant has — grouped by account, and scrollable rather than pushing the button
   off the screen on a tenant with a hundred of them. */
.copy-to { margin-top: 12px; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
.copy-to-head {
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    padding: 8px 12px; background: var(--card-2); border-bottom: 1px solid var(--line-soft);
}
.copy-to-head .hint { margin-right: auto; }
.copy-to-list { max-height: 320px; overflow-y: auto; padding: 4px 0; }
.copy-group + .copy-group { border-top: 1px solid var(--line-soft); }
.copy-group-head {
    display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
    padding: 8px 12px 4px;
}
.copy-group-name { font-size: 12px; font-weight: 700; color: var(--muted); margin-right: auto; }
/* The whole row is the label, so the text is a click target too — a 13px checkbox
   is not one on a phone. */
.copy-row {
    display: flex; align-items: center; gap: 10px; padding: 5px 12px 5px 22px;
    cursor: pointer; font-size: 13px;
}
.copy-row:hover { background: rgba(var(--text-rgb), .03); }
.copy-row input { flex: none; accent-color: var(--accent); }
.copy-row-text { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.copy-row-name { font-weight: 600; }
/* This destination already has a schedule and the copy REPLACES it. Amber and
   bordered like .tag-pending, because it is the one consequence the word "copy"
   does not carry, and there is no undo. */
.tag-replace {
    color: var(--warn); background: rgba(var(--warn-rgb), .14);
    border: 1px solid rgba(var(--warn-rgb), .45); cursor: help;
}
.sched-only-filter { align-self: flex-end; }

/* ---------------------------------------------------------------
   The contract-roll card on schedules.php.

   `notice` colours, not `card` colours: it describes schedules that have stopped
   firing, so it must not read like another panel of settings. It is the one card on
   that page rendered only when it has something to say.
   --------------------------------------------------------------- */
.roll-card { border-color: rgba(var(--warn-rgb), .38); border-left: 3px solid var(--warn); }
.roll-card h2 { color: var(--warn); }
/* from -> to, side by side, so the one component that changed is what the eye lands
   on. Monospaced digits because the two labels differ by three characters. */
.roll-move { display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
             font-variant-numeric: tabular-nums; }
.roll-from { color: var(--muted); text-decoration: line-through; }
.roll-arrow { color: var(--faint); }
.roll-to { color: var(--text); font-weight: 600; }
/* THE SELECTOR HAS TO OUT-SPECIFY `.tbl td`, which sets white-space: nowrap —
   the same reason `.legal-doc .tbl td` and `.sched-rules td` are written the way
   they are. `.roll-why` alone lost to it, so the whole sentence was one
   unbreakable line: its min-content width became the entire paragraph, the
   actions column next to it was squeezed to zero, and `.roll-acts`'s flex-end
   then overflowed the Delete button LEFTWARDS across the text. A cell that
   cannot wrap is what put a button on top of a sentence. */
.tbl td.roll-why { color: var(--muted); font-size: 12px;
                   min-width: 26ch; max-width: 46ch; white-space: normal; }
/* The min-width is the other half of the wrap, and it is what the narrow
   breakpoint needs: once the cell CAN wrap, its min-content is one word, so on a
   phone the table shrank the column to a few characters and the row grew to eight
   lines of it. .table-wrap already scrolls sideways on this page — that is the
   established answer for a table of prose here (.sched-rules, .legal-doc) — so a
   floor on the column buys a readable row and costs a scroll that was there anyway.
   ...and the action column is pinned to its content so no amount of prose beside
   it can take its width away: `width: 1%` plus nowrap is the shrink-to-fit trick,
   and it is what makes the overflow above impossible rather than merely unlikely. */
.tbl td.roll-act { width: 1%; white-space: nowrap; vertical-align: top; }
.roll-acts { display: flex; gap: 6px; justify-content: flex-end; flex-wrap: nowrap; }
/* One row per outcome after a Move all. Green / amber down the left edge, because
   "moved" and "not moved" is the only thing anyone reads this list for. */
.roll-res { display: block; margin-top: 4px; padding: 6px 10px; border-radius: 8px;
            background: var(--card-2); font-size: 12.5px; }
.roll-res-ok   { border-left: 3px solid var(--up); }
.roll-res-skip { border-left: 3px solid var(--warn); }

/* A strategy that has a schedule but is no longer being reported. Not an error —
   it may simply not be loaded in the Control Center yet — so it is grey rather
   than red, and the row stays editable so the schedule can be deleted. */
.tag-muted { color: var(--faint); background: var(--card-2); border: 1px dashed var(--line); }

/* ---------------------------------------------------------------
   Legal documents (terms / privacy / imprint)
   Prose, not a dashboard: the shared .tbl sets white-space:nowrap for
   numeric tables, which would force a legal table off the screen.
   --------------------------------------------------------------- */
.legal-doc { padding-top: 26px; padding-bottom: 60px; }
.legal-doc .legal-back { margin: 0 0 10px; font-size: 13px; }
.legal-doc h2 { font-size: 16px; margin: 0 0 10px; }
.legal-doc .card { margin-bottom: 16px; }
.legal-doc .hint { font-size: 14px; line-height: 1.65; }
.legal-doc ul.hint { padding-left: 22px; }
.legal-doc ul.hint li { margin-bottom: 7px; }
.legal-doc .tbl td,
.legal-doc .tbl th { white-space: normal; vertical-align: top; line-height: 1.55; }
.legal-doc .legal-rule { border: 0; border-top: 1px solid var(--line); margin: 26px 0 14px; }
/* Footer legal links, shared by the public pages via dp_legal_links_html(). */
.legal-links { color: var(--muted); font-size: 13px; }

/* ---------------------------------------------------------------
   Left navigation rail  (lib/ui.php: dp_sidebar)
   ---------------------------------------------------------------
   POSITIONED, never laid out. The rail is `position: fixed` and the content is
   shifted by `.rail ~ .wrap`, so a page that renders no rail is untouched — which
   matters, because the legal documents and the login card share .wrap and must not
   acquire a 212px hole where no navigation exists.

   Under 900px it becomes an off-canvas drawer. The breakpoint is not the existing
   640px one: the rail plus a 1240px content column stops fitting long before the
   top bar starts running out of room, and those are two different questions.
   --------------------------------------------------------------- */
.rail {
    position: fixed; left: 0; top: var(--topbar-h); bottom: 0; width: var(--rail-w);
    z-index: 26;                     /* over .topbar (20), under .menu-panel (40) */
    padding: 12px 10px;
    display: flex; flex-direction: column;
    overflow-y: auto; overscroll-behavior: contain;
    border-right: 1px solid var(--line-soft);
    background: rgba(var(--chrome-rgb), .72);
    backdrop-filter: blur(10px);
}
.rail nav { display: flex; flex-direction: column; gap: 3px; }
.rail-item, .rail-item:visited {
    display: flex; align-items: center; gap: 11px;
    padding: 10px 12px; border-radius: 9px;
    color: var(--text); text-decoration: none;
    font-size: 13.5px; font-weight: 600; white-space: nowrap;
}
.rail-item:hover { background: var(--card-2); }
/* The bar down the left edge is the redundant channel: `is-on` must not be carried
   by colour alone, and aria-current="page" covers the screen reader. */
.rail-item.is-on {
    background: var(--card-2); color: var(--accent);
    box-shadow: inset 2px 0 0 var(--accent);
}
.rail-ico { width: 18px; flex: none; text-align: center; font-size: 14px; color: var(--muted); }
.rail-item:hover .rail-ico { color: inherit; }
.rail-item.is-on .rail-ico { color: var(--accent); }
/* Strategies draws its bolt as inline SVG in currentColor rather than as a glyph,
   because a colour-emoji glyph ignores `color` — U+26A1 has emoji presentation, so
   the font painted it yellow next to two glyphs that follow the token. Same rule
   and same fix as `.menu-ico svg`; block-level so the 18px slot centres it the way
   text-align centres the others. */
.rail-ico svg { display: block; margin: 0 auto; }

/* A count on a rail entry. Amber and bordered like .tag-pending, because it means
   exactly one thing — this needs your attention — which is also why menu.js hides
   it at 0 rather than rendering a zero: a badge that is always there is furniture.
   `margin-left: auto` is what pushes it to the far edge of the flex row, and
   `flex: none` is what stops it being the shrinking item next to the label (the
   same fault `.switch` had inside `.switch-inline`). */
.rail-badge {
    margin-left: auto; flex: none;
    min-width: 18px; padding: 1px 6px; border-radius: 999px;
    font-size: 11px; font-weight: 700; line-height: 1.5; text-align: center;
    color: var(--warn); background: rgba(var(--warn-rgb), .16);
    border: 1px solid rgba(var(--warn-rgb), .5);
}
.rail-badge[hidden] { display: none; }

/* The hamburger. A plain button rather than .btn, so it can be square and quiet. */
.rail-toggle {
    display: none;                   /* desktop: the rail is already visible */
    align-items: center; justify-content: center;
    width: 32px; height: 32px; flex: none;
    margin-right: 2px; padding: 0;
    background: transparent; color: var(--text);
    border: 1px solid var(--line); border-radius: 9px;
    font-size: 15px; line-height: 1; cursor: pointer;
}
.rail-toggle:hover { background: var(--card-2); border-color: var(--accent); }
.rail-scrim { display: none; }

@media (min-width: 900px) {
    /* Only a page that HAS a rail gets the offset. The padding keeps .wrap's own
       `margin: 0 auto` centring the whole box, so the content is centred in the
       window rather than jammed against the rail on a wide screen. */
    .rail ~ .wrap {
        max-width: calc(1240px + var(--rail-w));
        padding-left: calc(var(--rail-w) + 22px);
    }
}

@media (max-width: 899px) {
    .rail {
        transform: translateX(-100%);
        transition: transform .18s ease;
        box-shadow: var(--shadow);
        background: var(--bg-2);     /* opaque: it now sits OVER the content */
    }
    .rail.is-open { transform: none; }
    .rail-toggle { display: inline-flex; }
    /* Below the top bar, so the hamburger that opened the drawer stays clickable. */
    .rail-scrim {
        display: block; position: fixed; inset: var(--topbar-h) 0 0 0;
        z-index: 25; background: rgba(var(--scrim-rgb), .55);
    }
    .rail-scrim[hidden] { display: none; }
}
@media (prefers-reduced-motion: reduce) {
    .rail { transition: none; }
}

/* ---------------------------------------------------------------
   Strategies page  (strategies.php / assets/strategies.js)
   --------------------------------------------------------------- */
.strat-filters {
    display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px;
    margin: 2px 0 14px;
}
.strat-filters label {
    display: flex; flex-direction: column; gap: 5px;
    font-size: 12px; color: var(--muted); flex: 1 1 200px; min-width: 150px;
}
.strat-filters .input, .strat-filters select { width: 100%; }
.strat-filters .strat-filter-state { flex: 0 1 150px; }
.strat-filters .strat-filter-clear { flex: none; align-self: flex-end; }

/* The bulk bar. Sticky because with several hundred rows the button that acts on
   the selection must not be somewhere above the scroll position — a bulk control you
   have to hunt for is a bulk control clicked from memory. */
.strat-bulk {
    position: sticky; top: var(--topbar-h); z-index: 15;
    display: flex; flex-wrap: wrap; align-items: center; gap: 12px;
    padding: 11px 14px; margin: 0 0 12px;
    background: var(--card-2); border: 1px solid var(--line);
    border-radius: 11px;
}
.strat-bulk-counts { font-size: 12.5px; color: var(--muted); line-height: 1.5; }
.strat-bulk-counts strong { color: var(--text); }
.strat-bulk-spacer { flex: 1 1 auto; }
.strat-bulk-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
/* The count in the button label is the warning: it is what tells someone who has
   forgotten about the one odd strategy that a row they did not think about is
   included. Never render this button without it. */
.strat-bulk-actions .btn { white-space: nowrap; }

.strat-tbl td, .strat-tbl th { white-space: nowrap; }
.strat-tbl .strat-pick { width: 34px; padding-right: 0; }
.strat-tbl input[type="checkbox"] { width: auto; margin: 0; accent-color: var(--accent); }
.strat-tbl .strat-name { white-space: normal; min-width: 160px; }
.strat-tbl .strat-acct { color: var(--muted); }
.strat-tbl tr.is-picked > td { background: rgba(var(--accent-rgb), .07); }
.strat-tbl tr.is-unreachable .strat-acct { color: var(--warn); }
.strat-tbl .row-actions { justify-content: flex-end; }

/* A row whose account is not reporting: shown, because which strategies were
   running when the VPS went away is exactly what the stored data is for — but its
   toggles are disabled, since pull.php would hold the command until the VPS came
   back and then fire it at a moment nobody chose. */
.strat-offline-tag { margin-left: 6px; }

/* Over the plan's per-minute command allowance. Amber rather than red: the selection
   is not wrong, it just cannot go out in one batch — and it is stated in the bulk bar
   before the click as well as in the dialog that intercepts it. */
.strat-overcap { color: var(--warn); font-weight: 600; }

/* The figures in that dialog: a two-column list, because "40 selected / 30 allowed /
   8 already used / 22 can be sent" is an arithmetic the reader has to be able to
   follow, and a paragraph of it cannot be scanned. */
.rate-figures {
    display: grid; grid-template-columns: 1fr auto; gap: 6px 18px;
    margin: 14px 0 4px; padding: 12px 14px;
    border: 1px solid var(--line-soft); border-radius: 9px; background: var(--bg-2);
    font-size: 13px;
}
.rate-figures dt { color: var(--muted); }
.rate-figures dd { margin: 0; text-align: right; font-family: var(--mono); color: var(--text); }
.rate-figures dt:last-of-type,
.rate-figures dd:last-of-type { font-weight: 700; }
.rate-figures dd:last-of-type { color: var(--warn); }


@media (max-width: 640px) {
    .strat-filters label { flex: 1 1 100%; }
    .strat-bulk { position: static; }
}

/* Selection, not consent, so it is centred rather than top-aligned (.checkbox-inline
   is shaped for paragraph-length legal labels) — but it is still a NATIVE checkbox,
   because it is the control that has to support an indeterminate state and be
   reachable by keyboard. */
.strat-pick-all {
    display: inline-flex; align-items: center; gap: 9px; flex: none;
    color: var(--text); font-size: 13px; font-weight: 600; cursor: pointer;
    white-space: nowrap;
}
.strat-pick-all input[type="checkbox"] {
    /* No `color-scheme` — see .checkbox-inline above. This box is the one the
       theme miss was reported on: it stayed a dark widget on Parchment. */
    flex: none; width: 17px; height: 17px; margin: 0;
    accent-color: var(--accent); cursor: pointer;
}

/* Sortable column headers on the Strategies table. A BUTTON inside the th rather
   than a handler on the th, so the control is reachable and operable from the
   keyboard; `aria-sort` on the th carries the direction for a screen reader and the
   arrow carries it for everyone else. */
.strat-sort {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 0; border: 0; background: none; cursor: pointer;
    font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
}
.strat-sort:hover { color: var(--text); }
.strat-sort:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
/* Reserved width, so turning the arrow on does not shift the label sideways. */
.strat-sort-arrow { display: inline-block; min-width: 9px; font-size: 9px; color: var(--accent); }
.strat-tbl th.is-sorted .strat-sort { color: var(--text); }

/* ---- signup onboarding guide ------------------------------------------ */
.onboard-steps {
    list-style: none;
    padding: 0; margin: 0;
    display: flex; flex-direction: column; gap: 32px;
}
.onboard-step {
    display: grid;
    grid-template-columns: 2.6rem 1fr;
    gap: 0 16px;
    align-items: start;
}
.step-num {
    width: 2.6rem; height: 2.6rem;
    border-radius: 50%;
    background: var(--accent-fill); color: var(--on-accent);
    display: flex; align-items: center; justify-content: center;
    font-weight: 700; font-size: .95rem; flex: none;
    margin-top: 2px;
}
.step-body { display: flex; flex-direction: column; gap: 10px; }
.step-title { font-weight: 600; font-size: 1.05rem; margin: 0; line-height: 2.6rem; }
.step-note { margin: 0; font-size: .9rem; }

/* Setup code box — prominent inline code + copy button */
.setup-code-display {
    display: flex; align-items: center; gap: 12px;
    padding: 14px 18px;
    background: var(--bg-2); border: 1px solid var(--line);
    border-radius: 8px; width: fit-content;
}
.setup-code-display code {
    font-family: var(--mono); font-size: 2rem; letter-spacing: .3em;
    color: var(--accent); font-weight: 700;
}

/* The Add-On download on account.php's VPS agents card.

   Bordered inset rather than a bare button in the flow: it sits above a table
   and below two paragraphs of prose, and without an edge a primary button there
   reads as an action ON the table below it. Same surface treatment as
   .setup-code-display, which is the other "here is the thing you came for" box. */
.agent-dl {
    display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap;
    padding: 14px 16px; margin: 12px 0 4px;
    background: var(--bg-2); border: 1px solid var(--line); border-radius: 8px;
}
/* flex: none, or the button is the shrinkable item next to a paragraph and its
   label wraps — the .switch-inline lesson one control over. */
.agent-dl > .btn { flex: none; }
.agent-dl-note { flex: 1 1 320px; min-width: 0; }
.agent-dl-note p { margin: 0; }
.agent-dl-note p + p { margin-top: 6px; }
.agent-dl > .hint { margin: 0; }

/* Screenshots in the setup guide.

   The zoom target is a <button> (see dp_onboard_shot) so it is operable from a
   keyboard, which means resetting the UA button styling: without appearance:none
   a screenshot arrives inside a grey bevelled chrome control. line-height: 0
   kills the inline-box descender gap that otherwise shows as a stripe of
   background under every image. */
.screenshot {
    margin: 0;
    display: flex; flex-direction: column; gap: 8px;
    /* flex-start, so the figure shrinks to whatever the picture actually is: a
       701px dialog shot stretched to the column is a blurred 701px dialog shot. */
    align-items: flex-start;
    max-width: 760px;
}
.shot-zoom {
    appearance: none; -webkit-appearance: none;
    display: block; padding: 0; margin: 0; line-height: 0;
    font: inherit; color: inherit; max-width: 100%;
    border: 1px solid var(--line); border-radius: 8px;
    background: var(--bg-2); overflow: hidden;
    cursor: zoom-in;
    transition: border-color .15s, box-shadow .15s;
}
.shot-zoom:hover { border-color: var(--accent); }
.shot-zoom:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* max-width (not width) so a narrow screenshot is never upscaled, height:auto so
   the ratio the width/height attributes reserved is the ratio drawn — that pair is
   what stops the step below jumping as the bytes land. */
.shot-zoom img { display: block; max-width: 100%; width: auto; height: auto; }
.screenshot figcaption { font-size: .8rem; color: var(--muted); }

/* Two small dialog shots that belong to one instruction, side by side. */
.shot-row { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-start; }
.shot-row .screenshot { flex: 0 1 auto; max-width: 340px; }

/* A step the installer usually performs itself — see dp_onboarding_section. */
.step-num-opt { background: var(--bg-2); color: var(--muted); border: 1px dashed var(--line); }
.onboard-step-opt .step-title { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* .step-title carries line-height: 2.6rem to sit level with the number circle, and
   a .tag inheriting that renders as a 42px-tall lozenge. */
.onboard-step-opt .step-title .tag { line-height: 1.6; font-weight: 600; }

/* Lightbox */
.shot-overlay {
    position: fixed; inset: 0;
    background: rgba(var(--scrim-rgb), .78);
    display: flex; align-items: center; justify-content: center;
    z-index: 9000; cursor: zoom-out; padding: 24px;
}
.shot-overlay[hidden] { display: none; }
.shot-overlay figure {
    margin: 0; display: flex; flex-direction: column; gap: 12px;
    align-items: center; max-width: 100%;
}
.shot-overlay img {
    display: block; max-width: 94vw; max-height: 82vh; width: auto; height: auto;
    border: 1px solid var(--line); border-radius: 8px; background: var(--card);
    box-shadow: var(--shadow);
}
.shot-overlay figcaption { color: var(--text); font-size: .95rem; text-align: center; }

/* Manual setup collapsible */
.onboard-manual > summary {
    cursor: pointer; list-style: none; padding: 0;
}
.onboard-manual > summary::-webkit-details-marker { display: none; }
.onboard-manual > summary::marker { display: none; }
.onboard-manual > summary .card-head-inline {
    display: flex; align-items: baseline; gap: 10px;
    padding: 16px 20px; border-bottom: 1px solid transparent;
}
.onboard-manual[open] > summary .card-head-inline { border-color: var(--line-soft); }
.onboard-manual > summary:hover .card-head-inline { background: rgba(var(--text-rgb), .02); }

@media (max-width: 640px) {
    .setup-code-display { flex-wrap: wrap; }
    .setup-code-display code { font-size: 1.5rem; }
    .shot-overlay { padding: 12px; }
    .shot-row .screenshot { max-width: 100%; }
}

/* ===================================================================
   Trade log (tradelog.php)

   Every colour here comes from a token. The calendar is the one surface in
   the app where a tint carries meaning, which makes it the one most at risk
   of becoming colour-only — so the rule is enforced twice: the cell prints
   its figure as well as wearing its tint, and the tint is built from
   --up-rgb / --down-rgb at low alpha so it follows all three palettes,
   including Beacon, where those two are the Okabe-Ito pair and the only hues
   on the page.
   =================================================================== */

/* ---- the period bar ---- */
.tl-bar {
    display: flex; flex-wrap: wrap; align-items: center; gap: 12px;
    margin: 2px 0 16px;
}
.tl-bar-spacer { flex: 1 1 auto; }
.tl-period { display: flex; align-items: center; gap: 6px; }
/* The arrows are square so the two of them frame the input symmetrically; a
   button sized by its label would make "‹" narrower than "›" in some fonts. */
.tl-step { width: 34px; padding: 0; text-align: center; font-size: 17px; line-height: 1; }
.tl-jump { display: inline-flex; }
.tl-jump .input { min-width: 150px; }
/* A number spinner for the year view — the type is swapped in JS because there
   is no native "year" input, and the arrows would otherwise sit on a control
   whose own stepper says something different. */
.tl-jump input[type="number"] { min-width: 100px; }
.tl-clock-warn, .hint.warn { color: var(--warn); }

/* ---- filters ---- */
.tl-filters { align-items: flex-start; }
.tl-acct-field {
    display: flex; flex-direction: column; gap: 5px;
    font-size: 12px; color: var(--muted); flex: 2 1 280px; min-width: 220px;
}
/* .strat-filters styles its LABELS as stacked fields (`flex-direction: column`,
   `flex: 1 1 200px`), which is right for the two boxes beside this one and wrong
   twice over inside the account picker: the caption became a 200px-wide flex item
   with a gap under it, and every account row stacked its checkbox ABOVE its name
   and centred the pair. Both are re-stated here rather than the shared rule being
   narrowed, because those three pages depend on it as it stands. */
.tl-filters .tl-acct-field > label { display: block; flex: none; }
/* Scrolls rather than growing: a tenant with 130 accounts would otherwise push
   the calendar off the screen, and this control is a filter rather than the
   content of the page. */
.tl-acct-list {
    max-height: 132px; overflow-y: auto;
    border: 1px solid var(--line); border-radius: 9px;
    padding: 6px; background: var(--card-2);
    display: flex; flex-direction: column; gap: 1px;
}
.tl-filters .tl-acct {
    display: flex; flex-direction: row; align-items: center; gap: 8px;
    flex: none; min-width: 0;
    padding: 4px 6px; border-radius: 6px; cursor: pointer;
    color: var(--text); font-size: 12.5px;
}
.tl-filters .tl-acct:hover { background: rgba(var(--text-rgb), .04); }
.tl-filters .tl-acct input { flex: none; margin: 0; accent-color: var(--accent); }
.tl-acct-name { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Which VPS an account is on, rendered only when the tenant has more than one —
   the same call the dashboard card makes, and the only thing distinguishing two
   accounts both called Sim101. */
.tl-acct-vps {
    flex: none; color: var(--faint); font-size: 11px;
    margin-left: 8px; font-family: var(--mono);
}

/* ---- the statistics strip ---- */
/* Flex-wrap rather than an auto-fit grid. A grid forces every column to one
   width, so ten figures across nine columns left the tenth alone against a row of
   empty cells — which reads as a broken panel rather than a wrapped one. Wrapping
   flex items stretch to fill whatever row they land on. */
.tl-stats {
    display: flex; flex-wrap: wrap;
    gap: 1px; margin: 0 0 16px;
    background: var(--line-soft); border: 1px solid var(--line-soft); border-radius: 11px;
    overflow: hidden;
}
/* A fifth of the row, so ten figures land as two rows of five rather than nine
   and a lonely tenth stretched across the width — which read as a panel that had
   broken rather than one that had wrapped. min-width still wins on a narrow
   screen, where the basis is what gives way. */
.tl-stat {
    display: flex; flex-direction: column; gap: 3px; padding: 10px 12px;
    background: var(--card); flex: 1 1 calc(20% - 1px); min-width: 118px;
}
.tl-stat-k { font-size: 10.5px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); }
.tl-stat-v {
    font-family: var(--mono); font-size: 15px; font-weight: 700;
    font-variant-numeric: tabular-nums; color: var(--text);
}
.tl-stat-v.pos { color: var(--up); }
.tl-stat-v.neg { color: var(--down); }
.tl-chart-wrap { height: 190px; }

/* ---- the calendar ---- */
/* Seven day columns plus an eighth for the week total. The week column is
   narrower and visually detached, because it is a summary of the row rather
   than another day in it. */
.tl-cal {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr)) minmax(0, .8fr);
    gap: 6px;
}
.tl-cal-h {
    font-size: 10.5px; text-transform: uppercase; letter-spacing: .5px;
    color: var(--muted); padding: 2px 4px 4px; text-align: center;
}
.tl-cal-h-week { color: var(--faint); }
.tl-cal-pad { min-height: 10px; }

.tl-year { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 8px; }

/* `overflow: hidden` and `min-width: 0` are not cosmetic. A grid item's default
   min-width is auto, so a figure wider than its column makes the CELL wider and
   the text lands on top of the next day — which on a phone rendered a Thursday
   loss underneath Friday. Clipping keeps a number inside the day it belongs to;
   shortMoney() in tradelog.js keeps it short enough not to be clipped. */
.tl-cell {
    display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
    min-width: 0; overflow: hidden;
    min-height: 66px; padding: 7px 8px;
    border: 1px solid var(--line); border-radius: 9px;
    background: var(--card-2); color: var(--text);
    font: inherit; text-align: left; cursor: pointer;
}
.tl-cell-month { min-height: 78px; }
.tl-cell:disabled { cursor: default; }
.tl-cell:hover:not(:disabled) { border-color: var(--accent); }
.tl-cell:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tl-cell-d { font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.tl-cell-p {
    font-family: var(--mono); font-size: 13.5px; font-weight: 700;
    font-variant-numeric: tabular-nums;
    max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
.tl-cell-p.pos { color: var(--up); }
.tl-cell-p.neg { color: var(--down); }
.tl-cell-n { font-size: 10.5px; color: var(--faint); }

/* The four states. A day with no trades, a day still to come, a day that traded
   and finished exactly flat, and a day that made or lost money are four facts,
   and a grid that drew the first three alike could not be read. */
.tl-cell.is-up   { background: var(--up-dim);   border-color: rgba(var(--up-rgb), .35); }
.tl-cell.is-down { background: var(--down-dim); border-color: rgba(var(--down-rgb), .35); }
.tl-cell.is-flat { background: var(--card-2);   border-color: var(--line); }
.tl-cell.is-empty { background: transparent; border-style: dashed; border-color: var(--line-soft); }
/* A future day is not an empty one — it is a cell nothing could have happened in
   yet — so it is faded rather than dashed, which here means "no data". */
.tl-cell.is-future { background: transparent; border-color: var(--line-soft); opacity: .45; }

.tl-week {
    display: flex; flex-direction: column; align-items: flex-end; justify-content: center;
    gap: 2px; padding: 7px 8px; min-height: 66px;
    border-left: 2px solid var(--line); border-radius: 0 9px 9px 0;
    background: rgba(var(--text-rgb), .02);
}
.tl-week-p {
    font-family: var(--mono); font-size: 12.5px; font-weight: 700;
    font-variant-numeric: tabular-nums; color: var(--muted);
}
.tl-week.pos .tl-week-p { color: var(--up); }
.tl-week.neg .tl-week-p { color: var(--down); }
.tl-week-n { font-size: 10px; color: var(--faint); }

/* A load in flight fades the grid rather than emptying it: the old month is
   still true until the new one arrives, and blanking it makes every arrow click
   flash. */
.tl-grid.is-busy, #tl-grid.is-busy { opacity: .55; transition: opacity .12s ease; }

a.btn.is-disabled { pointer-events: none; }

/* ---- the breakdowns ---- */
.tl-bd-grid {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 18px;
}
.tl-bd { min-width: 0; }
.tl-bd-head { display: flex; align-items: center; gap: 7px; margin: 0 0 8px; }
.tl-bd-title {
    margin: 0; font-size: 11px; font-weight: 700; letter-spacing: .5px;
    text-transform: uppercase; color: var(--muted);
}
/* A help marker, not a control: it opens nothing, so it is a span with a title
   and an aria-label rather than a button that would promise a dialog. */
.tl-bd-help {
    display: inline-flex; align-items: center; justify-content: center;
    width: 14px; height: 14px; border-radius: 50%; cursor: help;
    border: 1px solid var(--line); color: var(--faint); font-size: 9px; font-weight: 700;
}
.tl-bd-rows { display: flex; flex-direction: column; gap: 2px; }
/* Four columns: label, bar, figure, count. Fixed widths on three of them so the
   bars of every row start and end at the same x — a bar chart whose baseline
   moves per row is not a bar chart. */
.tl-bd-row {
    display: grid; grid-template-columns: 4.2em 1fr 4.6em 2.2em;
    align-items: center; gap: 8px;
    padding: 2px 4px; border-radius: 5px; font-size: 12px;
}
/* An instrument is "MNQ SEP26"; an hour is "09:00". Same grid, one column
   widened, rather than two row layouts to keep in step. */
.tl-bd-wide .tl-bd-row { grid-template-columns: 7.4em 1fr 4.6em 2.2em; }
.tl-bd-k { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tl-bd-row:hover { background: rgba(var(--text-rgb), .03); }
.tl-bd-row.is-empty { opacity: .45; }
.tl-bd-k { color: var(--muted); font-variant-numeric: tabular-nums; }
/* Diverging from the centre: loss grows leftward, profit rightward. Sign is
   therefore carried by POSITION as well as by colour and by the figure, which is
   what keeps it readable in Beacon and under a screen reader. */
.tl-bd-bar { display: flex; align-items: center; height: 10px; min-width: 0; }
.tl-bd-half { flex: 1 1 50%; display: flex; height: 100%; min-width: 0; }
.tl-bd-half-neg { justify-content: flex-end; border-right: 1px solid var(--line); }
.tl-bd-fill { display: block; height: 100%; border-radius: 2px; }
.tl-bd-fill-pos { background: rgba(var(--up-rgb), .55); }
.tl-bd-fill-neg { background: rgba(var(--down-rgb), .55); }
.tl-bd-v {
    font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right;
    font-size: 11.5px; color: var(--muted);
}
.tl-bd-v.pos { color: var(--up); }
.tl-bd-v.neg { color: var(--down); }
.tl-bd-n {
    font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right;
    font-size: 11px; color: var(--faint);
}

@media (max-width: 760px) {
    /* The week column is the first thing to go: at this width seven day cells
       are already narrow, and the row total is derivable from the cells beside
       it while a day's own figure is not. */
    .tl-cal { grid-template-columns: repeat(7, minmax(0, 1fr)); }
    .tl-cal-h-week, .tl-week { display: none; }
    .tl-cal { gap: 3px; }
    .tl-cell { min-height: 54px; padding: 5px 3px; }
    /* SIZED AGAINST THE VIEWPORT, not fixed. Seven columns have to fit whatever
       the phone is, and the widest thing a cell ever prints is six mono
       characters ("+$1.2k") — which fits a 420px screen at 10px and overflows a
       360px one, where several figures ellipsised. An ellipsised P&L in a trade
       log is not a cosmetic problem: it is a number the page declined to tell
       you. `vw` is supported everywhere and clamps at both ends, so the figure
       stays legible on a small phone and stops growing on a large one. Below
       about 330px it clips again, and that is the floor being honest: 8.5px is
       already the smallest this should ever render, and the full figure is in
       the cell's title and aria-label either way. */
    .tl-cell-p { font-size: clamp(8.5px, 2.6vw, 10.5px); letter-spacing: -.2px; }
    .tl-cell-d { font-size: clamp(8.5px, 2.4vw, 10px); }
    .tl-cell-n { display: none; }
    .tl-stat { flex-basis: 96px; min-width: 96px; }
    /* One panel per row rather than two cramped ones, and the bar gives up its
       width first: the figure and the count are the part that has to stay
       readable. */
    .tl-bd-grid { grid-template-columns: 1fr; gap: 14px; }
    .tl-bd-row { grid-template-columns: 3.8em 1fr 4.4em 2em; gap: 6px; }
    .tl-bd-wide .tl-bd-row { grid-template-columns: 7em 1fr 4.4em 2em; }
}
