/* =================== TRADING TERMINAL =================== */

.ex-body { min-height: 100vh; display: flex; flex-direction: column; overflow-x: hidden; overflow-y: auto; background: var(--bg-base); }

/* ---- Top nav ---- */
.ex-nav {
  height: 64px; flex-shrink: 0; display: flex; align-items: center; justify-content: space-between;
  padding: 0 16px; background: var(--bg-surface); border-bottom: 1px solid var(--border);
}
.ex-nav-l, .ex-nav-r { display: flex; align-items: center; gap: 18px; }
.ex-nav-r { gap: 10px; }
.ex-nav-links { display: flex; gap: 4px; }
.ex-nav-links a {
  font-size: 13px; font-weight: 500; color: var(--text-3); padding: 6px 11px;
  border-radius: var(--r-sm); transition: all 120ms;
}
.ex-nav-links a:hover { color: var(--text); background: var(--bg-elevated); }
.ex-nav-links a.active { color: var(--text); background: var(--bg-elevated); }
.bal-chip {
  display: flex; align-items: center; gap: 6px; font-size: 12px;
  background: var(--bg-elevated); border: 1px solid var(--border);
  padding: 5px 12px; border-radius: var(--r-md);
}
.avatar {
  width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center;
  font-size: 11px; font-weight: 600; color: var(--on-accent);
  background: linear-gradient(135deg, var(--accent), var(--accent-alt));
}
@media (max-width: 1100px) { .hide-md { display: none !important; } }
@media (max-width: 1400px) { .hide-lg { display: none !important; } }

/* ---- Instrument bar ---- */
.inst-bar {
  height: 64px; flex-shrink: 0; display: flex; align-items: center; gap: var(--s6);
  padding: 0 16px 0 8px; background: var(--bg-surface); border-bottom: 1px solid var(--border);
  overflow-x: auto; overflow-y: hidden;
}
.inst-pick {
  display: flex; align-items: center; gap: 9px; padding: 7px 8px;
  border-radius: var(--r-md); transition: background 120ms; flex-shrink: 0;
}
.inst-pick:hover { background: var(--bg-elevated); }
.inst-pair { font-size: 16px; font-weight: 600; }
.coin {
  width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center;
  font-size: var(--fs-2xs); font-weight: 700; color: var(--on-accent); flex-shrink: 0; background: var(--text-3);
}
.inst-stats { display: flex; align-items: center; gap: 26px; }
.istat { display: flex; flex-direction: column; gap: 1px; white-space: nowrap; }
.istat-k { font-size: 10px; color: var(--text-3); }
.istat-v { font-size: 13px; font-weight: 500; }
.istat-price .istat-v { font-weight: 600; }

/* ---- Grid: proportions measured off Binance at 1512x950 ----
   book 344 | chart 808 | markets 352 · chart row 524 · form row 334.
   The blotter sits BELOW the fold exactly as Binance does — that is the
   only reason their chart gets 524px where ours had 352. */
.ex-grid {
  /* Measured off Binance at 1512x950.
     344 + 4 + 808 + 4 + 352 = 1512 — two 4px gutters, flush to both edges.
     524 + 4 + 334 = 862 = the book column's full height.
     Side columns ramp proportionally so they hit 344/352 exactly at 1512 and
     stay locked above it, so the chart never grows as the window shrinks.
     The blotter sits BELOW the fold exactly as Binance does — that is the
     only reason their chart gets 524px where ours had 352. */
  flex: 1 0 auto;
  /* Real gutters on both axes, 8px. The previous 4px/0 welded the panels
     into a single slab divided by hairlines, and with square corners on top
     of that the whole terminal read as one 2010 table — which is most of what
     "looks old school" was pointing at.

     The arithmetic is unchanged where it matters: 320 + 8 + 784 + 8 + 320 =
     1440, so the chart still lands on Binance's measured 784px. The book's
     own 4px margins go, because the gap does that job now and doing it twice
     put the book 4px out. */
  display: grid; gap: 8px; background: var(--bg-base);
  /* 320px at 1440, which is Binance's figure exactly; still fluid either side
     of it so a 1280 laptop is not squeezed and a 1920 monitor does not hand
     the book half the screen. */
  grid-template-columns:
    clamp(272px, 22.23%, 340px)
    minmax(0, 1fr)
    clamp(272px, 22.23%, 340px);
  grid-template-rows: 524px 334px auto;
  /* Binance's spot layout, measured off XLM/USDT at 1440x900:

       book   x=4    w=320  h=862   (spans both rows)
       chart  x=328  w=784  h=524
       form   x=328  w=784  h=334   (buy beside sell, 368 each)
       pairs  x=1116 w=320          (top right)
       trades x=1116 w=320          (below it)

     The ticket sat here before and was moved to the right column when this
     page was asked for a swap-style panel. It is back under the chart, where
     it is wide enough to show BOTH sides at once — which is the part of
     Binance's layout worth having, because a ticket with no side to select
     cannot be on the wrong side. */
  grid-template-areas:
    "book chart   watch"
    "book form    trades"
    "blotter blotter blotter";
}
.p-watch  { grid-area: watch; }
.p-chart  { grid-area: chart; }
.p-book   { grid-area: book; }
.p-form   { grid-area: form; }
.p-trades { grid-area: trades; }
.p-blotter{ grid-area: blotter; min-height: 260px; }

/* Square corners are the other half of the slab problem. 8px is enough to
   read as a surface at this density without turning a trading terminal into
   a set of bubbles; overflow:hidden was already here, so children clip to it
   for free. */
.panel { background: var(--bg-surface); border-radius: 8px; display: flex; flex-direction: column; min-height: 0; min-width: 0; overflow: hidden; }
.panel-head {
  height: var(--h-panel-head); flex-shrink: 0; display: flex; align-items: center; justify-content: space-between;
  padding: 0 12px; border-bottom: 1px solid var(--border); gap: 8px;
}
.panel-title { font-size: var(--fs-title); line-height: var(--lh-title); font-weight: var(--fw-head); letter-spacing: -0.01em; }
@media (max-width: 1240px) {
  /* Two columns. The ticket keeps the top of the right-hand side at its
     natural 524px rather than stretching over both rows — spanning them gave
     it 862px to hold about 400px of form. Markets takes the row underneath,
     so the only panel that gives way is the tape. */
  .ex-grid {
    grid-template-columns: minmax(0, 1fr) clamp(280px, 30%, 340px);
    grid-template-rows: 524px 334px auto;
    grid-template-areas:
      "chart   form"
      "book    watch"
      "blotter blotter";
  }
  .p-trades { display: none; }
}
@media (max-width: 880px) {
  .ex-body { height: auto; overflow: auto; }
  .ex-grid { grid-template-columns: 1fr; grid-template-rows: none;
    grid-template-areas: "chart" "form" "book" "trades" "blotter"; }
  .p-chart { height: 320px; } .p-book, .p-trades { height: 288px; } .p-blotter { height: 248px; }
  .form-pair { grid-template-columns: 1fr; }
}

/* ---- Watchlist ---- */
.watch-search {
  flex: 1; min-width: 0; height: 26px; background: var(--bg-input); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: 0 9px; font-size: 12px; outline: none; transition: border-color 120ms;
}
.watch-search:focus { border-color: var(--accent); }
.watch-search::placeholder { color: var(--text-4); }
.watch-tabs { display: flex; gap: 3px; padding: 8px 10px; border-bottom: 1px solid var(--border-soft); }
.wt {
  font-size: 11px; padding: 4px 10px; border-radius: var(--r-sm);
  color: var(--text-3); font-weight: 500; transition: all 120ms;
}
.wt:hover { color: var(--text); background: var(--bg-elevated); }
.wt.active { background: var(--accent-dim); color: var(--accent); }
.watch-head, .book-head, .trades-head {
  display: grid; padding: 7px 12px; font-size: 10px; color: var(--text-3);
  border-bottom: 1px solid var(--border-soft); flex-shrink: 0;
}
.watch-head { grid-template-columns: 1fr 78px 58px; }
.watch-list { flex: 1; overflow-y: auto; }
/* Binance's market list row measures 25px with 12px/18px text. Ours was 32px
   from 7px of vertical padding — seven pixels a row across fifty rows is 350px
   of rail, which is why their list looks like a market feed and ours looked
   like a settings menu. */
.wrow {
  display: grid; grid-template-columns: 1fr 78px 58px;
  height: 25px; padding: 0 12px;
  font-size: var(--fs-data); line-height: var(--lh-data);
  cursor: pointer; transition: background 120ms; align-items: center;
}
.wrow:hover { background: var(--bg-elevated); }
.wrow.active { background: var(--bg-hover); box-shadow: inset 2px 0 0 var(--accent); }
.wrow-sym { font-weight: 500; display: flex; align-items: center; gap: 7px; }
.wrow-sym i { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; font-style: normal; }
/* min-width:0 is load-bearing: a grid item's automatic minimum is its content,
   so without it a long price ignores its 78px track entirely and runs into the
   change column. Ellipsis is the backstop — a clipped price is recoverable by
   widening the panel, a price fused to a percentage is not readable at all. */
.wrow-px, .wrow-chg {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  text-align: right; font-size: var(--fs-sm);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wrow-sym { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- Chart ---- */
.chart-toolbar {
  height: var(--h-panel-head); flex-shrink: 0; display: flex; align-items: center; gap: var(--s4);
  padding: 0 12px; border-bottom: 1px solid var(--border);
}
.tf-group { display: flex; gap: 2px; }
.tf {
  font-size: 11px; padding: 4px 9px; border-radius: var(--r-sm);
  color: var(--text-3); font-weight: 500; font-family: var(--font-mono); transition: all 120ms; font-variant-numeric: tabular-nums;
}
.tf:hover { color: var(--text); background: var(--bg-elevated); }
.tf.active { background: var(--accent-dim); color: var(--accent); }
.chart-tools { display: flex; gap: 2px; padding-left: 12px; border-left: 1px solid var(--border); }
.ctool { width: 26px; height: 26px; display: grid; place-items: center; border-radius: var(--r-sm); color: var(--text-3); transition: all 120ms; }
.ctool svg { width: 15px; height: 15px; }
.ctool:hover { color: var(--text); background: var(--bg-elevated); }
.ctool.active { color: var(--accent); background: var(--accent-dim); }
.ohlc { font-size: 11px; color: var(--text-3); display: flex; gap: 11px; margin-left: auto; white-space: nowrap; overflow: hidden; }
.ohlc b { font-weight: 500; }
.chart-wrap { flex: 1; position: relative; min-height: 0; }
.chart-wrap canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.chart-vol-wrap { height: 58px; flex-shrink: 0; position: relative; border-top: 1px solid var(--border-soft); }
.chart-vol-wrap canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

/* ---- Orderbook ---- */
.book-modes { display: flex; gap: 2px; }
.bm { width: 22px; height: 22px; border-radius: 4px; font-size: 9px; color: var(--text-4); transition: all 120ms; }
.bm:hover { color: var(--text-2); background: var(--bg-elevated); }
.bm.active { color: var(--accent); background: var(--accent-dim); }
.book-head, .trades-head { grid-template-columns: minmax(0,1.05fr) minmax(0,1fr) minmax(0,1fr); }
.book-asks, .book-bids { flex: 1 1 0; overflow: hidden; display: flex; flex-direction: column; min-height: 0; }
.book-asks { justify-content: flex-end; }
.brow {
  display: grid; grid-template-columns: minmax(0,1.05fr) minmax(0,1fr) minmax(0,1fr);
  padding: 0 var(--s3);
  font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: var(--fs-sm);
  position: relative; cursor: pointer; flex-shrink: 0;
}
.brow:hover { background: var(--bg-hover); }
.brow > span { position: relative; z-index: 1; }
.brow .ta-r { text-align: right; }
.bbar { position: absolute; top: 0; bottom: 0; right: 0; opacity: .13; z-index: 0; transition: width 180ms ease-out; }
.book-mid {
  display: flex; align-items: center; justify-content: space-between; padding: 8px 12px;
  border-top: 1px solid var(--border-soft); border-bottom: 1px solid var(--border-soft);
  background: var(--bg-surface); flex-shrink: 0;
}
.book-mid-px { font-size: 16px; font-weight: 600; }
.book-spread { font-size: 10px; }

/* ---- Trades ---- */
.trades-list { flex: 1; overflow-y: auto; }
/* The tape marked buy vs sell with colour and nothing else. Under deuteranopia
   #2EBD85 and #F6465D converge (simulated separation 1.17:1), so the side of
   every print in the tape was unreadable for roughly 1 in 12 men. WCAG 1.4.1
   is Level A and does not accept "the colours are conventional".

   The order book does NOT need this: its bids and asks are spatially
   separated, which is already a non-colour cue. Only the interleaved tape is
   ambiguous, so only the tape gets the caret. */
.trades-list .px::before {
  content: "\25B2"; font-size: 8px; line-height: 1;
  display: inline-block; width: 8px; margin-right: 4px;
  vertical-align: 1px; opacity: .85;
}
.trades-list .px.down::before { content: "\25BC"; }
.trow {
  display: grid; grid-template-columns: minmax(0,1.05fr) minmax(0,1fr) minmax(0,1fr);
  padding: 0 var(--s3);
  font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: var(--fs-sm);
}
.trow .ta-r { text-align: right; }
.trow:hover { background: var(--bg-elevated); }

/* ---- Order form ---- */
.side-toggle { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding: 10px; flex-shrink: 0; }
.st {
  height: 34px; border-radius: var(--r-md); font-size: 13px; font-weight: 600;
  background: var(--bg-elevated); color: var(--text-3); transition: all 120ms;
}
.st-buy.active  { background: var(--up); color: var(--on-up); }
.st-sell.active { background: var(--down); color: var(--on-down); }
.st:not(.active):hover { background: var(--bg-hover); color: var(--text-2); }
/* The tab row sat 2px left of everything beneath it: its own padding was 10px
   while .form-pair below uses var(--s3) = 12px, so the first tab started at
   x=362 against fields at x=364. Same inset now, and the row centres its
   contents — the 24px tabs were hanging at the top of a 35px box. */
.type-tabs {
  display: flex; align-items: center; gap: 3px;
  padding: 0 var(--s3) 10px;
  border-bottom: 1px solid var(--border-soft); flex-shrink: 0;
}
/* 14px/500 is the measured Binance size for anything interactive; these were
   left at 11px when the rest of the scale moved. */
.tt {
  font-size: var(--fs-ui); line-height: var(--lh-ui); font-weight: 500;
  padding: 4px 12px; border-radius: var(--r-sm);
  color: var(--text-3); transition: all 120ms;
}
.tt:hover { color: var(--text); background: var(--bg-elevated); }
.tt.active { background: var(--bg-hover); color: var(--text); }
.form-body { padding: 12px 10px; display: flex; flex-direction: column; gap: 11px; overflow-y: auto; flex: 1; }
.fld { display: flex; flex-direction: column; gap: 5px; }
.fld-k { font-size: 11px; color: var(--text-3); }
.fld-in {
  display: flex; align-items: center; background: var(--bg-input);
  border: 1px solid var(--border); border-radius: var(--r-md);
  padding: 0 11px; height: 36px; transition: border-color 120ms;
}
.fld-in:focus-within { border-color: var(--accent); }
.fld-in input {
  flex: 1; min-width: 0; background: none; border: none; outline: none;
  font-size: var(--fs-ui); font-weight: 500; line-height: var(--lh-ui);
  font-variant-numeric: tabular-nums;
}
.fld-suffix { font-size: 11px; color: var(--text-3); font-weight: 500; flex-shrink: 0; padding-left: 8px; }

.slider-wrap { padding: 2px 2px 0; }
.slider { width: 100%; -webkit-appearance: none; appearance: none; height: 3px; border-radius: 2px; background: var(--border-bright); outline: none; }
.slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 13px; height: 13px; border-radius: 50%;
  background: var(--accent-fill); cursor: pointer; border: 2px solid var(--bg-base);
  box-shadow: 0 0 0 1px var(--accent);
}
.slider::-moz-range-thumb { width: 13px; height: 13px; border-radius: 50%; background: var(--accent-fill); cursor: pointer; border: 2px solid var(--bg-base); }
.slider-ticks { display: flex; justify-content: space-between; margin-top: 7px; }
.slider-ticks button { font-size: 10px; color: var(--text-4); font-family: var(--font-mono); transition: color 120ms; padding: 2px 3px; font-variant-numeric: tabular-nums; }
.slider-ticks button:hover { color: var(--accent); }

.form-meta { display: flex; flex-direction: column; gap: 6px; font-size: 11px; padding: 10px 0; border-top: 1px solid var(--border-soft); }
.form-meta .num { font-size: 11px; }
.submit-btn { height: 40px; font-size: 13px; font-weight: 600; border-radius: var(--r-md); transition: all 120ms; }
.submit-btn.buy  { background: var(--up); color: var(--on-up); }
.submit-btn.sell { background: var(--down); color: var(--on-down); }
.submit-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }
.form-note { font-size: 10px; text-align: center; }

/* ---- Blotter ---- */
.blotter-tabs { height: var(--h-panel-head); flex-shrink: 0; display: flex; gap: 2px; padding: 0 10px; border-bottom: 1px solid var(--border); align-items: center; }
.bt-n { display: none; }  /* phone-length label; the wide one is .bt-w */
.bt {
  font-size: 12px; padding: 5px 11px; border-radius: var(--r-sm); color: var(--text-3);
  font-weight: 500; display: flex; align-items: center; gap: 6px; transition: all 120ms;
}
.bt:hover { color: var(--text); }
.bt.active { color: var(--text); background: var(--bg-elevated); }
.cnt {
  font-size: 10px; background: var(--accent-dim); color: var(--accent);
  padding: 1px 6px; border-radius: 999px; font-family: var(--font-mono); font-weight: 600; font-variant-numeric: tabular-nums;
}
.blotter-body { flex: 1; overflow-y: auto; min-height: 0; }
.bl-table { width: 100%; border-collapse: collapse; }
.bl-table th {
  text-align: left; font-size: 10px; font-weight: 500; color: var(--text-3);
  padding: 8px 12px; border-bottom: 1px solid var(--border-soft);
  position: sticky; top: 0; background: var(--bg-surface); z-index: 2; white-space: nowrap;
}
.bl-table td { padding: 8px 12px; font-size: 12px; border-bottom: 1px solid var(--border-soft); white-space: nowrap; }
.bl-table tbody tr:hover { background: var(--bg-elevated); }
.bl-empty { display: grid; place-items: center; height: 100%; color: var(--text-4); font-size: 12px; padding: 30px; text-align: center; }
.cancel-x { color: var(--text-3); font-size: 11px; padding: 3px 8px; border-radius: 4px; border: 1px solid var(--border); transition: all 120ms; }
.cancel-x:hover { color: var(--down); border-color: var(--down); background: var(--down-dim); }
.tag { font-size: 10px; padding: 2px 7px; border-radius: 4px; font-weight: 500; }
.tag-buy { background: var(--up-dim); color: var(--up-text); }
.tag-sell { background: var(--down-dim); color: var(--down-text); }
.tag-filled { background: var(--accent-dim); color: var(--accent); }
.tag-open { background: var(--warn-dim); color: var(--warn); }

/* ---- Demo strip ---- */
.demo-strip {
  flex-shrink: 0; text-align: center; font-size: 11px; padding: 6px;
  background: var(--warn-dim); border-top: 1px solid var(--warn-line);
  color: var(--warn); letter-spacing: .01em;
}
.demo-strip strong { font-weight: 600; }

/* ---- Toast ---- */
/* Toasts carry no interactive content and attach no dismiss handler, so they
   have no business absorbing taps. At z-index 900 over a fixed Buy/Sell bar the
   old box swallowed presses on the submit button — and the toast telling you to
   enter an amount physically blocked the field you needed. */
.toast-host { position: fixed; bottom: 42px; right: 18px; z-index: 900; display: flex; flex-direction: column; gap: 8px;
  pointer-events: none; }
.toast {
  background: var(--bg-elevated); border: 1px solid var(--border-bright);
  border-radius: var(--r-md); padding: 11px 15px; font-size: 12px;
  box-shadow: var(--shadow-lg); display: flex; align-items: center; gap: 10px;
  animation: toastIn .26s cubic-bezier(.2,.9,.3,1.2); min-width: 240px;
  pointer-events: none;
}
.toast.ok { border-left: 3px solid var(--up); }
.toast.err { border-left: 3px solid var(--down); }
@keyframes toastIn { from { opacity: 0; transform: translateX(24px) scale(.96); } to { opacity: 1; transform: none; } }

/* ---- Instrument dropdown ---- */
.inst-menu {
  position: absolute; top: 106px; left: 14px; z-index: 500; width: 300px; max-height: 420px;
  overflow-y: auto; background: var(--bg-elevated); border: 1px solid var(--border-bright);
  border-radius: var(--r-lg); box-shadow: var(--shadow-lg); padding: 6px; display: none;
}
.inst-menu.open { display: block; }
.im-row { display: grid; grid-template-columns: 1fr 90px 68px; align-items: center;
  padding: 8px 10px; border-radius: var(--r-sm); cursor: pointer; font-size: 12px; }
.im-row:hover { background: var(--bg-hover); }

/* ===========================================================
   REFINEMENT PASS — state coverage, data density, precision
   =========================================================== */

/* --- Every toggle gets a pressed state, not just hover --- */
.wt:active, .tf:active, .tt:active, .bm:active, .ctool:active, .bt:active,
.slider-ticks button:active { transform: scale(.96); }
.wt, .tf, .tt, .bm, .ctool, .bt, .slider-ticks button { transition: all var(--t-fast); }

/* --- Real disabled state (replaces the inline opacity hack) --- */
.fld-in.is-disabled { background: var(--bg-surface); border-color: var(--border-soft); }
.fld-in.is-disabled input { color: var(--text-4); cursor: not-allowed; }
.fld-in.is-disabled .fld-suffix { color: var(--text-4); }

/* --- Data rows: fixed line-height so density never drifts on tick --- */
.brow, .trow, .wrow { line-height: var(--lh-data); font-size: var(--fs-data); }
.brow { height: var(--h-brow); align-items: center; }
.trow { height: var(--h-row);  align-items: center; }

/* --- Order book: subtler depth, clearer level affordance --- */
.bbar { opacity: .1; transition: width var(--t-base); }
.brow:hover .bbar { opacity: .18; }
.brow { cursor: pointer; }
.brow::after {
  content: ''; position: absolute; inset: 0; border-top: 1px solid transparent;
  border-bottom: 1px solid transparent; pointer-events: none;
}
.brow:hover::after { border-color: var(--border-bright); }

/* --- Mid-price block: the anchor of the whole panel --- */
.book-mid { padding: var(--s2) var(--s3); }
.book-mid-px { font-size: var(--fs-lg); letter-spacing: normal; }

/* --- Watchlist: align the active marker with the panel edge --- */
.wrow { cursor: pointer; transition: background var(--t-fast); }
.wrow.active { box-shadow: inset 2px 0 0 var(--accent); }

/* --- Blotter: sticky header must sit above scrolled rows cleanly --- */
.bl-table th { backdrop-filter: blur(6px); }
.bl-table tbody tr { transition: background var(--t-fast); }

/* --- Inputs: remove spinner, select-all on focus feel --- */
.fld-in input::-webkit-outer-spin-button,
.fld-in input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.fld-in input { -moz-appearance: textfield; }

/* --- Slider: consistent across engines --- */
.slider { cursor: pointer; }
.slider::-webkit-slider-thumb { transition: transform var(--t-fast), box-shadow var(--t-fast); }
.slider:hover::-webkit-slider-thumb { transform: scale(1.15); }
.slider:active::-webkit-slider-thumb { transform: scale(1.05); }

/* --- Selection: brand-tinted, not browser blue --- */
::selection { background: var(--accent-dim); color: var(--text); }

/* --- Instrument bar: the price is the loudest thing on the page --- */
.istat-price .istat-v { font-size: var(--fs-2xl); letter-spacing: normal; line-height: 1.05; }
.istat-k { text-transform: uppercase; letter-spacing: .06em; font-size: var(--fs-2xs); }

/* --- Panel titles: quiet, uppercase, institutional --- */
/* Binance sets panel headings in sentence case at 16px/500, not 10px uppercase.
   The small-caps treatment made every panel read as a footnote and flattened
   the hierarchy against the data rows underneath. */
.panel-title { text-transform: none; letter-spacing: -0.01em;
  font-size: var(--fs-title); line-height: var(--lh-title);
  font-weight: 500; color: var(--text); }
.watch-head, .book-head, .trades-head { text-transform: uppercase; letter-spacing: .05em; font-size: var(--fs-2xs); }

/* --- Column headers and data share one right edge --- */
.book-head span:last-child, .trades-head span:last-child,
.brow span:last-child, .trow span:last-child { padding-right: 0; }


/* --- Chart "building history" state: honest about having no backfill --- */
.chart-overlay {
  position: absolute; inset: 0; z-index: 3; display: none;
  flex-direction: column; align-items: center; justify-content: center; gap: var(--s3);
  background: linear-gradient(180deg, var(--overlay-top), var(--overlay-btm));
  backdrop-filter: blur(2px); pointer-events: none; text-align: center; padding: var(--s4);
}
.chart-overlay.show { display: flex; }
.co-text { font-size: var(--fs-sm); color: var(--text-2); max-width: 300px; }
.co-sub  { font-size: var(--fs-xs); color: var(--text-3); }
.co-spinner {
  width: 22px; height: 22px; border-radius: 50%;
  border: 2px solid var(--border-bright); border-top-color: var(--accent);
  animation: spin 900ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }


/* ===========================================================
   BINANCE-STYLE ORDER ENTRY — Buy and Sell side by side
   =========================================================== */
.form-topbar { display: flex; align-items: center; padding: 8px 12px 0; flex-shrink: 0; }
.form-modes { display: flex; gap: 2px; }
.fm {
  font-size: var(--fs-sm); padding: 5px 10px; border-radius: var(--r-sm);
  color: var(--text-3); font-weight: var(--fw-label); transition: all var(--t-fast);
}
.fm:hover { color: var(--text); }
.fm.active { color: var(--text); background: var(--bg-elevated); }

.form-pair {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--s4);
  padding: 10px var(--s3) var(--s2); flex: 1; min-height: 0;
}
.oform { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.oform .fld { gap: 3px; }
.oform .fld-k { font-size: var(--fs-xs); color: var(--text-3); }
.oform .fld-in { height: 30px; }
.oform .fld-in input { font-size: var(--fs-ui); font-weight: 500; }
.oform .slider-wrap { padding: 2px 1px 0; }
.oform .slider-ticks { margin-top: 5px; }
.oform .form-meta { padding: 6px 0 2px; gap: 3px; border-top: none; font-size: var(--fs-xs); }
.oform .form-meta .num { font-size: var(--fs-xs); }
.oform .submit-btn { height: 32px; margin-top: auto; font-size: var(--fs-base); }
.form-note { padding: 0 0 6px; font-size: var(--fs-2xs); }

/* Sell-side slider takes the down colour so the two columns read at a glance */
.slider-sell { background: var(--border-bright); }
.slider-sell::-webkit-slider-thumb { background: var(--down); box-shadow: 0 0 0 1px var(--down); }
.slider-sell::-moz-range-thumb { background: var(--down); }
.oform[data-side="sell"] .slider-ticks button:hover { color: var(--down); }
.oform[data-side="buy"]  .slider-ticks button:hover { color: var(--up); }

/* ---- Order book grouping select ---- */
.book-group {
  height: 22px; background: var(--bg-input); color: var(--text-2);
  border: 1px solid var(--border); border-radius: 4px;
  font-size: var(--fs-2xs); padding: 0 4px; outline: none; cursor: pointer;
}
.book-group:hover { border-color: var(--border-bright); color: var(--text); }

/* ---- Bottom ticker strip ---- */
.ex-ticker {
  flex-shrink: 0; height: 28px; overflow: hidden; display: flex; align-items: center;
  background: var(--bg-surface); border-top: 1px solid var(--border);
  /* Sticky, not fixed: stays in flow so the blotter's last row lands on its
     top edge at max scroll instead of parking underneath. z-index is
     load-bearing — .brow > span carries z-index:1 and would overlap it. */
  position: sticky; bottom: 0; z-index: 20;
}
.ex-ticker-track { display: flex; gap: 24px; white-space: nowrap; padding-left: 12px; animation: exScroll 40s linear infinite; }
.ex-ticker-track:hover { animation-play-state: paused; }
@keyframes exScroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.ex-tk { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); }
.ex-tk b { font-weight: var(--fw-label); color: var(--text-2); }

/* ===========================================================
   SYSTEM RULES from the design audit
   S1 monospace tracking is never negative at data sizes
   S2 tabular figures on anything showing a number
   S7 weight 700 never appears in terminal chrome
   =========================================================== */
.mono, .num { letter-spacing: normal; }
.book-mid-px, .istat-price .istat-v { letter-spacing: -0.028em; }  /* display sizes only */
.pill { font-variant-numeric: tabular-nums; }
.bl-table td b, .wrow-sym { font-weight: var(--fw-label); }


/* ---- Binance-style inline-label fields: label sits inside the box ---- */
.fld-lbl {
  font-size: var(--fs-xs); color: var(--text-3); flex-shrink: 0;
  padding-right: var(--s2); white-space: nowrap;
}
.oform .fld-in { height: 32px; padding: 0 10px; }
.oform .fld-in input { text-align: right; padding-right: 6px; }
.oform .fld-in:focus-within .fld-lbl { color: var(--text-2); }


/* Order-entry density tweaks (kept from the height-aware pass) */
.type-tabs { position: relative; }
.tt-note { margin-left: auto; font-size: var(--fs-2xs); padding-right: 0; align-self: center; }
.oform .form-meta { flex-direction: row; justify-content: space-between; gap: var(--s3); padding: 4px 0 0; }
.oform .form-meta .row { flex-direction: column; align-items: flex-start; gap: 0; min-width: 0; }
/* Three label/value columns share one 100%-wide row with no slack, so a figure
   like "1,204.5512 RLUSD" wrapped its unit onto a second line — the number said
   one thing and the currency sat under it. Let the value shrink and ellipsize
   instead: a clipped unit is recoverable by widening, a silently wrapped one
   reads as a different number. */
.oform .form-meta .row > * { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.oform .slider-wrap { padding: 0; }
.oform .slider-ticks { margin-top: 4px; }


/* ---- Order book: fill the column, never wrap a column header ---- */
.book-head, .trades-head, .watch-head { white-space: nowrap; }
.book-head span, .trades-head span, .watch-head span { overflow: hidden; text-overflow: ellipsis; }
.brow span, .trow span { overflow: hidden; text-overflow: ellipsis; }
.book-asks, .book-bids { flex: 1 1 0; min-height: 0; }
.book-bids { justify-content: flex-start; }
.book-asks { justify-content: flex-end; }


/* ---- Change flash: the book is only as alive as its transitions ---- */
.brow.flash-up   { animation: rowFlashUp   .45s ease-out; }
.brow.flash-down { animation: rowFlashDown .45s ease-out; }
.trow.flash-up   { animation: rowFlashUp   .55s ease-out; }
.trow.flash-down { animation: rowFlashDown .55s ease-out; }
@keyframes rowFlashUp   { 0% { background: rgba(46,189,133,.22); } 100% { background: transparent; } }
@keyframes rowFlashDown { 0% { background: rgba(246,70,93,.22); }  100% { background: transparent; } }

/* Last price should ease between values rather than snapping */
.istat-price .istat-v, .book-mid-px { transition: color 180ms ease; }


/* ---- Book header unit sublabels ---- */
.bh-u { color: var(--text-4); margin-left: 3px; font-size: var(--fs-2xs); }

/* ---- Mid row: price, direction, fiat, spread ---- */
.book-mid-px   { font-size: var(--fs-xl); font-weight: var(--fw-head); line-height: 1.2; }
.book-mid-dir  { font-size: var(--fs-base); line-height: 1; }
.book-mid-fiat { font-size: var(--fs-xs); white-space: nowrap; }
.book-spread   { margin-left: auto; font-size: var(--fs-2xs); white-space: nowrap; }

/* ---- Chart readout row (Binance puts this under the toolbar) ---- */
.chart-readout {
  flex-shrink: 0; height: 22px; display: flex; align-items: center;
  padding: 0 12px; border-bottom: 1px solid var(--border-soft);
  overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
}
.chart-readout::-webkit-scrollbar { display: none; }
.ohlc {
  display: flex; align-items: center; gap: 10px; white-space: nowrap;
  font-size: var(--fs-2xs); color: var(--text-4); margin-left: 0;
}
.ohlc > span { display: inline-flex; align-items: center; gap: 4px; }
.ohlc b { font-weight: var(--fw-label); color: var(--text-2); }
.ohlc b.up { color: var(--up-text); }
.ohlc b.down { color: var(--down-text); }
.ohlc-t { color: var(--text-3); padding-right: var(--s2); border-right: 1px solid var(--border-soft); }
@media (max-width: 880px) { .chart-readout { display: none; } }


/* ---- Market Trades / My Trades ---- */
.trade-tabs { display: flex; gap: 2px; }
.ttab {
  font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .06em;
  padding: 4px 8px; border-radius: var(--r-sm); color: var(--text-4);
  font-weight: var(--fw-label); transition: all var(--t-fast);
}
.ttab:hover { color: var(--text-2); }
.ttab.active { color: var(--text); }
.ttab.active::after {
  content: ''; display: block; height: 2px; margin-top: 3px;
  background: var(--accent-fill); border-radius: 1px;
}

/* ---- BBO ---- */
.bbo-btn {
  font-size: var(--fs-2xs); font-weight: var(--fw-head); color: var(--accent);
  padding: 2px 5px; border-radius: 3px; flex-shrink: 0; margin-left: 4px;
  border: 1px solid var(--accent-line); background: var(--accent-dim);
  transition: all var(--t-fast); letter-spacing: .02em;
}
.bbo-btn:hover { background: var(--accent-fill); color: var(--on-accent-fill); }
.bbo-btn:active { transform: scale(.94); }

/* ---- Mobile: nav and instrument bar need to survive a 375px viewport ---- */
@media (max-width: 700px) {
  .ex-nav { height: 52px; padding: 0 10px; gap: 8px; }
  /* min-width:0 is what lets a flex item shrink below its content; without
     it the row keeps its natural width and overflows the nav instead of
     scrolling inside it. The overflow-x here does nothing without it. */
  .ex-nav-links {
    gap: 0;
    overflow-x: auto;
    scrollbar-width: none;
    flex: 1 1 auto;
    min-width: 0;
    -webkit-overflow-scrolling: touch;
  }
  .ex-nav-links::-webkit-scrollbar { display: none; }
  .ex-nav-r { flex: 0 0 auto; }

  /* A word sliced down the middle reads as broken; the same word fading out
     reads as "there is more, scroll". Same affordance the market list uses.
     The fade only applies while there IS more — a nav that fits is not
     dimmed at its edge for nothing. */
  .ex-nav-links.is-scrollable {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 22px), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 22px), transparent);
  }

  /* This was written when the nav had two links, and it pinned them at
     natural width so they could not silently shrink to nothing — at 320px
     "Trade" had been sitting entirely outside its own box.

     There are four now, and pinned at natural width they simply overflow:
     the row ran under the Connect button and the nav read "Tokens Trade E",
     with Swap not on screen at all. Their boxes were the right size, which
     is why a bounding-rect check called it fine; they were just painted
     beneath something else.

     So the links shrink again, but never below a floor — 96px is two
     destinations — and scroll horizontally past it, which is the pattern the
     filters and tab rails on this site already use. The wordmark keeps its
     ellipsis, and the actions never move. */
  @media (max-width: 380px) {
    .logo { min-width: 0; }
    .logo-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .ex-nav-links { flex: 1 1 auto; min-width: 96px; }
  }
  .ex-nav-links a { padding: 6px 8px; font-size: var(--fs-sm); }
  .bal-chip { display: none; }
  .logo-text { font-size: var(--fs-md); }

  .inst-bar { height: auto; min-height: 56px; padding: 8px 10px; flex-wrap: wrap; gap: 12px; }
  .inst-stats { gap: 14px; flex-wrap: wrap; }
  .istat-price .istat-v { font-size: var(--fs-xl); }
  .istat { gap: 0; }

  .ex-ticker { height: 26px; }
  .p-blotter { min-height: 200px; }
  .form-pair { gap: 10px; }
  .oform .fld-in { height: 34px; }        /* comfortable tap target */
  .oform .submit-btn { height: 40px; }
  .slider-ticks button { padding: 6px 4px; }   /* easier to hit */
}


/* ---- Favourite star (Binance slot, left of the pair name) ---- */
.fav { font-size: 15px; color: var(--text-4); padding: 4px 2px 4px 8px;
       transition: color var(--t-fast), transform var(--t-fast); flex-shrink: 0; }
.fav:hover { color: var(--text-2); }
.fav.on { color: var(--accent); }
.fav:active { transform: scale(.85); }

/* ---- Bottom-bar connection status ---- */
.ex-conn { display: flex; align-items: center; gap: 6px; flex-shrink: 0;
  padding: 0 12px; height: 100%; border-right: 1px solid var(--border);
  font-size: var(--fs-2xs); color: var(--text-3); white-space: nowrap;
  background: var(--bg-surface); position: relative; z-index: 2; }
.ex-conn .dot { width: 5px; height: 5px; }
.ex-conn.err .dot { background: var(--down); animation: none; }


/* ---- Drawing toolbar (Binance left rail) ---- */
.draw-tools {
  position: absolute; left: 0; top: 0; bottom: 0; width: 36px; z-index: 5;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding-top: 6px; background: var(--bg-surface); border-right: 1px solid var(--border);
}
.dt { width: 28px; height: 28px; display: grid; place-items: center;
  border-radius: var(--r-sm); color: var(--text-3); transition: all var(--t-fast); }
.dt svg { width: 16px; height: 16px; }
.dt:hover { color: var(--text); background: var(--bg-elevated); }
.dt.active { color: var(--accent); background: var(--accent-dim); }
.dt-danger:hover { color: var(--down); background: var(--down-dim); }
.dt-sep { width: 18px; height: 1px; background: var(--border); margin: 4px 0; }

/* Chart canvases start right of the rail; volume must share the offset or
   its bars drift out of line with the candles above them. */
.chart-wrap canvas { left: 36px; width: calc(100% - 36px); }
.chart-vol-wrap canvas { left: 36px; width: calc(100% - 36px); }
.chart-overlay { left: 36px; }
.chart-wrap { cursor: crosshair; }
.chart-wrap.tool-armed canvas { cursor: copy; }

@media (max-width: 880px) {
  .draw-tools { display: none; }
  .chart-wrap canvas, .chart-vol-wrap canvas { left: 0; width: 100%; }
  .chart-overlay { left: 0; }
}

/* ---- Watchlist token logos (66 pairs) ---- */
.wl-letter, .wl-logo { display: inline-grid; place-items: center; border-radius: 50%;
  flex-shrink: 0; font-style: normal; font-size: 7px; font-weight: 700; color: #fff; }
.wl-logo { position: relative; overflow: hidden; }
.wl-logo .wl-letter { position: absolute; inset: 0; width: 100% !important; height: 100% !important; }
.wl-logo img { position: relative; width: 100%; height: 100%; object-fit: cover;
  border-radius: 50%; display: block; }
.watch-list { scrollbar-width: thin; }


/* Panels must never let their content dictate a grid track's height. */
.p-watch, .p-book, .p-trades, .p-chart, .p-form { min-height: 0; overflow: hidden; }
.watch-list, .trades-list, .blotter-body { min-height: 0; }


/* Duration is set in JS from the measured track width; this is only a
   fallback for the first frame before that runs. Pause on hover so a row
   can actually be read. */
.ex-ticker-track { animation-duration: 280s; }
.ex-ticker:hover .ex-ticker-track { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) { .ex-ticker-track { animation: none; } }

/* Instrument-bar badge: same resolver as the watchlist, sized for the header */
.inst-badge { display: inline-flex; align-items: center; flex-shrink: 0; line-height: 0; }
.inst-badge .wl-logo, .inst-badge .wl-letter { font-size: 9px; }
.inst-badge svg { width: 100%; height: 100%; border-radius: 50%; display: block; }
.wl-logo svg { width: 100%; height: 100%; border-radius: 50%; display: block; }

/* ---- Book: shimmer while the first depth request is in flight ---- */
.book-loading .book-asks::after, .book-loading .book-bids::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(90deg, transparent, var(--shimmer), transparent);
  animation: bookShimmer 1.2s infinite;
}
.book-loading .book-asks, .book-loading .book-bids { position: relative; }
@keyframes bookShimmer { to { transform: translateX(100%); } }

/* ---- Empty search state ---- */
.watch-empty {
  padding: 24px 14px; text-align: center; color: var(--text-4);
  font-size: var(--fs-sm); line-height: 1.5;
}

/* ---- Demo-balance marker: this is not a connected wallet ---- */
.demo-bal { color: var(--text-4); font-size: var(--fs-2xs); margin-left: 4px; }

/* ===========================================================
   MOBILE / TOUCH
   The terminal made .ex-body the scroll container. On a phone that
   reads as a frozen screenshot: the document itself never scrolls,
   momentum is lost, and taps over the canvas do nothing because the
   gesture is owned by an element that cannot pan. Below 900px the
   document scrolls normally and every surface declares its own
   touch-action.
   =========================================================== */
@media (max-width: 900px) {
  html, body { overflow-x: hidden; }

  /* ROOT CAUSE of the broken mobile layout.
     The nav's right cluster — status pill plus Connect — measured 226px against
     a 375px viewport, and `justify-content: space-between` gave it no reason to
     yield. That one overflow widened the DOCUMENT to 470px. Because .p-form and
     .m-actions are position:fixed with left/right 0, fixed elements escape the
     `overflow-x: hidden` above and sized themselves to 470px too — which pushed
     the Buy/Sell bar to y=955 in an 812px viewport. Off-screen. Unreachable.
     A single unyielding flex child took the entire trading UI off the phone. */
  .ex-nav { padding: 0 12px; gap: 10px; }
  .ex-nav-l { flex: 1 1 auto; min-width: 0; gap: 12px; }
  .ex-nav-r { flex: 0 0 auto; min-width: 0; gap: 8px; }
  /* Not lost information: connection state is reported in the status bar at the
     foot of every page, so the pill is duplicate chrome on a narrow screen. */
  .ex-nav-r .pill { display: none; }
  .ex-nav-r .btn { flex-shrink: 0; white-space: nowrap; }

  .ex-body {
    min-height: 0; height: auto;
    overflow: visible;                 /* let the DOCUMENT scroll, not body */
    display: block;
  }
  .ex-grid { display: block; }
  .ex-grid > .panel { width: 100%; margin: 0 0 8px; }

  /* Panels size to content instead of fighting for grid tracks */
  .p-chart   { height: 340px; }
  .p-book    { height: 360px; margin: 0 0 8px; }
  .p-trades  { height: 300px; }
  .p-form    { height: auto; }
  .p-blotter { height: auto; min-height: 220px; }
  .p-watch   { height: 320px; display: flex; }

  /* Order: chart, ticket, book, trades, markets, blotter */
  .p-chart { order: 1; } .p-form { order: 2; } .p-book { order: 3; }
  .p-trades { order: 4; } .p-watch { order: 5; } .p-blotter { order: 6; }

  /* Vertical panes must scroll with momentum */
  .watch-list, .trades-list, .blotter-body, .book-asks, .book-bids, .form-pair {
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }
  .watch-list, .trades-list, .blotter-body { overflow-y: auto; }

  /* The canvas must not swallow a vertical swipe */
  .chart-wrap, .chart-wrap canvas, .chart-vol-wrap, .chart-vol-wrap canvas {
    touch-action: pan-y;
  }
  .ex-ticker { touch-action: pan-x; }

  /* ---- iOS auto-zoom ----
     Safari zooms the page whenever a focused input is under 16px, and this page
     anchors two position:fixed layers (the ticket sheet and the Buy/Sell bar) to
     the layout viewport that zoom rescales. So a 12px Amount field did not just
     look small: tapping it moved the sheet and the submit button off-axis. Every
     control the user can focus is raised to 16px on mobile. Desktop keeps 12px.
     The visible field height is set separately below, so nothing grows. */
  .oform .fld-in input,
  .fld-in input,
  .watch-search,
  .book-group,
  .winput,
  .sc-search { font-size: 16px; }

  /* The field looked 40px tall but only the inner input took focus — 22px of it
     was dead band. Let the input fill its shell. */
  .fld-in { min-height: 44px; }
  .fld-in input { height: 100%; align-self: stretch; }
  .oform .fld-in { height: 44px; }
  .watch-search { height: 36px; }

  /* ---- Percent slider ----
     `.slider` was a 3px-tall border box. The mobile rule only enlarged the THUMB,
     which paints outside that box, so the hit region never grew — roughly 16px
     of grabbable height for the control that decides what fraction of the balance
     an order spends, 140px above a live submit button. Give the input a 44px box
     and move the 3px rail onto the track pseudo-elements. Mobile only: on desktop
     .p-form is a fixed 334px grid row with overflow:hidden and +41px would clip
     the submit button. */
  .slider {
    display: block; height: 44px; background: none; border-radius: 0;
  }
  .slider-sell { background: none; }
  .slider::-webkit-slider-runnable-track {
    height: 3px; border-radius: 2px; background: var(--border-bright);
  }
  .slider::-moz-range-track {
    height: 3px; border-radius: 2px; background: var(--border-bright);
  }

  /* Tap targets: 44px is the accessible floor */
  .wrow { height: 44px; }
  /* A book row is not read-only: tapping one writes that price into BOTH order
     tickets, so it is a control and belongs above the density of the trade tape.
     36px rather than 44px is deliberate — the panel can only show so much depth,
     and 44px with the old floor pushed rows back into the clipped region. The
     height lives in --h-brow because levelsThatFit() has to divide by it; a
     literal here is what left the CSS and the JS 26-vs-20 out of step. */
  :root { --h-brow: 36px; }
  .trow { height: 26px; }
  .brow { -webkit-tap-highlight-color: transparent; }
  .brow:active { background: var(--bg-hover); }
  .tf, .tt, .wt, .bt, .ttab, .bm, .fm { min-height: 34px; padding-top: 8px; padding-bottom: 8px; }
  .slider-ticks button { min-height: 34px; min-width: 40px; }
  .slider::-webkit-slider-thumb {
    width: 20px; height: 20px;
    margin-top: -8.5px;                 /* (3 - 20) / 2, centres on the 3px rail */
  }
  .slider::-moz-range-thumb { width: 20px; height: 20px; }
  .btn-sm { height: 34px; }
  .fav { font-size: 20px; padding: 8px; }

  /* Horizontal control strips scroll rather than wrap into nothing */
  .chart-toolbar, .type-tabs, .watch-tabs, .blotter-tabs, .ex-nav-links {
    overflow-x: auto; -webkit-overflow-scrolling: touch;
    scrollbar-width: none; flex-wrap: nowrap;
  }
  .chart-toolbar::-webkit-scrollbar, .type-tabs::-webkit-scrollbar,
  .watch-tabs::-webkit-scrollbar, .blotter-tabs::-webkit-scrollbar,
  .ex-nav-links::-webkit-scrollbar { display: none; }

  /* Two columns of ticket on a phone is unusable */
  .form-pair { grid-template-columns: 1fr; gap: 14px; overflow: visible; }
  .oform .fld-in { height: 40px; }
  .oform .submit-btn { height: 46px; font-size: var(--fs-md); }

  .blotter-body { overflow-x: auto; }
  .bl-table { min-width: 560px; }

  /* The only destructive control in the blotter rendered at x=490 in a 375px
     viewport — off-screen, reachable only by a horizontal swipe inside the box
     that also scrolls vertically. Pin it. Scoped to .bl-act rather than
     :last-child on purpose: the Balances tab's last column is Value and the
     Fills tab's is Tx, and pinning those would cover the Reserved / Rate
     figures. */
  .cancel-x {
    min-height: 44px; padding: 0 14px; font-size: var(--fs-sm);
    display: inline-flex; align-items: center; justify-content: center;
  }
  .bl-table .bl-act {
    position: sticky; right: 0; background: var(--bg-surface);
    /* border-collapse drops borders on sticky cells in Chrome; repaint inset. */
    box-shadow: -8px 0 8px -8px var(--edge-shadow), inset 0 -1px 0 var(--border-soft);
  }
  .bl-table td.bl-act { z-index: 1; }
  .bl-table th.bl-act { z-index: 3; top: 0; }
  .bl-table tbody tr:hover td.bl-act { background: var(--bg-elevated); }
}

/* Remove the 300ms tap delay and stop double-tap zoom on controls */
button, .wrow, .brow, .trow, .cat, .tf, .tt, .wt, .bt, .ttab, .bm {
  touch-action: manipulation;
}

/* ===========================================================
   MOBILE LAYOUT — tabbed, matching Binance's phone terminal
   Stacking all six panels made an endless scroll. Binance shows
   one panel at a time behind Chart / Order Book / Trades / Data,
   with the ticket as a sheet over a sticky Buy/Sell bar.
   =========================================================== */
.m-tabs, .m-actions, .m-sheet-backdrop { display: none; }

@media (max-width: 900px) {
  /* --- compact pair header: price left, 2x2 stats right --- */
  .inst-bar { height: auto; padding: 10px 12px; align-items: flex-start; gap: 12px; }
  /* `1fr 1fr` sizes BOTH tracks to the single widest item (24h volume), so the
     block cost 2x149 + gap = 310px of a 320px viewport and the right-hand stats
     fell off the edge. `auto auto` lets each column size to its own content —
     ~109 + ~149 — recovering roughly 39px without truncating a single digit.
     Nothing in this bar may ever ellipsize a number, so no min-width:0 here. */
  .inst-stats {
    display: grid; grid-template-columns: auto auto; gap: 4px 14px;
    margin-left: auto; flex: 0 0 auto;
  }
  .istat { flex-direction: row; align-items: baseline; gap: 6px; }
  .istat-k { font-size: 9px; }
  .istat-v { font-size: var(--fs-xs); }
  .istat-price { grid-column: 1 / -1; flex-direction: column; align-items: flex-start; }
  .p-form .istat, .istat:nth-child(n+6) { display: none; }

  /* --- tab bar --- */
  .m-tabs {
    display: flex; gap: 2px; padding: 0 8px; flex-shrink: 0;
    background: var(--bg-surface); border-bottom: 1px solid var(--border);
    overflow-x: auto; scrollbar-width: none; position: sticky; top: 0; z-index: 30;
  }
  .m-tabs::-webkit-scrollbar { display: none; }
  /* Five tabs at 12px padding measured 385px against a 375px viewport, so the
     last one ("Orders") sat under the edge looking chopped rather than
     scrollable. Let them share the row and only scroll below ~340px. */
  .m-tab {
    flex: 1 1 auto; padding: 12px 6px 10px; font-size: var(--fs-sm);
    font-weight: var(--fw-label); color: var(--text-3); text-align: center;
    border-bottom: 2px solid transparent; white-space: nowrap;
  }
  .m-tab.active { color: var(--text); border-bottom-color: var(--accent); }

  /* --- one panel at a time --- */
  .ex-grid > .panel { display: none; margin: 0; }
  .ex-grid.show-chart   .p-chart,
  .ex-grid.show-book    .p-book,
  .ex-grid.show-trades  .p-trades,
  .ex-grid.show-watch   .p-watch,
  .ex-grid.show-blotter .p-blotter { display: flex; height: calc(100vh - 260px); min-height: 320px; }
  .ex-grid.show-chart .p-chart { height: 420px; }

  /* --- ticket becomes a sheet over a sticky action bar --- */
  .m-actions {
    display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
    padding: 8px 10px calc(8px + env(safe-area-inset-bottom));
    background: var(--bg-surface); border-top: 1px solid var(--border);
  }
  .m-act { height: 46px; border-radius: var(--r-md); font-weight: var(--fw-head); font-size: var(--fs-md); }
  /* Ink from the tokens, like the ticket's own Buy/Sell. White on --down
     measured 3.53:1 — the pairing base.css fixed with --on-down, which a
     hard-coded #fff here bypassed where the palette gate cannot see it. */
  .m-act.buy  { background: var(--up);   color: var(--on-up); }
  .m-act.sell { background: var(--down); color: var(--on-down); }

  /* --kb-inset is published by the visualViewport listener in exchange.js and is
     0px unless a ticket input has focus. Sitting the sheet on top of the
     keyboard is only half of it: the max-height must be bounded against the
     REMAINING viewport, not against 82vh, or the sheet keeps its full height and
     its Limit/Market tabs run off the top. Bounding it here is also what finally
     makes overflow-y:auto do something — until now scrollHeight equalled
     clientHeight, so the sheet could not be scrolled out from under the
     keyboard at all. */
  .p-form {
    display: block !important; position: fixed; left: 0; right: 0; z-index: 50;
    bottom: var(--kb-inset, 0px);
    max-height: min(82vh, calc(100vh - var(--kb-inset, 0px) - 48px));
    overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
    background: var(--bg-surface); border-top: 1px solid var(--border-bright);
    border-radius: var(--r-lg) var(--r-lg) 0 0; box-shadow: var(--shadow-lg);
    transform: translateY(102%);
    padding-bottom: calc(16px + env(safe-area-inset-bottom));
    /* Parked off-screen is not hidden: Tab and a screen reader's swipe still
       walked all fifteen controls, submit included, of a form nobody could
       see. visibility takes the closed sheet out of both. It flips after the
       220ms slide on the way out, and at once on the way in. */
    visibility: hidden;
    transition: transform 220ms cubic-bezier(.4,0,.2,1), bottom 120ms ease, max-height 120ms ease,
                visibility 0s linear 220ms;
  }
  .p-form.open {
    transform: translateY(0); visibility: visible;
    transition: transform 220ms cubic-bezier(.4,0,.2,1), bottom 120ms ease, max-height 120ms ease,
                visibility 0s;
  }
  .m-sheet-backdrop {
    display: block; position: fixed; inset: 0; z-index: 45;
    background: var(--scrim); opacity: 0; pointer-events: none;
    transition: opacity 220ms ease;
    /* The backdrop is not a scroll container, so overscroll-behavior would be a
       no-op here — touch-action is the half that actually stops the drag
       reaching the document. It suppresses panning, not click, so the
       tap-to-dismiss handler still fires. */
    touch-action: none;
  }
  .m-sheet-backdrop.open { opacity: 1; pointer-events: auto; }
  /* one side at a time in the sheet */
  .p-form.side-buy  .oform[data-side="sell"],
  .p-form.side-sell .oform[data-side="buy"] { display: none; }
  .p-form .form-pair { grid-template-columns: 1fr; }

  .ex-grid { padding-bottom: 8px; }
}

/* ===========================================================
   MOBILE: fill the viewport, no dead band
   A fixed 420px panel left ~250px of bare background between the
   ticker and the action bar. Let flex hand the active panel
   whatever is left instead of guessing a height.
   =========================================================== */
@media (max-width: 900px) {
  .ex-body {
    display: flex; flex-direction: column;
    min-height: 100vh; min-height: 100dvh;      /* dvh tracks browser chrome */
    overflow: visible;
  }
  .ex-grid { flex: 1 1 auto; display: block; min-height: 0; padding-bottom: 0; }

  /* The visible panel takes the whole remaining column. */
  .ex-grid.show-chart   .p-chart,
  .ex-grid.show-book    .p-book,
  .ex-grid.show-trades  .p-trades,
  .ex-grid.show-watch   .p-watch,
  .ex-grid.show-blotter .p-blotter {
    display: flex; height: 100%; min-height: 300px;
  }
  .ex-grid.show-chart .p-chart { height: 100%; }

  /* Binance's phone terminal has no ticker strip; on 375px it only
     collided with the connection badge. Keep the badge, drop the crawl. */
  /* .m-actions is 63px on a flat-bottom phone but 97px once
     env(safe-area-inset-bottom) applies, so a literal 63px reservation left the
     connection badge fully covered on every notched iPhone. Reserve the inset
     too. env() resolves to 0 elsewhere, so flat-bottom rendering is unchanged. */
  /* margin-bottom reserves flow space at the DOCUMENT end; it does not move a
     STUCK element. `position: sticky; bottom: 0` pins the ticker to the viewport
     bottom at every scroll offset below max — exactly where the Buy/Sell bar
     (z-index 40) sits, and the ticker is z-index 20. So on any scrolling page
     the connection badge was parked underneath the bar. Keep the margin (it
     still stops the document being clipped) and add the inset so the stuck
     position clears the bar too. Two-arg env() so it degrades to 0 rather than
     invalidating the whole declaration. */
  .ex-ticker {
    height: 26px;
    margin-bottom: calc(63px + env(safe-area-inset-bottom, 0px));
    bottom: calc(63px + env(safe-area-inset-bottom, 0px));
  }
  .ex-ticker-track { display: none; }
  .ex-conn { border-right: none; width: 100%; justify-content: center; }

  /* The pair header was 157px because the stats grid wrapped. Tighten it. */
  .inst-bar { padding: 8px 12px; gap: 8px; }
  .istat-price .istat-v { font-size: var(--fs-xl); line-height: 1.1; }
  .inst-stats { gap: 2px 12px; }
  .inst-pair { font-size: var(--fs-md); }
}

/* height:100% needs a parent with a definite height; .ex-grid's comes from
   flex growth, so the percentage resolved to auto and the panel collapsed to
   its min-height, leaving the gap. Grow the panel with flex instead. */
@media (max-width: 900px) {
  .ex-grid { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
  .ex-grid.show-chart   .p-chart,
  .ex-grid.show-book    .p-book,
  .ex-grid.show-trades  .p-trades,
  .ex-grid.show-watch   .p-watch,
  .ex-grid.show-blotter .p-blotter {
    display: flex; flex: 1 1 auto; height: auto; min-height: 300px;
  }
  /* The ticker and action bar are the only fixed-height items below it. */
  .ex-ticker { flex: 0 0 auto; }
}

/* The dead band was a unit mismatch, not a layout error: the body filled
   100dvh (visual viewport, 812px) while the fixed action bar anchored to the
   LAYOUT viewport (1019px), leaving 207px of page background between them.
   Fixed positioning uses the layout viewport, so the column must too. */
@media (max-width: 900px) {
  /* 100vh IS the layout viewport, which is exactly what `position: fixed`
     resolves against — so this is the only unit that agrees with the bar.
     An earlier `@supports (height: 100svh)` override sat here and undid that:
     svh is the SMALL viewport (812px against a 916px layout viewport), so the
     body stopped short and the fixed Buy/Sell bar landed in the gap below the
     fold, off-screen. Do not reintroduce svh or dvh here without moving the
     action bar out of fixed positioning too. */
  .ex-body { min-height: 100vh; }
  /* Belt and braces: the panel column keeps the same ground colour as the
     panels, so any residual sliver reads as chrome rather than a hole.
     The root gets it too. Browsers do not agree on whether `position: fixed`
     and `100vh` resolve against the same viewport — this pane resolves 100vh
     to 812px while anchoring fixed elements at 916px — so a strip below the
     body can survive any unit choice. Painting the root in the SURFACE colour
     makes that strip continuous with the action bar sitting in it, instead of
     reading as a black hole under the page. */
  html { background: var(--bg-surface); }
  .ex-grid { background: var(--bg-surface); }
}

/* ---- Modal: wallet connect and order confirmation ---- */
.modal-host { position: fixed; inset: 0; z-index: 400; display: flex;
  align-items: center; justify-content: center; padding: 16px; }
.modal-host[hidden] { display: none; }
.modal-backdrop { position: absolute; inset: 0; background: var(--scrim); backdrop-filter: blur(2px); }
/* The confirm dialog renders 684px tall in its worst case (Limit + protected
   route + a TrustSet step). Nothing in it could scroll and the host is fixed,
   so on a 553px viewport — an iPhone SE with Safari's toolbars up — the close X
   sat at y=-43 and the sign button ran past the bottom edge. This is the
   sign-and-submit gate of the product.
   max-height is 100%, NOT 100vh: the host is `position: fixed; inset: 0` with
   16px padding and border-box, so 100% resolves against whatever box fixed
   positioning actually uses. On iOS 100vh is the LARGE viewport and would still
   overflow — the same unit mismatch already documented for the action bar. */
.modal { position: relative; width: 100%; max-width: 420px; background: var(--bg-elevated);
  border: 1px solid var(--border-bright); border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg); overflow: hidden; animation: modalIn .16s ease-out;
  max-height: 100%; display: flex; flex-direction: column; }
@keyframes modalIn { from { opacity: 0; transform: translateY(8px) scale(.98); } to { opacity: 1; transform: none; } }
.modal-head { display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px; border-bottom: 1px solid var(--border); font-weight: var(--fw-head);
  flex: 0 0 auto; }
.modal-x { font-size: 20px; line-height: 1; color: var(--text-3); padding: 0 4px; }
.modal-x:hover { color: var(--text); }
.modal-body { padding: 16px; display: flex; flex-direction: column; gap: 12px;
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch; }

.wopt { display: flex; align-items: center; gap: 12px; padding: 12px 14px;
  border: 1px solid var(--border); border-radius: var(--r-md); background: var(--bg-surface);
  cursor: pointer; transition: all var(--t-fast); text-align: left; width: 100%; }
.wopt:hover:not(:disabled) { border-color: var(--accent); background: var(--bg-hover); }
.wopt:disabled { opacity: .7; cursor: not-allowed; }
.wopt-ic { width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center;
  background: var(--accent-dim); color: var(--accent); font-weight: 700; font-size: 12px; flex-shrink: 0; }
.wopt-t { display: flex; flex-direction: column; gap: 1px; }
.wopt-t b { font-size: var(--fs-base); font-weight: var(--fw-label); }
.wopt-t span { font-size: var(--fs-xs); color: var(--text-3); }

.wnote { font-size: var(--fs-xs); color: var(--text-3); line-height: 1.55; }
.wnote strong { color: var(--text-2); font-weight: var(--fw-label); }
.wsep { display: flex; align-items: center; gap: 10px; color: var(--text-4); font-size: var(--fs-2xs); }
.wsep::before, .wsep::after { content: ''; flex: 1; height: 1px; background: var(--border); }
.winput { height: 38px; background: var(--bg-input); border: 1px solid var(--border);
  border-radius: var(--r-md); padding: 0 12px; font-size: var(--fs-sm); outline: none;
  font-family: var(--font-mono); width: 100%; font-variant-numeric: tabular-nums; }
.winput:focus { border-color: var(--accent); }

/* Confirmation rows */
.crow { display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 7px 0; border-bottom: 1px solid var(--border-soft); font-size: var(--fs-sm); }
.crow:last-of-type { border-bottom: none; }
.crow span { color: var(--text-3); }
.crow b { font-weight: var(--fw-label); }
.cstep { display: flex; align-items: center; gap: 9px; padding: 9px 11px; border-radius: var(--r-md);
  background: var(--bg-surface); border: 1px solid var(--border); font-size: var(--fs-sm); }
.cstep.done { border-color: var(--up); color: var(--up); }
.cstep.active { border-color: var(--accent); }
.cstep-n { width: 20px; height: 20px; border-radius: 50%; display: grid; place-items: center;
  background: var(--bg-elevated); font-size: 10px; flex-shrink: 0; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* Wallet chip in the nav */
.wchip { display: flex; align-items: center; gap: 7px; font-size: var(--fs-sm);
  background: var(--bg-elevated); border: 1px solid var(--border);
  padding: 5px 10px; border-radius: var(--r-md); cursor: pointer; }
.wchip:hover { border-color: var(--border-bright); }
.wchip .dot { width: 6px; height: 6px; }
.wchip code { font-family: var(--font-mono); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
.wchip .ro { color: var(--warn); font-size: var(--fs-2xs); }
@media (max-width: 700px) { .wchip code { display: none; } }

/* ---- Chart view tabs ---- */
.chart-views { display: flex; gap: 2px; padding: 0 10px; height: 32px; align-items: center;
  border-bottom: 1px solid var(--border); flex-shrink: 0; }
.cv { font-size: var(--fs-sm); padding: 5px 11px; border-radius: var(--r-sm);
  color: var(--text-3); font-weight: var(--fw-label); transition: all var(--t-fast); }
.cv:hover { color: var(--text); }
.cv.active { color: var(--text); background: var(--bg-elevated); }

.depth-wrap { position: relative; flex: 1; min-height: 0; }
.depth-wrap canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.info-wrap { flex: 1; overflow-y: auto; padding: 14px 16px; font-size: var(--fs-sm); min-height: 0; }
.info-row { display: flex; justify-content: space-between; gap: 12px; padding: 7px 0;
  border-bottom: 1px solid var(--border-soft); }
.info-row:last-child { border-bottom: none; }
.info-row span { color: var(--text-3); }
.info-row b { font-weight: var(--fw-label); word-break: break-all; text-align: right; }
.info-note { color: var(--text-3); font-size: var(--fs-xs); line-height: 1.6; margin-top: 12px; }

/* ===========================================================
   NATIVE FEEL ON TOUCH

   The mobile LAYOUT was already right — one panel at a time, ticket
   as a sheet, sticky action bar. What was missing was everything that
   happens between a finger landing and the screen changing, which is
   what reads as "smooth".

   Three defaults were doing it:

     · -webkit-tap-highlight-color was left at the UA's translucent
       blue, so every tab, row and button flashed a rectangle drawn by
       the browser rather than by us. It had been cleared on .brow and
       nowhere else, so the order book felt right and the rest did not.

     · There is no :hover on a phone, which makes :active the only
       feedback a control can give. The six most-tapped things on the
       phone — .m-tab, .cv, .m-act, .inst-pick, .wrow, .trow — had no
       pressed state at all, so a tap read as nothing happening right
       up until the panel swapped. The desktop controls (.tf, .tt, .bt)
       already scale on press; these had simply been missed.

     · The document rubber-banded at the edges even though it does not
       scroll, and long-press raised the iOS selection callout over
       prices. Both say "web page" rather than "app".

   Everything here is feedback and gesture only; no layout moves.
   =========================================================== */
html { -webkit-tap-highlight-color: transparent; }

@media (max-width: 900px) {
  /* The document is a fixed shell — the active panel scrolls, not the
     page — so any overscroll here is a rubber-band or a Chrome
     pull-to-refresh, and both are pure noise. */
  html, body { overscroll-behavior: none; }

  /* Chrome is not text. Inputs and the explorer links still are, so a
     hash or an address can still be selected and copied. */
  .ex-nav, .m-tabs, .m-actions, .inst-bar, .panel-head,
  .m-tab, .cv, .tf, .tt, .wrow, .trow, .brow, .inst-pick {
    -webkit-user-select: none; user-select: none;
  }
  input, textarea, .tx-link, .wchip code, .addr {
    -webkit-user-select: text; user-select: text;
  }

  /* Pressed states: rows tint, buttons give slightly. Kept faster than
     the panel swap so the control answers before the screen does. */
  .m-tab:active, .cv:active, .inst-pick:active { background: var(--bg-elevated); }
  .wrow:active, .trow:active { background: var(--bg-hover); }
  .m-act:active { filter: brightness(.92); transform: scale(.985); }

  .m-tab { transition: color 120ms ease, border-color 120ms ease, background 90ms ease; }
  .cv, .inst-pick { transition: background 90ms ease, color 120ms ease; }
  .m-act { transition: filter 90ms ease, transform 90ms ease; }

  /* A panel swap was a hard cut out of display:none. Easing it in lets
     the eye follow the change instead of being handed a new screen.
     .p-form is excluded deliberately: it is a direct child of .ex-grid
     but it is the sheet, and it animates itself with a transform this
     keyframe would stomp on mid-slide. */
  .ex-grid > .panel:not(.p-form) { animation: mPanelIn 180ms cubic-bezier(.4,0,.2,1); }
  /* Opacity only over the chart — translating a live canvas re-rasterises
     it and the candles blur for a frame. Matches the :not() above on
     specificity deliberately; without .panel it loses and the chart
     slides with everything else. */
  .ex-grid > .panel.p-chart:not(.p-form) { animation-name: mPanelFade; }

  /* The chart's view switcher was a 25px target: the smallest thing on
     the page and under every platform's minimum. */
  .cv { padding: 10px 12px; }
}

@keyframes mPanelIn {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}
@keyframes mPanelFade { from { opacity: 0; } to { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  .ex-grid > .panel:not(.p-form) { animation: none; }
}

/* ===========================================================
   ONE CHART CONTROL ROW ON A PHONE

   Measured against Binance's own phone terminal at 375px: they spend
   287px above the first chart pixel, we spent 309px — and theirs
   includes a tag-chip row and a networks row we do not have. The
   density was not the problem; the structure was. Binance has one
   panel-tab row and one chart-control row. We had one panel-tab row
   and TWO chart rows, because the view switcher (Chart/Depth/Info)
   and the timeframes sat in separate 32px and 40px bars.

   The wrapper is display:contents everywhere else, so the desktop
   layout is untouched — .p-chart still sees both bars as its own
   children and the grid is unchanged.
   =========================================================== */
.chart-ctl { display: contents; }

@media (max-width: 900px) {
  .chart-ctl {
    display: flex; align-items: stretch; flex-shrink: 0;
    overflow-x: auto; scrollbar-width: none;
    border-bottom: 1px solid var(--border);
  }
  .chart-ctl::-webkit-scrollbar { display: none; }

  /* Without this the row ends by chopping "1d" through the middle of the
     glyph, which reads as a layout bug rather than as more content. Binance
     puts a chevron here; a mask costs no space and never overlaps a control.
     The masked pixels stay tappable — this only softens the paint. */
  .chart-ctl {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
            mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
  }

  /* Each bar keeps its own spacing but stops drawing its own rule and
     stops claiming a fixed height — the row above owns both now. */
  .chart-ctl > .chart-views,
  .chart-ctl > .chart-toolbar {
    border-bottom: 0; height: auto; flex: 0 0 auto; overflow: visible;
  }
  .chart-ctl > .chart-views { padding: 0 4px 0 8px; }
  .chart-ctl > .chart-toolbar { padding: 0 12px 0 4px; gap: var(--s3); }

  /* A divider between the two groups, so one row still reads as two ideas. */
  .chart-ctl > .chart-toolbar { border-left: 1px solid var(--border-soft); }

  /* .cv was given a 35px tap target earlier; the fixed 32px bar it used
     to live in was clipping it. */
  .cv { padding: 9px 10px; }
  .tf { padding: 8px 9px; }
}

/* ===========================================================
   BINANCE PARITY PASS — measured, not remembered

   Every number below was read off binance.com/en/trade/BTC_USDT at
   1440x900 with getComputedStyle, then read off ours the same way and
   diffed. Where the two disagreed, theirs won. Only the deviations
   listed at the foot of this block were kept.

   The pattern behind most of it: our data surfaces were built one or
   two steps too small and too quiet — 10px uppercase tracked labels
   where Binance uses 12px sentence case, 18px book rows where they use
   20px, an 11px form label where theirs is 14px. Individually each
   reads as a taste difference. Together they are why the terminal
   looked like a compact imitation rather than the thing itself.
   =========================================================== */

/* ---- Nav: 0 24px, not 0 16px ---- */
.ex-nav { padding: 0 24px; }

/* ---- Pair header ----
   Their pair name and price are the SAME size, 20/500. Ours shouted the
   price at 26/600 and whispered the pair at 16/600, which inverted the
   relationship: on Binance the pair is the heading and the price is its
   value, both equal weight. */
.inst-pair { font-size: 20px; font-weight: 500; line-height: 24px; }
.istat-price .istat-v { font-size: 20px; font-weight: 500; line-height: 24px; }

/* Their stat labels are sentence case at 12px. Ours were 10px uppercase
   with .06em tracking, which is a dashboard idiom, not a terminal one. */
.istat-k {
  text-transform: none; letter-spacing: normal;
  font-size: 12px; line-height: 18px; color: var(--text-4);
}

/* ---- Panel titles: 14/500/21 ---- (token change in base.css) ---- */
.panel-title { letter-spacing: normal; }

/* ---- Column headers ----
   Same story as the stat labels, plus 7px of vertical padding that made
   our header 31px tall against their 20px. */
.watch-head, .book-head, .trades-head {
  text-transform: none; letter-spacing: normal;
  font-size: 12px; line-height: 18px;
  height: 20px; padding: 0 16px; align-items: center;
}
.book-head, .trades-head { color: var(--text-4); }
.trades-head { font-weight: 500; }
.watch-head  { color: var(--text-3); }

/* ---- Order book ----
   20px rows on 20px line-height, three equal columns, 16px gutters. */
.brow { line-height: 20px; padding: 0 16px; grid-template-columns: 1fr 1fr 1fr; }
.book-head { grid-template-columns: 1fr 1fr 1fr; }
/* Their Total column is full-strength text, not a dimmed third value.
   Dimming it made the book read as two columns of data and one of
   footnotes. */
.brow .cum { color: var(--text); }

/* ---- Trades: 18px rows, 16px gutters ---- */
.trow { height: 18px; line-height: 18px; padding: 0 16px; grid-template-columns: 1fr 1fr 1fr; }
.trades-head { grid-template-columns: 1fr 1fr 1fr; }

/* ---- Market list: 24px rows, regular weight name ---- */
.wrow { height: 24px; padding: 0 16px; }
.wrow-sym { font-weight: 400; }
.watch-head { grid-template-columns: 1fr 78px 58px; }

/* ---- Order form ----
   Their input is a 40px box with a 1px #434C5A rule and no fill. Ours
   was a 32px box with a darker fill and a dimmer rule, which is why the
   ticket looked cramped next to theirs. */
.oform .fld-in {
  height: 40px; background: transparent; border-color: var(--border-bright);
}
.fld-lbl { font-size: 14px; font-weight: 500; color: var(--text-4); }

/* Their order-type tabs carry no pill — the active one is simply white
   and the rest are #707A8A. */
.tt { background: transparent; color: var(--text-4); padding: 4px 0; }
.tt:hover { background: transparent; color: var(--text-2); }
.tt.active { background: transparent; color: var(--text); }
.type-tabs { gap: 16px; }

/* Their submit button is 36px on a 6px radius at 14/500. */
.oform .submit-btn { height: 36px; font-size: 14px; font-weight: 500; border-radius: 6px; }

/* Their Log In sits at 32px. */
.ex-nav .btn { height: 32px; }

/* ---- DELIBERATE DEVIATIONS ----
   1. Buy/Sell text colour. Binance prints WHITE on both #2EBD85 and
      #F6465D. White on their green is about 2.5:1, under the 4.5:1 floor
      for body text. We keep near-black on green (12.4:1) and white on
      red. Geometry matches; only the ink differs. One line to revert:
      .submit-btn.buy { color: #fff; }
   2. Book price column keeps text-align:left like theirs, but our price
      column is not padded to their exact 93px because our pair codes are
      longer than BTC/USDT.
   =========================================================== */

/* ---- Parity pass, second sweep: the leftovers the first one exposed ---- */

/* -0.028em was tuned for a 26px display price. At Binance's 20px it is
   just tight — theirs tracks normal, and the negative value was making
   our digits look condensed beside it. Same for the book's mid price,
   which they set at 20/500 rather than our 16/600. */
.book-mid-px, .istat-price .istat-v { letter-spacing: normal; }
.book-mid-px { font-size: 20px; font-weight: 500; line-height: 24px; }

/* Line-heights the first sweep left on the old scale. */
.istat-k    { line-height: 22px; }
.watch-head { line-height: 16px; }
.tt         { line-height: 16px; }

/* ===========================================================
   BINANCE PARITY — PHONE

   Measured off the same page at 375x812. Two of these are leaks from
   the desktop pass: Binance does NOT use one size across breakpoints
   for the pair block, and appending desktop rules at the end of the
   file took the phone with them.
   =========================================================== */
@media (max-width: 900px) {
  /* Their phone header is 64px on 16px gutters. Ours was 52px and had
     just inherited the 24px desktop gutter. */
  .ex-nav { height: 64px; padding: 0 16px; }

  /* Their phone price is BIGGER than their desktop price - 28/500/30
     against 20/500/24 - because the phone has one hero and the desktop
     has six panels competing. The desktop pass set 20px globally. */
  .istat-price .istat-v { font-size: 28px; font-weight: 500; line-height: 30px; }

  /* And their phone stat labels go the other way: 10/400/16 in #929AA5,
     where the desktop runs 12/400/22 in #707A8A. Same leak, opposite
     direction, which is why matching one breakpoint broke the other. */
  .istat-k { font-size: 10px; line-height: 16px; color: var(--text-3); }

  /* Panel tabs are 14/500 on a 24px line with 7px of lead, sitting at
     their natural widths on a 16px gap. Ours were 12px stretched edge
     to edge, which spaced them like a segmented control rather than a
     tab bar. */
  .m-tabs { gap: 16px; padding: 0 16px; }
  .m-tab  { flex: 0 0 auto; font-size: 14px; font-weight: 500;
            line-height: 24px; padding: 7px 0 3px; }

  /* Chart controls run 12/400, not 11/500. */
  .tf { font-size: 12px; font-weight: 400; }
  .cv { font-size: 12px; font-weight: 500; }
}

/* Binance uses three radii where we used one: 4px on nav buttons, 8px on
   form inputs, 6px on the submit button. */
.ex-nav .btn { border-radius: 4px; }

@media (max-width: 900px) {
  /* Adopting Binance's 16px gutter cost the left cluster 8px, and the
     wordmark was the flex child that gave: .logo-text wanted 76px, got
     70, and rendered "XLM AR...".

     Trimming padding only bought 2px, because a mobile rule had already
     reduced it - the fix is structural, not arithmetic. The wordmark is
     brand and must never be what shrinks, so pin it and let the nav
     links absorb the pressure instead; they already scroll. The padding
     trim then just means nothing has to shrink at 375px at all. */
  .logo         { flex: 0 0 auto; }
  .ex-nav-links { flex: 0 1 auto; min-width: 0; }
  .ex-nav-links a { padding: 6px 6px; }
  .ex-nav-l     { gap: 8px; }
}

/* ===========================================================
   PHONE ORDER BOOK — Binance's split view

   Their phone book is not their desktop book stacked. It is a split:
   bids on the left reading Amount then Price, asks on the right reading
   Price then Amount, the two price columns meeting in the middle, and
   no Total column at all. You see both sides at once instead of
   scrolling past the spread to reach the bids.

   Done entirely in CSS over the existing markup, so the row pool, the
   flash-on-change diffing, the depth bars and click-to-fill are all
   untouched. The only markup added is a wrapper that is display:contents
   on desktop and a phone-only header row.
   =========================================================== */
.book-body   { display: contents; }
.book-head-m { display: none; }

@media (max-width: 900px) {
  .book-head   { display: none; }
  /* Two 1fr pairs with a gutter between them, not four bare 1fr columns -
     otherwise the two middle labels butt together and read "PricePrice".
     The 24px gap is the same air the two price columns have between them
     in the rows below. */
  .book-head-m {
    display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; column-gap: 0;
    font-size: 12px; line-height: 18px; height: 20px; align-items: center;
    padding: 0 12px; color: var(--text-4);
    border-bottom: 1px solid var(--border-soft); flex-shrink: 0;
  }
  .book-head-m span:nth-child(2) { padding-right: 12px; }
  .book-head-m span:nth-child(3) { padding-left: 12px; }

  /* Grid, not wrapped flex. The first attempt gave the mid row a 100%
     basis and the two columns a basis of 0, expecting the mid row to
     wrap onto its own line - but a zero-basis item always "fits", so all
     three landed on one line, the mid row took all 375px and both
     columns rendered at zero width. An empty order book.
     Areas say the intent directly and cannot be squeezed out. */
  .book-body {
    display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: auto 1fr;
    grid-template-areas: "mid mid" "bids asks";
    flex: 1 1 auto; min-height: 0;
  }
  .book-mid  { grid-area: mid; padding: 6px 12px; }
  .book-bids { grid-area: bids; min-width: 0; min-height: 0; }
  .book-asks { grid-area: asks; min-width: 0; min-height: 0; }

  /* The ▲/▼ modes hide a side with an inline display:none. Let whichever
     side survives span the full width rather than leaving half the panel
     blank. */
  .book-body:has(.book-asks[style*="none"]) { grid-template-areas: "mid mid" "bids bids"; }
  .book-body:has(.book-bids[style*="none"]) { grid-template-areas: "mid mid" "asks asks"; }

  /* Best ask belongs at the top of its column. In the stacked book the
     asks run worst-to-best down the DOM so the best one lands against
     the spread; side by side that puts the best ask at the bottom. */
  .book-asks { flex-direction: column-reverse; justify-content: flex-end; }

  .book-bids .brow, .book-asks .brow { grid-template-columns: 1fr 1fr; padding: 0 12px; }
  .book-bids .cum, .book-asks .cum   { display: none; }

  /* Bids mirror: amount outside, price inside against the centre. */
  .book-bids .brow .qty { order: 1; text-align: left; }
  .book-bids .brow .px  { order: 2; text-align: right; }
  .book-asks .brow .px  { text-align: left; }
  .book-asks .brow .qty { text-align: right; }

  /* Depth grows from the centre outwards on both sides. */
  .book-bids .bbar { right: 0; left: auto; }
  .book-asks .bbar { left: 0; right: auto; }
}

@media (max-width: 900px) {
  /* Binance's phone tape runs 20px rows where their desktop runs 18px -
     measured pitch 40px at 2x. Ours had inherited the desktop 18px. */
  .trow { height: 20px; line-height: 20px; }
}

/* Between 900px and roughly 1100px the book column narrows enough that
   "Order book" wrapped to two lines and pushed the 40px panel head to 60,
   shoving every row below it down. A panel title is a label, not copy. */
.panel-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ===========================================================
   THE SLIDER, AS BINANCE DRAWS IT

   Measured: a 1px track in #434C5A inset 8px from the input, five
   step markers that are 8px squares rotated 45deg, filled #EAECEF once
   passed and #181A20 before, and no percentage text anywhere.

   Ours was a 3px rounded bar with "0% 25% 50% 75% 100%" spelled out
   underneath. The labels are clearer than five unexplained diamonds, so
   this trades a little legibility for the parity that was asked for -
   but the tick BUTTONS survive as the diamonds themselves, so every
   click target that existed before still exists. Nothing became
   decorative.
   =========================================================== */
.slider-wrap, .oform .slider-wrap {
  position: relative; height: 18px; padding: 0 5px;
  display: flex; align-items: center;
}
/* The visible track is drawn here, not on the input, so the input can
   keep a 10px grab height while the line stays a hairline. */
.slider-wrap::before {
  content: ''; position: absolute; left: 5px; right: 5px; top: 50%;
  height: 1px; background: var(--border-bright); transform: translateY(-50%);
}
.slider, .slider-sell {
  position: relative; z-index: 3; width: 100%; height: 10px;
  background: transparent; border-radius: 0;
}
.slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 10px; height: 10px; border-radius: 1px; border: none;
  background: var(--accent-fill); box-shadow: none;
  transform: rotate(45deg); cursor: pointer;
}
.slider::-moz-range-thumb {
  width: 10px; height: 10px; border-radius: 1px; border: none;
  background: var(--accent-fill); transform: rotate(45deg); cursor: pointer;
}
.slider-sell::-webkit-slider-thumb { background: var(--down); box-shadow: none; }
.slider-sell::-moz-range-thumb     { background: var(--down); }
.slider:hover::-webkit-slider-thumb  { transform: rotate(45deg) scale(1.15); }
.slider:active::-webkit-slider-thumb { transform: rotate(45deg) scale(1.05); }

.slider-ticks, .oform .slider-ticks {
  position: absolute; left: 5px; right: 5px; top: 50%;
  transform: translateY(-50%); margin: 0; z-index: 2;
  display: flex; justify-content: space-between; align-items: center;
}
/* The button is the hit area; the diamond is drawn on ::before. Styling
   the button itself as the mark meant any rule that grew the tap target
   grew the mark with it - which is exactly what the phone's 40x34
   minimum did, inflating these into 52px lozenges lying across the
   Amount and Total fields. */
.slider-ticks button {
  flex: 0 0 auto; width: 12px; height: 16px; padding: 0; border: 0;
  font-size: 0; line-height: 0; background: none; position: relative;
  display: flex; align-items: center; justify-content: center;
}
.slider-ticks button::before {
  content: ''; width: 8px; height: 8px; border-radius: 1px;
  background: var(--bg-surface); border: 1px solid var(--border-bright);
  transform: rotate(45deg); transition: background 120ms, border-color 120ms;
}
/* The generic :active rule scales every control; without re-stating the
   rotation the diamond would spin back to a square on press. */
.slider-ticks button:active { transform: none; }
.slider-ticks button:active::before { transform: rotate(45deg) scale(.9); }
.oform[data-side="buy"]  .slider-ticks button:hover::before { background: var(--up);   border-color: var(--up); }
.oform[data-side="sell"] .slider-ticks button:hover::before { background: var(--down); border-color: var(--down); }

/* ===========================================================
   FLAT CONTROLS — Binance uses no tinted pills

   The systemic difference left after the type pass. Binance's terminal
   marks an active control two ways and only two: a 3px underline for
   anything that navigates between views, and colour alone for anything
   that toggles. There is no filled chip anywhere in their trade page.
   We had a gold-tinted pill on the timeframes, the market categories
   and the blotter tabs.

   Adopting their SHAPE and keeping OUR colour: underline and colour-only
   states, still gold. That is the one place I did not take their value,
   because gold is the brand and white is not.

   Measured targets: timeframes 12/500/18 at h20 with no fill; category
   tabs 14/500/22 at h38 on a 3px bottom border; market search h32 on r8
   with a 1px #434C5A rule and a 14/500 input.
   =========================================================== */

/* ---- Timeframes: colour only, no chip ---- */
.tf {
  font-size: 12px; font-weight: 500; line-height: 18px;
  padding: 1px 2px; border-radius: 0; background: transparent;
}
.tf:hover  { background: transparent; color: var(--text-2); }
.tf.active { background: transparent; color: var(--accent); }
.tf-group  { gap: 12px; }

/* ---- Market categories: 3px underline, Binance's navigation mark ---- */
.wt {
  font-size: 14px; font-weight: 500; line-height: 22px;
  padding: 8px 0 5px; border-radius: 0; background: transparent;
  border-bottom: 3px solid transparent;
}
.wt:hover  { background: transparent; color: var(--text); }
.wt.active { background: transparent; color: var(--text); border-bottom-color: var(--accent); }
/* The tabs now carry their own 8px/5px lead and 3px rule, so the row's
   own 8px of vertical padding was stacking on top of it - 55px against
   Binance's 38, which pushed the first market row 30px down the panel. */
.watch-tabs { gap: 16px; padding: 0 16px; }

/* ---- Blotter tabs: same navigation mark ---- */
.bt {
  font-size: 14px; font-weight: 500; line-height: 22px;
  padding: 6px 0 4px; border-radius: 0; background: transparent;
  border-bottom: 3px solid transparent;
}
.bt:hover  { background: transparent; color: var(--text); }
.bt.active { background: transparent; color: var(--text); border-bottom-color: var(--accent); }
.blotter-tabs { gap: 16px; padding: 0 16px; }

/* ---- Market search: theirs is a 32px box on an 8px radius ---- */
.p-watch .panel-head input,
.watch-search {
  height: 32px; border-radius: var(--r-md); background: transparent;
  border: 1px solid var(--border-bright); padding: 0 12px;
  font-size: 14px; font-weight: 500;
}

/* ---- Small type left on the old scale ---- */
.book-group           { font-size: 12px; }
.form-meta,
.oform .form-meta,
.form-meta .num,
.oform .form-meta .num { font-size: 12px; line-height: 18px; }
.ex-ticker-track > *   { font-size: 12px; line-height: 18px; }

/* ---- Phone keeps the values Binance uses at 375px ---- */
@media (max-width: 900px) {
  .tf { font-size: 12px; font-weight: 400; padding: 8px 2px; }
  .wt, .bt { font-size: 13px; padding: 8px 0 5px; }
  .watch-tabs, .blotter-tabs { gap: 14px; padding-left: 12px; padding-right: 12px; }
}

/* At 375px "Open orders" wrapped to two lines, making a 60px tab inside
   a 40px row. These are labels on a scrolling rail, not copy - the rail
   already scrolls when they do not fit. */
.wt, .bt, .m-tab, .tt, .tf, .cv { white-space: nowrap; }

/* Same reasoning as the chart-control row: a rail that scrolls should
   say so. Without this, "Balances" ends mid-glyph at the right edge and
   reads as a layout bug rather than as more content. Masked pixels stay
   tappable - this only softens the paint. */
@media (max-width: 900px) {
  .blotter-tabs, .watch-tabs, .m-tabs, .type-tabs {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
            mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
  }
}

@media (max-width: 900px) {
  /* Give the mark a 36px tap target without giving the mark 36px. */
  .slider-ticks button { min-width: 0; min-height: 0; width: 12px; height: 16px; }
  .slider-ticks button::after {
    content: ''; position: absolute; left: 50%; top: 50%;
    width: 36px; height: 36px; transform: translate(-50%, -50%);
  }
  /* The old -8.5px centred a round thumb on a 3px rail. The rail moved to
     the wrapper and the input is 10px, so that offset now hangs the thumb
     above the line. */
  .slider::-webkit-slider-thumb {
    width: 14px; height: 14px; margin-top: 0; transform: rotate(45deg);
  }
  .slider::-moz-range-thumb { width: 14px; height: 14px; }
}

/* Their depth bar is the full up/down colour at exactly 0.1 opacity,
   anchored to the row's right edge. Ours sat at 0.13, which read very
   slightly heavier against the same background. */
.bbar { opacity: .10; }
.brow:hover .bbar { opacity: .16; }
/* A tint at .10 sits on a #181A20 row at 1.17:1 and on a #FFFFFF row at
   1.09:1 — the same alpha does not buy the same depth read on a white ground,
   and the depth bar IS the order book's only picture of size. Light gets the
   alpha that reproduces dark's step. */
:root[data-theme="light"] .bbar { opacity: .11; }
:root[data-theme="light"] .brow:hover .bbar { opacity: .17; }

/* The last of the tinted chips. Binance's chart-source tabs take the
   same 3px underline as every other view switcher, and its icon toggles
   mark themselves with colour alone. */
.cv {
  font-size: 14px; font-weight: 500; line-height: 22px;
  padding: 8px 0 5px; border-radius: 0; background: transparent;
  border-bottom: 3px solid transparent;
}
.cv:hover  { background: transparent; color: var(--text); }
.cv.active { background: transparent; color: var(--text); border-bottom-color: var(--accent); }
.chart-views { gap: 16px; padding: 0 12px; height: auto; }

.bm:hover,  .ctool:hover  { background: transparent; color: var(--text-2); }
.bm.active, .ctool.active { background: transparent; color: var(--accent); }

@media (max-width: 900px) {
  /* The merged chart-control row is 35px; a 38px tab would grow it. */
  .cv { font-size: 13px; padding: 6px 0 4px; }
  .chart-ctl > .chart-views { gap: 14px; padding: 0 4px 0 12px; }
}

/* Binance puts a crosshair cursor on their chart's hit layer, which is the
   only signal that the surface is interactive at all. Ours read as a picture. */
.chart-wrap canvas { cursor: crosshair; }
/* A horizontal drag pans the chart; a vertical one still scrolls the page.
   pan-y was already set for that reason - keep it, and let the pointer
   handlers take the horizontal axis. */
@media (max-width: 900px) {
  .chart-wrap, .chart-wrap canvas { touch-action: pan-y; }
}

/* Back to the newest bar. Hidden until the chart has actually been panned
   away from it, so it never sits on top of the candles for no reason. */
.chart-latest {
  position: absolute; right: 70px; bottom: 12px; z-index: 4;
  width: 28px; height: 28px; display: none;
  align-items: center; justify-content: center;
  border-radius: 50%; color: var(--text-2);
  background: var(--bg-elevated); border: 1px solid var(--border-bright);
  box-shadow: var(--shadow-md);
}
.chart-latest svg { width: 15px; height: 15px; }
.chart-latest:hover  { color: var(--text); border-color: var(--accent); }
.chart-latest:active { transform: scale(.94); }
.chart-wrap.is-panned .chart-latest { display: flex; }
@media (max-width: 900px) { .chart-latest { width: 34px; height: 34px; bottom: 10px; } }

/* ===========================================================
   ORDER BOOK — the rest of Binance's shape

   Measured at 1440x900. Their book column is 320px wide with a 16px
   gutter, and it stacks: title row, then a control row, then column
   headers, then rows. Ours had the title, the layout switch and the
   grouping all crammed into one 40px head, which is why the top of the
   panel read as busy where theirs reads as calm.
   =========================================================== */

/* 16px gutters on every panel head — theirs are 16 throughout, ours 12. */
.panel-head { padding: 0 16px; }

/* The control row: icons hard left, grouping hard right, 28px of height. */
.book-ctl {
  display: flex; align-items: center; justify-content: space-between;
  padding: 4px 16px 6px; flex-shrink: 0;
}
.book-modes { display: flex; gap: 12px; }

/* Their layout switch is three 16px icons that SAY what they do - a red
   half over a green half, all green, all red - instead of our ▤▲▼
   glyphs at 9px, which said nothing and sat at the wrong end of the row. */
.bm {
  width: 16px; height: 16px; padding: 0; border-radius: 0;
  display: flex; align-items: center; justify-content: center;
  opacity: .45; transition: opacity 120ms;
}
.bm svg { width: 16px; height: 16px; display: block; }
.bm .bmk-a rect { fill: var(--down); }
.bm .bmk-b rect { fill: var(--up); }
.bm:hover  { opacity: .75; background: transparent; }
.bm.active { opacity: 1;   background: transparent; }

/* Grouping is plain text plus a chevron on Binance — no field, no fill,
   no rule. Ours was a bordered input box, which made the loudest thing
   in the panel header a dropdown nobody looks at. */
.book-group {
  -webkit-appearance: none; appearance: none;
  height: 18px; padding: 0 16px 0 0; border: 0; border-radius: 0;
  background: transparent; color: var(--text);
  font-size: 12px; line-height: 18px; font-weight: 400;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23707A8A' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat: no-repeat; background-position: right center; background-size: 12px;
  text-align: right; text-align-last: right;
}
.book-group:hover { color: var(--text); }

/* Their last-price row is 36px on a 16px gutter: the price at 20/500 in
   the trade's colour, the fiat value beside it at 12/400 in #707A8A.
   Ours was 42px on 12px with an 11px fiat and a 10px spread. */
.book-mid { height: 36px; padding: 0 16px; gap: 8px; }
.book-mid-fiat, .book-spread { font-size: 12px; line-height: 18px; color: var(--text-4); }
.book-mid-dir { font-size: 14px; }

/* ===========================================================
   THEME TOGGLE

   One button, two icons, whichever is not the current theme. Sits in the
   nav's right cluster beside Connect, at the same 32px as the button so
   the row keeps one baseline.
   =========================================================== */
.theme-btn {
  width: 32px; height: 32px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 4px; color: var(--text-3);
  transition: color 120ms, background 120ms;
}
.theme-btn svg { width: 18px; height: 18px; display: block; }
.theme-btn:hover  { color: var(--text); background: var(--bg-elevated); }
.theme-btn:active { transform: scale(.94); }
/* Dark is the default, so the sun (the thing you would switch TO) shows. */
.theme-btn .ic-moon { display: none; }
:root[data-theme="light"] .theme-btn .ic-sun  { display: none; }
:root[data-theme="light"] .theme-btn .ic-moon { display: block; }

/* The grouping chevron is a data URI, so its stroke cannot inherit a token.
   #707A8A on white is a muddy near-match; this is the light --text-4. */
:root[data-theme="light"] .book-group {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239C9C9C' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
}

/* The mascot mark carries its own dark glow; on a white nav it needs none. */
:root[data-theme="light"] .logo-mark { filter: none; }

/* Adding a 32px theme button to a nav that had 6px of slack at 375px put
   the row 34px over: "Trade" was clipped mid-word. Trimming gaps and
   padding recovers 28 of it, so the label gives up the rest — "Connect"
   says the same thing in 45px less. Binance's own phone header shows a
   two-word CTA for exactly this reason. */
@media (max-width: 900px) {
  .cw-more { display: none; }
  .theme-btn { width: 28px; height: 28px; }
  .theme-btn svg { width: 17px; height: 17px; }
  .ex-nav { gap: 6px; }
  .ex-nav-r { gap: 6px; }
}

/* Wallet panel: the address is the thing people come here to copy, so it
   gets its own row rather than being squeezed into a label/value pair. */
.wm-addr {
  display: flex; align-items: center; gap: 8px;
  background: var(--bg-input); border: 1px solid var(--border);
  border-radius: var(--r-md); padding: 8px 10px; margin-bottom: 12px;
}
.wm-addr code {
  flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere;
  font-size: 12px; line-height: 17px; color: var(--text-2);
}
.wm-copy {
  flex: 0 0 auto; font-size: 12px; font-weight: 500; height: 26px;
  padding: 0 10px; border-radius: var(--r-sm);
  background: var(--bg-elevated); color: var(--text-2);
  border: 1px solid var(--border-bright); transition: all 120ms;
}
.wm-copy:hover  { color: var(--text); border-color: var(--accent); }
.wm-copy:active { transform: scale(.96); }

/* ===========================================================
   ORDER CONFIRMATION

   The decision being confirmed is "this much of that, for this much" —
   it should be the loudest thing in the dialog, not the fifth of eight
   identical rows. The side is carried by colour as well as by a word,
   the way the Buy and Sell buttons already do.
   =========================================================== */
.cf-head {
  border-radius: var(--r-md); padding: 14px 16px; margin-bottom: 14px;
  border: 1px solid transparent;
}
.cf-head.buy  { background: var(--up-dim);   border-color: rgba(46,189,133,.28); }
.cf-head.sell { background: var(--down-dim); border-color: rgba(246,70,93,.28); }
.cf-get {
  font-size: 22px; line-height: 28px; font-weight: 600;
  letter-spacing: -.01em; font-variant-numeric: tabular-nums;
}
.cf-head.buy  .cf-get { color: var(--up-text); }
.cf-head.sell .cf-get { color: var(--down-text); }
.cf-pay { font-size: 13px; line-height: 20px; color: var(--text-3); margin-top: 2px;
          font-variant-numeric: tabular-nums; }

.cf-go { height: 42px; font-size: 14px; font-weight: 500; border-radius: 6px; margin-top: 4px; }
.cf-go.buy  { background: var(--up);   color: var(--on-up); }
.cf-go.sell { background: var(--down); color: var(--on-down); }
.cf-go:hover  { filter: brightness(1.06); }
.cf-go:active { transform: scale(.99); }
.cf-go:disabled { filter: grayscale(.4) brightness(.8); cursor: not-allowed; }

/* ===========================================================
   CONNECT BOX

   Grouped by what the visitor can act on right now, because "which of
   these eight do I have" is the first question and the kit's flat list
   never answered it.
   =========================================================== */
.wp-load { color: var(--text-3); font-size: 13px; padding: 8px 0 4px; }

.wp-head {
  font-size: 11px; font-weight: 500; letter-spacing: .08em;
  text-transform: uppercase; color: var(--text-4);
  margin: 14px 0 6px;
}
.wp-head:first-child { margin-top: 2px; }

.wp-row {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 10px 12px; margin-bottom: 6px; text-align: left;
  background: var(--bg-input); border: 1px solid var(--border);
  border-radius: var(--r-md); transition: border-color 120ms, background 120ms;
}
.wp-row:hover  { border-color: var(--border-bright); background: var(--bg-elevated); }
.wp-row:active { transform: scale(.995); }
.wp-row:disabled { opacity: .7; cursor: default; }
.wp-row.is-busy { border-color: var(--accent); }

.wp-ic { flex: 0 0 auto; width: 26px; height: 26px; border-radius: 6px; object-fit: contain; }
.wp-ic-x { background: var(--bg-elevated); }

.wp-txt { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.wp-txt b {
  font-size: 14px; font-weight: 500; line-height: 20px; color: var(--text);
  display: flex; align-items: center; gap: 8px;
}
.wp-txt > span {
  font-size: 12px; line-height: 17px; color: var(--text-3);
  overflow: hidden; text-overflow: ellipsis;
}
.wp-tag {
  font-style: normal; font-size: 10px; font-weight: 500; line-height: 15px;
  padding: 0 6px; border-radius: 4px; white-space: nowrap;
  background: var(--accent-dim); color: var(--accent);
}

.wp-go, .wp-get {
  flex: 0 0 auto; font-size: 12px; font-weight: 500; line-height: 18px;
  padding: 3px 9px; border-radius: var(--r-sm); white-space: nowrap;
}
.wp-go  { color: var(--accent); background: var(--accent-dim); }
.wp-get { color: var(--text-4); border: 1px solid var(--border-bright); }

/* Secret key is not a peer of the wallets above it. */
.wp-adv { margin-top: 14px; border-top: 1px solid var(--border-soft); padding-top: 10px; }
.wp-adv summary {
  font-size: 12px; color: var(--text-4); cursor: pointer;
  list-style: none; padding: 4px 0; user-select: none;
}
.wp-adv summary::-webkit-details-marker { display: none; }
.wp-adv summary::before { content: '▸ '; }
.wp-adv[open] summary::before { content: '▾ '; }
.wp-adv summary:hover { color: var(--text-2); }

@media (max-width: 900px) {
  .wp-row { padding: 12px; }
  .wp-txt > span { font-size: 12px; }
}

/* ===========================================================
   PHONE TRADE SCREEN — the Binance app's shape

   Their app does not make you tab away from the ticket to see the book.
   Both are on one screen: ticket left, book right, Buy/Sell segmented at
   the head of the ticket, open orders beneath. That is the difference
   between a trading app and a web page with a form on it — you can see
   what you are about to hit while you type the size.

   The ticket is a fixed bottom sheet everywhere else on the phone, so
   here it has to be unwound back into the flow.
   =========================================================== */
.side-toggle { display: none; }

@media (max-width: 900px) {
  /* align-items:start made the book size to its own content, and
     levelsThatFit() measures the panel — so it had rows only if it already
     had rows, and drew none. The ticket sets the row height; the book
     stretches to match it and then has something real to divide. */
  .ex-grid.show-trade {
    display: grid; grid-template-columns: minmax(0, 1.06fr) minmax(0, .94fr);
    gap: 8px; padding: 8px; align-items: stretch;
  }

  .ex-grid.show-trade .p-form {
    display: flex !important; flex-direction: column;
    position: static; transform: none; z-index: auto;
    max-height: none; height: auto; overflow: visible;
    border: 1px solid var(--border); border-radius: var(--r-md);
    box-shadow: none; padding-bottom: 0;
    /* In the flow here, never a sheet: visible at once, not after the
       sheet's 220ms hide delay. */
    visibility: visible; transition: visibility 0s;
  }
  .ex-grid.show-trade .p-book {
    display: flex; height: 100%; min-height: 0;
    border: 1px solid var(--border); border-radius: var(--r-md); overflow: hidden;
  }

  /* Buy/Sell segmented, the app's own proportions. */
  .ex-grid.show-trade .side-toggle {
    display: grid; grid-template-columns: 1fr 1fr; gap: 0;
    padding: 8px 8px 4px; flex-shrink: 0;
  }
  .st {
    height: 34px; font-size: 14px; font-weight: 500;
    color: var(--text-3); background: var(--bg-input);
    border: 1px solid var(--border); transition: all 120ms;
  }
  .st:first-child { border-radius: var(--r-sm) 0 0 var(--r-sm); }
  .st:last-child  { border-radius: 0 var(--r-sm) var(--r-sm) 0; border-left: 0; }
  .st-buy.active  { background: var(--up);   color: var(--on-up);   border-color: var(--up); }
  .st-sell.active { background: var(--down); color: var(--on-down); border-color: var(--down); }

  /* One side at a time, chosen by the segment above. */
  .ex-grid.show-trade .p-form.side-buy  .oform[data-side="sell"],
  .ex-grid.show-trade .p-form.side-sell .oform[data-side="buy"] { display: none; }
  .ex-grid.show-trade .form-pair { grid-template-columns: 1fr; padding: 0 8px 8px; }
  .ex-grid.show-trade .type-tabs { padding: 0 8px 8px; gap: 14px; }
  .ex-grid.show-trade .tt-note { display: none; }   /* no room beside a half-width ticket */


  /* The fixed Buy/Sell bar is the sheet's affordance; the trade screen has
     its own submit button, so the bar would be a second one. */
  .ex-grid.show-trade ~ .m-actions,
  body:has(.ex-grid.show-trade) .m-actions { display: none; }
}

@media (max-width: 900px) {
  /* The desktop grid-template-areas were still routing .p-form to "form" and
     .p-book to "book" — two different ROWS — so the trade screen stacked them
     despite having two columns. Clear the areas and place them by column. */
  .ex-grid.show-trade { grid-template-areas: none; grid-template-rows: auto; }
  .ex-grid.show-trade .p-form { grid-area: auto; grid-column: 1; grid-row: 1; }
  .ex-grid.show-trade .p-book { grid-area: auto; grid-column: 2; grid-row: 1; }

  /* Avbl / Max / Fee run as three columns on a full-width sheet. In a
     half-width ticket that clips every one of them; the app stacks them as
     label-left, value-right rows and so should we. */
  .ex-grid.show-trade .oform .form-meta {
    flex-direction: column; gap: 3px; padding: 6px 0 2px;
  }
  .ex-grid.show-trade .oform .form-meta .row {
    flex-direction: row; align-items: baseline; justify-content: space-between; gap: 8px;
  }
  .ex-grid.show-trade .oform .form-meta .row > *:first-child { flex: 0 0 auto; }
  .ex-grid.show-trade .oform .form-meta .row > *:last-child  { flex: 1 1 auto; text-align: right; }
}

@media (max-width: 900px) {
  /* ---- The book, as the app's reference column ----
     The split view (bids left, asks right) is right when the book owns the
     whole screen. In a 165px column beside the ticket it has nowhere to go —
     the headers clipped to "PriceAmou" and each side got two levels. The app
     stacks it here: asks, last price, bids, two columns, no Total. This has
     to unwind the split rules rather than sit alongside them. */
  .ex-grid.show-trade .p-book .panel-head,
  .ex-grid.show-trade .p-book .book-ctl,
  .ex-grid.show-trade .book-head-m { display: none; }

  .ex-grid.show-trade .book-head {
    display: grid; grid-template-columns: max-content minmax(0, 1fr); column-gap: 6px;
    padding: 8px 8px 4px; height: auto; font-size: 11px;
  }
  .ex-grid.show-trade .book-head span:last-child { display: none; }

  .ex-grid.show-trade .book-body {
    display: flex; flex-direction: column; grid-template-areas: none;
    flex: 1 1 auto; min-height: 0;
  }
  .ex-grid.show-trade .book-asks,
  .ex-grid.show-trade .book-bids {
    grid-area: auto; flex: 1 1 0; min-height: 0;
    display: flex; flex-direction: column;
  }
  .ex-grid.show-trade .book-asks { justify-content: flex-end; }
  .ex-grid.show-trade .book-mid {
    grid-area: auto; height: auto; padding: 5px 8px; gap: 4px;
    flex: 0 0 auto; flex-wrap: wrap;
  }
  .ex-grid.show-trade .book-mid-px { font-size: 16px; line-height: 22px; }
  .ex-grid.show-trade .book-mid-fiat { display: none; }
  .ex-grid.show-trade .book-spread { font-size: 10px; }

  /* Two columns, price then amount, both sides the same way round. The price
     takes what it needs and only the amount may ellipsize: at 1fr/1fr a
     320px screen gave prices 61px of the 65 they need, and 0.0019120,
     0.0019116 and 0.0019111 all read "0.00191…" — then one was tapped. */
  .ex-grid.show-trade .brow {
    grid-template-columns: max-content minmax(0, 1fr); column-gap: 6px;
    padding: 0 8px; height: 22px; line-height: 22px;
  }
  .ex-grid.show-trade .brow .cum { display: none; }
  .ex-grid.show-trade .book-bids .brow .px,
  .ex-grid.show-trade .book-asks .brow .px  { order: 1; text-align: left; }
  .ex-grid.show-trade .book-bids .brow .qty,
  .ex-grid.show-trade .book-asks .brow .qty { order: 2; text-align: right; }
  .ex-grid.show-trade .book-bids .bbar,
  .ex-grid.show-trade .book-asks .bbar { right: 0; left: auto; }
}

/* Buy/sell share of the visible book. Hidden until there is depth to
   describe, so it never shows a meaningless 50/50 on an empty book. */
.book-ratio {
  display: none; align-items: center; gap: 6px;
  padding: 6px 16px 8px; flex-shrink: 0;
  font-size: 11px; line-height: 16px; font-variant-numeric: tabular-nums;
}
/* Their APP carries this under the book; their desktop terminal does not.
   Matching each, rather than picking one and applying it to both. */
.book-ratio.ready { display: none; }
@media (max-width: 900px) { .book-ratio.ready { display: flex; } }
.br-b { color: var(--up-text); flex: 0 0 auto; }
.br-a { color: var(--down-text); flex: 0 0 auto; }
.br-bar {
  flex: 1 1 auto; height: 4px; border-radius: 2px;
  background: var(--down); overflow: hidden; min-width: 24px;
}
.br-bar i { display: block; height: 100%; background: var(--up); transition: width 220ms ease-out; }
@media (max-width: 900px) {
  .ex-grid.show-trade .book-ratio { padding: 5px 8px 7px; font-size: 10px; gap: 4px; }
}

@media (max-width: 900px) {
  /* ---- The ticket's fields in a half-width column ----
     Label, value, BBO and the asset code all on one line need about 141px
     of content in a 148px box, so the input was squeezed to 34px and
     5.50431 rendered as "5.5" — a truncated price in the field you type a
     price into. It also pushed the labels to x=28 while the order-type
     tabs, the meta rows and the submit button all sit flush at 17, which
     is the indent that reads as misalignment.

     The app stacks them: label on its own line, value beneath at full
     width. Wrapping does it without touching the markup — the label takes
     a whole line, everything else follows on the next. */
  /* Grid, not flex-wrap. Wrapping put the input alone on the second line
     where it stretched to 57px, the box overflowed, and BBO and the asset
     code spilled onto the field below it. Areas place all four explicitly
     and nothing can stretch into a row it does not own. */
  /* The value gets a row to itself. Beside BBO and the asset code it had
     47px at 320px, so UAF's 0.0000038 showed as "0.000" — every digit that
     matters hidden, in the field the price is typed into. BBO and the code
     move up to the label line; a long code ellipsizes there, not the price. */
  .ex-grid.show-trade .oform .fld-in {
    display: grid; height: auto; min-height: 0;
    grid-template-columns: max-content minmax(0, 1fr) auto;
    grid-template-areas: "lbl bbo suf" "val val val";
    align-items: center; column-gap: 6px; row-gap: 0;
    padding: 5px 10px 6px;
  }
  .ex-grid.show-trade .oform .fld-lbl {
    grid-area: lbl; padding-right: 0;
    font-size: 11px; font-weight: 400; line-height: 15px; color: var(--text-4);
  }
  .ex-grid.show-trade .oform .fld-in input {
    grid-area: val; min-width: 0; width: 100%; height: auto;
    text-align: left; padding: 0;
    font-size: 15px; line-height: 21px;
  }
  .ex-grid.show-trade .oform .bbo-btn    { grid-area: bbo; margin-left: 0; justify-self: end; }
  .ex-grid.show-trade .oform .fld-suffix {
    grid-area: suf; padding-left: 0; font-size: 11px;
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
}

/* ===========================================================
   THE TICKET AS A VERTICAL PANEL

   In the right column it is one form at a time behind a Buy/Sell
   segment, not buy-beside-sell. The segment and the one-side-at-a-time
   rules already existed for the phone sheet; they become the desktop
   behaviour too rather than being duplicated.
   =========================================================== */
.side-toggle { display: grid; grid-template-columns: 1fr 1fr; gap: 0; padding: 10px 12px 6px; flex-shrink: 0; }
.st {
  height: 34px; font-size: 14px; font-weight: 500;
  color: var(--text-3); background: var(--bg-input);
  border: 1px solid var(--border); transition: all 120ms;
}
.st:first-child { border-radius: var(--r-sm) 0 0 var(--r-sm); }
.st:last-child  { border-radius: 0 var(--r-sm) var(--r-sm) 0; border-left: 0; }
.st:hover { color: var(--text); }
.st-buy.active  { background: var(--up);   color: var(--on-up);   border-color: var(--up); }
.st-sell.active { background: var(--down); color: var(--on-down); border-color: var(--down); }

/* The dividing line is 1240px, not 900px, and it is not a guess about screen
   size — it is the line the LAYOUT already draws. Above it the ticket is a
   wide row under the chart with room for two forms; at 1240 and below the grid
   above falls back to two columns and the ticket becomes a tall right-hand
   column, 340px wide. Keying these to 900px put both forms side by side in
   that 340px column at 150px each: a price field, an amount field and a
   percentage slider in 150px, which is not a ticket.

   So: two forms only where two forms fit, and everywhere else one at a time
   behind the segment — which is what the segment is for. */
@media (max-width: 1240px) {
  .p-form.side-buy  .oform[data-side="sell"],
  .p-form.side-sell .oform[data-side="buy"] { display: none; }
  .form-pair { grid-template-columns: 1fr; }
  .side-toggle { display: grid; }
}
@media (min-width: 1241px) {
  /* The segment selects a side; with both on screen there is no side to
     select, so it goes. */
  .side-toggle { display: none; }
  .form-pair { grid-template-columns: 1fr 1fr; }
}

/* The submit already carries margin-top:auto, so it sits at the foot of the
   column however tall it gets. */
.oform { height: 100%; }

/* The meta ran as three columns because the ticket used to be full-width
   with buy beside sell. In a ~340px column that clips all three —
   "2,500.0000 XLM …", "0.000000 US…", "0.00001 X…". The ticket is a single
   vertical form everywhere now, so these are rows: label left, value right. */
.oform .form-meta {
  flex-direction: column; gap: 5px; padding: 8px 0 2px;
  border-top: 1px solid var(--border-soft); margin-top: 10px;
}
/* Under the chart there are 334px to fit a whole ticket into, so the meta
   gives back what it can: tighter rows, no top margin of its own. */
@media (min-width: 1241px) {
  .oform .form-meta { gap: 4px; padding: 6px 0 0; margin-top: 6px; }
  .oform .submit-btn { margin-top: 8px; }
}
.oform .form-meta .row {
  flex-direction: row; align-items: baseline; justify-content: space-between;
  gap: 10px; min-width: 0;
}
.oform .form-meta .row > *:first-child { flex: 0 0 auto; }
.oform .form-meta .row > *:last-child  { flex: 1 1 auto; text-align: right; overflow: visible; }

/* This rhythm was measured for the ticket as a TALL column: a 524px panel
   holds about 280px of form, and pinning the submit to the foot put all 240px
   of the difference into one gap. Spending it on the form instead was right
   there — and wrong here. The panel under the chart is 334px, so the same
   spacing overflowed it by 50px and put both submit buttons outside their own
   panel, at y=1040 against a panel ending at 990.

   It applies wherever the ticket is still a tall column — the phone sheet and
   the two-column layout at 1240 and below. Under the chart the form goes back
   to its compact spacing with the button pinned to the foot. */
@media (max-width: 1240px) {
  .oform { gap: 12px; }
  .oform .fld-in + .fld-in { margin-top: 0; }
  .oform .slider-wrap { padding: 6px 5px 2px; }
  .oform .form-meta { gap: 8px; padding: 12px 0 4px; margin-top: 4px; }
  .oform .submit-btn { margin-top: 14px; }
}
@media (max-width: 880px) { .oform .submit-btn { margin-top: auto; } }

/* ===========================================================
   PHONE PAIR HEADER — two rows, not five

   It was taking 156px of an 812px screen: the pair on one line, the
   price on another, the fiat value on a third, then a 2x2 of stats over
   two more. Together with the nav and the tab rail that is 257px — 32%
   of the phone gone before a single candle or order row.

   Row one is the pair and the price. Row two is the stats in a line.
   =========================================================== */
@media (max-width: 900px) {
  .inst-bar {
    display: grid; height: auto;
    grid-template-columns: auto minmax(0, auto) 1fr;
    grid-template-areas: "fav pick price" "stats stats stats";
    align-items: center; gap: 2px 8px; padding: 6px 12px 7px;
  }
  .fav       { grid-area: fav; }
  .inst-pick { grid-area: pick; }

  .istat-price {
    grid-area: price; justify-self: end;
    flex-direction: row; align-items: baseline; gap: 6px;
  }
  .istat-price .istat-v { font-size: 22px; line-height: 28px; }
  .istat-price .istat-k { font-size: 11px; }

  /* One line, scrollable rather than wrapped: four figures at 375px is
     tight and wrapping would put the header back to three rows. */
  .inst-stats {
    grid-area: stats; display: flex; flex-wrap: nowrap;
    gap: 0 14px; margin: 0; overflow-x: auto; scrollbar-width: none;
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent);
            mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent);
  }
  .inst-stats::-webkit-scrollbar { display: none; }
  .istat {
    flex: 0 0 auto; flex-direction: row; align-items: baseline;
    gap: 5px; white-space: nowrap;
  }
  .istat-k { font-size: 10px; line-height: 16px; }
  .istat-v { font-size: 11px; }
}

@media (max-width: 900px) {
  /* Six tabs at 14px need 410px of a 375px rail, so "Orders" — the one that
     tells you whether your order is live — sat off the edge behind the fade.
     Dropping "Order " from "Order Book" brings the row to 369px and every
     tab on screen. "Book" is what a trader calls it anyway. */
  .mt-long { display: none; }

  /* Same overflow one level down: the blotter's four sub-tabs want 392px of
     the same 375px rail, so "Balances" — where you check what you actually
     hold — hung off the edge. Shortened rather than scrolled, because a rail
     of four is meant to be read at a glance, not swiped.

     "Order History" and "Trade History" cannot both collapse to "History",
     so the pair splits by what each actually lists: orders you placed, and
     the fills they became. "Fills" is the word for it. */
  .bt-w { display: none; }
  .bt-n { display: inline; }

  /* The document ended at 749px in an 812px viewport: the grid's rows were
     sized by their content, so 63px of screen went unused under the status
     bar. The trade screen's single row takes what is left instead, which
     hands the extra straight to the book as more levels. */
  /* flex-basis:auto sizes the grid from its content and then declines to
     grow, so the trade screen sat 63px short of the viewport. Basis 0 makes
     it take the whole remainder, and the row is 1fr so the extra reaches the
     book as more levels rather than as padding. */
  .ex-grid.show-trade {
    grid-template-rows: minmax(0, 1fr); min-height: 0; flex: 1 1 0;
  }
  .ex-body { height: 100dvh; }
  .ex-grid { min-height: 0; }
}

@media (max-width: 900px) {
  /* The status bar reserves 63px beneath itself for the fixed Buy/Sell bar.
     The trade screen has its own submit button and hides that bar, so the
     reservation was 63px of empty screen — the whole of the gap under the
     status line. Keep only the safe-area inset there. */
  body:has(.ex-grid.show-trade) .ex-ticker {
    margin-bottom: env(safe-area-inset-bottom, 0px);
    bottom: env(safe-area-inset-bottom, 0px);
  }
}

/* ===========================================================
   THE BOOK FLASHES IN ITS OWN COLOUR

   A changed row flashes by DIRECTION of change, which is right on the
   trades tape — a fill was a buy or a sell. In the order book it fights
   the encoding already there: green and red mean bid and ask, so an ask
   flashing green says "this is a bid" for 450ms, ten rows at a time on a
   book that updates every ledger. That reads as noise, not information.

   Asks flash red and bids flash green here. The flash still says "this
   level moved"; it just stops contradicting the column it is in. A
   little softer too — .16 against .22 — because ten rows at once on a
   phone is a lot of tint.
   =========================================================== */
@keyframes bookFlashAsk { 0% { background: rgba(246, 70, 93, .16); } 100% { background: transparent; } }
@keyframes bookFlashBid { 0% { background: rgba(46, 189, 133, .16); } 100% { background: transparent; } }
.book-asks .brow.flash-up, .book-asks .brow.flash-down { animation-name: bookFlashAsk; }
.book-bids .brow.flash-up, .book-bids .brow.flash-down { animation-name: bookFlashBid; }


/* ============================================================
   DEPTH

   Measured against Binance's own terminal, ours ran 850 elements with zero
   gradients, zero backdrop blur and three box-shadows — which is what "flat"
   means quantitatively. Theirs is not glassmorphic either (solid #181A20
   panels, two translucent surfaces in the whole page), so the answer is not
   frosted glass; it is a light source. Every rule below implies one, and
   none of them touches the book, the tape or the chart, where a gradient
   behind a price column would cost legibility for decoration.
   ============================================================ */

/* A panel lit from above: one hairline on the top edge, a real shadow
   underneath. --shadow-sm is already per-theme and two-layered, so the panel
   lifts on white without needing a ground dark enough to crowd the text. */
.panel {
  box-shadow: 0 0 0 1px var(--lift-edge) inset, var(--shadow-sm);
}

/* The segmented controls, tabs and chips — the things a hand goes to. */
.st, .cv, .wt, .cat, .xp-chip, .bbo-btn, .btn-ghost, .dt-item, .tab {
  transition: background-color var(--t-base), color var(--t-base),
              border-color var(--t-base), box-shadow var(--t-base);
}
.st.active, .cv.active, .wt.active, .cat.active, .xp-chip.active {
  box-shadow: 0 1px 0 var(--lift-edge) inset,
              0 1px 3px var(--lift-shadow);
}

/* Buy and sell are filled, so they take the same treatment as the brand
   button: a light source rather than a flat block. The label contrast is
   untouched — the gradient is transparent through the middle band where the
   text sits. */
.st-buy.active, .st-sell.active, .submit-btn, .cf-go {
  background-image: linear-gradient(180deg, var(--gloss-top) 0%,
                                            transparent 46%,
                                            var(--gloss-bottom) 100%);
  box-shadow: 0 1px 0 rgba(255,255,255,.18) inset,
              0 1px 2px var(--lift-shadow);
}
.submit-btn { transition: filter var(--t-base), transform var(--t-base); }
.submit-btn:hover:not(:disabled) { filter: brightness(1.06); }
.submit-btn:active:not(:disabled) { transform: translateY(1px); }

/* Inputs read as recessed rather than drawn: the highlight goes on the
   BOTTOM edge, which is what a sunken surface catches. */
.fld-in, .winput, .watch-search, .sc-search, .xp-search, .sw-search {
  box-shadow: 0 1px 0 var(--lift-edge) inset;
}

/* The nav is the one place a little glass is honest, because it actually
   sits over scrolling content. --nav-glass is already translucent per theme;
   it just never had the blur that makes translucency mean anything. */
.ex-nav { backdrop-filter: blur(10px) saturate(1.4); -webkit-backdrop-filter: blur(10px) saturate(1.4); }

@media (prefers-reduced-motion: reduce) {
  .btn-primary, .submit-btn { transition: none; }
  .btn-primary:active, .submit-btn:active { transform: none; }
}

/* ===========================================================
   KEYBOARD FOCUS ON THE TERMINAL'S OWN CONTROLS

   `.slider` and `.book-group` set outline:none after base.css's ring, and
   the halo left behind is #0C0C0C on a #181818 panel (#FFF on #FFF in
   light) — no visible focus at all on the control that sizes an order.
   Two classes outrank the one-class outline:none wherever this sits.

   The market rows, the pair menu and the book levels are buttons now
   (exchange.js) and sit inside overflow containers that clip a ring drawn
   outside the box, so theirs is drawn inside it, halo within.
   =========================================================== */
.slider:focus-visible,
.book-group:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}
.slider:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 2px var(--focus-halo), 0 0 0 4px var(--focus-ring); }
.slider:focus-visible::-moz-range-thumb     { box-shadow: 0 0 0 2px var(--focus-halo), 0 0 0 4px var(--focus-ring); }
.wrow:focus-visible,
.im-row:focus-visible,
.brow:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: -2px;
  box-shadow: inset 0 0 0 4px var(--focus-halo);
  border-radius: var(--r-sm);
}

/* Order outcome inside the confirmation, beside the button it concerns.
   Empty, it takes no room; it stays rendered so the live region exists
   before anything is written to it. */
.cf-msg { margin: 0; }
.modal-body > .cf-msg:empty { margin-top: -12px; }   /* cancels the body's 12px gap */
.cf-msg.err { color: var(--down-text); }


/* ===========================================================
   THE CONFIRMATION SAYS WHAT IT COSTS

   Audited live at 1440px on a real account: the dialog read
   "1.00000 USDC for 5.70866 XLM", a limit price, the pair and the order
   type — and nothing about the network fee, nothing about the total XLM
   the signature would actually move, nothing about what the account has
   left afterwards. The headline is the decision; this block is the bill
   under it, and it is the last chance to see the bill before a wallet
   prompt appears.

   MARKUP this expects (exchange.js / swap.js write it; these rules only
   dress it). It sits directly after .cf-head and before the .crow list:

     <div class="cf-cost">
       <div class="cf-cost-row"><span>Amount</span>
         <b class="num">5.70866 XLM</b></div>
       <div class="cf-cost-row"><span>Network fee</span>
         <b class="num">0.00001 XLM</b></div>
       <div class="cf-cost-row cf-cost-total"><span>Total to pay</span>
         <b class="num">5.70867 XLM</b></div>
       <div class="cf-cost-row cf-cost-after"><span>Spendable after</span>
         <b class="num">160.50103 XLM</b></div>
     </div>

   A figure that is NOT known takes <b class="num cf-unknown">Not priced</b>
   in place of the number. Never a 0 standing in for an unmeasured value —
   that is the one failure mode a cost breakdown can have that is worse
   than not having one.
   =========================================================== */
.cf-cost {
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 4px 12px 6px;
  /* The same recessed hairline every field in this dialog carries, so the
     block reads as the same material as the inputs rather than as a card
     floating on the modal. It is also the only thing that defines the box
     in light, where #FAFAFA on #F5F5F5 is 1.03:1 of fill difference. */
  box-shadow: 0 1px 0 var(--lift-edge) inset;
}
.cf-cost-row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s3); padding: 6px 0;
  font-size: var(--fs-sm); line-height: 18px;
}
.cf-cost-row > span { color: var(--text-3); min-width: 0; }
.cf-cost-row > b {
  font-weight: var(--fw-label); color: var(--text-2);
  font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1;
  white-space: nowrap;
}

/* The total is the figure the signature moves, so it stops being one more
   row: a rule above it, the label in ink, the value one step up the type
   scale. Everything above it is an input to this number. --border-bright
   rather than --border-soft — this line is structural, not decorative. */
.cf-cost-total { border-top: 1px solid var(--border-bright); margin-top: 2px; padding-top: 9px; }
.cf-cost-total > span { color: var(--text-2); font-weight: var(--fw-label); }
.cf-cost-total > b { color: var(--text); font-weight: var(--fw-head); font-size: var(--fs-md); }

/* "What is left afterwards" is a consequence, not a cost. It belongs under
   the rule with the total, but at the quieter weight — two emphasised rows
   in a four-row block is no emphasis at all. */
.cf-cost-after { padding-top: 0; }
.cf-cost-after > span,
.cf-cost-after > b { color: var(--text-3); font-weight: var(--fw-body); }

/* The row that is a warning: the order would leave the account under its
   own reserve, or the fee is not the usual base fee. */
.cf-cost-row.is-warn > span,
.cf-cost-row.is-warn > b { color: var(--warn); }

/* A value the page does not know must not LOOK like a figure — no tabular
   column, no label weight. It takes the LABEL's tier rather than the
   dimmest one: this is a statement about money the reader is a click from
   signing, and --text-4 measures 4.23:1 on a light dialog. Written twice
   because .cf-cost-row > b would otherwise outrank a single-class rule. */
.cf-unknown,
.cf-cost-row > b.cf-unknown {
  color: var(--text-3); font-weight: var(--fw-body);
  font-variant-numeric: normal; font-feature-settings: normal;
}

/* .cf-head carries a 14px bottom margin from when it was the only block
   above a flat list of rows. In a flex body with a 12px gap that renders as
   26px of air between the decision and the bill for it, against 12px
   everywhere else in the same dialog. 2 + 12 restores the 14 the author
   meant. */
.modal-body > .cf-head { margin-bottom: 2px; }


/* ===========================================================
   LEAVING IS AS OBVIOUS AS CONTINUING

   The dialog's only exit was the × in the corner. Before a signature
   exists there is nothing to lose by leaving, and the control that says so
   belongs beside the one that commits: same row, same height, a third of
   the width and none of the colour.

   MARKUP:
     <div class="cf-actions">
       <button type="button" class="btn cf-cancel" id="cCancel">Cancel</button>
       <button type="button" class="btn cf-go buy" id="cGo">Sign and submit</button>
     </div>

   Drop btn-block from #cGo inside this row — the grid sizes both, and
   width:100% on a grid item plus a gap overflows the track.

   Once a transaction has been broadcast, Cancel takes [disabled]: there is
   nothing left to cancel and the honest action is Check. The × stays, so
   the dialog is never a trap.
   =========================================================== */
.cf-actions {
  display: grid; grid-template-columns: minmax(88px, .42fr) minmax(0, 1fr);
  gap: var(--s2); align-items: stretch; margin-top: 4px;
}
/* .cf-go sets its own margin-top for the stacked layout; in the row it
   would push the primary 4px below its twin. */
.cf-actions > .cf-go { margin-top: 0; width: auto; }

.cf-cancel {
  height: 42px; border-radius: 6px;
  font-size: var(--fs-md); font-weight: var(--fw-label);
  background: transparent; color: var(--text-2);
  border: 1px solid var(--border-bright);
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.cf-cancel:hover { background: var(--bg-hover); color: var(--text); border-color: var(--text-4); }
.cf-cancel:active { transform: scale(.99); }
/* base's .btn:disabled fills the box, which on a HOLLOW secondary reads
   louder disabled than it did enabled. Keep it hollow; take the ink down. */
.cf-cancel:disabled {
  background: transparent; color: var(--text-4);
  border-color: var(--border); transform: none;
}

/* .cf-go.buy sets --up at (0,2,0) and base's .btn:disabled is also (0,2,0)
   but loads first — so a disabled sign button kept its full green fill,
   and the gloss gradient and lift shadow from the DEPTH pass survived too.
   It looked pressable the whole time it was not. swap.css already patched
   its own .cf-go.swap variant; this is the same fix for the terminal's. */
.cf-go:disabled,
.cf-go.buy:disabled,
.cf-go.sell:disabled {
  background-color: var(--bg-hover); background-image: none;
  color: var(--text-4); box-shadow: none; filter: none;
  /* --bg-hover and --bg-elevated are the SAME #F5F5F5 in light, and the
     dialog is --bg-elevated — so a disabled fill alone left the primary
     button with no fill difference, no shadow and no edge: "Waiting for
     your wallet…" floated as loose grey text with no button around it, in
     the state the dialog spends most of its life in. The border is what
     holds the shape where the fill cannot. */
  border: 1px solid var(--border-bright);
}


/* ===========================================================
   IS THIS QUOTE STILL THE QUOTE?

   The dialog can sit open while the market moves — it survives a whole
   ledger close and says nothing. The figures in it are what gets signed.

   MARKUP — one line, directly under .cf-cost:
     <p class="cf-quote" id="cfQuote">Quoted <span class="num">6</span>s ago</p>
   and once the JS considers the quote past its window:
     <p class="cf-quote is-stale" id="cfQuote">Quoted <span class="num">48</span>s ago
       <button type="button" class="cf-requote" id="cfRequote">Re-quote</button></p>

   Quiet by design: fresh is grey text and nothing else — no dot, no icon,
   no colour. The marker and the warn tint arrive only when the quote has
   actually gone stale, so the state that matters is the only one that
   costs the reader any attention.
   =========================================================== */
.cf-quote {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--fs-xs); line-height: 16px; color: var(--text-4);
  font-variant-numeric: tabular-nums;
}
.cf-quote.is-stale { color: var(--warn); }
.cf-quote.is-stale::before {
  content: ''; flex: 0 0 auto;
  width: 5px; height: 5px; border-radius: 50%; background: var(--warn);
}
.cf-requote {
  margin-left: auto; flex: 0 0 auto;
  font-size: var(--fs-xs); line-height: 16px; font-weight: var(--fw-label);
  color: var(--accent); background: var(--accent-dim);
  border: 1px solid var(--accent-line); border-radius: var(--r-sm);
  padding: 2px 8px; transition: background var(--t-fast);
}
.cf-requote:hover { background: var(--accent-glow); }

/* The figures themselves are NOT dimmed when the quote ages. A number a
   person is about to sign has to stay at full legibility; the staleness
   goes on the block's edge and on the line that names the age. */
.cf-cost.is-stale { border-color: var(--warn-line); }


/* ===========================================================
   A BUTTON THAT CANNOT BUY MUST NOT LOOK LIKE ONE

   Watch-only session, audited live at 1440px: Buy and Sell rendered in
   full green and red, indistinguishable from a signing session. Pressing
   one raised a toast and opened the connect dialog — the right
   destination, learned only after committing to the click.

   This state is deliberately NOT [disabled]: the press is the route out of
   watch-only, and disabling it would remove the only affordance the state
   has. So the control stays live and gives up the colour instead — a
   neutral outlined button that reads as "this does something else".

   MARKUP — exchange.js adds the class and rewrites the label; it must not
   add the disabled attribute:
     <button class="btn btn-block submit-btn buy cant-sign" id="buyBtn"
             aria-describedby="ticketNote">Connect a wallet to buy</button>

   (`cant-sign`, not `is-locked`: swap.css already owns .modal-host.is-locked
   and two different meanings for one state word is how the next reader gets
   it wrong.)
   =========================================================== */
.submit-btn.cant-sign,
.submit-btn.cant-sign.buy,
.submit-btn.cant-sign.sell {
  background-color: var(--bg-elevated);
  background-image: none;           /* kills the DEPTH pass's gloss gradient */
  color: var(--text-2);
  border: 1px solid var(--border-bright);
  box-shadow: none;                 /* and its lift shadow */
  font-weight: var(--fw-label);
}
/* .submit-btn:hover brightens and lifts unconditionally; a button that is
   not a buy button should not behave like one under the cursor either. */
.submit-btn.cant-sign:hover {
  background-color: var(--bg-hover); color: var(--text);
  border-color: var(--accent);
  filter: none; transform: none;
}
.submit-btn.cant-sign:active { transform: translateY(1px); }

/* The note beside Limit/Market is 10px of --text-3 — right for "simulated ·
   live book", far too quiet to be the only thing between a watch-only
   session and a click on a green button. When it carries a LIMITATION
   rather than a description it becomes a chip.

   MARKUP: <span class="tt-note is-warn">watch-only · no signing</span>
   — exchange.js should drop the `dim` class when it adds is-warn. */
.tt-note.is-warn {
  font-size: var(--fs-xs); line-height: 16px; font-weight: var(--fw-label);
  color: var(--warn); background: var(--warn-dim);
  border: 1px solid var(--warn-line); border-radius: 999px;
  padding: 1px 8px;
}


/* ===========================================================
   DIALOG / TOAST AUDIT AT 1440px

   Everything below is an existing rule that read as unfinished on the
   live site, not a new feature.
   =========================================================== */

/* .toast-host is shrink-to-fit inside a fixed box, so a toast is as wide as
   its longest line and nothing ever capped it. The 20-second one — "A
   transaction is still awaiting validation. Do not resubmit — check Open
   orders or your wallet before placing it again." — is 130 characters and
   rendered as a single ~810px ribbon across the foot of a 1440px screen.
   The most important message the app can show was the one that looked
   least like a message. */
.toast {
  max-width: min(360px, calc(100vw - 36px));
  line-height: 17px;
  /* Several call sites lead with a coloured ● bullet. Centred, that bullet
     floats to the middle of a three-line message; top-aligned it sits on
     the first line where it belongs. */
  align-items: flex-start;
}
/* .toast.ok / .toast.err widen the left border from 1px to 3px, which slid
   their text 2px right of an unclassed toast stacked above them. */
.toast.ok, .toast.err { padding-left: 13px; }

/* The dialog title inherited the 13px body size while a panel title behind
   the scrim is 14px — the most important heading on screen sat a step below
   the chrome it was covering. */
.modal-head { font-size: var(--fs-md); line-height: var(--lh-title); }

/* A 20px glyph in a 12px-wide box: the close control of a modal dialog had
   a smaller hit area than an order-book row, and no hover shape at all. */
.modal-x {
  width: 28px; height: 28px; margin-right: -4px; padding: 0;
  display: grid; place-items: center; border-radius: var(--r-sm);
  transition: color var(--t-fast), background var(--t-fast);
}
/* --bg-surface, not --bg-hover: in light --bg-hover is byte-identical to
   the --bg-elevated head it sits on, so the chip would not exist there. */
.modal-x:hover { background: var(--bg-surface); }

/* .crow:last-of-type means "the last DIV among its siblings", and in the
   confirm dialog the last div is a .cstep — so the rule matched nothing and
   the final figure row kept a divider hanging in 12px of air above the step
   list. This says what it meant: the last crow of a run. swap.css's
   `#cfFig .crow:last-of-type` override carries an id and still outranks it. */
.crow:not(:has(~ .crow)) { border-bottom: none; }

/* .modal-body is a 12px-gap flex column, so adjacent .crow rows sat 12px
   apart ON TOP of their own 7px padding — a 1px divider with 19px of air on
   each side, eight times down the order confirmation and six down the
   wallet panel. They are a table, not eight floating blocks: cancel the gap
   between neighbours and let the divider do the separating. (A display:none
   row such as #cfBoundRow is not a flex item, contributes no gap, and is
   still matched by `+` — so the arithmetic holds either way.)
   Same idiom as .cf-msg:empty above. */
.modal-body > .crow + .crow { margin-top: -12px; }
/* Steps are a sequence; 12px between them broke the run into three cards. */
.modal-body > .cstep + .cstep { margin-top: -6px; }
/* The address block's own 12px margin plus the body gap was 24px. */
.modal-body > .wm-addr { margin-bottom: 0; }

/* Pending, active and done were three states with two appearances: a step
   not yet reached rendered at full --text, exactly like the one being
   worked on. And .cstep.done painted its LABEL with --up, which is the fill
   token; --up-text is the ink version, per the split in base.css. */
.cstep { color: var(--text-2); }
.cstep.active { color: var(--text); }
.cstep.done { color: var(--up-text); }
