/* =============================================================================
 * desktop.css — GhosterDex on a pointer and a big screen.
 *
 * Ships inside the mini app bundle. EVERY rule lives inside a min-width query,
 * so below the breakpoint this file contributes nothing and Telegram and the
 * Android app are untouched. That is enforced, not trusted:
 * `check-desktop-css.mjs` in the Ghosterdex Desktop workspace fails the sync if
 * any rule escapes one.
 *
 * There are no .tsx changes anywhere and there should not be. Everything the
 * shell positions is positioned in CSS, and the nav is already `position:
 * fixed`, so a full desktop layout is a restyle rather than a restructure.
 *
 * THREE RULES FOR EDITING THIS FILE
 *
 * 1. Prefix overrides with `body`, or reach the element through
 *    `.shell:has(> nav.bottom-nav)`. styles.css is imported from main.tsx
 *    rather than linked, so Vite emits it AFTER this file — source order is not
 *    ours to rely on and specificity has to do the work. Never @layer:
 *    unlayered styles beat layered ones, so this file would always lose.
 *
 * 2. There are four stylesheets. index.html loads ghost-lens.css,
 *    perps-pro.css and fund-pro.css before the bundle. ghost-lens.css
 *    re-declares the nav as `.bottom-nav.bottom-nav` — (0,2,0) — which beats
 *    `body nav.bottom-nav` (0,1,2), because class count is compared before
 *    element count. If a rule here half-works, suspect specificity before
 *    touching the declarations: the subset of properties the rival does not set
 *    still applies, which looks like a broken layout rather than a lost cascade.
 *
 * 3. `--gl-*` variables are scoped to ghost-lens's `.gl` container, not :root.
 *    An undefined var invalidates the whole declaration at computed-value time
 *    rather than falling back, so give each one a literal fallback.
 *
 * A corollary of (1): a :root block cannot take a `body` prefix, because :root
 * IS <html>. Use `:root:root`.
 * ========================================================================== */

@media (min-width: 1000px) {
  :root:root {
    /* The sidebar, and the content column beside it. */
    --gd-side: 284px;
    --gd-col: 880px;

    /* Sidebar surface. One step up from the page so the app reads as two
       planes rather than one flat field — the thing that most separates a
       desktop app from a phone screen stretched wide. */
    --gd-side-bg: color-mix(in srgb, var(--bg-elev) 82%, var(--bg));
    --gd-side-line: color-mix(in srgb, var(--ink) 9%, transparent);

    /* Upstream reserves --nav-h (84px) at the bottom edge for the floating nav,
       and fourteen rules read it: page and docs padding, the deposit wizard,
       .perps, .gifts/.gram, both gift FABs, .batch-bar, .perp-flash, the dock.
       The nav is a sidebar now, so collapsing the token reclaims that band
       everywhere at once instead of overriding fourteen rules and missing the
       fifteenth upstream adds next. */
    --nav-h: 12px;
  }

  /* ═══════════════════════════════════════════════════════════════════════
   * THE SHELL
   *
   * Scoped to `.shell:has(> nav.bottom-nav)` — only when the app proper is on
   * screen. App.tsx drops the nav entirely while the wallet gate is up, and the
   * gate wants a centred screen with no sidebar at all, not a sidebar with one
   * item missing.
   * ══════════════════════════════════════════════════════════════════════ */

  body .shell {
    max-width: none;
    margin: 0;
  }

  body .shell:has(> nav.bottom-nav) {
    padding-left: var(--gd-side);
  }

  /* The sidebar's surface, drawn as one continuous plane behind both the brand
     header and the nav. Those are two separate elements in the DOM — .topbar is
     the shell's first child, the nav is its last — and no CSS can put them in a
     shared box. A fixed pseudo-element behind both is what makes them read as
     one panel, and it costs no markup. */
  body .shell:has(> nav.bottom-nav)::before {
    content: '';
    position: fixed;
    inset: 0 auto 0 0;
    width: var(--gd-side);
    z-index: 29;
    background: var(--gd-side-bg);
    border-right: 1px solid var(--gd-side-line);
    pointer-events: none;
  }

  /* ── brand header ──────────────────────────────────────────────────────── */

  /* Lifted out of the content column into the top of the sidebar, where a
     desktop app's mark belongs. Same fixed treatment as the nav below it, and
     the two share the pseudo-element's surface. */
  body .shell:has(> nav.bottom-nav) > .topbar {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 31;
    width: var(--gd-side);
    height: 74px;
    padding: 0 18px;
    border-bottom: 1px solid var(--gd-side-line);
  }

  body .shell:has(> nav.bottom-nav) > .topbar .brand-logo {
    width: 32px;
    height: 32px;
    border-radius: 10px;
  }
  body .shell:has(> nav.bottom-nav) > .topbar .brand-name {
    font-size: 19px;
    letter-spacing: 0.2px;
  }

  /* ── the nav, from floating capsule to sidebar list ────────────────────── */

  /* The capsule's own chrome — glass fill, blur, border, 24px radius, drop
     shadow — all belonged to something floating over content. Sitting on the
     sidebar's surface it has nothing to float above, and keeping the shadow
     would draw a seam down the middle of a flat panel. */
  body .shell:has(> nav.bottom-nav) > nav.bottom-nav {
    top: 74px;
    right: auto;
    bottom: 0;
    left: 0;
    transform: none;
    width: var(--gd-side);
    max-width: none;
    height: auto;
    z-index: 30;
    padding: 14px 12px;
    margin-bottom: 0;
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;

    /* Flex column, not the capsule's 5-equal-columns grid: flex is what lets
       `margin-top: auto` below pin the last item to the sidebar's foot, the
       way near.com pins Account. ghost-lens's `.bottom-nav.bottom-nav` grid
       declaration loses to this selector's specificity. */
    display: flex;
    flex-direction: column;
    gap: 2px;
  }

  /* Menu is the sidebar's "account corner": it opens identity, settings and
     docs, which is exactly what lives at the bottom of every desktop app's
     sidebar. Pinning it also stops the five items floating as an island in an
     800px-tall panel — four nav rows read from the top, utilities read from
     the floor. */
  body .shell:has(> nav.bottom-nav) .nav-item:last-child {
    margin-top: auto;
    position: relative;
  }
  body .shell:has(> nav.bottom-nav) .nav-item:last-child::after {
    content: '';
    position: absolute;
    top: -8px;
    left: 2px;
    right: 2px;
    height: 1px;
    background: var(--gd-side-line);
  }

  /* Your Telegram handle, standing in the sidebar where you can set or clear it
     at any time — Gram Zone delivers to it, so it belongs in view rather than
     two taps inside a menu. Hidden on phones (styles.css) where the nav is
     already full, and absent inside Telegram, so the footer pinning has to move
     WITH it: whichever of the two starts the footer cluster carries the auto
     margin and the divider. The :has() test keeps that correct in both cases
     instead of assuming the row is always there. */
  body .shell:has(> nav.bottom-nav) .nav-item.nav-tg {
    display: flex;
    margin-top: auto;
    position: relative;
    overflow: hidden;
  }
  body .shell:has(> nav.bottom-nav) .nav-item.nav-tg::after {
    content: '';
    position: absolute;
    top: -8px;
    left: 2px;
    right: 2px;
    height: 1px;
    background: var(--gd-side-line);
  }
  /* Menu stops being the start of the cluster once the handle row is above it. */
  body .shell:has(> nav.bottom-nav) nav.bottom-nav:has(> .nav-tg) .nav-item:last-child {
    margin-top: 0;
  }
  body .shell:has(> nav.bottom-nav) nav.bottom-nav:has(> .nav-tg) .nav-item:last-child::after {
    display: none;
  }

  /* The sidebar gives the labels their size back: they were squeezed to 10.5px
     to fit five across a phone, and there is no reason to whisper here. */
  body .shell:has(> nav.bottom-nav) .nav-item {
    flex-direction: row;
    justify-content: flex-start;
    gap: 13px;
    height: 46px;
    padding: 0 14px;
    font-size: 14.5px;
    font-weight: 600;
    letter-spacing: 0.1px;
    border-radius: 12px;
    text-align: left;
    color: var(--text-dim);
    transition: background 0.16s, color 0.16s;
  }

  body .shell:has(> nav.bottom-nav) .nav-item svg {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
  }

  body .shell:has(> nav.bottom-nav) .nav-item.on {
    color: var(--text);
    background: color-mix(in srgb, var(--ink) 8%, transparent);
    box-shadow: none;
  }

  /* The active marker was a lit dash under a centred label. Upright it becomes
     a bar down the leading edge — the same signal, read the way a vertical list
     is read. */
  body .shell:has(> nav.bottom-nav) .nav-item::before {
    top: 50%;
    bottom: auto;
    left: 0;
    margin-left: 0;
    width: 3px;
    height: 18px;
    border-radius: 0 3px 3px 0;
    transform: translateY(-50%) scaleY(0);
  }
  body .shell:has(> nav.bottom-nav) .nav-item.on::before {
    transform: translateY(-50%) scaleY(1);
  }

  body .shell:has(> nav.bottom-nav) .nav-item .nav-ic {
    display: inline-flex;
    position: relative;
    flex-shrink: 0;
  }

  /* ── content column ────────────────────────────────────────────────────── */

  /* Named rather than matched with a :not() chain, so a new child upstream
     lands in the flow at full width and is obvious, instead of silently
     inheriting a column width that may be wrong for it.

     `.trade-page` is on the list because Trade does not put its .page directly
     under the shell — the chain there is `.shell > .trade-page > .page`, the
     wrapper holding the Swap|Perps toggle. Any rule keyed on `> .page` misses
     that whole tab silently, which is why the .page rules below match at any
     depth instead: there is only ever one .page mounted. */
  body .shell:has(> nav.bottom-nav) > .page,
  body .shell:has(> nav.bottom-nav) > .trade-page,
  body .shell:has(> nav.bottom-nav) > .docs,
  body .shell:has(> nav.bottom-nav) > .net-alert,
  body .shell:has(> nav.bottom-nav) > .error-note {
    width: min(var(--gd-col), 100%);
    margin-inline: auto;
  }

  body .shell:has(> nav.bottom-nav) .page {
    width: min(var(--gd-col), 100%);
    margin-inline: auto;
    padding: 30px 28px 72px;
  }
  body .shell:has(> nav.bottom-nav) > .net-alert {
    margin-top: 22px;
  }

  /* ═══════════════════════════════════════════════════════════════════════
   * SCREEN CONTENT
   *
   * The column is 880px, up from 480. Rows widen gracefully because they are
   * flex — icon, meta, value — the same shape a desktop wallet's balance list
   * has anyway. What does NOT widen gracefully is anything that was a
   * full-width block only because a phone had no other option.
   * ══════════════════════════════════════════════════════════════════════ */

  /* ── home: hero balance + quick-action rail ────────────────────────────── */

  /* near.com's home puts the balance at the top left and the actions beside
     it, not under it — the two things you came for, on one eye-line. The DOM
     is vault-card THEN actions-row, stacked; a grid on the page re-seats them
     side by side without touching markup.

     The `> * { 1 / -1 }` default is the safety net: any child this rule does
     not name — notices, the external-funds card, whatever upstream adds next —
     spans the full width in source order instead of being scattered by
     auto-placement. */
  body .shell:has(> nav.bottom-nav) .page:has(.vault-card) {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 236px;
    gap: 16px 18px;
    align-content: start;
  }
  body .shell:has(> nav.bottom-nav) .page:has(.vault-card) > * {
    grid-column: 1 / -1;
  }
  body .shell:has(> nav.bottom-nav) .page:has(.vault-card) > .vault-card {
    grid-column: 1;
    grid-row: 1;
    /* all sides: upstream's phone margin-top otherwise sinks the hero 14px
       below the action rail sharing its grid row */
    margin: 0;
    padding: 34px 38px 36px;
    border-radius: 26px;
  }

  /* The phantom watermark was sized for a 440px card; on a 570px hero it was
     a smudge in the corner. Scaled with the card, it is the thing that makes
     the vault read as an object rather than a rectangle with a number in it. */
  body .shell:has(> nav.bottom-nav) .vault-card::after {
    width: 220px;
    height: 220px;
    right: -28px;
    bottom: -44px;
  }
  body .shell:has(> nav.bottom-nav) .vault-sub {
    margin-top: 12px;
  }
  body .shell:has(> nav.bottom-nav) .shield-tag {
    font-size: 13px;
  }

  /* The three tiles become a vertical rail of pills filling the hero's height.
     Row 1 explicitly: auto-placement would land them there anyway, but only
     while vault-card stays the first child, and that is not a fact worth
     leaning on. */
  body .shell:has(> nav.bottom-nav) .page:has(.vault-card) > .actions-row {
    grid-column: 2;
    grid-row: 1;
    grid-auto-flow: row;
    grid-auto-columns: initial;
    grid-template-rows: repeat(3, 1fr);
    gap: 10px;
    margin-bottom: 0;
  }
  /* Scoped to the RAIL, and the scope is the whole point.
     `.actions-row .action-tile` also matches the asset-detail sheet, which
     renders the same markup inline inside .shell (Sheet.tsx does not portal).
     There the tiles sit side by side and wide, so row-direction + flex-start +
     zero vertical padding produced a 233x42 capsule holding a 40px circle with
     ONE pixel of clearance, radius 24px on a 42px box, and ~116px of dead space
     after the label. Measured on production 2026-08-06; reported as
     "this design does not look good".
     The `> .actions-row` child combinator is what excludes the sheet: the rail
     is a direct child of .page, the sheet's row is nested inside .sheet-body. */
  body .shell:has(> nav.bottom-nav) .page:has(.vault-card) > .actions-row .action-tile {
    flex-direction: row;
    justify-content: flex-start;
    gap: 13px;
    padding: 0 16px;
    font-size: 14px;
  }
  body .shell:has(> nav.bottom-nav) .page:has(.vault-card) > .actions-row .action-tile .bubble {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
  }

  /* Sheet tiles keep the shape they were designed for — bubble centred above a
     centred label — and only grow into the space a 540px modal gives them. A
     13px label and phone-sized padding read as a screenshot of a phone when the
     dialog is this wide. */
  body .shell:has(> nav.bottom-nav) .sheet .actions-row .action-tile {
    padding: 20px 10px 17px;
    gap: 11px;
    font-size: 14px;
    border-radius: 20px;
  }
  body .shell:has(> nav.bottom-nav) .sheet .actions-row .action-tile .bubble {
    width: 48px;
    height: 48px;
  }

  /* The number is the point of the screen, and 880px of hero can carry more
     than a phone's 46px. */
  body .shell:has(> nav.bottom-nav) .balance-value {
    font-size: 56px;
    letter-spacing: -1.8px;
  }

  /* The asset list is the desktop table it always wanted to be: taller rows,
     larger identity, and the extra width spent on breathing room rather than
     stretching the same 42px of icon across more emptiness. */
  body .shell:has(> nav.bottom-nav) .asset-list {
    border-radius: 20px;
    padding: 8px 22px;
  }
  body .shell:has(> nav.bottom-nav) .asset-row {
    padding: 16px 6px;
    gap: 15px;
  }
  body .shell:has(> nav.bottom-nav) .asset-row .token-icon {
    width: 46px;
    height: 46px;
  }
  body .shell:has(> nav.bottom-nav) .asset-row .sym {
    font-size: 15.5px;
  }
  body .shell:has(> nav.bottom-nav) .asset-row .name {
    font-size: 13px;
  }
  body .shell:has(> nav.bottom-nav) .section-title {
    font-size: 12.5px;
    letter-spacing: 2px;
    margin: 12px 2px 12px;
  }

  /* ── task screens: one column, centred, task-width ─────────────────────────
     near.com renders Swap as a ~500px card in the middle of the screen and it
     is the right call: a swap, a deposit wizard, a withdrawal form are single
     tasks, and spreading their inputs across 880px does not add information,
     it adds mouse travel. Each page is recognised by a class only that screen
     renders. The width override needs the full shell prefix because the
     generic `> .page` rule above sets width at (0,3,2) — one more :has() wins
     the tie. */
  body .shell:has(> nav.bottom-nav) .page:has(.amount-card),
  body .shell:has(> nav.bottom-nav) .page:has(.stepper),
  body .shell:has(> nav.bottom-nav) .page:has(.token-hero) {
    width: min(600px, 100%);
  }

  /* Activity is a reading list; near.com's History runs a narrow centred
     column for the same reason a receipt is narrow. */
  body .shell:has(> nav.bottom-nav) .page:has(.afr),
  body .shell:has(> nav.bottom-nav) .page:has(.tx-row) {
    width: min(680px, 100%);
  }

  /* ── gram zone: explore cards, two-up ──────────────────────────────────────
     The hub cards are near.com's "Explore" row in all but layout — icon, a
     sentence, a status tag. Two columns at this width; headings and the lead
     paragraph span both. Same full-span safety net as the home grid. */
  body .shell:has(> nav.bottom-nav) .page.gram {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px 14px;
    align-content: start;
  }
  body .shell:has(> nav.bottom-nav) .page.gram > * {
    grid-column: 1 / -1;
  }
  body .shell:has(> nav.bottom-nav) .page.gram > .gz-card {
    grid-column: auto;
    margin-bottom: 0;
  }

  /* The filter strips lose their phone scroller, for the same reason the Stars
     preset chips did: a desktop shows the options, it does not ask you to swipe
     to discover them. Both carry `no-scrollbar`, so what overflows here leaves
     NO trace — no bar, no cue, nothing cut off mid-pill to hint there is more.

     `.gf-active` is the sharp end: those pills are the only per-filter remove
     control (the alternative is "Clear all", which throws away every filter),
     the cap is 200 collections, and one "select all shown" tap can add dozens.
     Measured with active-state labels: at 1440px the 880px column hides a pill
     from 8 filters on, 5 of 12, 8 of 15; at 1000px, 10 of 15 — and the strip
     scrolls only via a wheel gesture nothing on screen suggests exists.
     `.gf-bar` is tighter than it looks too: its seven chips fit while idle, but
     the active labels ("Collections 12", "2.5-150 TON", "#437391") push it
     188px over at 1000px, taking Price and Gift ID off the end.

     Wrapping costs one or two rows of height, which a desktop has, and makes
     every filter clickable again. Verified at 1000px and 1440px: zero chips or
     pills offscreen at 15 active filters. */
  body .shell:has(> nav.bottom-nav) .gf-bar,
  body .shell:has(> nav.bottom-nav) .gf-active {
    flex-wrap: wrap;
    overflow-x: visible;
  }

  /* ── telegram gifts: a storefront shelf, not a stretched phone list ────────
     Two phone columns across 824px make two 400px cards whose lottie art was
     drawn for ~180px — art small, card huge, page short. Four-up returns the
     cards to the density the art was made for and turns the catalog into a
     shelf you scan, which is how every storefront on a desktop works. */
  body .shell:has(> nav.bottom-nav) .gift-grid {
    grid-template-columns: repeat(4, 1fr);
    gap: 12px;
  }
  body .shell:has(> nav.bottom-nav) .gift-card {
    padding: 12px;
    border-radius: 16px;
  }
  body .shell:has(> nav.bottom-nav) .gift-name {
    font-size: 13.5px;
  }
  body .shell:has(> nav.bottom-nav) .gift-price b {
    font-size: 14px;
  }

  /* ── stars & premium: fragment.com's buy page, ghost skin ──────────────────
     Both screens share .page.gz-prod, and their bones are already fragment's:
     centred hero, labelled sections, package rows with a radio and a selected
     accent (the same #4d8dff, even), a payment bar, one full-width CTA. What
     phones forced on them was scale — 21px title, 13px rows, chips in a
     horizontal scroller — and desktop's job is fragment's: a ~500px form
     column with the sizes a checkout deserves. */
  body .shell:has(> nav.bottom-nav) .page.gz-prod {
    width: min(560px, 100%);
  }
  body .shell:has(> nav.bottom-nav) .star-hero {
    gap: 9px;
    padding: 8px 0 18px;
  }
  body .shell:has(> nav.bottom-nav) .star-hero h1 {
    font-size: 30px;
    letter-spacing: -0.02em;
  }
  body .shell:has(> nav.bottom-nav) .star-hero p {
    font-size: 14.5px;
    max-width: 42ch;
  }
  body .shell:has(> nav.bottom-nav) .star-hero-orb {
    width: 66px;
    height: 66px;
    border-radius: 21px;
  }

  /* Inputs at checkout height (fragment's are ~48px). */
  body .shell:has(> nav.bottom-nav) .star-amount {
    padding: 6px 18px;
    border-radius: 16px;
  }
  body .shell:has(> nav.bottom-nav) .star-amount input {
    font-size: 26px;
  }
  body .shell:has(> nav.bottom-nav) .rc-input input {
    padding: 15px 4px;
    font-size: 15.5px;
  }

  /* The preset chips lose their phone scroller: a desktop shows the packages,
     it does not ask you to swipe to discover them. */
  body .shell:has(> nav.bottom-nav) .star-chips {
    flex-wrap: wrap;
    overflow-x: visible;
    gap: 8px;
  }
  body .shell:has(> nav.bottom-nav) .star-chip {
    padding: 10px 16px;
    border-radius: 12px;
    font-size: 13px;
  }

  /* Package rows at fragment's row height. */
  body .shell:has(> nav.bottom-nav) .pm-tier {
    padding: 14px 16px;
    border-radius: 15px;
  }
  body .shell:has(> nav.bottom-nav) .pm-tier-meta b {
    font-size: 15.5px;
  }
  body .shell:has(> nav.bottom-nav) .pm-tier-right b {
    font-size: 16px;
  }

  body .shell:has(> nav.bottom-nav) .star-paybar .spb-usd {
    font-size: 14px;
  }
  body .shell:has(> nav.bottom-nav) .star-paywith {
    padding: 9px 14px;
    font-size: 13px;
  }
  body .shell:has(> nav.bottom-nav) .page.gz-prod .cta {
    padding: 16px;
    font-size: 15.5px;
  }
  body .shell:has(> nav.bottom-nav) .gz-fineprint {
    font-size: 11.5px;
    margin-top: 12px;
  }

  /* Prose keeps a reading measure. An 880px line of body copy is not a wider
     paragraph, it is a harder one to read. */
  body .shell:has(> nav.bottom-nav) .docs-hero p,
  body .shell:has(> nav.bottom-nav) .perp-foot,
  body .shell:has(> nav.bottom-nav) .star-hero p,
  body .shell:has(> nav.bottom-nav) .gifts-lead {
    max-width: 62ch;
  }

  /* ═══════════════════════════════════════════════════════════════════════
   * BOTTOM-FIXED FURNITURE
   *
   * A family of elements pins itself to the bottom edge and centres with
   * `left: 50%` — the gifts batch bar and the perps toast. Centred on the
   * viewport they now sit half a sidebar left of the content they belong to,
   * which reads as a misplaced element rather than a deliberate one.
   * ══════════════════════════════════════════════════════════════════════ */

  body:has(.shell > nav.bottom-nav) .batch-bar,
  body:has(.shell > nav.bottom-nav) .perp-flash,
  /* Third member of the same family, and the only one Gram Zone actually
     mounts — `.batch-bar` is styled but rendered nowhere in src/. Missed on the
     first pass, so the "cart holds up to 200 gifts" toast landed 142px
     (--gd-side/2) left of the column at every width. Only `left` moves, so the
     translateX(-50%) and the cartToastIn keyframe's translate(-50%, 6px) both
     stay correct. */
  body:has(.shell > nav.bottom-nav) .cart-full-toast {
    left: calc(50% + var(--gd-side) / 2);
  }

  /* ── the activity dock lives in the sidebar ────────────────────────────────
   *
   * On a phone it is a floating puck in the thumb corner, because there is
   * nowhere else for it. The desktop sidebar has a column of dead space under
   * Activity — the owner circled it — and a progress card belongs beside the
   * tab it reports on, not hovering over the content in the far corner.
   *
   * Placed by arithmetic rather than reparented, because moving it in the DOM
   * would change nothing about where a `position: fixed` element paints while
   * risking the phone: nav.bottom-nav carries a backdrop-filter at phone width,
   * which makes it a containing block for fixed descendants, and the floating
   * puck would have been trapped inside the 64px bar it is supposed to sit
   * above.
   *
   * The sum is the sidebar's own geometry, each term named so a change to any
   * of them is followed here: nav top (74) + nav padding (14) + four nav rows
   * (46 each) + the gaps between and after them (2 each). Home, Trade, Gram
   * Zone and Activity are the four; the handle row and Menu are pinned to the
   * floor by margin-top:auto and do not enter into it. */
  body:has(.shell > nav.bottom-nav) .dock,
  body:has(.shell > nav.bottom-nav) .dock-puck {
    /* Above the sidebar, not under it.
       The dock ships at z-index 28. The sidebar's background panel
       (.shell::before) is 29 and the nav itself is 30, so moving the dock into
       that column placed it correctly and painted it behind the panel: present
       in the DOM, right size, right offset, invisible. A getBoundingClientRect
       check cannot catch that; only asking what is actually on top can. */
    z-index: 31;
    /* Anchored from the FLOOR, just above the handle row, rather than pinned
       under Activity at the top of the gap.
       Both readings are "under Activity", but the footer is where the eye
       already goes for status and it keeps the card next to the identity it
       belongs to instead of stranded in the middle of an 800px column. Same
       terms as the nav's own geometry, counted upward: nav padding (14) +
       Menu (46) + gap (2) + handle row (46), then 10px of air. */
    top: auto;
    bottom: calc(14px + 46px + 2px + 46px + 10px);
    /* No max-height, and no scroll container.
       Both were defensive and both did harm. ActivityDock renders at most three
       cards (all.slice(0, 3)), about 200px in all. The ceiling could never bind
       on a normal window, while on a short one it squeezed the dock to a ~90px
       stub. Worse, `overflow-y: auto` made it a scroll box: the pending ring
       animates outside the icon's bounds, so the browser found overflow in both
       axes and drew two scrollbars around a clipped circle. A container sized
       by its own content needs neither. */
    left: 12px;
    right: auto;
    width: calc(var(--gd-side) - 24px);
    max-width: none;
    margin: 0;
    transform: none;
  }

  /* Full form, not a circle. The puck is a phone affordance: it shrinks the
     card so it stops covering the screen it is reporting on. In a column that
     has nothing else to show, shrinking hides progress to free space nobody
     wants — so the puck stretches into the same card shape and keeps its
     contents legible. ActivityDock also starts EXPANDED at this width (see its
     collapsed initialiser), so this is the state a reader normally sees. */
  body:has(.shell > nav.bottom-nav) .dock-puck {
    height: auto;
    min-height: 46px;
    border-radius: 12px;
    justify-content: flex-start;
    gap: 10px;
    padding: 0 14px;
  }

  /* The pulse ring has to follow the shape it rings.
     `.dock-puck.pending::after` is `border-radius: 50%`, correct for the 50px
     circle it was drawn for, and a wide ELLIPSE once the puck stretches to a
     260x46 card. puckPulse then scales that ellipse outward past the edges, so
     minimising the dock drew a lens around the card with the icon apparently
     clipped by it. Reported as "still same issue… it happens when you minimize
     it". Same radius as the puck, so it hugs the card instead. */
  body:has(.shell > nav.bottom-nav) .dock-puck.pending::after {
    border-radius: 13px;
  }

  /* The gift FABs pin to the viewport's right edge, which on a wide screen
     strands them hundreds of pixels from the column they act on.

     The first attempt at this overshot in the other direction, and the sign is
     why: the column is centred inside a shell whose left padding IS the
     sidebar, so the sidebar pushes the column RIGHT and therefore SHRINKS its
     distance from the viewport's right edge. The old formula ADDED half the
     sidebar where it had to subtract it — 284px of error — and landed both
     buttons inside the column, floating over the gift grid. Measured on
     production at 1920px before the fix: column 622-1582, buttons 1310-1360,
     i.e. 222px in from the column's right edge.

     Column right edge, measured from the viewport's right, is
     `50% - (side + col)/2`. What gets subtracted from it is the WIDEST thing
     that will ever hang off this anchor, and that is not the 50px circle — the
     cart grows into a pill carrying the GRAM total, measured at 141px for
     "148.51 GRAM" and 185px for "12,999.9999 GRAM". Reserving only the circle
     (the `- 62px` this rule carried) parked the anchor so tight to the column
     that the pill spilled 87-123px back over the cards. 200px reserves the
     widest realistic pill plus a gap, so where the gutter can hold it the
     total sits entirely beside the grid rather than on top of it.

     Right-anchored on purpose: a narrow button stacked above a wide one is
     read as a stack only when their right edges line up. Left-anchoring the
     pill would clear the grid at 1920px and then hang its right edge 47px past
     the round button above it, which is the raggedness this started as.

     The 24px floor takes over below ~1500px, where the gutter is narrower than
     the pill and no anchor can win: at 1440px the gutter is 138px against a
     149px pill. Overlapping there is the same compromise the phone makes.

     Measured against the deployed sheet, cart holding one item:
       1920px  gutter 338  pill 1633-1782  clears the column by 51px
       1440px  gutter 138  pill 1267-1416  overlaps 35px (was 87)
     Right edges aligned with the back-to-top button at both. */
  body:has(.shell > nav.bottom-nav) .gift-fab {
    right: max(24px, calc(50% - (var(--gd-side) + var(--gd-col)) / 2 - 200px));
  }

  /* Upstream lifts both FABs ~58px while the activity dock is on screen,
     because on a phone the dock occupies the same bottom-right corner. On
     desktop it cannot: the dock now sits in the SIDEBAR, under Activity (see
     the rule above), so it is not merely clear of the FABs in the gutter — it
     is on the opposite side of the screen from them. The lift is a jump with
     nothing behind it.

     This reasoning was first written when the dock was a centred bar in the
     content column, and the measurements then were dock-right vs FAB-left of
     1002 vs 1126 at 1000px, 1202 vs 1384 at 1600px, 1362 vs 1544 at 1920px.
     Moving the dock only widened that gap, so the conclusion survived its own
     premise changing — but the premise is restated rather than left stale. */
  body:has(.shell > nav.bottom-nav).dock-active .gift-fab.cart {
    bottom: calc(var(--nav-h) + 18px);
  }
  body:has(.shell > nav.bottom-nav).dock-active .gift-fab.totop {
    bottom: calc(var(--nav-h) + 18px + 60px);
  }

  /* ═══════════════════════════════════════════════════════════════════════
   * SHEETS → CENTRED MODALS
   * ══════════════════════════════════════════════════════════════════════ */

  /* A panel rising from the bottom edge is a thumb-reach affordance. On a
     1440px-tall display it puts the controls most of a metre from the click
     that opened them. */
  body .sheet {
    top: 50%;
    bottom: auto;
    left: 50%;
    transform: translate(-50%, -50%);
    max-width: 540px;
    max-height: 82vh;
    border-radius: 24px;
    border: 1px solid color-mix(in srgb, var(--ink) 14%, transparent);
    padding: 22px 24px 24px;
    box-shadow: 0 40px 90px -30px rgba(0, 0, 0, 0.72);
    animation: gdSheetIn 0.24s cubic-bezier(0.22, 1, 0.36, 1);
  }

  /* Upstream's sheetUp keyframe animates translate(-50%, 42%) → (-50%, 0),
     which assumes the sheet is only centred horizontally. Reusing it would
     fight the vertical centring and drop the panel half off-screen. */
  @keyframes gdSheetIn {
    from { transform: translate(-50%, calc(-50% + 10px)); opacity: 0; }
    to   { transform: translate(-50%, -50%); opacity: 1; }
  }

  /* "Drag me down to dismiss", with nothing to drag and no such gesture. */
  body .sheet-grab {
    display: none;
  }

  /* Above the perps terminal, not under it. The app's sheets are z 40/41 and
     the .gl-screen overlay is z 3000 — an ordering nothing on a phone could
     ever observe, because the full-screen terminal covered every button that
     opens a sheet. Desktop insets the terminal beside the sidebar, so the
     topbar wallet button stays clickable while it is open — and the sheet it
     opens landed BEHIND the terminal: a modal you could summon and never see.
     ghost-lens's own sheets sit at 3300 for exactly this reason; the app's
     join them there. */
  /* Backdrop and sheet share ONE z-index, deliberately.
     Sheets open from sheets here (Pay with, from the cart) and Sheet.tsx emits
     each as a bare backdrop+panel sibling pair, so the DOM runs
     backdrop1, sheet1, backdrop2, sheet2. Splitting them 3400/3401 put every
     backdrop below every sheet: the second backdrop dimmed nothing and left
     the cart's "Buy N privately" lit and clickable behind an open modal —
     invisible on a phone only because both panels sit on the bottom edge and
     the second physically covers the first, which centring undoes.
     Equal z-index hands the ordering to document order, which IS open order,
     so each pair stacks on the last for ANY depth rather than the two levels a
     sibling-selector chain can reach. Verified by hit-testing a point over
     sheet 1 and outside sheet 2: top element goes sheet1 -> backdrop2. */
  body .sheet-backdrop,
  body .sheet {
    z-index: 3401;
  }

  /* The gift-art viewer has to come up WITH that stack, not stay behind it.
     `.gd-zoom` is deliberately rendered outside the sheet (see styles.css) and
     cleared it on a phone at z 60 against the sheet's 41. Lifting the sheet
     stack to 3400/3401 for the perps terminal left the viewer under BOTH the
     sheet and its full-viewport backdrop: "View full size" opened a lightbox
     that could not be seen, and the click meant to dismiss it landed on the
     backdrop and closed the whole gift instead. One number above the stack.
     `body .gd-zoom` is (0,1,1) so it beats the base (0,1,0) — necessary,
     because Vite emits the bundled styles.css AFTER this file. */
  body .gd-zoom {
    z-index: 3500;
  }

  /* ghost-lens.css ships a second family of sheets for the perps and lens
     screens — its own section header calls them "sheets, matching the app's
     own". They are: a fixed flex container with `align-items: flex-end` holding
     a card with a top-only radius. Same treatment, or the app centres one kind
     of modal and slides the other up off the floor.

     These centre more simply than .sheet: the container does it with flex, so
     the card needs no transform and its slide-up keyframe (ending at
     translateY(0)) stays correct as written. */
  body .gl-float,
  body .gl-sheet {
    align-items: center;
  }
  body .gl-float-card,
  body .gl-card {
    max-width: 540px;
    border-radius: var(--gl-radius-lg, var(--radius-lg, 24px));
    border: 1px solid var(--gl-edge-soft, var(--stroke-soft, rgba(255, 255, 255, 0.06)));
    box-shadow: 0 40px 90px -30px rgba(0, 0, 0, 0.72);
  }

  /* ═══════════════════════════════════════════════════════════════════════
   * POINTER AND KEYBOARD
   * ══════════════════════════════════════════════════════════════════════ */

  /* Upstream hides every scrollbar (`::-webkit-scrollbar { width: 0 }`), which
     is right on touch — content moves under your finger and a bar is clutter.
     With a mouse it removes both the only sign that a list continues below the
     fold and the only thing you can drag to get there. */
  body ::-webkit-scrollbar {
    width: 11px;
    height: 11px;
  }
  body ::-webkit-scrollbar-track {
    background: transparent;
  }
  body ::-webkit-scrollbar-thumb {
    background: color-mix(in srgb, var(--ink) 13%, transparent);
    border: 3px solid transparent;
    background-clip: content-box;
    border-radius: 999px;
  }
  body ::-webkit-scrollbar-thumb:hover {
    background: color-mix(in srgb, var(--ink) 26%, transparent);
    background-clip: content-box;
  }
  body {
    scrollbar-width: thin;
    scrollbar-color: color-mix(in srgb, var(--ink) 15%, transparent) transparent;
  }

  /* Focus, two registers — because the app has two kinds of focusable thing.

     Buttons, links, nav rows: a ring. Tab is how a screen-reader or
     motor-impaired user reaches them, and with no ring the position is
     invisible. :focus-visible so a mouse click never draws one. No
     border-radius here: setting it in a focus rule MUTATES the element's own
     radius for the focused moment, which is how every input briefly became a
     stadium-shaped pill.

     Text controls: no ring at all — the FIELD lights up instead. Every input
     in this app is chromeless inside a styled container, and upstream already
     wires the containers: .text-field/.amount-card/.search-box/.pp-field all
     brighten on :focus-within. A ring on the inner <input> double-draws inside
     an already-lit field, hugging the bare control instead of the box that
     visually IS the field. (:focus-visible matches text inputs on click, not
     just on Tab, so exclusion is the only clean way out.) The caret and the
     lit container carry the focus signal. */
  body :focus-visible {
    outline: 2px solid color-mix(in srgb, var(--ink) 62%, transparent);
    outline-offset: 2px;
  }
  body input:focus-visible,
  body textarea:focus-visible,
  body select:focus-visible {
    outline: none;
  }

  /* The two containers upstream never wired — Stars' recipient and amount
     fields. Same treatment as .text-field:focus-within, character for
     character: one focus language across the whole app. */
  body .rc-input:focus-within,
  body .star-amount:focus-within {
    border-color: color-mix(in srgb, var(--ink) 45%, transparent);
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--ink) 7%, transparent);
    transition: border-color 0.2s, box-shadow 0.2s;
  }

  /* A caret in the brand ink, not browser default — the smallest detail a
     focused field has, and the one you stare at while typing. */
  body input,
  body textarea {
    caret-color: var(--ink);
  }

  @media (hover: hover) {
    body .shell:has(> nav.bottom-nav) .nav-item:not(.on):hover {
      color: var(--text);
      background: color-mix(in srgb, var(--ink) 5%, transparent);
    }
    body .sheet-close:hover {
      color: var(--text);
      border-color: color-mix(in srgb, var(--ink) 30%, transparent);
    }
    /* Storefront cards answer the pointer; upstream only gave them :active,
       because a phone has no hover to answer. */
    body .shell:has(> nav.bottom-nav) .gift-card:hover,
    body .shell:has(> nav.bottom-nav) .gz-card:hover {
      transform: translateY(-2px);
      border-color: color-mix(in srgb, var(--ink) 28%, transparent);
    }
  }

  /* ═══════════════════════════════════════════════════════════════════════
   * PERPS — a terminal, not a tall phone screen
   *
   * On a phone this screen time-shares one column: a `.pp-modes` toggle swaps
   * between the chart and the order ticket, because both cannot fit. A desktop
   * has room for the chart, the book and the ticket at once, which is the whole
   * reason a trading terminal looks the way it does — you watch the book while
   * you set the order.
   *
   * Specificity note: several rules here carry `.pp-root.pp-root`. Upstream
   * addresses this screen as `.pp-root.pp-mode-chart .pp-body` (0,3,0), so
   * `body .pp-body` (0,1,1) loses in chart mode and the layout would apply in
   * one mode and not the other. The doubled class is the same idiom
   * ghost-lens.css already uses for the nav.
   * ══════════════════════════════════════════════════════════════════════ */

  /* .gl-screen is `position: fixed; inset: 0`, so the terminal covers the
     sidebar and the only way out is its own back button. Inset it by the
     sidebar: the nav stays reachable and the app stays one place. */
  body:has(.shell > nav.bottom-nav) .gl-screen {
    left: var(--gd-side);
  }

  /* A terminal is not a document.
   *
   * Every label and number carried the text I-beam and was drag-selectable, so
   * a click on empty chrome left a caret sitting in the middle of the ticket
   * and a drag across the ladder painted it blue. Both are document behaviours
   * and neither reference terminal has them: you cannot accidentally select
   * Bybit's order book, because a book is a readout, not prose.
   *
   * Inputs opt straight back in — a price you cannot select is a price you
   * cannot correct — and buttons keep their pointer. Scoped to .pp-root so the
   * wallet screens are untouched, where selecting an address is the point. */
  body .pp-root {
    user-select: none;
    -webkit-user-select: none;
    cursor: default;
  }
  body .pp-root input,
  body .pp-root textarea {
    user-select: text;
    -webkit-user-select: text;
    cursor: text;
  }
  body .pp-root button {
    cursor: pointer;
  }

  /* The book's lean is stated twice — once under the Book/Trades tabs and
     again at the foot of the ladder, the same two numbers in two dressings.
     The foot is where both reference terminals put it, and in a column where
     furniture already claims more than half the height, a duplicate costs
     rows that the ladder needs. */
  body .pp-root .pp-right > .pp-ratio {
    display: none;
  }

  /* ── the ladder fills its column ───────────────────────────────────────────
   * The book must never scroll — that is the whole parity point, and a depth
   * ladder you have to scroll is a list.
   *
   * Doing it by arithmetic failed twice, and instructively. Summing the
   * furniture missed 68px of padding and flex gaps. Taking overhead as the
   * remainder (scrollHeight − rows) fixed that but could not grow back: once
   * the content FITS, scrollHeight equals clientHeight, so the leftover space
   * counts as overhead and the count is pinned at whatever it shrank to. The
   * pane was 264px inside a 439px column with 175px going spare.
   *
   * So: no arithmetic. The pane fills the column, the two ladders split what
   * is left in equal shares, and JS reads the height it was handed. flex-basis
   * 0 is what breaks the circularity — the container's height stops depending
   * on how many rows are in it. */
  body .pp-root .pp-right > .pp-book {
    flex: 1;
    min-height: 0;
  }

  /* ── the column carries BOTH halves ────────────────────────────────────────
   * The book says what people are willing to do; the tape says what they
   * actually did. A phone shows one at a time because it has room for one — a
   * 700px desktop column showing one wastes half of it, and every reference
   * terminal stacks them. So the tabs above stop being a switch here and the
   * lower half appears, carrying the tape and the movers list.
   *
   * 42% is the tape's share: enough for ~12 prints and the movers' chips plus
   * ten rows, while leaving the ladder the majority it needs to stay
   * unscrolled (see the flex-basis note above — the same reasoning applies to
   * this split, which is why it is a percentage of the column rather than a
   * pixel height that would fight the ladder for the remainder). */
  body .pp-root .pp-right > .pp-lower {
    display: flex;
    flex-direction: column;
    flex: 0 0 42%;
    min-height: 0;
    border-top: 1px solid var(--gd-side-line, rgba(255, 255, 255, 0.07));
    margin-top: 6px;
    padding-top: 2px;
  }
  /* The tape is no longer one of two tabs up top — it lives below now, so the
     switch that hid the book to show it would only blank half the column. */
  body .pp-root .pp-righttabs button[data-tab='tape'] {
    display: none;
  }
  body .pp-root .pp-lowertape,
  body .pp-root .pp-movers {
    flex: 1 1 0;
    min-height: 0;
    overflow: hidden;
  }
  body .pp-root .pp-lowertape { overflow-y: auto; }
  body .pp-root .pp-book > .pp-rows {
    flex: 1 1 0;
    min-height: 0;
    align-content: start;
    overflow: hidden;
  }

  /* ── three columns: Price · Size · Total ───────────────────────────────────
   * Both references carry the running total behind each level, and both align
   * the three as a table. Ours were flex/space-between, which lets each row
   * set its own column positions from its own text widths — fine for two
   * cells, visibly ragged with three.
   *
   * One grid, shared by the header and every row, so the columns line up by
   * construction. .pp-depth is absolutely positioned and so is not a grid
   * item — the bar keeps spanning the full row behind the numbers. */
  body .pp-root .pp-book > .pp-bookhead,
  body .pp-root .pp-book .pp-row {
    display: grid;
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr) minmax(0, 1fr);
    align-items: center;
    column-gap: 6px;
  }
  body .pp-root .pp-book > .pp-bookhead > span:nth-child(2),
  body .pp-root .pp-book .pp-row > .pp-sz {
    text-align: right;
  }
  body .pp-root .pp-book > .pp-bookhead > span:nth-child(3),
  body .pp-root .pp-book .pp-row > .pp-tot {
    text-align: right;
    color: var(--gl-market-dim, #8b95a3);
    font-variant-numeric: tabular-nums;
  }

  /* The mid/spread block is 49px of a column the ladder is short of. It states
     one price and one spread; both references give that a single row. */
  body .pp-root .pp-book > .pp-spread {
    padding: 2px;
    line-height: 1.25;
  }
  body .pp-root .pp-book > .pp-spread .pp-mid-px {
    font-size: 14.5px;
  }
  body .pp-root .pp-book > .pp-spread .pp-mid-sub {
    font-size: 10px;
  }

  /* ── the chrome above the body ─────────────────────────────────────────────
   * Every pixel here is a pixel the order book does not get, and the ladder is
   * what the reader came for. Ours spent 244px on five stacked bars where the
   * reference terminals spend roughly 135 — and the difference was showing up
   * as a ladder four levels deep instead of ten.
   *
   * Tightened, not removed: each bar still holds what it held. */
  body .pp-root .gl-head.pp-head {
    min-height: 0;
    padding-top: 6px;
    padding-bottom: 6px;
  }
  /* Folded into the strip by build(), so it is a flex item on the instrument
     bar rather than a row of its own. It keeps the symbol picker and the
     change chip; it loses only the line it used to sit on. */
  body .pp-strip > .pp-pairrow {
    flex: none;
    align-items: center;
    gap: 10px;
    padding: 0 14px 0 0;
    margin-right: 4px;
    border-right: 1px solid var(--gl-edge-soft, rgba(255, 255, 255, 0.06));
  }
  body .pp-root .pp-charttabs {
    padding-top: 4px;
    padding-bottom: 0;
  }
  body .pp-root.pp-root .pp-tfs {
    padding-top: 4px;
    padding-bottom: 6px;
  }
  /* The Book/Trades switch sits above the ladder and is the last of the
     column's own furniture. */
  body .pp-root .pp-righttabs {
    padding-top: 0;
    padding-bottom: 0;
  }

  /* ── the instrument bar ────────────────────────────────────────────────── */

  /* The stats sit in a two-column label/value grid pushed to the far right by
     `margin-left: auto`. Across 1400px that leaves the price at one end of the
     bar and three 10.5px numbers at the other, with a metre of nothing between
     — the emptiest thing on the screen.

     Column flow turns the same alternating spans into a row of label-over-value
     pairs beside the price. No markup changes: the spans already alternate
     label, value, label, value, so filling column-first across two rows lands
     each pair in its own column. That is the shape every terminal's instrument
     bar has, and why Bybit can show eight stats without it reading as clutter. */
  /* Grounded with a hairline: margin carries the 22px inset (not padding), so
     the rule stops where the content stops instead of running edge to edge. */
  body .pp-strip {
    align-items: center;
    gap: 0;
    margin: 0 22px 14px;
    padding: 0 0 14px;
    border-bottom: 1px solid var(--gl-edge-soft, rgba(255, 255, 255, 0.06));
  }
  /* Mark and Oracle are columns of their own now, so the text line that
     repeated them under the price is a third copy of the same two numbers
     inside 400px. The node stays and keeps being written, so a window dragged
     below 1000px shows it already correct.

     Scoped through .pp-last on purpose: .pp-mark is ALSO the class on the
     drawn market badge, and `body .pp-mark` would blank every market logo. */
  body .pp-last .pp-mark {
    display: none;
  }

  /* Funding moved to the bar, where both references put it — and the order
     book gets the height back, which is what the ladder was short of. */
  body .pp-book > .pp-fund {
    display: none;
  }

  body .pp-stats {
    margin-left: 10px;
    margin-right: auto;
    grid-template-columns: none;
    grid-template-rows: auto auto;
    grid-auto-flow: column;
    gap: 2px 0;
    font-size: 11px;
    min-width: 0;
  }
  /* Bybit's bar separates each stat with a vertical rule. Label and value are
     two spans stacked in one grid column, so each carries a border-left and
     the pair reads as one divided cell — the 2px row gap nicks the line, which
     at hairline weight reads as intended, not broken.
     CHILD combinator: as a descendant this would also reach any span nested
     inside a label and draw a hairline mid-word. */
  body .pp-stats > span {
    border-left: 1px solid var(--gl-edge-soft, rgba(255, 255, 255, 0.06));
    padding: 0 14px;
  }
  /* Under column flow the first column is spans 1 AND 2 — resetting only
     :first-child leaves a stray rule beside the first value. */
  body .pp-stats > span:nth-child(-n + 2) {
    border-left: 0;
    padding-left: 0;
  }
  /* Hyperliquid's labels are dotted because they are hoverable definitions
     (the title comes from STAT_DEF). text-decoration, not border-bottom:
     these spans carry 14px of horizontal padding and a border paints across
     the padding box, so the rule would start 14px left of the first glyph. */
  body .pp-stats > span:nth-child(odd) {
    color: var(--gl-market-faint, #5f6873);
    text-decoration: underline dotted;
    text-decoration-color: var(--gl-edge, rgba(255, 255, 255, 0.14));
    text-underline-offset: 3px;
    text-decoration-skip-ink: none;
    cursor: help;
  }
  /* Values were right-aligned to sit under a label in a two-column grid. In
     their own column they align left, under their own label. */
  body .pp-stats > span:nth-child(even) {
    text-align: left;
    font-size: 13.5px;
    font-weight: 600;
    color: var(--gl-market, var(--text));
  }
  /* .pp-up/.pp-down are (0,1,0) and lose to the rule above, so change and
     funding — the two columns both references colour — would render neutral. */
  body .pp-stats > span:nth-child(even).pp-up {
    color: var(--gl-bid, #34e8a4);
  }
  body .pp-stats > span:nth-child(even).pp-down {
    color: var(--gl-ask, #ff5c7a);
  }
  body .pp-last .pp-big {
    font-size: 34px;
  }

  /* The chart/ticket toggle answers a question a desktop does not ask. */
  body .pp-head .pp-modes {
    display: none;
  }

  /* ── the terminal grid ─────────────────────────────────────────────────── */

  /* `.pp-split` holds the ticket and the book as a 57/43 pair. `display:
     contents` dissolves that box so both become items of the body's own grid,
     which is what lets the chart sit beside them as a third column instead of
     stacked above. It is the one thing that makes this layout reachable from
     CSS at all.

     Every cell is named, so the screen's other children — .pp-drawbarstrip,
     .pp-chartpane, .pp-quick, all zero-sized in this mode — auto-place into
     implicit rows below and contribute nothing. */
  /* Chart-dominant, like every terminal that is taken seriously: the book
     needs two numbers and a depth bar, the ticket needs input width, and every
     remaining pixel belongs to the chart. Row gap is 0 because the vertical
     rhythm is done with margins on the chart row instead — that is what lets
     the tabs strip and the positions table below it touch and read as ONE
     stitched panel, the way Bybit's bottom section is one surface with its
     tabs inside. */
  body .pp-root.pp-root .pp-body {
    display: grid;
    grid-template-columns: 46px minmax(0, 1fr) 272px 320px;
    grid-template-rows: minmax(280px, 1fr) auto auto;
    grid-template-areas:
      'rail chart book form'
      'tabs tabs  tabs form'
      'pos  pos   pos  form';
    gap: 0 12px;
    padding: 0 22px 18px;
    overflow: hidden;
  }

  /* The two !important declarations in this file, and the only place they are
     correct: perps-pro.js writes `display` INLINE on both of these elements —
     `display: grid` on the split, `display: none` on the chart, because on a
     phone the mode toggle shows one or the other. An inline style outranks
     every stylesheet rule regardless of specificity, so this is not a cascade
     shortcut, it is the only instrument that reaches them.

     The toggle that would fight back is hidden above, so the JS never
     re-asserts a mode after load. */
  /* No display here any more, and that is load-bearing: the JS owns it. It
     used to carry `display: block !important` to beat book mode's inline
     `none` — right when the phone JS insisted the chart was gone, wrong now
     that setMode shows it on desktop itself (open() clears the phone-only
     chartHidden preference) and renderChartTab needs to HIDE it whenever the
     Overview/Data/Feed pane takes this same grid cell. An !important would
     have pinned the canvas visible under every pane. */
  body .pp-root.pp-root .pp-body > .pp-chartwrap {
    grid-area: chart;
    margin-bottom: 10px;
    /* drawChart measures `parentElement.clientWidth`, which INCLUDES this
       element's 4px side padding, while the canvas renders at 100% of the
       content box under border-box sizing. The bitmap comes out 8px wider than
       its display box, so the plot is squeezed and the crosshair maths drifts
       from the price axis. Pre-existing, and a narrower chart column makes the
       fraction worse — zeroing the padding here removes the discrepancy at
       this width without touching the phone. */
    padding: 0;
    background: var(--gl-panel, rgba(255, 255, 255, 0.04));
    border: 1px solid var(--gl-edge-soft, rgba(255, 255, 255, 0.06));
    border-radius: 14px;
    overflow: hidden;
  }

  /* Overview, Data and Feed render here — same cell as the chart, exactly one
     of the two displayed at a time (renderChartTab's contract). Scrolls on its
     own: token details and funding charts are taller than one screen. */
  body .pp-root.pp-root .pp-body > .pp-chartpane {
    grid-area: chart;
    /* Overview/Data/Feed take the rail's gutter too — renderChartTab hides the
       rail alongside the canvas, and without this the pane would sit beside a
       46px hole. The implicit `rail-start`/`chart-end` lines come free from the
       area names above. */
    grid-column: rail-start / chart-end;
    margin-bottom: 10px;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 14px;
    background: var(--gl-panel, rgba(255, 255, 255, 0.04));
    border: 1px solid var(--gl-edge-soft, rgba(255, 255, 255, 0.06));
    border-radius: 14px;
  }

  /* ── the drawing rail ──────────────────────────────────────────────────────
   * A permanent column down the chart's left edge, which is where every
   * terminal keeps its tools — not a strip a pencil reveals under the chart.
   *
   * It stays a .pp-body child rather than moving inside .pp-chartwrap. Three
   * things fall out of that for free: this `>` combinator keeps matching;
   * renderChartTab keeps the authority to hide the rail when the Overview pane
   * takes the chart's cell; and the buttons stay outside the chart's
   * capture-phase pointerdown, which starts a 550ms timer to open the
   * order-staging sheet — a slow click on a tool would otherwise stage a trade.
   *
   * `display` is deliberately absent. The JS owns it (it is written inline in
   * four places) and must keep owning it, or the rail would strand itself
   * beside the Overview pane. open() sets S.drawBar and calls renderDrawBar
   * behind the same width gate instead.
   *
   * 46px: .pp-drawtool is 34px square, and 46 − 2(border) − 10(padding) = 34.
   * ────────────────────────────────────────────────────────────────────────── */
  body .pp-root.pp-root .pp-body > .pp-drawbarstrip {
    grid-area: rail;
    flex-direction: column;
    gap: 8px;
    margin-bottom: 10px;
    padding: 8px 5px;
    overflow-x: hidden;
    overflow-y: auto;
    background: var(--gl-panel, rgba(255, 255, 255, 0.04));
    border: 1px solid var(--gl-edge-soft, rgba(255, 255, 255, 0.06));
    border-radius: 14px;
  }
  body .pp-root .pp-drawbarstrip .pp-drawtool {
    flex: none;
  }
  /* Utilities to the foot, the way every rail anchors its trash — and the only
     grouping the column needs. */
  body .pp-root .pp-drawbarstrip .pp-drawall {
    margin-top: auto;
    border-style: solid;
  }
  /* A permanently red trash on a permanently visible rail is a standing alarm.
     Grey at rest, red on intent — and it sits a full column away from the
     nearest drawing tool, because Clear wipes every shape with no confirm. */
  body .pp-root .pp-drawbarstrip .pp-drawclear {
    color: var(--gl-market, #9aa4b2);
  }
  body .pp-root .pp-drawbarstrip .pp-drawclear:hover,
  body .pp-root .pp-drawbarstrip .pp-drawclear:focus-visible {
    color: var(--red, #ff5c7a);
  }
  /* :hover before .on — both are (0,4,1) and a tie goes to source order. */
  body .pp-root .pp-drawbarstrip .pp-drawtool:hover {
    background: var(--gl-panel-2);
  }
  /* A solid accent-green square in the left gutter competes with the bids and
     longs on the chart beside it. Tint the ground, accent the glyph. */
  body .pp-root .pp-drawbarstrip .pp-drawtool.on {
    background: var(--gl-panel-2);
    color: var(--gl-accent, #34e8a4);
    border-color: color-mix(in srgb, var(--gl-accent, #34e8a4) 40%, transparent);
  }

  body .pp-root.pp-root .pp-body > .pp-split {
    display: contents !important;
  }

  /* The chart tabs are a .pp-root child above the body, so they span the
     terminal — align their inset with the grid's. */
  body .pp-root .pp-charttabs {
    padding-left: 22px;
    padding-right: 22px;
  }

  /* ── the chart toolbar ─────────────────────────────────────────────────────
   * Target: timeframes far left, icon-only chart controls far right. What was
   * there instead: five intervals, then 472px of nothing, then seven mismatched
   * text-and-icon buttons — including two pairs that did the same job.
   *
   * `.pp-root.pp-root` is not a typo. `.pp-root.pp-compactchart .pp-tfs`
   * (0,3,0) re-asserts overflow-x, and compactchart is the RESTING desktop
   * state: S.mode defaults to 'book', setMode toggles the class from it, and
   * the mode switcher is hidden here — so `body .pp-root .pp-tfs` (0,2,1)
   * would lose every time. The doubled class is the same idiom ghost-lens.css
   * already uses on the nav.
   * ────────────────────────────────────────────────────────────────────────── */
  body .pp-root.pp-root .pp-tfs {
    align-items: center;
    gap: 6px;
    overflow: visible;
    padding: 6px 22px 10px;
  }

  body .pp-root .pp-tfs button {
    padding: 6px 10px;
    color: var(--gl-market, #9aa4b2);
  }
  /* Restated: the rule above is (0,2,2) and would otherwise outrank
     `.pp-tfs button.on` (0,2,1) and kill the selected interval. */
  body .pp-root .pp-tfs button.on {
    background: var(--gl-panel-2);
    color: var(--gl-mine, #eef2f8);
  }

  /* Square icon targets, and the margin reset that makes hiding safe.
     `.pp-tfs .pp-tool` sets `padding: 0 11px` with NO height, so an icon-only
     tool boxes to its 15px glyph — under WCAG 2.5.8's 24px minimum, today, on
     phone as well. It also puts `margin-left: auto` on EVERY tool, with a
     sibling rule walking it back for all but the first; `display: none` does
     not remove an element from sibling matching, so hiding the first tool
     would strand the push on a hidden node and collapse the whole cluster
     leftward. Resetting the margin here is what makes the hides below safe. */
  body .pp-root .pp-tfs .pp-tool {
    margin: 0;
    width: 32px;
    height: 32px;
    padding: 0;
    display: grid;
    place-items: center;
    border-radius: 8px;
    color: var(--gl-market, #9aa4b2);
  }
  /* One push, on the last member of the left group. Must follow the reset —
     equal specificity, and the shorthand above zeroes margin-right. */
  body .pp-root .pp-tfs .pp-more {
    margin-right: auto;
  }
  /* Three icon sets ship at three sizes (15/2.0, 16/1.9, 17/1.7). Presentation
     attributes on <svg> rank below author styles, so they unify from here with
     no JS change. */
  body .pp-root .pp-tfs .pp-tool > svg {
    display: block;
    width: 17px;
    height: 17px;
    stroke-width: 1.7;
  }
  body .pp-root .pp-tfs .pp-tool:hover {
    background: var(--gl-panel-2);
    color: var(--gl-mine, #eef2f8);
  }

  /* The four that go.
     .pp-pencil    gated S.drawBar, which is permanently true here — gates nothing.
     .pp-drawsheet opened the 84-tool sheet; the rail's own all-tools button
                   holds the identical openDrawTools reference.
     .pp-fullbtn / .pp-expand  full screen. Removed rather than restyled: the
                   class it sets, html.pp-fullchart, force-hides seven regions
                   with `display: none !important` and NO media guard, which on
                   this grid half-collapses the terminal (the ticket and book
                   survive on their named areas while everything else vanishes).
                   Its sibling also calls toggleLandscape, which needs a native
                   signer no browser has and toasts "Rotate the phone" on every
                   click. Deleting both controls makes that whole cluster
                   unreachable at this width instead of fighting an !important.
                   The chart already owns ~55% of the viewport; F11 is the
                   browser's own answer.
     display:none, not visibility — these must leave the tab order too.
     .pp-rot needs no rule: it is inline-hidden and the only line that clears
     it runs inside the full-screen path that no longer exists here. */
  body .pp-root .pp-tfs .pp-pencil,
  body .pp-root .pp-tfs .pp-drawsheet,
  body .pp-root .pp-tfs .pp-fullbtn,
  body .pp-root .pp-tfs .pp-expand {
    display: none;
  }

  /* Hide-the-chart is a phone-space affordance (book and ticket, nothing
     else). On desktop the chart owns a column; offering the control would
     offer a hole. open() clears the persisted preference for the same reason. */
  body .pp-root .pp-hidechart {
    display: none;
  }

  /* Each region on its own raised panel. On a phone these share one column and
     separate by whitespace; side by side they need edges, or the screen reads
     as loose furniture on a void instead of a terminal. Same glass language as
     the rest of the app — --gl vars with literal fallbacks, per rule 3. */
  body .pp-root.pp-root .pp-split > .pp-right {
    grid-area: book;
    margin-bottom: 10px;
    padding: 12px;
    background: var(--gl-panel, rgba(255, 255, 255, 0.04));
    border: 1px solid var(--gl-edge-soft, rgba(255, 255, 255, 0.06));
    border-radius: 14px;
  }
  body .pp-root.pp-root .pp-split > .pp-form {
    grid-area: form;
    padding: 14px;
    background: var(--gl-panel, rgba(255, 255, 255, 0.04));
    border: 1px solid var(--gl-edge-soft, rgba(255, 255, 255, 0.06));
    border-radius: 14px;
  }

  /* The bottom section is ONE panel with the tabs inside it, Bybit-style. Two
     sibling elements cannot share a box, so it is stitched: the tabs strip is
     the panel's top edge (side + top borders, top radius), the table is its
     body (side + bottom borders, bottom radius), zero row-gap lets them touch,
     and the tabs' own border-bottom — which upstream already draws — becomes
     the interior seam under the tab row. */
  body .pp-root.pp-root .pp-body > .pp-tabs {
    grid-area: tabs;
    padding: 8px 16px 0;
    background: var(--gl-panel, rgba(255, 255, 255, 0.04));
    border: 1px solid var(--gl-edge-soft, rgba(255, 255, 255, 0.06));
    border-bottom: 1px solid var(--gl-edge-soft, rgba(255, 255, 255, 0.06));
    border-radius: 14px 14px 0 0;
  }
  body .pp-root.pp-root .pp-body > .pp-positions {
    grid-area: pos;
    padding: 8px 16px 12px;
    background: var(--gl-panel, rgba(255, 255, 255, 0.04));
    border: 1px solid var(--gl-edge-soft, rgba(255, 255, 255, 0.06));
    border-top: 0;
    border-radius: 0 0 14px 14px;
  }

  /* Book mode caps the chart at a 190px strip so the price stays in sight
     above the ticket. Side by side there is nothing to make room for, and the
     chart should take the height the grid gives it. */
  body .pp-root.pp-compactchart .pp-chartwrap {
    height: auto;
    min-height: 0;
    flex: initial;
    overflow: hidden;
  }

  /* Three columns of different natural heights, each scrolling on its own
     rather than the page scrolling all of them. */
  body .pp-root.pp-root .pp-split > .pp-form,
  body .pp-root.pp-root .pp-split > .pp-right {
    min-height: 0;
    max-height: none;
    overflow-y: auto;
    overflow-x: hidden;
  }

  /* The positions table is the one place a wide screen pays off for free:
     more rows visible without scrolling. */
  body .pp-root.pp-root .pp-body > .pp-positions {
    min-height: 0;
    max-height: 260px;
    overflow-y: auto;
  }

  /* ── positions: a table, not a stack of tiles ──────────────────────────────
   * The tile was built from a side-by-side with Bybit and is right on a phone,
   * where one position fills the width. Both desktop references show a table:
   * labels once at the top, one line per position, columns you can read down.
   *
   * Every item is placed EXPLICITLY. `display: contents` on the two wrappers
   * promotes their children to grid items, which makes them GRANDCHILDREN of
   * the card — so a `.pp-poscard > *` rule would match the dissolved wrappers
   * (which generate no box) and never the real cells. Sparse auto-placement
   * would then wrap the data onto rows 2 and 3, because document order runs
   * column 1, column 6, column 2 and the cursor only moves forward.
   *
   * One shared 7-column template on the header and the rows, so they align by
   * construction rather than by matching numbers in two places. */
  body .pp-root.pp-root .pp-positions > .pp-poshead-row,
  body .pp-root.pp-root .pp-positions > .pp-poscard {
    display: grid;
    /* The action track is a FIXED width, not auto. The header's last cell is
       empty, so an auto track collapses there to ~0 while the card's fills to
       the buttons — and the two grids then distribute the difference across
       their fr tracks differently, so the columns drift apart by a few pixels
       each. A fixed track makes the header and the rows share one geometry. */
    grid-template-columns:
      minmax(110px, 1.25fr) minmax(64px, 0.9fr) minmax(76px, 1fr)
      minmax(76px, 1fr) minmax(76px, 1fr) minmax(72px, 0.9fr)
      minmax(104px, 1.15fr) 168px;
    align-items: center;
    column-gap: 12px;
  }

  body .pp-root.pp-root .pp-positions > .pp-poshead-row {
    padding: 2px 12px 6px;
    font-size: 11px;
    font-weight: 600;
    color: var(--gl-market-faint, #5f6873);
    border-bottom: 1px solid var(--gl-edge-soft, rgba(255, 255, 255, 0.06));
    position: sticky;
    top: 0;
    z-index: 1;
    background: var(--gl-panel, rgba(255, 255, 255, 0.04));
  }
  /* Numeric headings sit over right-aligned figures. */
  body .pp-root.pp-root .pp-positions > .pp-poshead-row > span:nth-child(n + 2) {
    text-align: right;
  }

  body .pp-root.pp-root .pp-positions > .pp-poscard {
    padding: 6px 12px;
    margin: 0;
    border-radius: 0;
    border-width: 0 0 1px;
  }
  body .pp-root.pp-root .pp-poscard > .pp-poshead,
  body .pp-root.pp-root .pp-poscard > .pp-posgrid {
    display: contents;
  }

  /* The explicit placement the wrappers can no longer provide. */
  body .pp-root.pp-root .pp-poscard .pp-posid { grid-area: 1 / 1; }
  body .pp-root.pp-root .pp-posgrid > .pp-poscell:nth-child(1) { grid-area: 1 / 2; }
  body .pp-root.pp-root .pp-posgrid > .pp-poscell:nth-child(2) { grid-area: 1 / 3; }
  body .pp-root.pp-root .pp-posgrid > .pp-poscell:nth-child(3) { grid-area: 1 / 4; }
  body .pp-root.pp-root .pp-posgrid > .pp-poscell:nth-child(4) { grid-area: 1 / 5; }
  body .pp-root.pp-root .pp-posgrid > .pp-poscell:nth-child(5) { grid-area: 1 / 6; }
  body .pp-root.pp-root .pp-poscard .pp-pospnl { grid-area: 1 / 7; }
  body .pp-root.pp-root .pp-poscard > .pp-posact { grid-area: 1 / 8; }
  /* Margin and value are loose spans rather than labelled cells, so they stay
     one strip under the row instead of pretending to be two more columns. */
  body .pp-root.pp-root .pp-poscard > .pp-posmeta { grid-area: 2 / 1 / auto / -1; }
  /* TP/SL brackets, when present, get their own line under the row. */
  body .pp-root.pp-root .pp-poscard > .pp-poschips { grid-area: 3 / 1 / auto / -1; }

  /* The header states each label once; repeating it in every cell is the tile
     idiom and is what makes a stack of them unreadable as a column. */
  body .pp-root.pp-root .pp-posgrid > .pp-poscell > :not(.gl-num) {
    display: none;
  }
  body .pp-root.pp-root .pp-posgrid > .pp-poscell {
    padding: 0;
    background: none;
    border: 0;
    text-align: right;
  }
  body .pp-root.pp-root .pp-posgrid > .pp-poscell > .gl-num {
    text-align: right;
    font-size: 13px;
  }

  /* P&L keeps its figure but loses the stacked caption — the column is named. */
  body .pp-root.pp-root .pp-poscard .pp-pospnl {
    flex-direction: row;
    align-items: baseline;
    justify-content: flex-end;
    gap: 6px;
    text-align: right;
  }
  body .pp-root.pp-root .pp-poscard .pp-pospnl-k {
    display: none;
  }

  body .pp-root.pp-root .pp-poscard .pp-posid {
    min-width: 0;
  }
  body .pp-root.pp-root .pp-poscard > .pp-posmeta {
    font-size: 11px;
    padding-top: 2px;
  }
  /* Row actions compress to a trailing cluster rather than a full-width bar. */
  body .pp-root.pp-root .pp-poscard > .pp-posact {
    display: flex;
    gap: 6px;
    margin: 0;
  }
  body .pp-root.pp-root .pp-poscard > .pp-posact .pp-posbtn {
    width: auto;
    padding: 5px 10px;
    font-size: 11.5px;
  }

  /* ═══════════════════════════════════════════════════════════════════════
   * THE GATE
   *
   * The one view with no sidebar, so it centres on the viewport. Upstream pins
   * its CTA to the bottom edge because that is where a thumb rests; on desktop
   * that is simply a long way from the sentence it answers.
   * ══════════════════════════════════════════════════════════════════════ */

  body .gate {
    justify-content: center;
    gap: 34px;
    padding-bottom: 60px;
  }
  body .gate-cta {
    max-width: 340px;
    margin: 0 auto;
  }
}

/* Wide desktops. A small step: past this the balance rows start to hollow out,
   a symbol on the far left and a number on the far right with nothing between,
   and width stops being the same thing as density. */
@media (min-width: 1600px) {
  :root:root {
    --gd-col: 960px;
  }
}

/* =============================================================================
 * The Gram wallet, on the same big screen.
 *
 * The public home is the vault home's twin: the wallet card and the action
 * rail on one eye-line, the tabs and the lists across the full width below.
 * Same grid, same safety net for children this file does not name. The
 * collectibles vitrine becomes a gallery: four across, since two tiles at
 * this width were posters rather than a collection.
 * ========================================================================== */
@media (min-width: 1000px) {
  body .shell:has(> nav.bottom-nav) .page.public-page:has(.pub-card) {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 236px;
    gap: 16px 18px;
    align-content: start;
  }
  body .shell:has(> nav.bottom-nav) .page.public-page:has(.pub-card) > * {
    grid-column: 1 / -1;
  }
  body .shell:has(> nav.bottom-nav) .page.public-page:has(.pub-card) > .pub-card {
    grid-column: 1;
    grid-row: 1;
    margin: 0;
    padding: 34px 38px 36px;
    border-radius: 26px;
  }
  body .shell:has(> nav.bottom-nav) .page.public-page:has(.pub-card) > .actions-row {
    grid-column: 2;
    grid-row: 1;
    grid-auto-flow: row;
    grid-auto-columns: initial;
    grid-template-rows: repeat(3, 1fr);
    gap: 10px;
    margin-bottom: 0;
  }
  body .shell:has(> nav.bottom-nav) .page.public-page:has(.pub-card) > .actions-row .action-tile {
    flex-direction: row;
    justify-content: flex-start;
    gap: 13px;
    padding: 0 16px;
    font-size: 14px;
  }
  body .shell:has(> nav.bottom-nav) .page.public-page:has(.pub-card) > .actions-row .action-tile .bubble {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
  }
  body .shell:has(> nav.bottom-nav) .pub-total,
  body .shell:has(> nav.bottom-nav) .pub-total-usd b {
    font-size: 56px;
    letter-spacing: -1.8px;
  }
  body .shell:has(> nav.bottom-nav) .pub-total-usd span {
    font-size: 30px;
  }
  body .shell:has(> nav.bottom-nav) .page.public-page .pub-vault.compact {
    margin-top: 0;
  }
  body .shell:has(> nav.bottom-nav) .page.public-page .pub-tabs {
    margin-top: 6px;
  }
  body .shell:has(> nav.bottom-nav) .page.public-page .gw-list.asset-list {
    border-radius: 20px;
    padding: 8px 22px;
  }
  body .shell:has(> nav.bottom-nav) .nft-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 16px;
  }
  body .shell:has(> nav.bottom-nav) .page.public-page:has(.afr) {
    display: block;
  }
}
@media (min-width: 1600px) {
  body .shell:has(> nav.bottom-nav) .nft-grid {
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
}
