/* ============================================================================
   ZOLTRAK — shell chrome (sidebar, topbar, view frame)
   Loaded AFTER knull.css. Owns ONLY the app frame; every component inside a
   view comes from knull.css. Module-specific CSS lives in zoltrak.css.
   ============================================================================ */

/* ── THE LIVE GRAY RAMP ──────────────────────────────────────────────────
   knull-store ships the first-pass ramp (white headings/numbers). On
   2026-07-20 Landon toned the neutrals down across Nexus AND Vortex —
   "every plain white words/numbers → the dark gray we have" — so that gold
   and the neon green/red are the ONLY bright things on screen. Zoltrak must
   match its siblings, so we apply the live ramp here. Do NOT restore white.  */
.kn-app {
  /* The warm marble slab is gone — the app is black now, with the violet
     coming from the ::after light wash knull.css already paints. Setting the
     token to none removes the photo without touching knull.css. */
  --kn-marble: none;
  /* Measured against the black ground, the bottom two steps of the ramp were
     failing legibility outright: --kn-dim sat at 3.9:1 and --kn-faint at
     2.85:1, well under the 4.5:1 floor. Those two carry the micro-labels —
     "cash", "24h", "MC", every column header — so the app's smallest text was
     also its lowest contrast, which does not read as subtle, it reads as out
     of focus. Every step now clears 4.5:1 and the four-step ladder is intact. */
  /* Their text is near-white (var(--z-num)) over a muted gray. Landon's standing
     rule across Nexus and Vortex is NO white words, so this lands between the
     two: a light cool gray that is clearly brighter and more legible than the
     old warm ramp, without becoming white. The hue also turns cool to match
     the new ground — a warm gray on a blue-black reads as dirty. */
  /* The reference uses WHITE for every headline number and a cool gray for
     the uppercase labels under them. That is the opposite of the no-white
     rule from Nexus and Vortex, but it is what this palette is, and it was
     asked for on the dot. */
  /* The greys in the reference lean VIOLET, not blue. var(--z-muted) is Tailwind's
     blue-grey and it fought the accent; these carry the same hue as the
     purple, which is why the sidebar labels there sit quietly instead of
     reading as a second colour. */
  --kn-white: var(--z-num);
  --kn-text:  var(--z-text);
  --kn-muted: var(--z-muted);
  --kn-dim:   var(--z-dim);
  --kn-faint: var(--z-faint);
  --kn-axis:  var(--z-faint);
  --kn-line:  var(--z-line);

  /* One violet, not two. The store ships --kn-gold var(--z-accent-fill) for active states
     and --kn-gold-mid var(--z-accent) for link text, and 41 rules across the modules
     colour accent TEXT with the second one. Six percent apart is too close to
     read as a deliberate hierarchy and just close enough to read as a mistake
     when an active pill sits beside a link. The literal var(--z-accent) still appears
     as a mid stop inside gradients and ring edges, which is what it is for. */
  /* THE ACCENT. Their palette runs one saturated colour against the
     near-black; ours is violet where theirs is gold, in the darker tone the
     Zoltrak wordmark uses rather than the bright var(--z-accent-fill). Deep enough to read
     as the brand, still clearing 4.5:1 on the new ground.

     --kn-gold-hi stays brighter on purpose: it is the one step reserved for
     the thing you are actually meant to look at (an active tab, a live dot),
     and if every accent is the same weight nothing is emphasised. */
  /* THE MISS. var(--z-accent) is Tailwind purple-500 and leans magenta; the
     reference's violet is blue-leaning — logo, price pill, active nav and
     every link are var(--z-accent) / var(--z-accent-fill). One notch around the wheel, and it is
     the whole difference between "pink-ish" and the colour in the picture. */
  /* Hue 263-273. Missed in BOTH directions before: var(--z-accent) (hue 280) was too
     magenta, var(--z-accent) (hue 258) too blue, and both were far less saturated
     than the real thing. */
  --kn-gold:      var(--z-accent);
  --kn-gold-hi:   var(--z-accent-fill);
  --kn-gold-line: var(--z-accent-fill);
  --kn-gold-mid:  var(--z-accent);
  --kn-gold-deep: var(--z-accent-deep);

  /* the subtle black fade: accent surfaces sink into the ground rather than
     sitting on it as flat blocks */

}
/* accent fills take the fade, not a flat violet */
.kn-app .kn-btn.primary, .kn-app .z-tab.on::after {
  background: var(--z-accent-fade);
}

/* ── RAMP REPAIR ─────────────────────────────────────────────────────────
   Remapping --kn-white is not enough: knull.css hardcodes near-white and pure
   -white LITERALS inside its most-used components, and a token override cannot
   reach them. Every literal below is repaired to the live ramp so that gold and
   the neon green/red stay the only bright things on screen. Line numbers are
   knull.css's, kept so this block can be re-checked when the store updates.   */
.kn-app .kn-nav:hover,
.kn-app .kn-nav:hover .ico   { color: var(--kn-text); }        /* 174,175 var(--z-text) */
.kn-app .kn-nav.on           { color:var(--kn-muted); }        /* 180 var(--z-accent-fill) */
.kn-app .kn-btn              { color: var(--kn-text); }        /* 189 var(--z-text) */
.kn-app .kn-pill:hover       { color: var(--kn-text); }        /* 213 var(--z-text) */
.kn-app .kn-pill.on          { color:var(--kn-muted); }        /* 215 var(--z-accent-fill) */
.kn-app .kn-input-wrap input { color: var(--kn-text); }        /* 230 var(--z-text) */
.kn-app .kn-tip              { color: var(--kn-text); }        /* 308 var(--z-text) */
.kn-app .kn-tip b            { color:var(--kn-muted);       /* 311 #fff  ← was the loudest leak */
                               font-variant-numeric: tabular-nums; }
.kn-app .kn-toast            { color: var(--kn-text); }        /* 315 var(--z-text) */
.kn-app .kn-menu .mi         { color: var(--kn-text); }        /* 333 var(--z-text) */
.kn-app .kn-menu .mi:hover   { color:var(--kn-muted); }        /* 334 #fff */
.kn-app ::selection          { background: rgba(167,139,250,.25); color:var(--kn-muted); }
/* the measured type classes all resolve --kn-white, which the ramp already
   remaps — listed here only so it is obvious they were considered, not missed:
   .kn-h1 .kn-big .kn-kpi .kn-stat .kn-title .kn-table .sym .kn-datechip span */

/* big money numbers take the gold gradient instead of a flat fill — same
   treatment as Nexus's Portfolio Value and Vortex's account balance */
/* The reference has NO gradient text. Every headline number — balance, win
   rate, profit factor, open positions — is flat white, and the only coloured
   figures are the P&L ones, which are green or red. A violet gradient on a
   count reads as decoration; white reads as data. The class is kept so the
   markup does not have to change in a dozen modules. */
.kn-gold-grad {
  background: none;
  -webkit-background-clip: border-box; background-clip: border-box;
  -webkit-text-fill-color: var(--kn-white); color: var(--kn-white);
  filter: none;
}
/* neon values must keep their colour when they land on a gradient number */
.kn-pos.kn-gold-grad, .kn-neg.kn-gold-grad {
  background: none; -webkit-text-fill-color: currentColor;
}

.num, .kn-num { font-variant-numeric: tabular-nums; }

/* the violet ground: two soft pools of light over black, no texture */
/* The ground is black. The only colour in it is a dark violet bloom pulled
   into the corners — anchored AT the corners rather than floating near the
   middle, so the centre of the screen stays properly black and the glow reads
   as light leaking in from the edges. Two strong corners, two faint ones, so
   it is lit rather than symmetrical. */
.kn-app::after {
  background:
    var(--z-bloom) !important;
}

/* ── EMBERS ON THE GROUND ────────────────────────────────────────────────
   A fixed, full-viewport layer that KnullFX seeds with drifting embers at
   boot. It sits at z-index 0 — above the colour wash, below every panel —
   so the embers rise through the gaps between boxes rather than over the
   top of the numbers. pointer-events:none keeps it out of the way of every
   click, and .kn-app>* would otherwise force it up to z-index 1. */
.kn-app > .z-embers {
  position: fixed; inset: 0; z-index: 0;
  pointer-events: none; overflow: hidden;
}
/* Reduced motion gets the texture without the movement: the embers still sit
   in the ground, scattered and dimmed, they just stop drifting. Hiding them
   outright would mean anyone with that OS setting sees a plain black page. */
@media (prefers-reduced-motion: reduce) {
  .kn-app > .z-embers .kn-ember { animation: none; opacity: .38; }
}

/* The Knull mark is already violet, so no hue rotation — rotating it would
   swing it off to green. Only a touch of glow to sit it in the dark. */
.z-brand-mark { filter: drop-shadow(0 0 22px rgba(124,58,237,.5)); }

/* ============================== FRAME ==================================== */
/* The shell owns the viewport; the VIEW scrolls, not the window. Anything
   that lets body scroll brings back the 44px dead strip under the ticker --
   see .z-view below. */
html, body { height: 100%; }
body.kn-app { overflow: hidden; }
body.kn-app {
  margin: 0; min-height: 100dvh;
  display: flex; flex-direction: column;
  overflow-x: hidden;
  font-family: var(--kn-font);
  color: var(--kn-text);
  -webkit-font-smoothing: antialiased;
}
/* knull.css already sets `.kn-app>*{position:relative;z-index:1}` so the marble
   slab sits behind the app. Modules that append a FIXED overlay straight to
   <body> (drawers, scrims, toasts) inherit that z-index:1 and get trapped under
   the sidebar — so they are explicitly lifted back out of the stack here. Any
   new overlay should carry .z-overlay or data-z-overlay to opt in.            */
body.kn-app > .z-overlay,
body.kn-app > [data-z-overlay],
body.kn-app > .kn-toast,
body.kn-app > .z-cal-scrim,
body.kn-app > .z-cal-drawer,
body.kn-app > .z-hm-scrim,
body.kn-app > .z-hm-modal,
body.kn-app > .z-jr-scrim,
body.kn-app > .z-jr-drawer { position: fixed; z-index: 60; }
/* 90 put the toast UNDER the command palette (z-index 95), which meant the one
   message that has to reach you during a paste -- "could not read the clipboard,
   press Ctrl+V" -- was painted behind the palette's own dark overlay. A toast is
   the top layer by definition; nothing in this app should ever cover it. */
body.kn-app > .kn-toast    { z-index: 100; }
/* An open dropdown is re-parented here (see modules/dropdown.js), and
   knull.css's `.kn-app>*{z-index:1}` would otherwise trap it beneath the very
   panel that opened it -- the bug reporter's scrim alone sits at 96. Above
   every overlay, still below the toast. */
body.kn-app > .z-dd-list   { position: fixed; z-index: 98; }

/* ── PHONE: ROWS OF CONTROLS SWIPE, THEY DO NOT VANISH ──────────────────
   Outcome/emotion/window filters, timeframe pills, segmented switches: every
   one of these is a nowrap flex row that fits a desktop and simply ran off a
   375px screen -- the last three filters on the journal were 127px past the
   edge with no way to reach them. Swiping is the phone answer; wrapping turns
   one control into three stacked rows and buries the content. */
@media (max-width: 720px) {
  /* THE WIDTH IS THE CONTAINER'S, WHATEVER JS MEASURED. The layout engine
     writes an inline pixel width from S.el.clientWidth; if it measures while
     the shell is still settling it can bank a wider box and never re-measure,
     and the card then runs off the screen with its stats cut. An inline style
     beats a stylesheet, so this is the rare rule that has earned !important. */
  .kn-app .z-grid.is-narrow > .z-w { width: 100% !important; }

  .kn-app .z-pillrow,
  .kn-app .td-tfs,
  .kn-app .zpu-seg {
    overflow-x: auto; flex-wrap: nowrap;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
  }
  .kn-app .z-pillrow::-webkit-scrollbar,
  .kn-app .td-tfs::-webkit-scrollbar,
  .kn-app .zpu-seg::-webkit-scrollbar { display: none; }
  .kn-app .z-pillrow > *,
  .kn-app .td-tfs > *,
  .kn-app .zpu-seg > * { flex: none; }
}

/* ============================ SURFACES ==================================
   ELEVATION, not flatness.

   Everything was pure black on pure black — page, panel, card, row, all
   #000 — separated only by a hairline. That is why the app read as flat and
   unfinished no matter how the type was tuned: with no tonal difference
   between a surface and the thing it sits on, nothing looks placed, it just
   looks drawn. Axiom and FOMO are both very dark, but neither is black-on-
   black; their cards sit a few points ABOVE the ground and that single fact
   is most of why they look built rather than sketched.

   So there is a real ramp now. Four steps, each a few points apart — enough
   to read as depth, not enough to turn gray:

     ground   var(--z-card)   the page behind everything
     panel    var(--z-card)   large containers, the widget shell
     card     var(--z-card-hi)   the boxes you actually read
     raised   var(--z-raised)   hover, active rows, inputs, chips

   The neutrals carry a slight violet bias rather than being true gray. A
   pure #111 next to a violet accent reads as dirty; biased a few points
   toward the accent it reads as deliberate, and it costs no saturation.

   Hairlines step with the surface they sit on, because one border colour
   across four elevations is either invisible on the light ones or too loud
   on the dark ones.                                                        */
.kn-app {
  /* Structure taken from tradevisorai: a COOL near-black rather than a violet
     one, a card that sits a clear step above it, and a border light enough to
     actually draw the box (theirs is var(--z-line) — ours had been half that and the
     edges were doing nothing). Their hue is 210; keeping the neutrals cool and
     letting violet be the only colour on screen is what makes one accent read
     as deliberate instead of as tinting. */
  /* Sampled from the reference dashboard, not invented. Its ground is a very
     dark blue-black, its cards sit a few points ABOVE that, and the border is
     visible enough to actually draw each box. There is no page-wide bloom in
     it — the background is flat and the only glow is around the logo. */
  /* MEASURED from zoltrak-store/references/Zoltrak Theme.png. Every eyeballed
     guess before this was about TEN TIMES too light: the reference ground is
     essentially pure black at 1% lightness and the cards sit at 3%, not the
     5-8% that "a card has to lift off the ground" reasoning kept producing.
     The board is nearly black throughout and the separation comes from the
     BORDER, which is dark violet (hue 260) rather than a neutral gray. */
  /* These now come from tokens.css, which zoltrak-store generates. Do not
     paste hex values back in here — a second copy is a second source of truth
     that disagrees with the first the moment either moves. */

  /* knull.css builds a card as a fill plus a masked gradient ring. The fills
     take the ramp; the rings become flat hairlines so an edge separates
     rather than glows. */
  /* FLAT. This was --z-card-hi over --z-card, i.e. a top-to-bottom gradient.
     Invisible on a KPI tile, but on a tall card — the calendar — it reads as
     the card fading out from the top. Both ends are the same colour now. */
  --kn-fill-hi:  var(--z-card);
  --kn-fill-lo:  var(--z-card);
  --kn-panel-hi: var(--z-card);
  --kn-panel-lo: var(--z-panel);

  /* the card edge catches the bloom at the top and dies into black at the
     bottom, the way theirs does — a flat ring on a black card reads as a
     sticker, a fading one reads as lit */
  /* flat hairlines — the reference draws every card with one even border,
     not a gradient edge */
  --kn-ring-panel: linear-gradient(var(--z-line), var(--z-line));
  --kn-ring-card:  linear-gradient(var(--z-line), var(--z-line));
  --kn-ring-btn:   linear-gradient(var(--z-raised), var(--z-raised));
  --kn-ring-soft:  linear-gradient(var(--z-line-soft), var(--z-line-soft));

  /* a real drop shadow now that there is something for it to fall onto */
  --kn-glow-panel: 0 20px 48px -30px #000, 0 2px 0 rgba(255,255,255,.014) inset;
  --kn-glow-card:  0 14px 34px -26px #000, 0 1px 0 rgba(255,255,255,.018) inset;
  --kn-glow-kpi:   0 14px 34px -26px #000, 0 1px 0 rgba(255,255,255,.018) inset;

  --kn-line: var(--z-line);
  --kn-row:  var(--z-line-soft);
}
body.kn-app { background: var(--z-ground); }
.kn-app .kn-box  { background: linear-gradient(180deg, var(--kn-fill-hi), var(--kn-fill-lo)); }
.kn-app .kn-panel { background: linear-gradient(180deg, var(--kn-panel-hi), var(--kn-panel-lo)); }

/* the top edge catches the light, the way a raised surface does */
.kn-app .kn-box::after { content: none; }

/* ============================ THE SCALE =================================
   Rebuilt as one ladder instead of a pile of per-component tweaks. FOMO and
   Axiom are both dense, but what makes them read as OFFICIAL is that the
   steps are consistent: one size for every row, one for every card title,
   one for every micro-label — and a big, unambiguous jump up to the hero
   number. When those steps blur into each other the page feels off even
   though nothing is obviously wrong.

       28  page greeting / profile name
       30  the single biggest number on a screen (balance, day net)
       24  hero-strip stat values
       21  KPI values
       18  stat-band values
     15.5  card section titles
     13.5  table rows, list rows, body
       13  labels
     12.5  sub-values, buttons
       11  table headers, uppercase micro-labels

   Spacing runs on a 4px rhythm: rows 11/18, card headers 14/18, card bodies
   4/18/16, heroes 17/20, page 16/20.

   A deliberate deviation from the measured Knull scale (38/31/23/22/17…),
   which reads airy beside FOMO. Kept in ONE block so it re-tunes in one edit.
   Module CSS is injected at render time — after this file — so every override
   is .kn-app-prefixed to win on specificity rather than order.             */
.kn-app {
  /* The ladder lives in the store too, not just the palette. knull.css ships
     its own (38/31/23/22/17/14/13/12.5/11) which reads airy beside FOMO, and
     carries a half-pixel at --kn-small that is exactly the kind of value that
     makes text look soft. */
  --kn-h1:    var(--z-h1);
  --kn-big:   var(--z-big);
  --kn-kpi:   var(--z-kpi);
  --kn-stat:  var(--z-stat);
  --kn-title: var(--z-title);
  --kn-nav:   var(--z-nav);
  --kn-label: var(--z-label);
  --kn-small: var(--z-small);
  --kn-th:    var(--z-th);

  /* radii too — knull.css's 20/15/11/9 against the store's 14/10/999 */
  --kn-r-panel: var(--z-r-card);
  --kn-r-card:  var(--z-r-card);
  --kn-r-pill:  var(--z-r-box);
  --kn-r-btn:   var(--z-r-box);
}

.kn-app .z-view { padding: 16px 20px 20px; }
.kn-app .td, .kn-app .zlb, .kn-app .zfw, .kn-app .zac, .kn-app .z-cal, .kn-app .ztr { gap: 12px; }

/* ── leaderboard ─────────────────────────────────────────────────────── */
.kn-app .zlb-tbl td { padding: 13px 20px; font-size:15px; }
.kn-app .zlb-tbl th { padding: 10px 18px; }
.kn-app .zlb-tbl th:first-child, .kn-app .zlb-tbl td:first-child { padding-left: 20px; width: 60px; }
.kn-app .zlb-tbl th:last-child,  .kn-app .zlb-tbl td:last-child  { padding-right: 20px; }
.kn-app .zlb-who { gap: 12px; }
.kn-app .zlb-who-a { width: 36px; height: 36px; font-size:14px; }
.kn-app .zlb-who-n { font-size:16px; }
.kn-app .zlb-net { font-size:16px; }
.kn-app .zlb-rk { font-size:14px; }
.kn-app .zlb-medal { width: 28px; height: 28px; font-size:14px; }
.kn-app .zlb-medal.m1 { width: 32px; height: 32px; font-size:16px; }
.kn-app .zlb-medal.m2 { width: 30px; height: 30px; font-size:14px; }
.kn-app .zlb-crown { top: -10px; width: 16px; height: 12px; }
.kn-app .zlb-tbl tbody tr.r2 td, .kn-app .zlb-tbl tbody tr.r3 td { padding-top: 13px; padding-bottom: 13px; }
.kn-app .zlb-tbl tbody tr.r1 td { padding-top: 17px; padding-bottom: 14px; }
.kn-app .zlb-tbl tbody tr.r1 .zlb-who-n { font-size:18px; }
.kn-app .zlb-tbl tbody tr.r2 .zlb-who-n { font-size:17px; }
.kn-app .zlb-tbl tbody tr.r3 .zlb-who-n { font-size:16px; }
.kn-app .zlb-tbl tbody tr.r1 .zlb-who-a { width: 36px; height: 36px; font-size:17px; }
.kn-app .zlb-tbl tbody tr.r2 .zlb-who-a { width: 34px; height: 34px; font-size:16px; }
.kn-app .zlb-tbl tbody tr.r1 .zlb-net { font-size:18px; }
.kn-app .zlb-tbl tbody tr.r2 .zlb-net { font-size:18px; }
.kn-app .zlb-ava { width: 58px; height: 58px; font-size:24px; }
.kn-app .zlb-you-t h2 { font-size:22px; }
.kn-app .zlb-you-id { padding: 17px 20px; }
.kn-app .zlb-st { padding: 15px 18px; gap: 7px; }
.kn-app .zlb-st .v { font-size:27px; }
.kn-app .zlb-bhead { padding: 14px 20px 12px; }
.kn-app .zlb-seg button { padding: 7px 16px; font-size:14px; }

/* ── follows ─────────────────────────────────────────────────────────── */
.kn-app .zfw-grid { grid-template-columns: repeat(auto-fill, minmax(276px, 1fr)); gap: 12px; }
.kn-app .zfw-card { padding: 15px 16px 14px; gap: 12px; }
.kn-app .zfw-h-a { width: 42px; height: 42px; font-size:17px; }
.kn-app .zfw-h-t p { font-size:16px; }
.kn-app .zfw-h-t .sub { font-size:12px; }
.kn-app .zfw-s .v { font-size:17px; }
.kn-app .zfw-ava { width: 50px; height: 50px; font-size:22px; }
.kn-app .zfw-me { padding: 17px 20px; gap: 14px; }
.kn-app .zfw-me h2 { font-size:20px; }
.kn-app .zfw-c { padding: 15px 18px; gap: 7px; }
.kn-app .zfw-c .v { font-size:27px; }
.kn-app .zfw-tabs button { padding: 8px 18px; font-size:14px; }

/* ── trading dashboard ───────────────────────────────────────────────── */
.kn-app .td-bal-v { font-size:34px; }
.kn-app .td-wr-v { font-size:27px; }
.kn-app .td-wl { font-size:17px; }
/* the KPI tiles and the stat band are the SAME component — matching their
   padding and height is what stops the KPI row reading as two empty voids */
.kn-app .td-kpi, .kn-app .td-stat { min-height: 88px; padding: 15px 17px 16px; }
.kn-app .td-coin { width: 32px; height: 32px; }
.kn-app .td-coin svg { width: 18px; height: 18px; }
.kn-app .td-tbl td { padding: 11px 20px; font-size:14px; }
.kn-app .td-tbl th { padding: 0 18px 8px; }
.kn-app .td-tx { padding: 11px 20px; }
.kn-app .td-card-h { padding: 14px 18px 10px; }
.kn-app .td-bal-spark { height: 94px; }
.kn-app .td-asset p { font-size:14px; }
.kn-app .td-tx-m p, .kn-app .td-tx-r p { font-size:14px; }

/* ── account ─────────────────────────────────────────────────────────── */
.kn-app .zac-ava { width: 72px; height: 72px; font-size:29px; }
.kn-app .zac-id h2 { font-size:24px; }
.kn-app .zac-hstats .v { font-size:19px; }
.kn-app .zac-hero { padding: 18px 20px; gap: 16px; }
.kn-app .zac-rail button { font-size:14px; padding: 10px 12px; }
.kn-app .zac-ph { padding: 15px 20px 13px; }
.kn-app .zac-pb { padding: 4px 20px 16px; }
.kn-app .zac-row { padding: 15px 0; }
.kn-app .zac-row-t { font-size:15px; }

/* ── calendar ────────────────────────────────────────────────────────── */
.kn-app .z-cal-month { font-size:19px; }
.kn-app .z-cal-day .dpnl { font-size:23px; }
.kn-app .z-cal-day { min-height: 98px; }
.kn-app .z-cd-date { font-size:18px; }
.kn-app .z-cd-netval { font-size:27px; }
.kn-app .z-hm-sym { font-size:15px; }
.kn-app .z-hm-net { font-size:18px; }
.kn-app .z-hm-r { padding: 10px 20px; }

/* ── trade ───────────────────────────────────────────────────────────── */
.kn-app .ztr-in { font-size:27px; }
.kn-app .ztr-st .v { font-size:18px; }
.kn-app .ztr-r { padding: 11px 15px; font-size:14px; }
.kn-app .ztr-leg { padding: 14px 16px 13px; }

/* ============================== MOTION =================================
   Vortex's motion vocabulary, ported: things BLUR into focus rather than
   just fading, charts WIPE in behind a moving edge, and a band of light
   crosses a card on hover. This sits alongside the Knull store's kit
   (knull-effects.css), which still owns ripples, embers, aurora, veins,
   bloom and the P&L flash.

   One rule holds throughout: an entrance animation is ALWAYS applied and
   uses backwards fill — never a resting opacity:0 waiting on a JS class.
   That pattern (the store's .kn-fx-cascade) is what once left the KPI strip
   permanently invisible. Nothing here can strand an element blank. */

/* ── STORE FITTINGS ───────────────────────────────────────────────────────
   Where a store effect is right but its default size is not. .kn-app-prefixed
   because module CSS is injected at render time and would otherwise win on
   source order (see THE SCALE). */

/* L44 bouncing dots ship at 7px for a standalone loader. In the broker status
   chip they stand in for a single 6px status dot, so at ship size they were
   wider than the word next to them and pushed "Syncing" off the chip. */
.kn-app .z-br-status .kn-fx-dots > i { width: 5px; height: 5px; margin: 0 1.5px; }
.kn-app .z-br-status .kn-fx-dots { margin-right: 2px; }

/* L107 label swap inside the address button: the grid sizes to the WIDER of
   the two labels, so the button must not also try to size itself from the
   text that is currently showing, or it steps a few px on every copy. */
.kn-app .z-br-addr .kn-fx-swap { line-height: 1.35; }

/* Blur was 13px, which is more than the height of most of the type in this app
   — every label was unreadable for the first half of its entrance, and with a
   stagger running to ~500ms that reads as "the page is blurry" rather than as
   an animation. 4px keeps the softening legible as motion; the blur is also
   gone by 55% so text sharpens well before the movement finishes. */
@keyframes z-blur-in {
  from { opacity: 0; filter: blur(4px); transform: translateY(9px) scale(1.012); }
  55%  { filter: blur(0); }
  to   { opacity: 1; filter: blur(0); transform: none; }
}
@keyframes z-wipe      { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
@keyframes z-ring      { 70%  { box-shadow: 0 0 0 10px rgba(167,139,250,0); }
                         100% { box-shadow: 0 0 0 0  rgba(167,139,250,0); } }

/* ── view entrance: every card blurs into focus, staggered ─────────────── */
/* Keyed off the module root, NOT .kn-page.on — that class is added inside a
   requestAnimationFrame, which never fires in a background tab, so the whole
   entrance would sit frozen until you looked at it. */
.z-enter > *, .z-stagger > * {
  animation: z-blur-in .6s cubic-bezier(.2, .7, .2, 1) backwards;
}
.z-enter > :nth-child(2) { animation-delay:  80ms; }
.z-enter > :nth-child(3) { animation-delay: 160ms; }
.z-enter > :nth-child(4) { animation-delay: 240ms; }
.z-enter > :nth-child(5) { animation-delay: 320ms; }
.z-enter > :nth-child(n+6) { animation-delay: 400ms; }

/* ── rows and tiles stagger inside their card ─────────────────────────── */
.z-stagger > * { animation-duration: .5s; }
.z-stagger > :nth-child(1)  { animation-delay:  40ms; }
.z-stagger > :nth-child(2)  { animation-delay:  85ms; }
.z-stagger > :nth-child(3)  { animation-delay: 130ms; }
.z-stagger > :nth-child(4)  { animation-delay: 175ms; }
.z-stagger > :nth-child(5)  { animation-delay: 220ms; }
.z-stagger > :nth-child(6)  { animation-delay: 265ms; }
.z-stagger > :nth-child(7)  { animation-delay: 310ms; }
.z-stagger > :nth-child(8)  { animation-delay: 355ms; }
.z-stagger > :nth-child(9)  { animation-delay: 400ms; }
.z-stagger > :nth-child(10) { animation-delay: 445ms; }
.z-stagger > :nth-child(n+11) { animation-delay: 490ms; }

/* ── charts wipe in behind a moving edge ──────────────────────────────── */
.td-chart canvas, .td-bal-spark canvas, .td-perf canvas {
  animation: z-wipe 1.2s cubic-bezier(.4, 0, .2, 1) both;
}

/* ── a band of light crosses a card on hover ──────────────────────────── */
.td-kpi, .td-stat, .td-wr, .zfw-card, .zac-hero, .zlb-you,
.z-hm-cell, .zac-panel, .zfw-top {
  /* isolation gives each card its own stacking context, which is what lets the
     light band below sit at z-index:-1 -- above the card's background, beneath
     its content. Without a context here a negative child sinks behind the
     background and disappears entirely. */
  position: relative; overflow: hidden; isolation: isolate;
}
/* ── THE BAND OF LIGHT IS GONE ────────────────────────────────────────────
   It was a blur(9px) shaft of light, 22% alpha at its core, that crossed every
   KPI tile, stat, Follows card, account hero and the leaderboard card on hover.
   It is the "hovering a box makes everything in it blurry" fault, reported
   three separate times.

   Two attempts were made to keep the effect and cure the symptom -- animating
   left instead of transform, then dropping it behind the content at z-index
   -1 -- and neither worked, because neither addressed the cause. Measured on a
   frozen frame, the text under the band lost ~10% of its edge contrast either
   way. A blurred wash travelling over live type hazes that type. That is what
   the effect DOES; there is no version of it that does not.

   Deleted, not commented out: the shaft, its hover trigger and its keyframes.
   If it is ever wanted again it belongs on a surface with no text on it. */

/* ── a slow gold ring breathes out of today, and out of first place ───── */
.z-cal-day.today::after, .zlb-medal.m1::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none; box-shadow: 0 0 0 0 rgba(167,139,250,.6);
  animation: z-ring 2.6s ease-out infinite;
}
.zlb-medal.m1::before { border-radius: 50%; }

/* the breathing-veins overlay (E22) reads the marble through a token so the
   bundler's asset inlining reaches it */

/* Reduced motion means no MOVEMENT, not no feedback — entrances become plain
   fades and the ambient loops stop. (Windows: Settings → Accessibility →
   Visual effects → Animation effects. Off = this branch.) */
@media (prefers-reduced-motion: reduce) {
  .z-enter > *, .z-stagger > * { animation: kn-fade-in .3s backwards; }
  .td-chart canvas, .td-bal-spark canvas, .td-perf canvas { animation: none; }
  .z-cal-day.today::after, .zlb-medal.m1::before { animation: none; }
}


/* ── SCROLLBARS ─────────────────────────────────────────────────────────
   The OS default is a white slab against a black app. Gray, thin, and it
   only shows up against the track when there is something to scroll.     */
/* html and body are named outright — the page itself scrolls on the viewport,
   and a `.kn-app *` descendant selector never matches the body it lives on. */
/* Form controls don't inherit font-size from the UA stylesheet — they sit at
   13.3333px, a third of a pixel off the grid, which is exactly the softness
   this pass is chasing. :where() keeps the specificity at zero so any
   component that sets its own size still wins regardless of load order. */
/* Every element in the app resolves border-color to currentColor, which the
   ramp sets to var(--z-muted) — so ANY border that appears without an explicit colour
   paints as a pale gray hairline against black. That is where the "white
   outlines" keep coming from. :where() keeps this at zero specificity, so it
   only fills in the default and never beats a component's own border. */
:where(.kn-app, .kn-app *, .kn-app *::before, .kn-app *::after) {
  border-color: var(--z-line);
}

.kn-app :where(button, input, select, textarea) { font-size: inherit; }

html, .kn-app, .kn-app * { scrollbar-width: thin; scrollbar-color: var(--z-scroll) transparent; }
html::-webkit-scrollbar, .kn-app::-webkit-scrollbar,
.kn-app ::-webkit-scrollbar { width: 11px; height: 11px; }
html::-webkit-scrollbar-track, .kn-app::-webkit-scrollbar-track,
.kn-app ::-webkit-scrollbar-track { background: transparent; }
html::-webkit-scrollbar-thumb, .kn-app::-webkit-scrollbar-thumb,
.kn-app ::-webkit-scrollbar-thumb {
  background: var(--z-scroll); border-radius: 8px;
  border: 3px solid transparent; background-clip: content-box;
}
.kn-app ::-webkit-scrollbar-thumb:hover { background: var(--z-scroll-hi); background-clip: content-box; }
.kn-app ::-webkit-scrollbar-corner { background: transparent; }

/* ── CRISPNESS ──────────────────────────────────────────────────────────
   Two things were making text look soft:
   1. the neon glow. At 20px it reads as neon; at 10px the halo is wider
      than the strokes and the glyph turns to mush. The glow is dialled
      back so small numbers stay sharp and big ones still burn.
   2. backdrop-filter on the sticky chrome. It forces the bar onto its own
      composited layer, and on a fractional device-pixel ratio that layer
      resamples — every label in it goes soft. The bar is opaque enough not
      to need it.                                                          */
.kn-app {
  /* The crispness pass took these down to 9px/.42, which killed the neon.
     Against a pure black ground they can burn again — a two-stop shadow gives
     a tight core that keeps the glyph edge readable plus a wide bloom that
     actually glows, instead of one fat halo that just softens the digits. */
  /* Their green is a true green and their red a true red, not our mint and
     coral. Hues taken from the reference; the glow stays, because that has
     been asked for every time. */
  /* The glow is a TOKEN, not an effect someone can quietly drop — it has been
     asked for in every single round of this. */
  --kn-green: var(--z-green);
  --kn-red:   var(--z-red);
  --kn-glow-green: var(--z-glow-green);
  --kn-glow-red:   var(--z-glow-red);
}
.kn-app .z-bar, .kn-app .z-ticker {
  -webkit-backdrop-filter: none; backdrop-filter: none;
  background: var(--z-panel);
}
/* text under ~12px carries no halo at all */
/* Only the genuinely tiny text stays flat — at 11px a halo is wider than the
   strokes and the glyph turns to mush. Everything at 13px and up keeps the
   neon, which is where it reads as neon rather than as blur. */
.kn-app .kn-tiny, .kn-app .zpu-f, .kn-app .zfw-h-t .sub,
.kn-app .zpu-hist, .kn-app .z-cal-day .dtr { text-shadow: none !important; }

/* ============================== TOP BAR ==================================
   A terminal is judged on how much it can show at once. The left rail cost
   190px of every board's width and repeated information the page already
   states, so navigation lives up here instead — Axiom's shape. The chrome
   around it is FOMO's: search in the middle, wallet on the right, a ticker
   along the bottom of the window.                                          */
.z-bar {
  position: sticky; top: 0; z-index: 55;
  display: flex; align-items: center; gap: 10px;
  height: 64px; padding: 0 16px; flex: none;
  background: rgba(0, 0, 0, .88);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  box-shadow: inset 0 -1px 0 var(--z-raised);
}
.z-bar button { font-family: inherit; color: inherit; -webkit-appearance: none; appearance: none; }

/* ══════════════════ HIDING THE TOP BAR ═══════════════════════════════════
   The handle rides the bar's bottom edge, half in and half out, so it reads
   as belonging to the bar rather than floating over the page. 20px: small
   enough to ignore while you work, big enough to hit.

   THE BAR STAYS IN THE DOM WHEN HIDDEN. display:none would take the handle
   with it and leave no way back, so it collapses to height 0 with its
   children hidden and overflow VISIBLE — the handle is then the one thing
   still painted, sitting on the top edge of the window. It also means the
   bar can animate rather than blink, and that nothing has to be rebuilt to
   bring it back. */
.z-barhide {
  position: absolute; right: 14px; bottom: -10px; z-index: 3;
  width: 34px; height: 20px; padding: 0;
  display: grid; place-items: center;
  border: 0; border-radius: 0 0 7px 7px; cursor: pointer;
  background: var(--z-raised);
  box-shadow: inset 0 0 0 1px var(--z-line), 0 4px 10px -6px #000;
  color: var(--z-dim);
  transition: color .15s ease, background .15s ease, transform .15s ease;
}
.z-barhide svg { width: 13px; height: 13px; display: block; }
.z-barhide:hover { color: var(--z-num); background: var(--z-card-hi); }
.z-barhide:focus-visible { outline: 2px solid var(--z-accent-fill); outline-offset: 2px; }

/* The collapsed bar. The vertical padding reset is insurance, not a fix — the
   bar's padding is 0 16px today, so those two lines currently change nothing.
   They are here because height:0 on a box that later gains vertical padding
   collapses to the padding rather than to nothing, and the failure looks like
   "the button does nothing" rather than like a padding bug. */
.z-bar.z-bar-hid {
  height: 0; padding-top: 0; padding-bottom: 0;
  overflow: visible;
  box-shadow: none; background: transparent;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
/* Everything except the handle. Not [hidden] and not visibility — trap 8: any
   author rule outranks the browser's [hidden], and a merely invisible tab row
   is still in the tab order, so keyboard focus would walk through eight
   buttons nobody can see. display:none takes them out of the tree entirely. */
.z-bar.z-bar-hid > *:not(.z-barhide) { display: none; }
.z-bar.z-bar-hid .z-barhide { bottom: -22px; border-radius: 0 0 7px 7px; }
.z-bar.z-bar-hid .z-barhide svg { transform: rotate(180deg); }

/* A bar that snaps out of existence reads as a glitch; one that folds reads as
   a control. Height only — the children are display:none by then, so there is
   nothing inside to reflow against. */
.z-bar { transition: height .16s ease; }
@media (prefers-reduced-motion: reduce) { .z-bar { transition: none; } }

/* ── OUT OF THE APP ───────────────────────────────────────────────────────
   A small chevron at the far left of the bar, back to the site. Quiet on
   purpose: it must not compete with the logo beside it, so it is the faint
   ramp until you point at it. 18px of glyph in a 26px target — small to look
   at, not small to hit.

   ALL THE WAY LEFT, at Landon's call. It is first in the bar now, and the
   negative left margin pulls it back out of the bar's own 16px padding so it
   sits against the edge rather than indented with the content. Not the full
   16: the target keeps 4px, which puts the glyph itself about 8px in — a
   control flush against the window edge reads as clipped, not as placed. */
.z-home {
  flex: none; display: grid; place-items: center;
  width: 26px; height: 26px; margin-left: -12px; margin-right: -3px;
  border-radius: 8px;
  color: var(--z-faint); text-decoration: none;
  transition: color .15s ease, background .15s ease;
}
.z-home svg { width: 18px; height: 18px; display: block; }
.z-home:hover { color: var(--kn-muted); background: rgba(255,255,255,.05); }
.z-home:focus-visible { outline: 2px solid var(--z-accent-fill); outline-offset: 2px; }
@media (max-width: 720px) { .kn-app .z-home { display: none; } }

.z-brand { display: flex; align-items: center; gap: 11px; flex: none; cursor: pointer; user-select: none; }
/* The mark wears the wordmark's gradient, not a copy of it. The glyph PNG is
   used as a MASK and the gradient is the paint behind it, so the Z and the
   letters beside it are literally the same ramp — purple at the top sinking to
   near-black — and they cannot drift apart when a token moves. Baking the
   gradient into the PNG would have been simpler and would have drifted the
   first time --z-accent changed. */
.z-brand-mark {
  width: 46px; height: 46px; flex: none;
  /* --z-accent-hi, matching .z-brand-name exactly. The de-pink sweep took this
     to --z-accent-fill because the guard matched an earlier .z-brand-mark rule
     (the drop-shadow one, 155) instead of this one -- which would have left the
     Z and the letters beside it on two different ramps, the precise thing the
     mask was built to prevent. */
  background: linear-gradient(180deg, var(--z-accent-hi) 0%, var(--z-accent-soft) 42%,
              var(--z-accent-deep) 76%, var(--z-raised) 100%);
  -webkit-mask: url("assets/zoltrak-glyph.png") center / contain no-repeat;
          mask: url("assets/zoltrak-glyph.png") center / contain no-repeat;
}
/* The wordmark is lit from the top and sinks into the ground: a mid violet at
   the cap height falling through a deep purple to near-black at the baseline.
   The last stop is not pure #000 — on a black bar that would cut the bottom
   off every letter and the word would read as broken rather than as shaded. */
.z-brand-name {
  font-size:21px; font-weight: 800; letter-spacing: .19em;
  background: linear-gradient(180deg, var(--z-accent-hi) 0%, var(--z-accent-soft) 42%, var(--z-accent-deep) 76%, var(--z-raised) 100%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
  padding-bottom: 1px;   /* the clip box would otherwise crop the descender row */
}

/* ── BRAND LETTERS (store effect L14) ─────────────────────────────────────
   KnullFX.letters() splits the wordmark into one <i> per character at boot so
   they can cascade in. That breaks the gradient above in a way that is easy to
   miss: background-clip:text clips the PARENT's background to the parent's own
   paint, and a transformed child is composited in its own layer that the clip
   never reaches — so mid-animation the letters are transparent text over no
   background, i.e. gone. Each letter therefore carries its own copy of the
   ramp. It is a vertical gradient over the same line height, so a split
   wordmark and an unsplit one paint identically once the cascade has landed.

   Do NOT repeat the parent's padding-bottom here: the letters sit inside the
   parent's clip box already, and as inline-blocks the extra pad grew the top
   bar's line box by a whole pixel. Measured split vs unsplit: same 29px
   height, 1.06px wider over 123px — inline-block shaping absorbing the
   .19em tracking per letter, not a rounding fault. */
.z-brand-name > i {
  background: inherit;
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}

/* ── the tabs ───────────────────────────────────────────────────────────── */
.z-nav { display: flex; align-items: center; gap: 2px; flex: none; }
.z-tab {
  position: relative; border: 0; background: none; cursor: pointer;
  padding: 11px 17px; border-radius: 9px;
  font-size:15px; font-weight: 500; color: var(--kn-dim); white-space: nowrap;
  transition: color .15s ease, background .15s ease;
}
.z-tab:hover { color: var(--kn-text); background: rgba(167,139,250,.06); }
.z-tab.on { color:var(--kn-muted); font-weight: 600; background: rgba(167,139,250,.1); }
.z-tab.on::after {
  content: ""; position: absolute; left: 13px; right: 13px; bottom: -1px; height: 2px;
  border-radius: 2px 2px 0 0; background: var(--kn-gold);
  box-shadow: 0 0 12px rgba(167,139,250,.9);
}

/* ── search ─────────────────────────────────────────────────────────────── */
.z-search {
  font-size: 15px;
  flex: 1; min-width: 0; max-width: 460px; margin: 0 auto;
  display: flex; align-items: center; gap: 9px;
  height: 40px; padding: 0 12px 0 14px; border-radius: 10px; cursor: text;
  border: 1px solid var(--z-line); background: rgba(255,255,255,.022);
  color: var(--kn-faint); transition: border-color .16s ease, background .16s ease;
}
.z-search:hover { border-color: rgba(167,139,250,.35); background: rgba(167,139,250,.05); }
.z-search svg { width: 14px; height: 14px; flex: none; }
.z-search span { flex: 1; min-width: 0; text-align: left; font-size:14px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.z-search kbd {
  flex: none; font-family: inherit; font-size:11px; letter-spacing: .04em;
  padding: 2px 6px; border-radius: 5px; color: var(--kn-dim);
  box-shadow: inset 0 0 0 1px var(--z-line);
}
/* Paste. cursor:pointer is load-bearing -- .z-search sets cursor:text on the
   whole control and children inherit it, so without this the button shows an
   I-beam and reads as decoration. Ink stays grey: purple is a surface here,
   never the label (CLAUDE.md trap 5). */
.z-search .z-paste {
  cursor: pointer; padding: 3px 9px; font-size: 11.5px; letter-spacing: .02em;
  color: var(--kn-muted); background: rgba(139, 48, 239, .10);
  box-shadow: inset 0 0 0 1px rgba(139, 48, 239, .28);
  transition: background .15s, color .15s, box-shadow .15s;
}
.z-search .z-paste:hover {
  color: var(--kn-text); background: rgba(139, 48, 239, .22);
  box-shadow: inset 0 0 0 1px rgba(139, 48, 239, .5);
}
.z-search .z-paste:active { transform: translateY(1px); }
.z-search .z-paste.ok {
  color: var(--kn-green); background: rgba(46, 230, 168, .14);
  box-shadow: inset 0 0 0 1px rgba(46, 230, 168, .4);
}
.z-search .z-paste.bad {
  color: var(--kn-red); background: rgba(242, 85, 74, .14);
  box-shadow: inset 0 0 0 1px rgba(242, 85, 74, .4);
}

/* ── wallet ─────────────────────────────────────────────────────────────── */
/* ── THE BALANCE CLUSTER ──────────────────────────────────────────────────
   Three figures and the account button, on one row. The old version was two
   panels butted together with a 1px seam and 16px of padding each, plus a
   Deposit button crammed into the second one — which is why it read as a wall
   of numbers with no space in it.

   Now: one surface, hairline separators between the figures, and the avatar
   set apart from them because it is a different KIND of thing. Deposit moved
   into the account menu, beside the balance it changes. */
/* NO BOX. The three figures sat on a raised panel inside a bar that is already
   a panel -- a lighter rectangle floating on a dark one, drawing a border
   around numbers that were never a group you act on. The hairlines between
   them already do the separating. */
.z-wallet { display: flex; align-items: center; gap: 7px; flex: none;
  padding: 4px 2px; background: none; box-shadow: none; }
/* line-height was 1, which makes the line box exactly as tall as the font —
   so every ascender and descender spilled out of it and .z-wallet's
   overflow:hidden (there for the rounded corners) shaved them off. 1.2 gives
   the glyphs their own box back. */
/* The line boxes are set in px, not as a ratio. 17px x 1.2 is 20.4, which made
   the block 55.8 tall; the bar then centred it on a half pixel and every glyph
   in it went soft again. 21 + 3 + 14 + 18 of padding is 56, and (68 - 56) / 2
   is a whole 6. */
.z-wallet-c { display: flex; flex-direction: column; gap: 1px; padding: 2px 6px;
  background: none; }
/* the wallet figures are money — green, like every other dollar amount */
/* ── DEMO / LIVE ──────────────────────────────────────────────────────────
   Rides the top bar next to the wallet. The one thing a trader must never be
   unsure about is whether the money is real, so this is present on every view
   and coloured, not just labelled. */
.kn-app .z-mode { display:flex; align-items:center; gap:7px; flex:none;
  border:0; cursor:pointer; font:inherit; font-size:11px; font-weight:800;
  letter-spacing:.1em; padding:7px 11px; border-radius:10px; margin-right:8px;
  color:var(--kn-faint); background:rgba(139,48,239,.16);
  box-shadow:inset 0 0 0 1px rgba(139,48,239,.4); transition:all .15s; }
.kn-app .z-mode:hover { color:var(--kn-text); background:rgba(139,48,239,.28); }
.kn-app .z-mode-dot { width:6px; height:6px; border-radius:50%; flex:none;
  background:var(--z-accent-soft, #8b30ef); }
.kn-app .z-mode b { font-weight:700; font-size:12px; letter-spacing:0;
  color:var(--kn-text); font-variant-numeric:tabular-nums; }
.kn-app .z-mode.live { color:var(--kn-faint); background:rgba(46,230,168,.16);
  box-shadow:inset 0 0 0 1px rgba(46,230,168,.42); }
.kn-app .z-mode.live .z-mode-dot { background:var(--kn-green); }
@media (max-width:1180px){ .kn-app .z-mode b { display:none; } }

.z-wallet-c b { display: flex; align-items: center; gap: 5px;
  font-size: 16px; line-height: 19px; font-weight: 700; color: var(--kn-green);
  text-shadow: var(--kn-glow-green); font-variant-numeric: tabular-nums; }
/* THE LABEL, which is the whole point of the rebuild: a dollar figure with no
   word beside it could be cash or could be everything you own, and those stop
   being the same number the moment you hold a coin. Small caps, the same
   treatment every other label in the app gets. */
.z-wallet-c i, .kn-app .z-wal-k { font-style: normal; font-size: 9.5px;
  line-height: 12px; font-weight: 600; letter-spacing: .09em;
  text-transform: uppercase; color: var(--kn-faint);
  font-variant-numeric: tabular-nums; white-space: nowrap; }
/* the day's change keeps its own colour inside the label */
.kn-app .z-wal-k .kn-pos { color: var(--kn-green); }
.kn-app .z-wal-k .kn-neg { color: var(--kn-red); }
/* ── A REASON MUST NEVER WIDEN THE BAR ────────────────────────────────────
   When the net worth cannot be read, this label carries why instead of the
   words "Net worth". The first sentence written into it -- "the price service
   is rate-limiting us" -- pushed the top bar past its own width at 1920, 1440,
   1180, 1024 and 768, which the account-control test caught. The sentences are
   short now AND capped here, because the next one somebody writes will not be.
   The full text stays reachable as the chip's tooltip. */
.kn-app .z-wal-k.z-wal-why { max-width: 16ch; overflow: hidden;
  text-overflow: ellipsis; color: var(--kn-dim); text-transform: none;
  letter-spacing: .02em; }
/* hairlines, not gaps: three figures need separating without three boxes */
.kn-app .z-wal-sep { width: 1px; height: 22px; flex: none; align-self: center;
  background: var(--kn-line); }
/* and the avatar sits apart from the numbers -- it is not one of them */
.kn-app .z-wallet .z-mebtn { margin-left: 2px; }

/* ── the way into Account ───────────────────────────────────────────────── */
.z-me {
  font-size: 14px;
  display: flex; align-items: center; gap: 10px; flex: none; cursor: pointer;
  padding: 6px 14px 6px 7px; border: 0; border-radius: 10px;
  background: rgba(255,255,255,.022); box-shadow: inset 0 0 0 1px var(--z-line);
  transition: box-shadow .16s ease, background .16s ease;
}
.z-me:hover { background: rgba(167,139,250,.07); box-shadow: inset 0 0 0 1px rgba(167,139,250,.4); }
.z-me-t { display: flex; flex-direction: column; line-height: 1.3; text-align: left; }
.z-me-n { font-size:16px; font-weight: 600; color: var(--kn-text); }
.z-me-s { font-size:13px; color:var(--kn-muted); }
.z-avatar {
  width: 38px; height: 38px; flex: none; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size:16px; font-weight: 700; color:var(--kn-muted);
  background: radial-gradient(120% 120% at 50% 0%, rgba(167,139,250,.2), rgba(6,5,11,.9));
  box-shadow: inset 0 0 0 1px rgba(139,92,246,.5);
}
.z-avatar.lg { width: 42px; height: 42px; font-size:19px; }
.z-avatar.xl { width: 66px; height: 66px; font-size:29px; }
/* When a profile picture is set, Z.profile.avatarInner() puts an <img> in here
   instead of a letter. overflow:hidden is what makes the circle clip it, and
   object-fit:cover is what stops a portrait squashing into a square. */
.z-avatar { overflow: hidden; }
.z-avatarimg { width: 100%; height: 100%; border-radius: 50%;
  object-fit: cover; object-position: center; display: block; }

.z-burger { font-size: 15px; display: none; padding: 0 8px; border: 0; background: none; cursor: pointer;
  color: var(--kn-dim); }
.z-burger svg { width: 18px; height: 18px; display: block; }

/* ── context strip: which book am I looking at ──────────────────────────── */
.z-ctx { flex: none; display: flex; align-items: center; gap: 12px; padding: 12px 20px 0; }
.z-ctx[hidden] { display: none; }
.z-books { display: flex; gap: 3px; padding: 3px; border-radius: 11px;
  background: var(--z-card); box-shadow: inset 0 0 0 1px var(--z-line); }
.z-books button { border: 0; background: none; cursor: pointer; padding: 6px 16px;
  border-radius: 8px; font-size:14px; font-weight: 600; color: var(--kn-dim);
  transition: color .15s, background .15s; }
.z-books button:hover { color:var(--kn-muted); }
.z-books button.on { color:var(--kn-muted); background: rgba(167,139,250,.12);
  box-shadow: inset 0 0 0 1px rgba(139,92,246,.4); }
.z-ctx-t { font-size:14px; color: var(--kn-faint); }

/* ── the ticker along the bottom ────────────────────────────────────────── */
.z-ticker {
  /* static, not sticky: it is the last row of a full-height column now, so it
     cannot be anywhere but the bottom. */
  position: static; z-index: 54; flex: none;
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  height: 30px; padding: 0 16px; overflow: hidden;
  background: rgba(0, 0, 0, .9);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  box-shadow: inset 0 1px 0 var(--z-raised);
  font-size:12px; font-variant-numeric: tabular-nums;
}
/* Watchlist button first and pinned, coins after it. margin-right:auto on the
   COINS is what keeps the right-hand group (Live, theme, bug, socials) pushed
   to the far edge -- .z-ticker is space-between, so without it the three groups
   would spread across the bar and the button would drift away from the corner. */
.z-tick-l { display: flex; align-items: center; gap: 18px; min-width: 0;
  overflow: hidden; flex: 0 1 auto; margin-right: auto; }
.z-ticker > .z-watch-btn { flex: none; }

/* ---- dragging a coin ----
   The bar is only 34px tall, so the drop marker is a rule down the seam rather
   than a gap opening up: at this height an inserted gap makes the whole row
   twitch and reads as a glitch. */
.z-wt[draggable="true"] { cursor: grab; }
.z-wt.dragging { opacity: .4; cursor: grabbing; }
.z-wt.dropbefore, .z-wt.dropafter { position: relative; }
.z-wt.dropbefore::before, .z-wt.dropafter::after {
  content: ""; position: absolute; top: 4px; bottom: 4px; width: 2px;
  border-radius: 2px; background: var(--z-accent, #8b30ef);
  box-shadow: 0 0 8px rgba(139,48,239,.9);
}
.z-wt.dropbefore::before { left: -10px; }
.z-wt.dropafter::after { right: -10px; }
.z-tick { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
.z-tick b { font-weight: 600; color: var(--kn-dim); }
.z-tick span { color: var(--kn-text); }
.z-tick-r { display: flex; align-items: center; gap: 3px; flex: none; color: var(--kn-faint); }

/* ── FOUR GROUPS, THREE LINES ─────────────────────────────────────────────
   Status (connected + demo/live), our tools, other people's platforms, and the
   way out. Drawn as real <i class="z-sep"> elements in app.html rather than
   ::before pseudo-elements: the pseudo version measured correctly everywhere I
   could test and still did not appear on the one screen that mattered. A real
   element is one thing that either exists in the DOM or does not.

   Bright enough to read at a glance. The first attempt was 17% white at 16px
   on a near-black bar, which getComputedStyle happily confirmed and no eye
   could find. */
.kn-app .z-sep {
  /* 2px, not 1. Every previous attempt was a single pixel, and every time I
     checked it I was looking at a 3x screenshot where one pixel is three --
     so it read fine to me and vanished on the actual screen. Verified at 1x
     now, which is the only scale that counts.

     The margins are what make the GROUPING read: 5px between items inside a
     group, ~22px and a bright rule between groups. */
  flex: none; align-self: center; width: 2px; height: 22px; border-radius: 1px;
  margin: 0 4px;
  /* A SOLID COLOUR, not white at an opacity. 55% white over a bar that is
     itself 90% black lands around #8c8c8c and reads as "hardly there".
     #9aa1b8 is a flat value that cannot be diluted by whatever is behind it,
     and it sits just under --z-num #c3c7d6 so it is still not the brightest
     thing on the bar -- house rule 6. */
  background: #7b8195;
}

/* ── watchlist ─────────────────────────────────────────────────────────── */
.z-watch-btn {
  flex: none; display: inline-flex; align-items: center; gap: 7px;
  height: 22px; padding: 0 10px; border: 0; border-radius: 7px; cursor: pointer;
  background: var(--z-raised); color: var(--kn-muted);
  font: inherit; font-size: 12px; letter-spacing: .02em;
  transition: background .15s, color .15s;
}
.z-watch-btn:hover { background: rgba(139,48,239,.18); color: var(--kn-text); }
.z-watch-btn[aria-expanded="true"] { background: rgba(139,48,239,.24); color: var(--kn-text); }

/* a watched coin in the bar */
/* ---- a watched coin on the bar: mark, then price ---------------------------
   The price is green with its glow because that is what money looks like in
   this app. It is NOT a gain/loss signal here -- a price has no direction --
   so it does not flip red; the 24h move lives in the hover card where it can
   carry the colour honestly. */
.z-wt { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
  cursor: pointer; border: 0; background: none; font: inherit; padding: 0 2px;
  color: inherit; border-radius: 6px; transition: background .14s; }
.z-wt:hover { background: rgba(139,48,239,.14); }
.z-wt-ic { width: 15px; height: 15px; flex: none; display: grid;
  place-items: center; }
.z-wt-ic svg { width: 100%; height: 100%; display: block; }
/* a memecoin has no mark, so it gets its two letters on the house gradient */
.z-wt-ic-t { border-radius: 5px; font-size: 8px; font-weight: 800;
  letter-spacing: -.2px; color: var(--z-ink-accent);
  background: linear-gradient(150deg, #8b30ef, #4c1a94); }
.z-wt-px { font-weight: 700; font-variant-numeric: tabular-nums;
  color: var(--kn-green); text-shadow: var(--kn-glow-green); }
.z-wt-chg { font-weight: 600; font-variant-numeric: tabular-nums; }
.z-wt-empty { color: var(--kn-faint); font-size: 12px; }
/* the + on the Watchlist button */
.z-watch-plus { font-style: normal; font-weight: 700; font-size: 14px;
  line-height: 1; color: var(--kn-muted); opacity: .75; }
.z-watch-btn:hover .z-watch-plus { opacity: 1; color: var(--kn-text); }

/* ---- the hover card -------------------------------------------------------
   Body-level and fixed, because .z-ticker is overflow:hidden with a
   backdrop-filter -- that combination clips a popover parented inside it even
   when the popover is position:fixed. Shown by toggling [hidden]: no opacity or
   transform transition, which would put this text on its own compositing layer
   and soften every glyph on the way in. */
.z-wtpop { position: fixed; z-index: 90; width: 208px; padding: 11px 12px 10px;
  border-radius: 12px; pointer-events: none;
  background: linear-gradient(180deg, rgba(16,11,30,.98), rgba(7,5,13,.99));
  box-shadow: inset 0 0 0 1px rgba(139,48,239,.34),
              0 18px 44px -18px #000, 0 0 30px -14px rgba(139,48,239,.6);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px); }
.z-wtpop[hidden] { display: none; }
.z-wtpop-h { display: flex; align-items: center; gap: 8px; padding-bottom: 9px;
  margin-bottom: 8px; border-bottom: 1px solid var(--kn-line); }
.z-wtpop-ic { width: 20px; height: 20px; flex: none; display: grid;
  place-items: center; }
.z-wtpop-ic svg { width: 100%; height: 100%; display: block; }
.z-wtpop-h b { font-size: 13px; font-weight: 700; color: var(--kn-text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.z-wtpop-h i { margin-left: auto; font-style: normal; font-size: 9.5px;
  font-weight: 700; letter-spacing: .12em; color: var(--kn-faint); }
/* the 24h shape. It sits between the name and the numbers because that is the
   order you read it in: what is it, what has it been doing, what is it worth. */
.z-wtpop-gr { display: block; margin: 0 0 9px; }
.z-wtpop-gr svg { display: block; width: 100%; height: auto; }
.z-wtpop-g { display: grid; gap: 5px; }
.z-wtpop-kv { display: flex; align-items: baseline; gap: 10px; }
.z-wtpop-kv i { font-style: normal; font-size: 9.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--kn-faint); }
.z-wtpop-kv b { margin-left: auto; font-size: 12.5px; font-weight: 700;
  color: var(--kn-text); font-variant-numeric: tabular-nums; }
.z-wtpop-kv b.kn-pos { color: var(--kn-green); text-shadow: var(--kn-glow-green); }
.z-wtpop-kv b.kn-neg { color: var(--kn-red); text-shadow: var(--kn-glow-red); }

/* the panel sits ON the bar, so it opens where the button is */
.z-watch {
  position: fixed; left: 14px; bottom: 42px; z-index: 80;
  width: min(360px, calc(100vw - 28px));
  display: flex; flex-direction: column;
  border-radius: 12px; overflow: hidden;
  background: linear-gradient(180deg, var(--kn-panel-hi), var(--kn-panel-lo));
  box-shadow: 0 0 0 1px var(--kn-line), 0 26px 60px rgba(0,0,0,.66);
}
.z-watch[hidden] { display: none; }
.z-watch-head { display: flex; align-items: center; gap: 9px; padding: 11px 12px;
  border-bottom: 1px solid var(--kn-line); color: var(--kn-faint); }
.z-watch-head input { flex: 1; min-width: 0; border: 0; background: none; outline: none;
  color: var(--kn-text); font: inherit; font-size: 13px; }
.z-watch-head input::placeholder { color: var(--kn-faint); }
.z-watch-x { flex: none; width: 22px; height: 22px; border: 0; border-radius: 6px;
  cursor: pointer; background: var(--z-raised); color: var(--kn-muted);
  display: flex; align-items: center; justify-content: center; }
.z-watch-x:hover { background: rgba(139,48,239,.16); }
.z-watch-list { max-height: 46vh; overflow: auto; padding: 6px; }
.z-watch-sec { padding: 7px 8px 5px; font-size: 10px; letter-spacing: .13em;
  text-transform: uppercase; color: var(--kn-faint); }
.z-watch-row { display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px 9px; border: 0; border-radius: 8px; cursor: pointer;
  background: none; color: inherit; font: inherit; text-align: left; }
.z-watch-row:hover { background: rgba(139,48,239,.1); }
.z-watch-row b { font-size: 13px; font-weight: 700; color: var(--kn-muted); }
.z-watch-row .n { flex: 1; min-width: 0; font-size: 12px; color: var(--kn-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.z-watch-row .c { font-size: 12px; font-variant-numeric: tabular-nums; }
.z-watch-row .rm { flex: none; width: 20px; height: 20px; border-radius: 5px;
  display: flex; align-items: center; justify-content: center; color: var(--kn-faint); }
.z-watch-row .rm:hover { background: rgba(242,85,74,.16); color: var(--kn-red); }
.z-watch-none { padding: 14px 10px; font-size: 12.5px; color: var(--kn-faint); }

/* ── the top-bar wallet ────────────────────────────────────────────────── */
.z-wal-sol b { display: inline-flex; align-items: center; gap: 6px; }
/* the SOL amount and its dollar value are one pair -- same size, same weight.
   #z-wal-cash beats .z-wallet-c b on specificity, which is what carried the
   old smaller size. */
.kn-app .z-wallet-c b, .kn-app #z-wal-cash {
  font-size: 15px; font-weight: 700; letter-spacing: -.2px;
}
/* No fill here. The mark carries Solana's own gradient as a fill= attribute on
   each path, and a CSS fill on the <svg> inherits down and BEATS a presentation
   attribute — so "fill: currentColor" would have silently repainted the brand
   logo in the app's text colour. */
.z-sol { flex: none; }
.z-walbtn {
  flex: none; width: 30px; height: 30px; margin-left: 2px;
  border: 0; border-radius: 8px; cursor: pointer; padding: 0;
  background: var(--z-raised); color: var(--kn-muted);
  display: inline-flex; align-items: center; justify-content: center;
  transition: background .15s, color .15s;
}
.z-walbtn:hover { background: rgba(139,48,239,.2); color: var(--kn-text); }

/* ── the account control, bottom-right in the ticker ─────────────────────
   It used to sit in the top bar wearing your handle. Two problems with that:
   Supabase builds the handle out of the front of your email address, so the
   chip was publishing part of an email on every screen; and a full-size purple
   button for a thing you press twice ever was the loudest control on the
   busiest bar in the app.

   Now it is the size of the tools beside it in the ticker and its label is the
   ACTION -- "Log in" / "Log out" -- never the person. */
.kn-app .z-acct {
  flex: none; border: 0; cursor: pointer; font: inherit;
  font-size: 11.5px; font-weight: 650; letter-spacing: .02em;
  padding: 3px 9px; border-radius: 7px; white-space: nowrap; line-height: 18px;
  color: var(--kn-faint); background: transparent;
  box-shadow: inset 0 0 0 1px var(--kn-line);
  transition: color .16s ease, background .16s ease, box-shadow .16s ease;
}
.kn-app .z-acct:hover { color: var(--z-num); background: rgba(139,48,239,.16);
  box-shadow: inset 0 0 0 1px rgba(139,48,239,.45); }
.kn-app .z-acct:active { transform: translateY(1px); }
/* signed out it is the way in, so it carries a hint of the purple surface --
   still small, still not shouting */
.kn-app .z-acct.out { color: var(--kn-muted); background: rgba(139,48,239,.14);
  box-shadow: inset 0 0 0 1px rgba(139,48,239,.4); }
.kn-app .z-acct.out:hover { color: var(--z-num); background: rgba(139,48,239,.26);
  box-shadow: inset 0 0 0 1px rgba(139,48,239,.7); }
.kn-app .z-acct.off { opacity: .5; }

/* ── THE TIGHT BAND, MEASURED ─────────────────────────────────────────────
   Between 1001px and 1180px the nav is still six inline tabs (498px, and it
   does not fold into the burger until 1000px) and the wordmark is still
   showing (192px with the mark). The bar wanted 1127px in a 1024px window and
   ran off the right-hand edge, quietly, because the overflow is hidden. That
   was true before any of this session's changes and is fixed here: dropping
   the wordmark in this band frees 134px. The glyph stays, so the brand is
   still there and still clickable. */
@media (max-width: 1180px) { .kn-app .z-brand-name { display: none; } }

/* .z-barctl is a view's own controls, and the one that uses it is the
   dashboard's drag-and-resize layout editor, which is not usable at phone
   width anyway. It goes at the width the wallet already goes. */
@media (max-width: 700px) { .kn-app .z-barctl { display: none; } }

/* The last 31px at exactly 1024 -- iPad landscape and every small laptop.
   Six tabs at 10px of side padding and a 10px gap between bar items is more
   air than that width can pay for. Measured, not guessed. */
@media (max-width: 1100px) {
  .kn-app .z-bar { gap: 7px; }
  .kn-app .z-bar .z-tab { padding: 0 7px; }
}

/* ── the brand reads as a button, because it is one ───────────────────────
   It already went home on click and looked like a static logo, so nobody
   clicked it. Now it says so on hover and spins once when used. */
.kn-app .z-brand { border-radius: 10px; padding: 2px 6px; margin-left: -6px;
  transition: background .18s ease; }
.kn-app .z-brand:hover { background: rgba(139,48,239,.1); }
.kn-app .z-brand:active { transform: translateY(1px); }
.kn-app .z-brand .z-brand-mark { transition: transform .5s cubic-bezier(.3,.8,.3,1); }
.kn-app .z-brand.spin .z-brand-mark { transform: rotate(360deg); }
@media (prefers-reduced-motion: reduce) {
  .kn-app .z-brand .z-brand-mark { transition: none; }
}
.z-walbtn[aria-expanded="true"] { background: rgba(139,48,239,.26); color: var(--kn-text); }

.z-walpanel {
  position: fixed; right: 14px; top: 52px; z-index: 80;
  /* narrower: this is a menu, not a page. 430 was sized for the old wallet
     panel's table of accounts; the account menu is a column of short rows
     and a balance, and the extra 110px only made the rows look stranded. */
  width: min(286px, calc(100vw - 28px));
  border-radius: 12px; overflow: hidden;
  background: linear-gradient(180deg, var(--kn-panel-hi), var(--kn-panel-lo));
  box-shadow: 0 0 0 1px var(--kn-line), 0 26px 60px rgba(0,0,0,.66);
}
.z-walpanel[hidden] { display: none; }
.z-wp-h { display: flex; align-items: center; gap: 9px; padding: 15px 17px;
  border-bottom: 1px solid var(--kn-line); }
.z-wp-h h4 { margin: 0; font-size: 13px; font-weight: 600; letter-spacing: .13em;
  text-transform: uppercase; color: var(--kn-muted); }
/* NOT SCOPED TO ITS OLD PARENT. This was `.z-wp-h .z-wp-x`, and when the panel
   header became .z-me-id the descendant selector stopped matching — so the
   close button fell back to a browser-default bordered square sitting in the
   corner of the account menu. A control's own class is enough to style it. */
.kn-app .z-wp-x { margin-left: auto; flex: 0 0 auto; width: 26px; height: 26px;
  border: 0; border-radius: 7px; cursor: pointer; padding: 0;
  background: rgba(255,255,255,.04); color: var(--kn-faint);
  display: flex; align-items: center; justify-content: center; transition: all .14s; }
.kn-app .z-wp-x:hover { color: var(--kn-text); background: rgba(139,48,239,.2); }
.z-wp-b { padding: 16px 17px 17px; }

/* the totals block */
.z-wp-tot { display: grid; grid-template-columns: 1fr 1fr; gap: 1px;
  background: var(--kn-line); border: 1px solid var(--kn-line);
  border-radius: 10px; overflow: hidden; margin-bottom: 12px; }
.z-wp-tot > div { background: var(--kn-panel-lo); padding: 14px 16px; }
.z-wp-tot .k { margin: 0; font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--kn-faint); }
/* both of these are money, so both take the green and its glow */
.z-wp-tot .v { margin: 8px 0 0; font-size: 23px; font-weight: 700; line-height: 1;
  color: var(--kn-green); text-shadow: var(--kn-glow-green);
  font-variant-numeric: tabular-nums; }

/* one connected wallet */
.z-wp-row { display: flex; align-items: center; gap: 12px; padding: 12px 2px;
  border-top: 1px solid var(--z-line-soft); }
.z-wp-row:first-of-type { border-top: 0; }
.z-wp-ic { flex: none; width: 34px; height: 34px; border-radius: 9px;
  display: flex; align-items: center; justify-content: center;
  background: var(--z-raised); color: var(--kn-muted); }
.z-wp-t { flex: 1; min-width: 0; }
.z-wp-t b { display: block; font-size: 14.5px; font-weight: 600; color: var(--kn-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.z-wp-t span { display: block; font-size: 12.5px; color: var(--kn-faint);
  font-variant-numeric: tabular-nums; }
.z-wp-amt { flex: none; text-align: right; font-size: 15px; font-weight: 600;
  color: var(--kn-dim); font-variant-numeric: tabular-nums; }
/* the dollar figure under each wallet is money too */
.z-wp-amt small { display: block; margin-top: 2px; font-size: 13px; font-weight: 600;
  color: var(--kn-green); text-shadow: var(--kn-glow-green); }
.z-wp-copy { flex: none; width: 26px; height: 26px; border: 0; border-radius: 7px;
  cursor: pointer; background: none; color: var(--kn-faint);
  display: flex; align-items: center; justify-content: center; }
.z-wp-copy:hover { background: rgba(139,48,239,.16); color: var(--kn-text); }

.z-wp-none { padding: 6px 2px 14px; font-size: 13.5px; color: var(--kn-faint); line-height: 1.55; }
.z-wp-act { display: flex; gap: 8px; margin-top: 15px; }
.z-wp-act .kn-btn { flex: 1; justify-content: center; padding: 10px 14px; font-size: 13.5px; }
.z-wp-note { margin: 12px 0 0; font-size: 12px; color: var(--kn-faint); line-height: 1.55; }

/* ── social links ──────────────────────────────────────────────────────── */
.z-socs { display: inline-flex; align-items: center; gap: 3px; }
/* 30px hit area inside a 34px bar -- the glyph itself is set on .z-soc svg so
   the two stay in step; sizing only the box would leave a small mark floating
   in a big button. */
.z-soc { width: 30px; height: 30px; border: 0; border-radius: 7px; cursor: pointer;
  background: none; color: var(--kn-faint); padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  transition: background .15s, color .15s; }
/* CSS beats the inline width/height attributes on the <svg>, so the glyph size
   lives here next to the box it sits in rather than in the markup that builds
   it. */
.z-soc svg { width: 19px; height: 19px; }
.z-soc:hover { background: rgba(139,48,239,.18); color: var(--kn-text); }
/* ── app tools: theme wheel + bug report ────────────────────────────────
   Same 30px box as a social link -- they sit in the same row and a second
   size there reads as one of them being broken. The rule after them is what
   says "these two are ours, those four go somewhere else". */
/* The rule that used to hang off the right of this is gone -- the group
   separators above draw all of them, the same way. */
.z-tools { display: inline-flex; align-items: center; gap: 3px; position: relative; }
.z-tool[data-tool="bug"]:hover { color: var(--kn-red); background: rgba(242,85,74,.14); }
/* The bar is overflow:hidden, so anything past the right edge is simply gone --
   it does not scroll, it disappears. Measured at 375px the right group ran to
   425px, 50px off-screen, and the two buttons this block adds were what pushed
   it there. On a phone the four social links are the ones to drop: they are
   outbound links, while Theme and Report a bug do something. The label on the
   watchlist button goes too, which buys back another 46px. */
/* ── WHAT THE TICKER DROPS WHEN IT RUNS OUT OF ROOM ───────────────────────
   Measured, at three widths. The coin strip already shrinks to nothing, so the
   overflow is the right-hand group itself: 550px of controls in a 700px bar,
   and 465px of bar content in a 360px window. That was true BEFORE the Log in
   button joined it — the socials were simply being clipped off the end and
   nobody missed them, because .z-ticker is overflow:hidden and says nothing.

   Now the order is deliberate: decoration goes first, function last. Links to
   other people's platforms, then the word "Feeds" (the coloured dot carries the
   status on its own), then the tools that are not phone tasks. Log in is the
   last thing standing, because it is the only copy of itself. */
@media (max-width: 760px) {
  .kn-app .z-socs, .kn-app #z-sep-socs { display: none; }
  .kn-app .z-feed span { display: none; }
}
@media (max-width: 560px) {
  .kn-app [data-tool="theme"], .kn-app [data-tool="store"] { display: none; }
}
@media (max-width: 420px) {
  .kn-app [data-tool="wallet"] { display: none; }
}
@media (max-width: 560px) {
  .kn-app .z-tick-r { gap: 3px; }
  .kn-app .z-sep { height: 18px; margin: 0 3px; }
  .kn-app .z-watch-btn > span { display: none; }
  /* The coins hold the far-left slot at flex:none, which is right on a desktop
     and wrong on a phone: three prices need 263px of a 375px bar and simply
     pushed the Live dot and both tool buttons off the end (the bar is
     overflow:hidden, so "off the end" means gone). Here they shrink and scroll
     instead -- every coin still reachable, nothing else displaced.
     Scrollbars hidden because overflow-x:auto also computes overflow-y:auto and
     reserves a ~10px gutter inside a 34px bar. */
  .kn-app .z-tick-l { flex: 1 1 auto; min-width: 0; gap: 14px;
    overflow-x: auto; scrollbar-width: none; }
  .kn-app .z-tick-l::-webkit-scrollbar { display: none; }
  .kn-app .z-ticker { gap: 10px; padding: 0 12px; }
}
/* The mode switch. It lives here rather than in the top bar because this pill
   is already where the eye goes to ask "is this thing on", and the answer to
   that question and "is this money real" are the same answer. */
.kn-app .z-tick-live { display: inline-flex; align-items: center; gap: 6px;
  border: 0; cursor: pointer; font: inherit; font-size: 12px; font-weight: 700;
  letter-spacing: .06em; padding: 4px 10px; border-radius: 8px;
  color: var(--kn-green); background: rgba(46,230,168,.1);
  box-shadow: inset 0 0 0 1px rgba(46,230,168,.3); transition: all .15s; }
.kn-app .z-tick-live:hover { background: rgba(46,230,168,.2); }
.kn-app .z-tick-live i { width: 5px; height: 5px; border-radius: 50%;
  background: var(--kn-green); box-shadow: 0 0 8px var(--kn-green);
  animation: kn-heart 1.8s ease-in-out infinite; }
.kn-app .z-tick-live b { font-weight: 700; letter-spacing: 0;
  font-variant-numeric: tabular-nums; }
/* demo is red, and says so loudly. Being unsure whether the money is real is
   the one confusion this app must never allow. */
.kn-app .z-tick-live.demo { color: var(--kn-red); background: rgba(242,85,74,.12);
  box-shadow: inset 0 0 0 1px rgba(242,85,74,.42); }
.kn-app .z-tick-live.demo:hover { background: rgba(242,85,74,.22); }
.kn-app .z-tick-live.demo i { background: var(--kn-red);
  box-shadow: 0 0 8px var(--kn-red); }

/* ═══════════════════ THE ACCOUNT BUTTON AND ITS MENU ═══════════════════
   One place for who you are, which account you are trading, and everything
   you do to either. The ring around the avatar carries the state the old
   bottom-right chip carried: green is real money, red is the practice desk.
   Being unsure which one you are on is the confusion this app must never
   allow, so it is on screen at all times and does not need a hover. */
.kn-app .z-mebtn { position: relative; display: inline-flex; align-items: center;
  justify-content: center; width: 34px; height: 34px; padding: 0; border: 0;
  cursor: pointer; font: inherit; border-radius: 50%; background: none;
  box-shadow: 0 0 0 2px rgba(46,230,168,.55); transition: box-shadow .15s, transform .15s; }
.kn-app .z-mebtn:hover { transform: translateY(-1px); }
.kn-app .z-mebtn.demo { box-shadow: 0 0 0 2px rgba(242,85,74,.6); }
.kn-app .z-mebtn.live { box-shadow: 0 0 0 2px rgba(46,230,168,.6),
  0 0 14px -4px rgba(46,230,168,.75); }
.kn-app .z-mebtn-av { display: block; width: 30px; height: 30px; border-radius: 50%;
  overflow: hidden; display: grid; place-items: center; font-size: 13px;
  font-weight: 800; color: var(--kn-text);
  background: var(--z-purple, linear-gradient(180deg,#6b21c4,#1b0d33)); }
.kn-app .z-mebtn-av img, .kn-app .z-mebtn-av .z-avatarimg {
  width: 100%; height: 100%; object-fit: cover; display: block; }
/* the dot repeats the ring for anyone who cannot pick a 2px colour out */
/* ── NOT .z-me-n. THAT NAME WAS ALREADY TAKEN. ────────────────────────────
   .z-me-n is the account menu's profile NAME (see the 16px rule further up),
   and leaderboard.js falls back to document.querySelector(".z-me-n, ...") to
   find out what you are called. Nothing had rendered that class since the
   top-bar chip was removed -- its own comment says so -- so this pill became
   the only match, and with unread messages showing, your name on the profile
   card would have come back as the unread count.

   Trap 17, and the second time this session: grep a class before you name one.

   The unread count beside "Friends" in the account menu. Same figure as the
   dot on the avatar; it takes the right-hand end of the row so the label and
   the icon keep the alignment every other row in the menu has. */
.kn-app .z-me-unread { margin-left: auto; font-style: normal; font-size: 10.5px;
  font-weight: 800; color: var(--kn-green); padding: 1px 7px; border-radius: 999px;
  background: rgba(46,230,168,.14); font-variant-numeric: tabular-nums; }
.kn-app .z-mebtn-dot { position: absolute; right: -1px; bottom: -1px; width: 9px;
  height: 9px; border-radius: 50%; background: var(--kn-green);
  box-shadow: 0 0 0 2px #0a0910; }
.kn-app .z-mebtn.demo .z-mebtn-dot { background: var(--kn-red); }

/* the menu itself reuses .z-walpanel's box; these are its insides */
.kn-app .z-me-id { display: flex; align-items: center; gap: 10px; padding: 14px 15px 12px; }
.kn-app .z-me-av { width: 34px; height: 34px; flex: 0 0 auto; border-radius: 50%;
  overflow: hidden; display: grid; place-items: center; font-size: 14px;
  font-weight: 800; color: var(--kn-text);
  background: var(--z-purple, linear-gradient(180deg,#6b21c4,#1b0d33)); }
.kn-app .z-me-av img, .kn-app .z-me-av .z-avatarimg {
  width: 100%; height: 100%; object-fit: cover; display: block; }
.kn-app .z-me-t { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.kn-app .z-me-t b { font-size: 14px; color: var(--kn-text); }
.kn-app .z-me-t span { font-size: 11.5px; color: var(--kn-faint); }
.kn-app .z-me-sec { padding: 12px 15px; border-top: 1px solid var(--kn-line); }
.kn-app .z-me-sec.last { padding-bottom: 14px; }
.kn-app .z-me-lbl { margin: 0 0 8px; font-size: 10px; letter-spacing: .13em;
  text-transform: uppercase; color: var(--kn-faint); }
/* the switch: two halves of one control, so it reads as a choice already made
   rather than as two buttons you might press by accident */
/* ── ONE SWITCH, NOT TWO BUTTONS ─────────────────────────────────────────
   The segmented pair read as two things you could press, which is the wrong
   shape for a state you are already in: nothing about it said "you are HERE
   and could be THERE". A sliding thumb does — the state is a position, and
   moving it is obviously one action rather than a choice between two. */
/* STACKED, LIVE ON TOP. Side by side, the pair had to be read left to right
   to work out which one you were on; stacked it reads as two accounts with
   one of them lit, and the panel gets to be narrower for it. */
.kn-app .z-me-acct { display: flex; align-items: stretch; gap: 8px; }
.kn-app .z-me-seg { position: relative; display: grid; flex: 1 1 auto;
  grid-template-rows: 1fr 1fr; padding: 3px; border-radius: 14px;
  background: rgba(6,5,11,.75); box-shadow: inset 0 0 0 1px var(--kn-line);
  cursor: pointer; }
/* the thumb, sitting under the labels and sliding between them */
.kn-app .z-me-seg::before { content: ""; position: absolute; z-index: 0;
  left: 3px; right: 3px; top: 3px; height: calc(50% - 3px); border-radius: 11px;
  background: rgba(242,85,74,.16); box-shadow: inset 0 0 0 1px rgba(242,85,74,.5);
  transform: translateY(100%);
  transition: transform .2s cubic-bezier(.4,0,.2,1), background .2s, box-shadow .2s; }
/* live is the FIRST row now, so live is the thumb at rest */
.kn-app .z-me-seg.live::before { transform: none;
  background: rgba(46,230,168,.16); box-shadow: inset 0 0 0 1px rgba(46,230,168,.55); }
/* wiping the practice desk: quiet until you are on it, loud once it is armed */
.kn-app .z-me-reset { flex: 0 0 auto; align-self: center; border: 0; cursor: pointer;
  font: inherit; font-size: 11.5px; font-weight: 700; padding: 8px 13px; border-radius: 10px;
  color: var(--kn-faint); background: rgba(6,5,11,.75);
  box-shadow: inset 0 0 0 1px var(--kn-line);
  transition: color .16s, background .16s, box-shadow .16s; }
.kn-app .z-me-reset:hover { color: var(--kn-red); }
.kn-app .z-me-reset.armed { color: var(--kn-red); background: rgba(242,85,74,.14);
  box-shadow: inset 0 0 0 1px rgba(242,85,74,.55); }
.kn-app .z-me-segb { position: relative; z-index: 1; border: 0; cursor: pointer;
  font: inherit; font-size: 12.5px; font-weight: 700; padding: 8px 0;
  border-radius: 999px; color: var(--kn-faint); background: none;
  transition: color .16s; }
.kn-app .z-me-segb:hover { color: var(--kn-muted); }
.kn-app .z-me-segb.on { color: var(--kn-red); }
.kn-app .z-me-segb.live.on { color: var(--kn-green); }
/* the figure sits ABOVE the switch now, and carries no caption -- the control
   directly under it already says which account it belongs to */
.kn-app .z-me-bal { display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 9px; }
.kn-app .z-me-bal span { font-size: 12px; color: var(--kn-faint); }
.kn-app .z-me-bal b { font-size: 19px; font-weight: 700; color: var(--kn-text);
  font-variant-numeric: tabular-nums; }
.kn-app .z-me-sub { display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-top: 5px; font-size: 11.5px; color: var(--kn-faint); }
.kn-app .z-me-sub code { cursor: pointer; font-family: ui-monospace, monospace;
  color: var(--kn-dim); }
.kn-app .z-me-sub code:hover { color: var(--kn-text); }
/* Deposit, beside the balance it changes */
.kn-app .z-me-dep { margin-left: 10px; border: 0; cursor: pointer; font: inherit;
  font-size: 11.5px; font-weight: 700; padding: 5px 11px; border-radius: 8px;
  color: var(--kn-green); background: rgba(46,230,168,.12);
  box-shadow: inset 0 0 0 1px rgba(46,230,168,.45); }
.kn-app .z-me-dep:hover { background: rgba(46,230,168,.22); }
.kn-app .z-me-note { margin: 9px 0 0; font-size: 11.5px; line-height: 1.55;
  color: var(--kn-faint); }
/* the rows: full-width targets, because a menu item you have to aim at is a
   menu item people miss */
.kn-app .z-me-item { display: flex; align-items: center; gap: 10px; width: 100%;
  border: 0; cursor: pointer; font: inherit; font-size: 13px; font-weight: 600;
  text-align: left; padding: 9px 10px; margin: 0 -10px; border-radius: 9px;
  color: var(--kn-muted); background: none; }
.kn-app .z-me-item + .z-me-item { margin-top: 1px; }
.kn-app .z-me-item:hover { color: var(--kn-text); background: rgba(139,48,239,.16); }
.kn-app .z-me-item svg { flex: 0 0 auto; opacity: .8; }
.kn-app .z-me-item.danger { color: var(--kn-red); }
.kn-app .z-me-item.danger:hover { background: rgba(242,85,74,.14); }

/* ── NO NATIVE SPINNERS, ANYWHERE ─────────────────────────────────────────
   Chrome draws its own little grey up/down arrows inside every number input.
   They are the browser's chrome, not this app's: they ignore the theme, sit at
   a fixed size, and appear white against every dark panel here. Three modules
   had already killed them on their own inputs one class at a time, which meant
   every new number field arrived with them again.

   Killed once, app-wide. Where a control genuinely wants steppers it draws its
   own -- see .zac-num and the rule fields, which are typed into rather than
   nudged. */
.kn-app input[type=number]::-webkit-outer-spin-button,
.kn-app input[type=number]::-webkit-inner-spin-button {
  -webkit-appearance: none; appearance: none; margin: 0; }
.kn-app input[type=number] { -moz-appearance: textfield; appearance: textfield; }

/* Online. Same shape as the feed badge beside it -- they are both status,
   and two different-looking status chips in one corner reads as clutter. */
.kn-app .z-online { display: inline-flex; align-items: center; gap: 6px; border: 0;
  cursor: default; font: inherit; font-size: 12px; padding: 4px 9px;
  border-radius: 8px; color: var(--kn-muted); background: rgba(255,255,255,.03);
  box-shadow: inset 0 0 0 1px var(--kn-line); }
/* the dot is gone -- the person IS the indicator, and two green things saying
   the same thing was one too many.
   DIRECT CHILDREN ONLY: the hover panel is inside this button and has its own
   <b> counts and <i> dots. Unscoped, these two rules hid them -- the panel
   opened reading "online / offline" with no numbers and no colour. */
.kn-app .z-online > i { display: none; }
.kn-app .z-online-p { width: 13px; height: 13px; flex: none; color: var(--kn-green);
  filter: drop-shadow(0 0 5px rgba(46,230,168,.75)); }
.kn-app .z-online span { font-weight: 700; color: var(--kn-text);
  font-variant-numeric: tabular-nums; }
.kn-app .z-online > b { display: none; }
.kn-app .z-online:hover .z-online-p {
  filter: drop-shadow(0 0 8px rgba(46,230,168,.95)); }
/* ── THE HOVER IS OURS, NOT THE BROWSERS ─────────────────────────────────
   Opens UPWARD: this button lives in the bottom bar, so a panel hanging below
   it would be off screen. .z-ticker is overflow:hidden, which would clip a
   child -- so the panel is positioned against the button and allowed out by
   the rule below, which lifts the button out of the clip for exactly as long
   as it is hovered. */
/* a social with no destination yet -- visible, and visibly not a link */
.kn-app .z-soc-off { opacity: .38; cursor: default; }
.kn-app .z-soc-off:hover { opacity: .5; }
.kn-app .z-online { position: relative; }
.kn-app .z-online-tip {
  position: absolute; bottom: calc(100% + 9px); right: -4px; z-index: 90;
  display: flex; flex-direction: column; gap: 5px; width: max-content;
  max-width: 260px; padding: 10px 13px; border-radius: 11px; text-align: left;
  background: linear-gradient(180deg, var(--kn-panel-hi), var(--kn-panel-lo));
  box-shadow: 0 0 0 1px var(--kn-line), 0 14px 34px rgba(0,0,0,.7);
  opacity: 0; visibility: hidden; transform: translateY(3px);
  transition: opacity .13s, transform .13s, visibility .13s;
  pointer-events: none; }
.kn-app .z-online:hover .z-online-tip,
.kn-app .z-online:focus-visible .z-online-tip {
  opacity: 1; visibility: visible; transform: none; }
.kn-app .z-online-tr { display: flex; align-items: center; gap: 8px;
  font-size: 12px; color: var(--kn-muted); white-space: nowrap; }
.kn-app .z-online-tr b { font-weight: 700; color: var(--kn-text);
  font-variant-numeric: tabular-nums; }
.kn-app .z-online-tr i { width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: var(--kn-line); }
.kn-app .z-online-tr i.on { background: var(--kn-green);
  box-shadow: 0 0 7px var(--kn-green); }
/* the bar clips its children; a hovered badge is let out for the duration */
.kn-app .z-ticker:has(.z-online:hover) { overflow: visible; }

/* ── THE SEARCH DECIDES WHAT IT IS SEARCHING FIRST ────────────────────────
   One row of kinds under the field. Whichever is lit is the only kind of
   thing the results can contain, so an empty field can sit empty instead of
   filling with every page in the app. */
.kn-app .z-cmd-tabs { flex: none; display: flex; gap: 6px;
  padding: 9px 14px 11px; border-bottom: 1px solid var(--kn-line); }
.kn-app .z-cmd-tab { border: 0; cursor: pointer; font: inherit; font-size: 12px;
  font-weight: 700; padding: 6px 14px; border-radius: 999px;
  color: var(--kn-faint); background: rgba(255,255,255,.03);
  box-shadow: inset 0 0 0 1px var(--kn-line);
  transition: color .16s, background .16s, box-shadow .16s; }
.kn-app .z-cmd-tab:hover { color: var(--kn-muted); }
/* ── THE SELECTED LOOK, AND THE ONLY ONE ──────────────────────────────────
   A filled purple pill was rejected twice. What Landon wants -- and what the
   ticket's amount presets have always done -- is a DARK panel with a purple
   ring: the selection is a lit edge, not a slab of colour. Purple is a
   surface, never a fill you drop text on.

   Copied deliberately from .ztk-presets button.on so the two cannot drift. */
.kn-app .z-cmd-tab.on { color: var(--kn-text);
  background: linear-gradient(180deg, rgba(22,16,38,.9), rgba(12,8,22,.8));
  box-shadow: inset 0 0 0 1px rgba(107,39,214,.6); }
.kn-app .z-cmd-tab[disabled] { opacity: .45; cursor: not-allowed; }
.kn-app .z-cmd-tab[disabled]:hover { color: var(--kn-faint); }

/* Feed health. Real status of the three services this app reads, not a
   decorative "connected" badge. */
.kn-app .z-feed { display: inline-flex; align-items: center; gap: 6px; border: 0;
  cursor: default; font: inherit; font-size: 12px; padding: 4px 9px;
  border-radius: 8px; color: var(--kn-faint); background: rgba(255,255,255,.03);
  box-shadow: inset 0 0 0 1px var(--kn-line);
  /* the row itself sets gap:14px, so pull back against it -- these two belong
     together as one status cluster, not as two separate items in the row */
  margin-right: -8px; }
.kn-app .z-feed i { width: 5px; height: 5px; border-radius: 50%;
  background: var(--kn-faint); }
.kn-app .z-feed.ok { color: var(--kn-green); }
.kn-app .z-feed.ok i { background: var(--kn-green); box-shadow: 0 0 7px var(--kn-green); }
.kn-app .z-feed.slow { color: #f0aa3c; }
.kn-app .z-feed.slow i { background: #f0aa3c; box-shadow: 0 0 7px #f0aa3c; }
.kn-app .z-feed.down { color: var(--kn-red); }
.kn-app .z-feed.down i { background: var(--kn-red); box-shadow: 0 0 7px var(--kn-red);
  animation: kn-heart 1.4s ease-in-out infinite; }

/* ── command palette ────────────────────────────────────────────────────── */
.z-cmd {
  position: fixed; inset: 0; z-index: 95; display: flex; justify-content: center;
  padding: 12vh 20px 20px; background: rgba(0,0,0,.7);
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
}
.z-cmd[hidden] { display: none; }
.z-cmd-box {
  width: min(560px, 100%); max-height: 62vh; display: flex; flex-direction: column;
  --ring: var(--kn-ring-panel); border-radius: 16px; overflow: hidden; background: var(--z-card);
  box-shadow: 0 40px 90px rgba(0,0,0,.9);
}
.z-cmd-head { flex: none; display: flex; align-items: center; gap: 11px;
  padding: 14px 16px; box-shadow: inset 0 -1px 0 var(--z-raised); }
.z-cmd-head svg { width: 16px; height: 16px; flex: none; color: var(--kn-faint); }
.z-cmd-head input { flex: 1; min-width: 0; border: 0; background: none; outline: none;
  color: var(--kn-text); font-family: inherit; font-size:17px; padding: 0; }
.z-cmd-head input::placeholder { color: var(--kn-faint); }
.z-cmd-head kbd { font-family: inherit; font-size:11px; padding: 2px 6px; border-radius: 5px;
  color: var(--kn-dim); box-shadow: inset 0 0 0 1px var(--z-line); flex: none; }
.z-cmd-list { flex: 1; min-height: 0; overflow-y: auto; padding: 6px; }
.z-cmd-i { display: flex; align-items: center; gap: 11px; width: 100%; text-align: left;
  border: 0; background: none; cursor: pointer; padding: 9px 11px; border-radius: 9px;
  color: var(--kn-text); }
.z-cmd-i:hover, .z-cmd-i.sel { background: rgba(167,139,250,.1); }
.z-cmd-k { flex: none; width: 26px; height: 26px; border-radius: 8px; display: grid;
  place-items: center; font-size:11px; font-weight: 700; color:var(--kn-muted);
  background: rgba(167,139,250,.1); box-shadow: inset 0 0 0 1px rgba(139,92,246,.3); }
.z-cmd-t { flex: 1; min-width: 0; font-size:15px; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; }
.z-cmd-t em { font-style: normal; color: var(--kn-faint); font-size:13px; margin-left: 8px; }
.z-cmd-v { flex: none; font-size:14px; font-variant-numeric: tabular-nums; }
.z-cmd-none { padding: 26px; text-align: center; font-size:14px; color: var(--kn-muted); }

/* ============================== MAIN ===================================== */
/* knull.css gives every .kn-app child `position:relative;z-index:1`, which makes
   .z-main a STACKING CONTEXT — so a drawer inside it (z-index:61) was still
   losing to the sidebar's z-index:40. On desktop the rail and the content never
   overlap, so main can safely outrank it and let drawers cover the full window.
   Under 1000px the sidebar goes position:fixed and must win, so main stays low. */
.z-main {
  /* min-height:0 is load-bearing. A column flex item defaults to
     min-height:auto, which refuses to shrink below its content -- so main grew
     to the full page, pushed the ticker to document y 2028, and the window
     scrolled again. .z-view can only become the scroll container if its parent
     is allowed to be shorter than what it holds. */
  flex: 1; min-width: 0; min-height: 0;
  display: flex; flex-direction: column;
  z-index: 2;
}

/* view frame — pages cross-fade (theme rule: no loaders, no data-rain) */
/* THE SCROLL CONTAINER. It has to be this element and not the window: body is
   height:100% and cannot grow, so content that overflowed it used to spill out
   of the box entirely, and the sticky ticker -- clamped to that box -- stopped
   44px short of the floor with rows hidden underneath. Scrolling here instead
   means the ticker is the last row of the shell and sits on the floor by
   construction. */
.z-view {
  flex: 1; min-height: 0;
  overflow-y: auto; overflow-x: hidden;
  padding: 16px 22px 22px;
}
.kn-page { opacity: 0; transition: opacity .22s ease; }
.kn-page.on { opacity: 1; }

/* ============================ SHARED BITS ================================ */
/* section header used at the top of most views */
.z-viewhead {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin: 0 0 14px;
}
.z-viewhead .z-vh-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
/* A PILL DRAWN AS A <button> KEEPS THE BROWSER'S OWN CHROME unless told not
   to. knull.css's .kn-pill sets colour, padding and a radius and nothing else,
   so any module that reached for <button> instead of <span> shipped a grey
   embossed OS button with the pill's text inside it. journal.js did it twice —
   the Wins/Losses/Open row and the "how you felt" picker — and tradingdash.js
   does it too. dashboard.js had already worked around it privately.

   DELIBERATELY NOT .kn-app-PREFIXED. At (0,1,1) this sits BELOW .kn-pill.on
   and .kn-pill:hover at (0,2,0), so the active fill and the hover still win;
   prefixing it would raise it above them and flatten every selected pill in
   the app. Only the user-agent chrome is reset here. */
button.kn-pill {
  -webkit-appearance: none; appearance: none;
  border: 0; background: none;
  font-family: inherit; line-height: 1.5;
}

/* THE BALANCE, WITH THE BUTTON UNDER IT. Only the word "cash" was meant to
   go; removing the figure with it left the top bar with no balance at all.
   The button wears the terminal's own Buy colours on purpose: same green,
   same near-black plate, same lit rim, so the one place in the app that asks
   for money looks like the one place that spends it. */
.kn-app .z-cashcol { display: flex; flex-direction: column; align-items: flex-end;
  gap: 3px; }
.kn-app .z-cashcol > b { line-height: 1.1; }
.kn-app .z-deposit {
  -webkit-appearance: none; appearance: none; border: 0; cursor: pointer;
  font: inherit; font-size: 12.5px; font-weight: 800; letter-spacing: .02em;
  height: 22px; padding: 0 10px; border-radius: 7px; white-space: nowrap;
  color: var(--kn-green, #2ee6a8);
  background: linear-gradient(180deg, #0d2a20, #07160f);
  box-shadow: inset 0 0 0 1px rgba(46,230,168,.55);
  transition: box-shadow .16s ease, transform .12s ease;
}
.kn-app .z-deposit:hover {
  box-shadow: inset 0 0 0 1px rgba(46,230,168,.9), 0 0 20px -6px rgba(46,230,168,.7);
}
.kn-app .z-deposit:active { transform: translateY(1px); }
@media (max-width: 860px) { .kn-app .z-deposit { display: none; } }

/* ── WHO ELSE IS HERE ────────────────────────────────────────────────────
   A lit green figure and a count, left of the SOL balance. The glow is the
   point: it is the only thing in the bar that says the app is alive rather
   than reporting on something that happened. */
/* The "people online" chip is gone with the invented count behind it. */


/* the explanation, mounted on BODY -- a fixed child of a transformed ancestor
   positions against that ancestor, and the top bar is inside the app shell */
.z-tip {
  position: fixed; left: 0; top: 0; z-index: 200; max-width: 300px;
  padding: 12px 14px; border-radius: 11px; background: #0b0912;
  box-shadow: inset 0 0 0 1px rgba(46,230,168,.34), 0 26px 60px -22px #000;
  pointer-events: none; opacity: 0; transform: translateY(4px);
  transition: opacity .13s ease, transform .13s ease;
}
.z-tip.on { opacity: 1; transform: translateY(0); }
.z-tip b { display: block; font-size: 12.5px; font-weight: 700;
  color: var(--kn-text); margin-bottom: 6px; }
.z-tip span { display: block; font-size: 12px; line-height: 1.55;
  color: var(--kn-muted); }
.z-tip i { display: block; margin-top: 8px; padding-top: 8px; font-style: normal;
  border-top: 1px solid var(--kn-row); font-size: 11.5px; line-height: 1.55;
  color: var(--kn-faint); }

.z-pillrow { display: flex; align-items: center; gap: 4px; }

/* generic empty state */
.z-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px; padding: 44px 20px; text-align: center;
}
.z-empty svg { opacity: .35; }
.z-empty p { margin: 0; font-size:14px; color: var(--kn-muted); }

/* Scrim for the mobile drawer. MUST sit BELOW the nav — at 45 it
   covered the sidebar it was meant to dim, so every nav button was unclickable
   under 1000px (elementFromPoint over "Calendar" returned the scrim). */
.z-scrim {
  position: fixed; inset: 0; z-index: 35;
  background: rgba(0, 0, 0, .62); backdrop-filter: blur(2px);
  opacity: 0; transition: opacity .2s ease;
}
.z-scrim.on { opacity: 1; }

/* ============================ RESPONSIVE ================================= */
@media (max-width: 1180px) {
  .z-search { max-width: 240px; }
  .z-me-t { display: none; }
  .z-tab { padding: 8px 10px; font-size:14px; }
}
@media (max-width: 1000px) {
  /* the tab row becomes a drawer; the bar keeps brand, search and wallet */
  .z-burger { display: inline-flex; align-items: center; }
  .z-nav {
    position: fixed; left: 0; top: 68px; bottom: 34px; width: 210px; z-index: 58;
    flex-direction: column; align-items: stretch; gap: 3px; padding: 12px;
    background: var(--z-card); box-shadow: 8px 0 60px rgba(0,0,0,.85), inset -1px 0 0 var(--z-raised);
    transform: translateX(-102%); transition: transform .26s cubic-bezier(.4,0,.2,1);
  }
  .z-nav.open { transform: none; }
  .z-tab { text-align: left; padding: 11px 13px; font-size:16px; }
  .z-tab.on::after { display: none; }
  .z-brand-name { display: none; }
  .z-view { padding: 10px 12px 16px; }
  .z-ctx { padding: 10px 12px 0; }
}
@media (max-width: 700px) {
  /* THE CLUSTER GOES, THE ACCOUNT STAYS. Hiding .z-wallet wholesale used to be
     harmless: the way into your account was a button in the ticker. That
     button is now the avatar inside this cluster, so hiding the lot left a
     phone with no way to log in, log out, or switch between the practice desk
     and real money at all.

     The balance chips are what crowd a narrow bar. A 34px circle is not. */
  .z-wallet { display: flex; gap: 0; }
  .z-wallet .z-wallet-c { display: none; }
  .z-search span { display: none; }
  /* the control is a 44px icon square down here; the pill has nowhere to go */
  .z-search kbd { display: none; }
  .z-search { max-width: 44px; flex: none; }
}

@media (prefers-reduced-motion: reduce) {
  .kn-page, .z-nav, .z-scrim { transition: none !important; }
}

/* ── NEON ENFORCEMENT ─────────────────────────────────────────────────────
   .kn-pos / .kn-neg are the app's semantic gain/loss classes and they must NEVER
   lose. Every module injects its own <style> at runtime, i.e. AFTER this file,
   so a plain module rule like `.zd-num{color:var(--kn-text)}` beat
   `.kn-neg{color:var(--kn-green)}` on source order at equal specificity. The
   result was money that kept its coloured halo but rendered ramp-gray — the
   "glowing red that isn't red" bug.

   !important is deliberate here. These two classes are a cross-cutting semantic,
   not a look a component may override; if a module wants a quieter number it
   should simply not apply .kn-pos/.kn-neg.                                    */
.kn-app .kn-pos {
  color: var(--kn-green) !important;
  text-shadow: var(--kn-glow-green) !important;
}
.kn-app .kn-neg {
  color: var(--kn-red) !important;
  text-shadow: var(--kn-glow-red) !important;
}
/* a gradient number that is also pos/neg keeps the neon, not the gold fill */
.kn-app .kn-pos.kn-gold-grad, .kn-app .kn-neg.kn-gold-grad {
  background: none !important; -webkit-text-fill-color: currentColor !important;
}

/* module-level gain/loss modifiers that never used the semantic classes */
.kn-app .z-cal-tip .tt-n.pos,
.kn-app .z-cal-tip .tt-r.up .val { color: var(--kn-green); text-shadow: var(--kn-glow-green); }
.kn-app .z-cal-tip .tt-n.neg,
.kn-app .z-cal-tip .tt-r.dn .val { color: var(--kn-red); text-shadow: var(--kn-glow-red); }

/* status dots and micro-captions stay quiet — a glowing 9px label is noise */
.kn-app .kn-tiny.kn-pos, .kn-app .kn-tiny.kn-neg,
.kn-app .z-br-status, .kn-app .z-br-addr { text-shadow: none !important; }

/* ── LEADERBOARD ──────────────────────────────────────────────────────── */
/* The leaderboard owns its own styles now — modules/leaderboard.js injects
   them under #z-lb-css in the .zlb-* namespace. Nothing lives here. */

/* ── ACCOUNT ──────────────────────────────────────────────────────────── */
/* The account page owns its styles now — modules/account.js injects them
   under #z-ac-css in the .zac-* namespace. Nothing lives here. */

/* ============================ SECOND ROW =================================
   The views sit one step below the brand. Home owns the top row on its own,
   so this one is deliberately quieter: smaller, no pill, just a rule that
   lights under the active view.                                          */
.z-sub {
  position: sticky; top: 68px; z-index: 54; flex: none;
  display: flex; align-items: center; gap: 2px;
  height: 42px; padding: 0 20px;
  background: var(--z-ground);
  box-shadow: inset 0 -1px 0 var(--z-line);
  overflow-x: auto; scrollbar-width: none;
}
.z-sub::-webkit-scrollbar { display: none; }
.z-sub-sp { flex: 1; min-width: 8px; }
.z-sub-t {
  position: relative; flex: none; border: 0; background: none; cursor: pointer;
  padding: 0 13px; height: 42px; font-family: inherit;
  font-size: 14px; font-weight: 500; color: var(--kn-dim);
  white-space: nowrap; transition: color .15s ease;
}
.z-sub-t:hover { color: var(--kn-text); }
.z-sub-t.on { color:var(--kn-muted); }
.z-sub-t.on::after {
  content: ""; position: absolute; left: 11px; right: 11px; bottom: 0; height: 2px;
  border-radius: 2px 2px 0 0; background: var(--kn-gold);
  box-shadow: 0 0 12px rgba(167,139,250,.65);
}
@media (max-width: 900px) { .z-sub { top: 68px; padding: 0 12px; } }

/* ====================== THE REFERENCE SITE'S SIGNATURE ====================
   tradevisorai is not a gray card system — its CSS tokens say that, but the
   tokens are mostly unused Tailwind defaults. What the page actually does is
   three things, and they are what carry the look:

     1. a pure black ground with ONE large accent bloom from the top, fading
        out before it reaches the fold
     2. cards that are near-black, lit along their top edge, not filled gray
     3. a single accent CTA: a bright vertical gradient with a glow under it,
        which is the only saturated thing on the screen

   The bloom lives on .kn-app::after (above). This block is 2 and 3.       */

/* the primary action, their gold replaced by our violet */
.kn-app .kn-btn.primary,
.kn-app .zh-go.buy, .kn-app .zh-go.sell,
.kn-app .ztr-cta.ztr-cta, .kn-app .ztk-go.ztk-go {
  position: relative;
}
.kn-app .kn-btn.primary {
  --kn-btn-wash: transparent;
  background: linear-gradient(var(--kn-btn-wash), var(--kn-btn-wash)),
              linear-gradient(180deg, var(--z-accent-fill) 0%, var(--z-accent) 46%, var(--z-accent-fill) 100%);
  color: #0c0618;
  box-shadow: 0 8px 26px -10px rgba(139,92,246,.65),
              inset 0 1px 0 rgba(255,255,255,.22);
}
/* was filter: brightness(1.07) -- see the note on .kn-btn:hover in knull.css.
   Same two-layer trick: the accent gradient is stated once above and the hover
   only lifts the wash sitting on top of it. */
.kn-app .kn-btn.primary:hover {
  --kn-btn-wash: rgba(255,255,255,.10);
}

/* Panels get the faintest top-lit wash so a black card on a black page still
   reads as a surface. Kept under 3% — anything more and it turns gray, which
   is the thing the reference deliberately avoids. */
.kn-app .kn-box::before, .kn-app .kn-panel::before { opacity: .92; }
.kn-app .kn-box, .kn-app .kn-panel {
  background-image: linear-gradient(180deg, var(--kn-fill-hi), var(--kn-fill-lo));
}

/* ══════════════════ A COUNT IS NOT MONEY ═══════════════════════════════
   House rule, already written down: money is --kn-green / --kn-red WITH its
   glow; counts take the colour but not the glow. .kn-pos / .kn-neg carry both
   as a unit, so anything that is a tally rather than an amount was wearing
   the money treatment by accident.

   It shows worst exactly where Landon spotted it — the Current run tile, a
   26-34px/700 digit wearing a 6px and an 18px halo at once. At that size the
   two blurs stack into a smear and the number reads soft and out of focus
   beside the crisp type next to it. The glow is sized for a 13-15px figure in
   a row of them, not for one large numeral alone on a tile.

   THESE ARE SEPARATE CLASSES RATHER THAN A MODIFIER ON .kn-pos, AND THAT IS
   NOT A STYLE CHOICE. .kn-app .kn-pos below sets its colour and its glow with
   !important, which no amount of specificity beats — my first attempt was
   `.kn-app .kn-count.kn-pos { text-shadow: none }` and it changed precisely
   nothing, measured identical to the unfixed element. The rule's own comment
   says what to do instead: a component that wants a quieter number should not
   apply .kn-pos at all. So a count says it is a count and gets the colour on
   its own terms. */
.kn-app .kn-count-up { color: var(--kn-green); }
.kn-app .kn-count-dn { color: var(--kn-red); }

/* headings and the hero numbers sit brighter than body copy, the way their
   white display type does against gray sub-labels — still not pure white */
/* headline numbers are the top of the grey ramp — see --z-num */
.kn-app .kn-h1, .kn-app .kn-big, .kn-app .kn-kpi, .kn-app .kn-stat,
.kn-app .zd-hero-val, .kn-app .zh-big, .kn-app .zh-px b,
.kn-app .td-kpi-v, .kn-app .zlb-hero-v { color: var(--kn-white); }

/* ── the tab row ─────────────────────────────────────────────────────────
   Eight tabs, a brand, a search field, the wallet and the profile all share
   one row, so the tabs take the smallest treatment that still reads as
   navigation: no pill, no box, just weight and a lit underline on the active
   one. Anything heavier and the row becomes a wall of chrome above the data
   it is meant to frame. */
.z-nav { display: flex; align-items: center; gap: 1px; flex: none; min-width: 0; }
.z-tab {
  position: relative; flex: none; border: 0; background: none; cursor: pointer;
  padding: 0 13px; height: 40px; border-radius: var(--z-r-box);
  font-family: inherit; font-size: 14px; font-weight: 500; color: var(--z-muted);
  white-space: nowrap; transition: color .15s ease, background .15s ease;
}
.z-tab:hover { color: var(--z-text); background: var(--z-raised); }
.z-tab.on { color:var(--kn-muted); font-weight: 600; }
.z-tab.on::after {
  content: ""; position: absolute; left: 11px; right: 11px; bottom: -1px; height: 2px;
  border-radius: 2px 2px 0 0;
  background: linear-gradient(90deg, var(--z-accent), var(--z-accent-fill), var(--z-accent));
  box-shadow: 0 0 12px var(--z-accent);
}
.z-tab:focus-visible { outline: none; box-shadow: inset 0 0 0 1px var(--z-accent-fill); }

@media (max-width: 1500px) { .z-tab { padding: 0 10px; font-size: 13px; } }
@media (max-width: 1280px) { .kn-app .z-bar .z-search span { display: none; } }

/* the build stamp — quiet enough to ignore, legible enough to read off a
   screenshot, which is the only reason it exists */
.z-build-legacy {
  font-variant-numeric: tabular-nums; letter-spacing: .04em;
  color: var(--z-faint); opacity: .75;
}

/* ── LABELS ARE GREY ─────────────────────────────────────────────────────
   This block used to argue for a purple gradient on accent labels: lit at the
   cap height, sinking to deep at the baseline, on the reasoning that the bright
   top stop is where the eye reads a letterform. The reasoning was sound and the
   result was wrong — that bright top stop IS the pink, and it was on every
   accent label in the app.

   The measurements that settle it, against the card ground #0a0910:
     #a344ff 4.56:1   #8b30ef 3.56:1   #6b27d6 2.72:1
     #7723b4 2.53:1   #66318d 2.25:1   #3d1080 1.50:1   grey #9fa3b5 7.91:1

   Purple is dark in luminance. Small text on a near-black ground needs a LIGHT
   purple to be legible, and a light purple is what reads as pink. No value
   satisfies both. Five rounds of "try a deeper one" failed for that reason and
   not for want of care.

   So labels are grey. Purple keeps every job where contrast is not the
   constraint: the logo, solid button fills, rings, glows, borders and icons. */

/* .kn-link is in the family below and therefore grey too. Colour was the only
   thing marking it clickable, so an underline takes that over — affordance has
   to survive the colour going. */
.kn-app .kn-link{text-decoration:underline;text-underline-offset:3px;
  text-decoration-color:rgba(159,163,181,.4)}
.kn-app .kn-link:hover{text-decoration-color:var(--kn-muted)}

.kn-app :is(
  /* dashboard + account */
  .zd-cap b, .zd-range b, .zd-trio .k.lead, .zd-prof-id p, .zd-day.today .zd-day-n,
  .zac-id .at, .zac-hstats .v,
  /* selected controls, everywhere */
  .kn-pill.on, .zh-stages button.on, .zh-presets button.on, .zpu-amts button.on,
  .ztr-slip button.on, .z-cal-yctoggle button.on, .z-hm-scope button.on,
  .zlb-seg button.on, .zlb-tfs button.on, .zfw-tabs .on b,
  /* terminal, pulse, trade */
  .zh-h button.act, .zh-cp button.zh-copy, .zpu-ch h4, .zpu-f.warn, .zpu-hist b,
  .ztr-tok b, .ztr-r .k, .v.gold, .ztk-fee b, .ztr-note b, .ztk-wa,
  /* journal, leaderboard, follows, connections */
  .kn-link, .z-cd-date, .z-hm-ch h3, .zlb-you-t .tag, .zlb-foot em,
  .zlb-who-n, .zlb-hero .v, .zfw-h-t .v, .zfw-badge,
  .z-br-count
) {
  /* No gradient, no purple. See the note above: no purple is both legible as
     small text on this ground and free of pink. These are labels. */
  background: none;
  -webkit-text-fill-color: var(--kn-muted); color: var(--kn-muted);
}
/* -webkit-text-fill-color INHERITS, but the gradient behind it does not. A
   child of a gradiented element therefore inherits the transparency with
   nothing to show through and disappears — which is exactly what happened to
   .zlb-tag inside .zlb-who-n. Descendants are reset explicitly. */
.kn-app :is(
  /* dashboard + account */
  .zd-cap b, .zd-range b, .zd-trio .k.lead, .zd-prof-id p, .zd-day.today .zd-day-n,
  .zac-id .at, .zac-hstats .v,
  /* selected controls, everywhere */
  .kn-pill.on, .zh-stages button.on, .zh-presets button.on, .zpu-amts button.on,
  .ztr-slip button.on, .z-cal-yctoggle button.on, .z-hm-scope button.on,
  .zlb-seg button.on, .zlb-tfs button.on, .zfw-tabs .on b,
  /* terminal, pulse, trade */
  .zh-h button.act, .zh-cp button.zh-copy, .zpu-ch h4, .zpu-f.warn, .zpu-hist b,
  .ztr-tok b, .ztr-r .k, .v.gold, .ztk-fee b, .ztr-note b, .ztk-wa,
  /* journal, leaderboard, follows, connections */
  .kn-link, .z-cd-date, .z-hm-ch h3, .zlb-you-t .tag, .zlb-foot em,
  .zlb-who-n, .zlb-hero .v, .zfw-h-t .v, .zfw-badge,
  .z-br-count
) * {
  -webkit-text-fill-color: currentColor;
  background: none;
}

/* Moving the pill into the gradient family costs it its selection fill: the
   family sets `background` to the ramp, and background-clip:text needs that
   background to be the ramp. The lit pill's dark fill (knull.css 214) is
   therefore re-hung on ::after, behind the text.

   ::after and not ::before — ::before is already the 1px ring (knull.css 111),
   and quietly stealing it would have deleted the pill's edge instead. An inset
   box-shadow was the other option and is wrong here: it paints over the whole
   padding box, which with background-clip:text means straight over the glyphs. */
.kn-app .kn-pill.on { position: relative; }
.kn-app .kn-pill.on::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit; z-index: -1;
  background: linear-gradient(180deg, rgba(22,16,38,.70), rgba(12,8,22,.62));
  pointer-events: none;
}

/* .kn-pill.on WAS excluded here, on the reasoning that a selected control has
   to read as selected and that a flat colour says so most clearly. Landon
   looked at the lit timeframe pill against the wordmark above it and called the
   purple wrong, which it was — flat --z-accent is brighter and more magenta
   than the ramp everything else in this family uses.

   The affordance worry still stands in general, but not here: the lit pill also
   carries a filled background, so "selected" was never riding on the text
   colour alone. It has been moved into the family.

   Still deliberately OUT: the five avatar letters (.zd-prof-ava, .zlb-ava,
   .zlb-who-a, .zfw-ava, .zfw-h-a). A single capital clipped to a vertical ramp,
   sitting on a purple disc, goes muddy at the bottom -- the disc is already
   carrying the colour and the letter only has to stay legible on it.

   .kn-link and today's calendar date WERE out on the same affordance reasoning.
   Landon pointed straight at a link and asked for every instance of the flat
   tone to become the ramp, so they are in. The affordance argument was not
   wrong, it was just outvoted by the person who has to look at it.

   A P&L value that happens to sit in one of those slots keeps its own colour —
   green and red always outrank decoration. */
/* This guard only covered two members, and it only worked at all because
   .kn-pos/.kn-neg are re-asserted with !important further up -- currentColor
   had something to fall back to. .zac-hstats .v.pos uses a bare `.pos` with no
   such backstop, so when that slot joined the family the LIFETIME NET figure
   lost its green and rendered in the purple ramp instead. Landon spotted it.

   So: the guard now covers the whole family, matches both the semantic classes
   and the bare .pos/.neg some modules use, and states the colour outright
   rather than trusting currentColor to have survived. Green and red always
   outrank decoration. */
.kn-app :is(
  /* dashboard + account */
  .zd-cap b, .zd-range b, .zd-trio .k.lead, .zd-prof-id p, .zd-day.today .zd-day-n,
  .zac-id .at, .zac-hstats .v,
  /* selected controls, everywhere */
  .kn-pill.on, .zh-stages button.on, .zh-presets button.on, .zpu-amts button.on,
  .ztr-slip button.on, .z-cal-yctoggle button.on, .z-hm-scope button.on,
  .zlb-seg button.on, .zlb-tfs button.on, .zfw-tabs .on b,
  /* terminal, pulse, trade */
  .zh-h button.act, .zh-cp button.zh-copy, .zpu-ch h4, .zpu-f.warn, .zpu-hist b,
  .ztr-tok b, .ztr-r .k, .v.gold, .ztk-fee b, .ztr-note b, .ztk-wa,
  /* journal, leaderboard, follows, connections */
  .kn-link, .z-cd-date, .z-hm-ch h3, .zlb-you-t .tag, .zlb-foot em,
  .zlb-who-n, .zlb-hero .v, .zfw-h-t .v, .zfw-badge,
  .z-br-count
):is(.kn-pos, .pos) {
  background: none;
  color: var(--kn-green); -webkit-text-fill-color: var(--kn-green);
}
.kn-app :is(
  /* dashboard + account */
  .zd-cap b, .zd-range b, .zd-trio .k.lead, .zd-prof-id p, .zd-day.today .zd-day-n,
  .zac-id .at, .zac-hstats .v,
  /* selected controls, everywhere */
  .kn-pill.on, .zh-stages button.on, .zh-presets button.on, .zpu-amts button.on,
  .ztr-slip button.on, .z-cal-yctoggle button.on, .z-hm-scope button.on,
  .zlb-seg button.on, .zlb-tfs button.on, .zfw-tabs .on b,
  /* terminal, pulse, trade */
  .zh-h button.act, .zh-cp button.zh-copy, .zpu-ch h4, .zpu-f.warn, .zpu-hist b,
  .ztr-tok b, .ztr-r .k, .v.gold, .ztk-fee b, .ztr-note b, .ztk-wa,
  /* journal, leaderboard, follows, connections */
  .kn-link, .z-cd-date, .z-hm-ch h3, .zlb-you-t .tag, .zlb-foot em,
  .zlb-who-n, .zlb-hero .v, .zfw-h-t .v, .zfw-badge,
  .z-br-count
):is(.kn-neg, .neg) {
  background: none;
  color: var(--kn-red); -webkit-text-fill-color: var(--kn-red);
}

/* ============================ THE POLISH PASS ============================
   Small things, all of which were inconsistent rather than wrong. Consistency
   is most of what reads as "official" — a board where every card, row and
   control behaves the same way looks designed even when no single element is
   remarkable.                                                              */

/* 1. ONE FOCUS RING. Controls had three different focus treatments and some
      had none, which is both a polish problem and an accessibility one. */
.kn-app :is(button, a, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--z-accent-fill);
  outline-offset: 2px;
  border-radius: var(--z-r-box);
}

/* 2. ONE ROW HOVER. Tables were hovering with four different tints. */
.kn-app :is(.zd-tbl, .zh-tbl, .zlb-tbl, .td-tbl, .z-jr-table) tbody tr {
  transition: background .13s ease;
}
.kn-app :is(.zd-tbl, .zh-tbl, .zlb-tbl, .td-tbl, .z-jr-table) tbody tr:hover {
  background: var(--z-raised);
}

/* 3. NUMBERS LINE UP. Any figure that sits in a column gets tabular digits;
      proportional ones make a column of prices look ragged even when it is
      perfectly aligned. */
.kn-app :is(td, th, .kn-kpi, .kn-stat, .kn-big, .kn-h1) { font-variant-numeric: tabular-nums; }

/* 4. CARDS LIFT ON HOVER, once, everywhere. */
.kn-app .kn-box { transition: box-shadow .2s ease, border-color .2s ease; }

/* 5. THE TOP EDGE CATCHES LIGHT. One hairline along the top of every card —
      the cheapest thing that makes a flat fill read as a surface rather than
      a rectangle, and the reference does it too. */
.kn-app .kn-box::after {
  content: ""; position: absolute; inset: 0 0 auto; height: 1px;
  background: linear-gradient(90deg,
    transparent, rgba(255,255,255,.055) 22%, rgba(255,255,255,.055) 78%, transparent);
  pointer-events: none; border-radius: inherit;
}

/* 6. SCROLLBARS match the surface they sit on rather than the OS. */
.kn-app ::-webkit-scrollbar-thumb { background: var(--z-scroll); }
.kn-app ::-webkit-scrollbar-thumb:hover { background: var(--z-scroll-hi); }

/* 7. NOTHING SELECTS BY ACCIDENT in the chrome. Dragging across a toolbar
      and highlighting half the labels looks unfinished. */
.kn-app :is(.z-bar, .z-ticker, .z-nav, .z-w-head) { -webkit-user-select: none; user-select: none; }

/* 8. DISABLED READS AS DISABLED, consistently. */
.kn-app :is(button, input):disabled { opacity: .45; cursor: not-allowed; }

/* the slot a view drops its own controls into, at the right end of the bar */
.z-barctl { display: flex; align-items: center; gap: 8px; flex: none; }
.z-barctl:empty { display: none; }

/* ── ONE SECTION TITLE, ONE TABLE HEAD ───────────────────────────────────
   DASHBOARD-STYLE section 3. The board's widget title measured 13px / 600 /
   .13em / uppercase / --z-muted, and its table head 11px / 400 / --z-dim with
   NO uppercase. Seven modules were using .kn-title at 17px near-white, and four
   different table-head treatments had grown up beside it -- so the same object
   looked like a different object on every page.

   Stated here rather than in each module: shell.css loads before the modules
   inject, but .kn-app raises the specificity enough to win, and one rule is one
   place to change it. Layout (padding, borders, alignment) stays with each
   module; only the type is centralised. */
.kn-app :is(
  .kn-title,                 /* brokers, calendar, journal, pulse, tradingdash */
  .ztr-h h3,                 /* trade */
  .zlb-bh-l h3,              /* leaderboard */
  .z-hm-h-l h3,              /* the journal's month view */
  .zpu-bar-l h3,             /* pulse */
  .zac-ph h3                 /* the account panel */
){
  font-size: 13px; font-weight: 600; letter-spacing: .13em;
  text-transform: uppercase; color: var(--kn-muted);
}
/* A person's NAME is not a section title -- it is the one place on a page where
   the type is allowed to be big and sentence-case. But it was three different
   sizes across three pages (20 / 22 / 24), so it takes the dashboard's. */
.kn-app :is(.zd-prof-id h2, .zlb-you-t h2, .zac-id h1, .zac-id h2){
  font-size: 20px; font-weight: 700; letter-spacing: -.3px; text-transform: none;
}
.kn-app :is(.zlb-tbl, .ztk-tbl, .td-tbl, .z-jr-table, .zd-tbl, .z-cal-yctable) th{
  font-size: 11px; font-weight: 400; letter-spacing: .02em;
  text-transform: none; color: var(--kn-dim);
}

/* ── THE ZOOM ─────────────────────────────────────────────────────────────
   Zoltrak was designed and reviewed at 80% browser zoom, so at 100% every
   size reads about a quarter too big. Rather than rewrite several hundred
   hardcoded px literals across knull.css, shell.css and every module, the
   whole app is scaled once here.

   `zoom` and NOT `transform: scale()`: zoom is a real layout scale, so the
   grid reflows, hit-testing stays honest and canvases still rasterise at
   native resolution. A transform would just resample the finished page and
   put the blur straight back.

   Anything sized against the viewport has to be divided by this same factor
   -- see --z-zoom-div -- because 100vh is measured before the scale is
   applied. Change the number in one place and the whole app follows. */
:root { --z-zoom: 1; --z-zoom-div: 1; }   /* div is 1 / zoom */
.kn-app { zoom: var(--z-zoom); }

/* ── THE GROUND ───────────────────────────────────────────────────────────
   #060510 is measured off fomo.family -- their html, body and root container
   all sit on it. Not pure black, and that matters: GMGN uses #111214 and
   DexScreener #131722, so none of the three use #000. A chart on pure black
   has no separation between the plot and the page, and that flatness is what
   reads as cheap. Each surface below steps up a little from the last. */
/* ── THESE THREE WERE LITERALS, AND THEY OUTRANKED THE TOKENS ────────────
   #000 and #0a0a0d were written out by hand here, and because ".kn-app .kn-box"
   (0,2,0) beats knull.css's ".kn-box" (0,1,0), this rule was what actually
   painted every card in the app. --z-card was not reaching the screen at all.

   That is not a small thing. It means the whole black-and-grey change was half
   a change: the edges moved because they come from --z-line, the ground and the
   card fills did not move at all, because they were being set right here. And
   the twenty-five variations I built on top of it were varying a value the app
   was ignoring — which is exactly what Landon spotted when he said the greys in
   the examples were not the greys he had.

   It is the failure the top of tokens.css already warns about, word for word:
   "the tokens were right and the app still looked wrong, because the tokens
   were not where the colours actually lived." 396 literals the first time; this
   is the one that mattered most. */
body.kn-app { background: var(--z-ground); }
.kn-app .kn-box, .kn-app .kn-panel { background: var(--z-card); }
.kn-app .ztk-chart-card { background: var(--z-ground); }

/* ═══════════════════════════════════════════════════════════════════════════
   THE PHONE
   Zoltrak is a dense terminal and it should stay one — 44px targets everywhere
   would turn a board that shows sixty coins into one that shows nine, which is
   a different and worse product. What is set here are FLOORS, not a redesign:
   the point below which a number cannot be read and a control cannot reliably
   be hit.

   Prefixed with .kn-app for the same reason everything in THE SCALE is: this
   file has to win over module CSS regardless of injection order.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 760px) {
  /* 8px to 10px was in use for table headers, transaction counts, day totals
     and captions. On a phone that is not small text, it is text you give up on.

     Two things worth knowing about this rule:

     · It NAMES the offenders. A blanket floor like
       `[class] { font-size: max(11px, 1em) }` resolves 1em against the PARENT,
       so it does not clamp a small element -- it flattens the whole type scale
       to the inherited size.

     · It uses !important, which the rest of this file avoids. These sit under
       module selectors carrying their own .kn-app prefix, so they outrank a
       plain shell rule (trap 2) and the floor silently did nothing. Inside a
       phone-only media query, "nothing goes below 11px here" is exactly the
       kind of statement !important is for. */
  .kn-app .z-view .kn-tiny,
  .kn-app .z-view .zd-cap,
  .kn-app .z-view .zd-title,
  .kn-app .z-view .zd-day-v,
  .kn-app .z-view .zd-day-t,
  .kn-app .z-view .zpu-txn,
  .kn-app .z-view .zpu-addr,
  .kn-app .z-view [class*="-tag"],
  .kn-app .z-view [class*="-cap"],
  .kn-app .z-view [class*="-addr"],
  .kn-app .z-view [class*="-title"],
  .kn-app .z-view th { font-size: 11px !important; }

  /* A MIS-TAP HERE COSTS MONEY. 30px is the floor: comfortably hittable
     without collapsing the density the board is for. Inputs get more, because
     a text field you cannot land in is a field you cannot use. */
  .kn-app .z-view button,
  .kn-app .z-view a[role="button"] { min-height: 30px; }
  .kn-app .z-view input:not([type="color"]):not([type="range"]):not([type="checkbox"]),
  .kn-app .z-view select,
  .kn-app .z-view textarea { min-height: 36px; font-size: 16px; }
}

/* ── live trading, in the wallet menu ────────────────────────────────────────
   It moved here when the Trade tab went. Connected and armed are shown as two
   separate facts on purpose: collapsing them into one switch is how somebody
   signs a transaction they thought they were previewing. */
.kn-app .z-wp-live {
  margin: 12px 0 0; padding: 12px 13px; border-radius: 11px;
  background: rgba(155,130,238,.05);
  box-shadow: inset 0 0 0 1px var(--kn-line);
}
.kn-app .z-wp-live.on {
  background: rgba(46,230,168,.06);
  box-shadow: inset 0 0 0 1px rgba(46,230,168,.28);
}
.kn-app .z-wp-live .k {
  display: flex; align-items: center; gap: 7px; margin: 0 0 6px;
  font-size: 11px; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--kn-faint);
}
.kn-app .z-wp-live.on .k { color: var(--kn-green); }
.kn-app .z-wp-dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--kn-green);
  box-shadow: 0 0 8px 1px rgba(46,230,168,.6);
}
.kn-app .z-wp-live .kn-btn { width: 100%; }
.kn-app .z-wp-liveact { display: flex; gap: 7px; }
.kn-app .z-wp-liveact .kn-btn { flex: 1; }

/* ── A FIGURE THAT IS STILL LANDING ─────────────────────────────────────────
   Set for the second or two after a trade, while book.js holds the last total
   both of its sources agreed on. Opacity only: no pulse, no colour change, no
   movement. The whole point of the state is that the number stops jumping
   about, so the marker for it must not jump about either. */
.kn-app .z-settling { opacity: .55; transition: opacity .18s ease; }
@media (prefers-reduced-motion: reduce) { .kn-app .z-settling { transition: none; } }
