/* Vice Hub — dashboard styles. v1.76.4
   Layers on vice.css tokens. PATH RULE: lives in vice/ ROOT (deep pages serve
   at /hub with no directory, so assets resolve at the domain root). */

/* ══ THE STACKING LADDER ══════════════════════════════════════════════════
   Every fixed/sticky chrome surface in the Hub gets its layer from here. Use a
   token; do not write a raw z-index for a new overlay.

   Why it is based at 200 and not at 60: gridstack's SCRIPT writes an inline
   `style="z-index: 100"` on all 8 resize handles of every widget. Inline beats
   a stylesheet, so `.editing … .ui-resizable-handle { z-index: 5 }` was dead
   code — computed value 100, verified. The old ladder ran 60/80/86/88/90/92,
   entirely UNDER that, so in Customize the 80 handles painted over the toolbar,
   the menus, the add-widget tray, every dialog and the inspector — and
   elementFromPoint confirmed they intercepted the taps, not merely covered
   them. On touch the corner handles are 51×51px, so they blanketed the modals.

   Re-basing above 100 rather than `!important`-ing the handle rule is
   deliberate: it is declarative, and it survives a gridstack upgrade changing
   that inline number (audit F-Z).

   Handles sit at gridstack's 100 — above widget content, below all chrome. */
:root {
  --z-grid-handle: 100; /* gridstack's inline value — documented, not set here */
  --z-bar: 200;         /* sticky toolbar                                      */
  --z-menu: 300;        /* ⋮ + layout dropdowns                                */
  --z-veil: 400;        /* gallery preview + wallet sheet                      */
  --z-max: 500;         /* a widget blown up in-page                           */
  --z-modal: 600;       /* dialogs                                             */
  --z-drawer: 700;      /* inspector drawer                                    */
  --z-palette: 800;     /* command palette                                     */
  --z-toast: 900;       /* confirmations + undo — must outrank every surface   */
  --z-wall: 1000;       /* wallpaper mode owns the screen                      */

  /* The iOS/Ionic drawer curve. Sheets are the one surface `--ease-out` reads
     wrong on: it lands a full-height panel too abruptly for something the
     thumb can also drag. Hub-local on purpose — `vice.css` is shared by nine
     entry points, and a motion token used only by `.vsheet` does not justify
     busting all nine caches. Promote it if a second surface wants it. */
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
}

/* ── page frame ─────────────────────────────────────────────────────────── */
.hub-page { display: flex; flex-direction: column; min-height: 100vh; }
.hub-main { flex: 1; width: 100%; max-width: none; padding: 0 16px 40px; }

/* ── toolbar ────────────────────────────────────────────────────────────── */
.hub-bar {
  position: sticky; top: 0; z-index: var(--z-bar);
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  padding: 6px 16px;
  margin: 0 -16px 10px;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
}
.hub-brand { display: inline-flex; align-items: center; }
.hub-brand .vt-flamingo { width: 20px; height: 20px; transition: transform 160ms var(--ease-out); }
@media (hover: hover) and (pointer: fine) { .hub-brand:hover .vt-flamingo { transform: scale(1.08); } }
.hub-bar-title { color: var(--text); font-weight: 700; font-size: var(--fs-ui); letter-spacing: 0.2px; white-space: nowrap; }
.hub-bar-title strong { color: var(--text); }
.hub-bar .spacer { flex: 1; }
.hub-live {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--text2); font-size: var(--fs-meta);
  background: none; border: 0; padding: 4px 7px; border-radius: var(--radius);
  font-family: inherit; cursor: pointer;
  transition: background 140ms var(--ease-out), transform 140ms var(--ease-out);
}
.hub-live:hover { background: var(--bg3); }
.hub-live:active { transform: scale(0.97); }
.hp-txt { font-variant-numeric: tabular-nums; white-space: nowrap; }
.hp-txt b { color: var(--text); font-weight: 600; }
.hp-txt em { font-style: normal; font-size: var(--fs-meta); }
.hp-txt em.up { color: var(--green); }
.hp-txt em.down { color: var(--red); }
.hub-live .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); animation: hubPulse 2.2s ease-in-out infinite; }
@keyframes hubPulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }

.hub-btn {
  display: inline-flex; align-items: center; gap: 6px;
  background: none; color: var(--text3);
  border: none; border-radius: var(--radius);
  font: inherit; font-size: var(--fs-meta); padding: 5px 9px; cursor: pointer;
  transition: color 140ms var(--ease-out), background 140ms var(--ease-out), transform 140ms var(--ease-out);
}
.hub-btn i, .hub-btn svg { width: 13px; height: 13px; flex: none; }
.hub-btn:hover { color: var(--text); background: var(--bg3); }
.hub-btn:active { transform: scale(0.97); }
.hub-btn:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
.hub-btn.primary { background: var(--teal); border-color: var(--teal); color: var(--bg); font-weight: 600; }
.hub-btn.primary:hover { filter: brightness(1.08); color: var(--bg); }
.hub-btn.editing-on { background: var(--pink); border-color: var(--pink); color: #fff; font-weight: 600; }

/* overflow menu */
.hub-menu-wrap { position: relative; }
.hub-menu {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: var(--z-menu);
  min-width: 220px; padding: 5px;
  background: var(--bg2); border: 1px solid var(--border2); border-radius: var(--radius-lg);
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.55);
  display: none;
}
.hub-menu.open { display: block; }
.hub-menu button, .hub-menu label, .hub-menu a {
  display: flex; align-items: center; gap: 9px; width: 100%;
  background: none; border: 0; border-radius: var(--radius);
  color: var(--text2); font: inherit; font-size: var(--fs-data);
  padding: 7px 10px; cursor: pointer; text-align: left;
}
.hub-menu button:hover, .hub-menu label:hover, .hub-menu a:hover { background: var(--bg3); color: var(--text); }
.hub-menu button i, .hub-menu button svg, .hub-menu label i, .hub-menu label svg,
.hub-menu a i, .hub-menu a svg { width: 13px; height: 13px; flex: none; }
.hub-menu .sep { height: 1px; margin: 5px 4px; background: var(--border); }
.hub-menu .danger { color: var(--red); }
.hub-menu .danger:hover { color: var(--red2); }
/* the theme radios: the active look states itself with a trailing tick */
.hub-menu [data-vtheme-pick].on { color: var(--teal); }
.hub-menu [data-vtheme-pick].on::after {
  content: ''; margin-left: auto; flex: none;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--teal);
}

/* ── grid canvas ────────────────────────────────────────────────────────── */
#hub-grid { min-height: 60vh; }
.editing #hub-grid {
  background-image:
    linear-gradient(color-mix(in srgb, var(--border) 55%, transparent) 1px, transparent 1px),
    linear-gradient(90deg, color-mix(in srgb, var(--border) 55%, transparent) 1px, transparent 1px);
  background-size: calc(100% / 24) var(--hub-cell, 36px);
  border-radius: var(--radius-lg);
  outline: 1px dashed var(--border2);
  outline-offset: 4px;
}

.grid-stack-item-content.hw {
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025), 0 3px 12px rgba(0, 0, 0, 0.22);
}
.editing .grid-stack-item-content.hw { border-color: var(--border2); }
.grid-stack-item.ui-draggable-dragging .hw,
.grid-stack-item.ui-resizable-resizing .hw {
  border-color: var(--teal);
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.6), 0 0 0 1px var(--teal-glow);
}
.grid-stack-placeholder > .placeholder-content {
  background: var(--teal-glow) !important;
  border: 1px dashed var(--teal) !important;
  border-radius: var(--radius-lg);
}

/* widget chrome — a whisper, not a bar: hairline divider, 11px muted title,
   actions hidden until the block is hovered (always shown on touch / edit) */
.hw-head {
  display: flex; align-items: center; gap: 7px; flex: none;
  padding: 0 4px 0 10px;
  border-bottom: 1px solid var(--border);
  color: var(--text3); font-size: var(--fs-meta); font-weight: 500; letter-spacing: 0.05em;
  min-height: 27px;
  user-select: none;
}
.hw-head > i, .hw-head > svg { width: 11.5px; height: 11.5px; flex: none; color: var(--text3); opacity: 0.8; }
/* .hw-title is an <h2> (audit F-01 — it is the Hub's only heading level under
   the section <h1>). It must not inherit the UA's 1.5em/bold heading styling;
   the chrome's own type is the design. */
.hw-title {
  flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text3); transition: color 160ms var(--ease-out);
  font-size: inherit; font-weight: inherit; line-height: inherit;
  letter-spacing: inherit; margin: 0;
}
.hw:hover .hw-title, .hw:focus-within .hw-title { color: var(--text2); }
.hw-head { cursor: grab; }
.hw-head:active { cursor: grabbing; }
.hw-btns { display: flex; align-items: center; gap: 1px; }
.hw-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 21px; height: 21px; border-radius: var(--radius);
  background: none; border: 0; color: var(--text3); cursor: pointer;
  transition: color 140ms var(--ease-out), background 140ms var(--ease-out), transform 120ms var(--ease-out);
}
.hw-btn i, .hw-btn svg { width: 11.5px; height: 11.5px; flex: none; }
.hw-btn:hover { color: var(--text); background: var(--bg4); }
.hw-btn:active { transform: scale(0.92); }
.hw-btn:focus-visible { outline: 2px solid var(--teal); outline-offset: -1px; }
.hw-btn[data-act="remove"]:hover { color: var(--red); }
/* view mode hides the settings gear; remove (X) is always one hover away */
body:not(.editing) .hw-btn[data-act="settings"] { display: none; }
/* actions fade in on hover (fine pointers only — touch keeps them visible) */
@media (hover: hover) and (pointer: fine) {
  body:not(.editing) .hw-btns { opacity: 0; transition: opacity 150ms var(--ease-out); }
  body:not(.editing) .hw:hover .hw-btns,
  body:not(.editing) .hw:focus-within .hw-btns { opacity: 1; }
}

.hw-body { position: relative; flex: 1; min-height: 0; overflow: hidden; container-type: inline-size; }
/* widget-level responsiveness: rows adapt to the BLOCK's width, not the page's */
@container (max-width: 250px) {
  .vrow { padding: 5px 9px; gap: 7px; }
  .vrow .name { display: none; }
  .vrow .chg { min-width: 60px; font-size: var(--fs-meta); }
  .vrow .sym { min-width: 38px; }
  .vtabs { padding: 6px 8px; }
}
@container (max-width: 210px) {
  .vrow .px { display: none; }
}
/* TV embeds render at full desktop scale — 24px headlines inside 300px blocks.
   Render them at 80% (width/height 125% + scale .8) so their type sits next to
   our 11-12.5px scale instead of shouting over it. */
.hw-body > .tv-wrap,
.vpanel-body > .tv-wrap {
  position: absolute; top: 0; left: 0; width: 125%; height: 125%;
  transform: scale(0.8); transform-origin: 0 0;
  display: flex; flex-direction: column; justify-content: center;
}
.grid-stack-item-content.hw:fullscreen .tv-wrap,
.vpanel:fullscreen .tv-wrap { width: 100%; height: 100%; transform: none; }
/* TV's TA gauge pads generously — render it denser than the other embeds */
.hw-body > .tv-wrap.tv-ta { width: 138%; height: 138%; transform: scale(0.725); }

/* resize grip: invisible until the block is hovered, always functional */
.grid-stack-item > .ui-resizable-handle {
  opacity: 0;
  transition: opacity 150ms var(--ease-out);
}
.grid-stack-item:hover > .ui-resizable-handle { opacity: 0.55; }
.tv-wrap:not(.tv-fixed) .tradingview-widget-container,
.tv-wrap:not(.tv-fixed) .tradingview-widget-container__widget,
.tv-wrap:not(.tv-fixed) iframe { width: 100% !important; height: 100% !important; }
/* tv-fixed: the widget sizes its own band (ticker tape) — keep TV's height
   and center it instead of stretching the iframe over a dark shelf */
.tv-wrap.tv-fixed .tradingview-widget-container { flex: none; width: 100% !important; }
.tv-wrap.tv-fixed iframe { width: 100% !important; }
.tv-wrap .tradingview-widget-copyright { display: none; }

/* fullscreen */
.grid-stack-item-content.hw:fullscreen { background: var(--bg); border-radius: 0; }

/* lucide draws at stroke 2 — too heavy at these sizes; thin the chrome */
.hub-bar svg, .hub-menu svg, .hw-head svg, .hw-btn svg,
.hub-modal svg, .hub-tray-item svg, .vsuite svg { stroke-width: 1.5; }

/* iframes swallow the mouse mid-drag/resize — park them while interacting */
body.hub-dragging iframe { pointer-events: none; }

/* "Add widget" only exists in edit mode */
body:not(.editing) .hub-btn.edit-only { display: none; }

/* loading / error / empty states inside widgets */
.hw-note {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 8px; text-align: center;
  color: var(--text3); font-size: var(--fs-meta); padding: 18px; line-height: 1.55;
}
.hw-note i, .hw-note svg { width: 16px; height: 16px; }

/* ── vice-native widget internals ──────────────────────────────────────── */
.vw-scroll {
  position: absolute; inset: 0; overflow-y: auto; overflow-x: hidden;
  overscroll-behavior: contain; scrollbar-gutter: stable;
}
.vw-scroll::-webkit-scrollbar { width: 6px; }
.vw-scroll::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
.vw-scroll::-webkit-scrollbar-track { background: transparent; }

/* watchlist / movers rows */
.vrow {
  display: flex; align-items: baseline; gap: 10px;
  padding: 6px 12px; border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  font-size: var(--fs-meta);
}
.vrow:last-child { border-bottom: 0; }
.vrow .sym { font-weight: 600; color: var(--text); min-width: 46px; }
.vrow .name { color: var(--text3); font-size: var(--fs-meta); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.vrow .px { margin-left: auto; color: var(--text); font-variant-numeric: tabular-nums; }
.vrow .chg { min-width: 78px; text-align: right; font-variant-numeric: tabular-nums; font-size: var(--fs-meta); white-space: nowrap; }
.vrow .chg.up { color: var(--green); }
.vrow .chg.down { color: var(--red); }
.vrow.flash { animation: vFlash 600ms var(--ease-out); }
@keyframes vFlash { 0% { background: var(--bg4); } 100% { background: transparent; } }

/* movers tabs */
.vtabs {
  display: flex; gap: 4px; padding: 7px 10px;
  border-bottom: 1px solid var(--border); flex: none;
  /* tabs sit at the 14px control floor now, so a narrow column must scroll
     them rather than clip — matches .vsc-views */
  overflow-x: auto; scrollbar-width: none;
}
.vtabs::-webkit-scrollbar { display: none; }
.vtab { flex: none; }
.vtab {
  background: none; border: 1px solid var(--border2); border-radius: 999px;
  color: var(--text3); font: inherit; font-size: var(--fs-ui); padding: 3px 10px; cursor: pointer;
  transition: color 140ms var(--ease-out), border-color 140ms var(--ease-out), transform 120ms var(--ease-out);
}
.vtab:active { transform: scale(0.95); }
.vtab:not(.on):hover { color: var(--text2); border-color: var(--border3); }
.vtab.on { color: var(--bg); background: var(--teal); border-color: var(--teal); font-weight: 600; }
.vtab.on.down { background: var(--red); border-color: var(--red); }
.vcol { position: absolute; inset: 0; display: flex; flex-direction: column; }
.vcol .vw-scroll { position: relative; inset: auto; flex: 1; min-height: 0; }

/* fear & greed gauge — a stat tile, hugs its content */
.fng { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px; padding: 8px; }
.fng svg { width: min(62%, 170px); height: auto; margin-bottom: -6px; }
.fng .val { font-size: clamp(17px, 2.4vh, 22px); font-weight: 700; color: var(--text); line-height: 1.1; font-variant-numeric: tabular-nums; }
.fng .lab { font-size: var(--fs-meta); color: var(--text2); }
.fng .sub { font-size: var(--fs-meta); color: var(--text3); }

/* news list */
.vnews a {
  display: block; padding: 8px 12px; border-bottom: 1px solid var(--border);
  color: var(--text2); font-size: var(--fs-meta); line-height: 1.45; text-decoration: none;
  transition: color 140ms var(--ease-out), background 140ms var(--ease-out);
}
.vnews a:hover { color: var(--text); background: var(--bg3); }
.vnews .meta { display: block; margin-top: 3px; color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.03em; }

/* notes */
.vnotes {
  position: absolute; inset: 0; width: 100%; height: 100%; resize: none;
  background: transparent; border: 0; color: var(--text);
  font: inherit; font-size: var(--fs-data); line-height: 1.6; padding: 12px 14px;
}
.vnotes:focus { outline: none; }
.vnotes::placeholder { color: var(--text3); }

/* session clocks */
.vclocks { position: absolute; inset: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); align-content: center; gap: 8px; padding: 12px; }
.vclock { text-align: center; padding: 10px 6px; border: 1px solid var(--border); border-radius: var(--radius); }
.vclock.open { border-color: color-mix(in srgb, var(--green) 40%, var(--border)); }
.vclock.open .city { color: var(--green); }
.vclock .city { font-size: var(--fs-meta); color: var(--text3); letter-spacing: 0.07em; text-transform: uppercase; }
.vclock .time { font-size: clamp(14px, 2.2vh, 18px); font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; margin: 3px 0 4px; }
.vclock .mkt { font-size: var(--fs-meta); }
.vclock .mkt.open { color: var(--green); }
.vclock .mkt.closed { color: var(--text3); }

/* suite links */
.vsuite { position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: center; gap: 8px; padding: 14px; }
.vsuite a {
  display: flex; align-items: center; gap: 10px; padding: 8px 11px;
  border: 1px solid var(--border2); border-radius: var(--radius);
  color: var(--text2); font-size: var(--fs-meta); text-decoration: none;
  transition: color 140ms var(--ease-out), border-color 140ms var(--ease-out), transform 140ms var(--ease-out);
}
.vsuite a:hover { color: var(--text); border-color: var(--border3); transform: translateY(-1px); }
.vsuite a i, .vsuite a svg { width: 14px; height: 14px; flex: none; color: var(--text3); }
.vsuite .gold svg { color: var(--gold); } .vsuite .pink svg { color: var(--pink); } .vsuite .teal svg { color: var(--teal); }

/* alerts */
.valerts-add { display: flex; gap: 6px; padding: 9px 10px; border-bottom: 1px solid var(--border); flex: none; }
.valerts-add input {
  min-width: 0; background: var(--bg3); border: 1px solid var(--border2); border-radius: var(--radius);
  color: var(--text); font: inherit; font-size: var(--fs-data); padding: 6px 9px;
}
.valerts-add input.sym { width: 74px; flex: none; text-transform: uppercase; }
.valerts-add input.tgt { flex: 1; }
.valerts-add button { flex: none; }
.valert-row { display: flex; align-items: center; gap: 9px; padding: 6px 12px; border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent); font-size: var(--fs-meta); }
.valert-row .sym { font-weight: 700; color: var(--text); }
.valert-row .cond { color: var(--text2); flex: 1; font-variant-numeric: tabular-nums; }
.valert-row .now { color: var(--text3); font-size: var(--fs-meta); font-variant-numeric: tabular-nums; }
/* fired rows are styled once, in the history block below — a green-tint rule
   here fought it and could never render */
.valert-row .del { width: 22px; height: 22px; }

/* ── modals (settings / add tray / import) ─────────────────────────────── */
.hub-modal-veil {
  position: fixed; inset: 0; z-index: var(--z-modal);
  background: rgba(4, 3, 9, 0.66); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 8vh 16px 16px; overflow-y: auto;
}
.hub-modal {
  width: 100%; max-width: 520px;
  background: var(--bg2); border: 1px solid var(--border2); border-radius: var(--radius-lg);
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.6);
  animation: hubModalIn 200ms var(--ease-out);
}
.hub-modal.wide { max-width: 760px; }
@keyframes hubModalIn { from { opacity: 0; transform: translateY(8px) scale(0.985); } }
.hub-modal-head {
  display: flex; align-items: center; gap: 9px;
  padding: 11px 15px; border-bottom: 1px solid var(--border);
  color: var(--text); font-weight: 600; font-size: var(--fs-data);
}
.hub-modal-head > i, .hub-modal-head > svg { width: 13px; height: 13px; color: var(--teal); flex: none; }
.hub-modal-head .x { margin-left: auto; }
.hub-modal-body { padding: 16px; }
.hub-modal-foot { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--border); }

/* settings form */
.hf-field { margin-bottom: 14px; }
.hf-field:last-child { margin-bottom: 0; }
.hf-label { display: block; margin-bottom: 6px; color: var(--text2); font-size: var(--fs-meta); letter-spacing: 0.03em; }
.hf-help { margin-top: 5px; color: var(--text3); font-size: var(--fs-meta); line-height: 1.5; }
.hf-input, .hf-select, .hf-textarea {
  width: 100%; background: var(--bg3); border: 1px solid var(--border2); border-radius: var(--radius);
  color: var(--text); font: inherit; font-size: var(--fs-ui); padding: 8px 10px;
}
.hf-textarea { min-height: 74px; resize: vertical; line-height: 1.55; }
.hf-input:focus-visible, .hf-select:focus-visible, .hf-textarea:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
.hf-toggle { display: flex; align-items: center; gap: 9px; color: var(--text2); font-size: var(--fs-ui); cursor: pointer; }
.hf-toggle input { width: 15px; height: 15px; accent-color: var(--teal); }

/* add-widget tray */
.hub-tray-cat { margin: 4px 0 8px; color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.08em; text-transform: uppercase; }
.hub-tray-cat:not(:first-child) { margin-top: 18px; }
.hub-tray-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(206px, 1fr)); gap: 8px; }
.hub-tray-item {
  display: flex; align-items: center; gap: 10px; text-align: left;
  background: var(--bg3); border: 1px solid var(--border2); border-radius: var(--radius);
  color: var(--text2); font: inherit; font-size: var(--fs-data); padding: 9px 11px; cursor: pointer;
  transition: color 150ms var(--ease-out), border-color 150ms var(--ease-out), transform 150ms var(--ease-out);
}
.hub-tray-item:hover { color: var(--text); border-color: var(--teal); transform: translateY(-1px); }
.hub-tray-item:active { transform: scale(0.98); }
.hub-tray-item i, .hub-tray-item svg { width: 14px; height: 14px; flex: none; color: var(--text3); }
.hub-tray-item:hover i, .hub-tray-item:hover svg { color: var(--teal); }
.hub-tray-item .d { display: block; margin-top: 2px; color: var(--text3); font-size: var(--fs-meta); font-weight: 400; }
.hub-tray-item strong { font-weight: 600; }

/* focus mode: one ticker, every angle */
#hub-focus.focus-on { background: var(--teal); border-color: var(--teal); color: var(--bg); font-weight: 600; }
#hub-focus.focus-on:hover { filter: brightness(1.08); color: var(--bg); }
body.focused #hub-layout-wrap, body.focused #hub-add, body.focused #hub-edit, body.focused #hub-tidy { display: none; }

/* toast action + empty state */
.hub-toast { display: flex; align-items: center; gap: 12px; }
.hub-toast-act {
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--teal); font: inherit; font-size: var(--fs-data); font-weight: 600;
}
.hub-toast-act:hover { text-decoration: underline; }
.hub-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; min-height: 46vh; text-align: center; color: var(--text3); font-size: var(--fs-data);
}
/* the class display:flex would otherwise beat the hidden attribute */
.hub-empty[hidden] { display: none; }
.hub-empty i, .hub-empty svg { width: 26px; height: 26px; stroke-width: 1.25; opacity: 0.5; }
.hub-empty p { margin: 0; }
.hub-empty .hub-btn { margin-top: 4px; }
/* (the palette button stays on mobile — an old rule here hid the Hub's only
   search affordance on ≤768px screens) */

/* clickable data rows broadcast the linked symbol */
.vrow.clickable { cursor: pointer; transition: background 120ms var(--ease-out); }
.vrow.clickable:hover { background: var(--bg3); }
.vrow-h { color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.06em; text-transform: uppercase; }
.vrow-h .sym, .vrow-h .chg { color: var(--text3); font-weight: 500; font-size: var(--fs-meta); }

/* native Vice Chart (bot engine via demo.js) */
.vchart { position: absolute; inset: 0; }
.vchart-err {
  position: absolute; left: 0; right: 0; bottom: 0; display: none;
  padding: 6px 11px; color: var(--red2); font-size: var(--fs-meta);
  background: color-mix(in srgb, var(--bg2) 85%, transparent);
  border-top: 1px solid var(--border);
}
.vchart-err.on { display: block; }

/* candle-close countdown */
.vcd { position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: center; gap: 12px; padding: 12px 14px; }
.vcd-row { display: flex; align-items: center; gap: 10px; font-size: var(--fs-meta); }
.vcd-row .tf { color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.07em; min-width: 20px; }
.vcd-row .left { color: var(--text); font-variant-numeric: tabular-nums; min-width: 78px; }
.vcd-row .bar { flex: 1; height: 3px; border-radius: 2px; background: var(--bg4); overflow: hidden; }
.vcd-row .bar > span { display: block; height: 100%; width: 100%; background: var(--teal); border-radius: 2px; transform: scaleX(0); transform-origin: left; transition: transform 1s linear; }

/* alert history */
.vhist-h {
  display: flex; align-items: center; justify-content: space-between;
  padding: 6px 12px 4px; color: var(--text3);
  font-size: var(--fs-meta); letter-spacing: 0.06em; text-transform: uppercase;
  border-bottom: 1px solid var(--border);
}
.vhist-h button { background: none; border: 0; color: var(--text3); font: inherit; font-size: var(--fs-meta); letter-spacing: 0.06em; text-transform: uppercase; cursor: pointer; padding: 0; }
.vhist-h button:hover { color: var(--text); }
.valert-row.fired { background: transparent; opacity: 0.6; }
.valert-row.fired .cond { color: var(--green); }

/* command palette */
.hub-palette-veil {
  position: fixed; inset: 0; z-index: var(--z-palette);
  background: rgba(4, 3, 9, 0.6); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  display: flex; justify-content: center; align-items: flex-start; padding: 14vh 16px;
}
.hub-palette {
  width: 100%; max-width: 440px; overflow: hidden;
  background: var(--bg2); border: 1px solid var(--border2); border-radius: var(--radius-lg);
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.6);
  animation: hubModalIn 160ms var(--ease-out);
}
.hub-palette-input {
  width: 100%; background: var(--bg3); border: 0; border-bottom: 1px solid var(--border);
  color: var(--text); font: inherit; font-size: var(--fs-data); padding: 11px 14px;
}
.hub-palette-input:focus { outline: none; }
.hub-palette-list { max-height: 320px; overflow-y: auto; padding: 5px; }
.hub-palette-item {
  display: flex; align-items: center; gap: 9px; width: 100%;
  background: none; border: 0; border-radius: var(--radius);
  color: var(--text2); font: inherit; font-size: var(--fs-data);
  padding: 7px 10px; cursor: pointer; text-align: left;
}
.hub-palette-item.sel, .hub-palette-item:hover { background: var(--bg3); color: var(--text); }
.hub-palette-item i, .hub-palette-item svg { width: 13px; height: 13px; flex: none; stroke-width: 1.5; color: var(--text3); }
.hub-palette-empty { padding: 14px; color: var(--text3); font-size: var(--fs-meta); text-align: center; }

/* toast */
.hub-toast {
  position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); z-index: var(--z-toast);
  background: var(--bg3); border: 1px solid var(--border2); border-radius: var(--radius);
  color: var(--text); font-size: var(--fs-data); padding: 9px 15px;
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.5);
  animation: hubToastIn 180ms var(--ease-out);
}

/* ── responsive ────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
  .hub-main { padding: 0 10px 28px; }
  .hub-bar { padding: 8px 10px; margin: 0 -10px 10px; gap: 8px; }
  .hub-bar .hub-btn span { display: none; }
  .hub-bar .hub-btn { padding: 7px 9px; }
}
/* gridstack collapses to 6 columns below 900px — the 24-col guides would lie */
@media (max-width: 900px) {
  .editing #hub-grid { background-image: none; }
}
@media (prefers-reduced-motion: reduce) {
  .hub-live .dot { animation: none; }
  .hub-modal, .hub-toast { animation: none; }
}

/* liquidity map */
.vliq { position: absolute; inset: 0; }

/* native heatmap */
.vheat { position: absolute; inset: 2px; }

/* coin icons in data rows */
.vic-slot { display: inline-flex; width: 16px; height: 16px; flex: none; align-self: center; }
.vic { width: 16px; height: 16px; border-radius: 50%; }
.vic-f {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--bg4); color: var(--text3);
  font-size: var(--fs-meta); font-weight: 700;
}
.vrow-h .vic-slot { visibility: hidden; }

/* funding venue columns — drop columns as the block narrows */
.vrow .fr { min-width: 62px; font-size: var(--fs-meta); }
.vrow .coi { min-width: 64px; font-size: var(--fs-meta); }
@container (max-width: 470px) { .vrow .cbyb { display: none; } }
@container (max-width: 410px) { .vrow .cbin { display: none; } }
@container (max-width: 340px) { .vrow .coi { display: none; } }
@container (max-width: 280px) { .vrow .cokx { display: none; } }

/* candle close: UTC clock + local time */
.vcd-clock {
  text-align: center; padding-bottom: 7px; margin-bottom: 2px;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}
.vcd-clock { min-width: 0; }
.vcd-clock .utc {
  color: var(--text); font-size: clamp(var(--fs-title), 2.6vh, 22px); font-weight: 700;
  line-height: 1.15; font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vcd-clock .utc span { margin-left: 5px; color: var(--text3); font-size: var(--fs-meta); font-weight: 500; letter-spacing: 0.07em; }
.vcd-clock .local { margin-top: 1px; color: var(--text3); font-size: var(--fs-meta); font-variant-numeric: tabular-nums; }

/* ── v2.19.0: section nav — four places, icons + words, one contained
   segmented control. The active place is unmissable: filled pill, teal
   icon. Right-side controls sit in labeled groups split by hairlines. ── */
.hub-nav {
  display: flex; align-items: center; gap: 2px; flex: none;
  margin-left: 12px; padding: 3px;
  background: var(--bg2); border: 1px solid var(--border); border-radius: 999px;
  min-width: 0; overflow-x: auto; scrollbar-width: none;
}
.hub-nav::-webkit-scrollbar { display: none; }
.hub-nav a {
  display: inline-flex; align-items: center; gap: 7px; flex: none;
  color: var(--text3); text-decoration: none; white-space: nowrap;
  font-size: var(--fs-ui); font-weight: 500; padding: 6px 12px; border-radius: 999px;
  transition: color 120ms var(--ease-out), background 120ms var(--ease-out);
}
.hub-nav a i, .hub-nav a svg { display: inline-block; width: 13px; height: 13px; stroke-width: 1.7; opacity: 0.75; }
.hub-nav a:hover { color: var(--text); background: var(--bg3); }
.hub-nav a:active { transform: scale(0.97); }
.hub-nav a.active { color: var(--text); background: var(--bg4); font-weight: 600; }
.hub-nav a.active i, .hub-nav a.active svg { color: var(--teal); opacity: 1; }
.hub-nav a:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
/* hairline group dividers on the right cluster */
.hub-div { width: 1px; height: 18px; flex: none; background: var(--border2); margin: 0 4px; }
body.in-section #hub-div-dash { display: none; }
/* the palette button wears its shortcut — one glance says what it is */
.hub-btn .kbd {
  padding: 1.5px 5px; border: 1px solid var(--border2); border-radius: 4px;
  color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.04em; line-height: 1.3;
}
@media (max-width: 860px) {
  .hub-nav { margin-left: 8px; }
  .hub-nav a span { display: none; } /* icons carry the nav; words return on tap targets' tooltips */
  .hub-nav a { padding: 6px 10px; }
  .hub-nav a i, .hub-nav a svg { width: 14px; height: 14px; }
  .hub-btn .kbd { display: none; }
}

/* quiet the right side of the bar: Focus and search are icon chips —
   their labels live in tooltips (and the Focus chip only speaks when
   it has something to say: "SOL · exit") */
#hub-focus:not(.focus-on) span { display: none; }
#hub-focus, #hub-pal { padding: 5px 6px; }

/* section boards are ephemeral — layout controls are dashboard-only */
body.in-section #hub-layout-wrap,
body.in-section #hub-edit,
body.in-section #hub-add,
body.in-section #hub-tidy { display: none; }

/* pin-to-dashboard button: the one accent in an ephemeral block's header */
.hw-btn.hw-pin:hover { color: var(--teal); border-color: var(--teal); }

/* ── v1.12.0: section replica pages (static panels, no gridstack) ─────── */
.hub-sec {
  padding: 0 clamp(8px, 2.5vw, 56px) 28px;
  max-width: 1760px; margin: 0 auto;
}
/* pages glide in instead of popping — barely-there, honest about motion */
.hub-sec > .vpage-grid, .hub-sec > .vpage-bar { animation: hubPageIn 220ms var(--ease-out) backwards; }
/* `backwards`, NOT `both`. `both` RETAINS the to-frame after the animation
   ends, and a retained transform keyframe leaves the element with a computed
   `transform: matrix(1,0,0,1,0,0)` forever — which makes it a containing block
   for every `position: fixed` DESCENDANT. Measured: the Scanner's DEX picker,
   styled as a bottom sheet, resolved its `bottom: 0` against `.vsc` and landed
   at y=659 in a 660px viewport — entirely off-screen, and silently, because it
   still painted. `backwards` keeps the pre-start frame (so there is no flash of
   the un-animated state) and drops the retained one, so the transform really is
   `none` once the page has settled. Applies to all five section shells. */
@keyframes hubPageIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .hub-sec > .vpage-grid, .hub-sec > .vpage-bar { animation: none; } }
.vpage-bar {
  display: flex; align-items: center; gap: 6px;
  padding: 10px 2px 12px; flex-wrap: wrap;
}
.vpage-chip {
  font: inherit; font-size: var(--fs-ui); padding: 6px 13px; cursor: pointer;
  background: var(--bg3); color: var(--text3);
  border: 1px solid var(--border2); border-radius: var(--radius);
  transition: color 160ms var(--ease-out), border-color 160ms var(--ease-out), background 160ms var(--ease-out), transform 140ms var(--ease-out);
}
.vpage-chip:hover { color: var(--text); border-color: var(--border3); }
.vpage-chip:active { transform: scale(0.96); }
.vpage-chip.on { color: var(--text); background: var(--bg4); border-color: var(--border3); font-weight: 600; }
.vpage-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
  padding-top: 10px;
}
.vpage-grid > .span2 { grid-column: 1 / -1; }
.vpanel {
  display: flex; flex-direction: column; min-height: 330px;
  background: var(--bg2); border: 1px solid var(--border);
  border-radius: var(--radius-lg, 6px); overflow: hidden;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.03), 0 8px 22px rgba(0,0,0,0.22);
}
.vpanel.vp-hero { min-height: 480px; }
.vpanel.vp-tall { min-height: 460px; }
.vpanel.vp-rows { min-height: 330px; }
.vpanel-head {
  display: flex; align-items: center; gap: 7px; flex: none;
  height: 30px; padding: 0 8px 0 11px;
  border-bottom: 1px solid var(--border);
  color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.04em;
}
.vpanel-head > i, .vpanel-head > svg { width: 12px; height: 12px; flex: none; stroke-width: 1.5; }
.vpanel-head .vp-title {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-weight: 600; text-transform: none;
}
.vpanel-head .hw-btns { display: inline-flex; gap: 3px; opacity: 0; transition: opacity 160ms var(--ease-out); }
.vpanel:hover .vpanel-head .hw-btns, .vpanel:focus-within .vpanel-head .hw-btns { opacity: 1; }
@media (hover: none) { .vpanel-head .hw-btns { opacity: 1; } }
.vpanel-head .hw-btn { padding: 3px 6px; border: none; background: transparent; }
.vpanel-body { position: relative; flex: 1; min-height: 0; container-type: inline-size; }
.vpanel-body .vw-scroll { position: absolute; inset: 0; overflow-y: auto; }
/* …but widgets that stack chrome above their scroll (news tabs) keep flex flow —
   equal specificity let the absolute rule above swallow the tab row */
.vpanel-body .vcol .vw-scroll { position: relative; inset: auto; flex: 1; min-height: 0; }
.vpanel:fullscreen { min-height: 100vh; background: var(--bg); }
@media (max-width: 900px) {
  .vpage-grid { grid-template-columns: 1fr; }
  .vpanel { min-height: 300px; }
}

/* cross-asset screener rows: 4 numeric columns */
.vscr .vscr-r { display: grid; grid-template-columns: 22px 76px 1fr 96px 110px 110px; align-items: center; }
.vscr .vscr-r .chg.fr { text-align: right; }
.vscr .vscr-c { cursor: pointer; user-select: none; }
.vscr .vscr-c:hover { color: var(--text); }
@container (max-width: 560px) { .vscr .vscr-r { grid-template-columns: 22px 70px 1fr 92px 100px; } .vscr .capr { display: none; } }
@container (max-width: 430px) { .vscr .vscr-r { grid-template-columns: 22px 64px 1fr 92px; } .vscr .cvol { display: none; } }

/* ── Vice Chart Pro (vchart.js — own engine: candles + <Vice> indicators +
      order-book depth heatmap). Fills its panel; toolbar, legend rows like
      the reference, hairline chips throughout. ─────────────────────────── */
.vpanel.vp-chartpro { min-height: 660px; }
@media (max-width: 900px) { .vpanel.vp-chartpro { min-height: 540px; } }
.vcp { position: absolute; inset: 0; display: flex; flex-direction: column; }
.vcp-bar {
  position: relative; display: flex; align-items: center; gap: 6px;
  flex-wrap: wrap; padding: 7px 10px; border-bottom: 1px solid var(--border);
}
.vcp-syms, .vcp-tfs { display: flex; align-items: center; gap: 3px; }
.vcp-sp { flex: 1; }
.vcp-chip {
  padding: 3px 8px; border: 1px solid transparent; border-radius: 4px;
  background: none; color: var(--text2); font: 600 var(--fs-meta)/1.4 var(--mono);
  cursor: pointer; transition: color 120ms, background 120ms, border-color 120ms;
}
.vcp-chip:hover { color: var(--text); }
.vcp-chip.on { background: var(--bg4); color: var(--text); border-color: var(--border2); }
.vcp-input {
  width: 74px; padding: 3px 8px; border: 1px solid var(--border); border-radius: 4px;
  background: var(--bg2); color: var(--text); font: 500 var(--fs-meta)/1.4 var(--mono);
}
.vcp-input:focus { outline: none; border-color: var(--border2); }
.vcp-btn {
  display: inline-flex; align-items: center; gap: 5px; padding: 4px 8px;
  border: none; border-radius: var(--radius); background: none;
  color: var(--text3); font: 600 var(--fs-meta)/1.5 var(--mono); cursor: pointer;
  transition: color 120ms, background 120ms, transform 120ms var(--ease-out);
}
.vcp-btn:hover { color: var(--text); background: var(--bg3); }
.vcp-btn:active { transform: scale(0.97); }
.vcp-btn.on { background: color-mix(in srgb, var(--pink) 16%, transparent); color: var(--pink); border-color: color-mix(in srgb, var(--pink) 45%, transparent); }
.vcp-btn i, .vcp-btn svg { width: 13px; height: 13px; stroke-width: 1.8; }
.vcp-stage { position: relative; flex: 1; min-height: 0; }
.vcp-box { position: absolute; inset: 0; }
.vcp-err {
  display: none; padding: 6px 11px; color: var(--red2); font-size: var(--fs-meta);
  background: color-mix(in srgb, var(--bg2) 85%, transparent);
  border-top: 1px solid var(--border);
}
.vcp-err.on { display: block; }

/* legend rows — Velo puts live values top-left; so do we */
.vcp-legend {
  position: absolute; top: 6px; left: 10px; z-index: 3;
  display: flex; flex-direction: column; gap: 2px; max-width: min(72%, 560px);
  pointer-events: none;
}
.vcp-lrow {
  display: flex; align-items: center; gap: 8px; padding: 1px 0;
  font-size: var(--fs-meta); color: var(--text2); pointer-events: auto; min-height: 18px;
}
.vcp-ohlc { color: var(--text); flex-wrap: wrap; row-gap: 0; }
.vcp-ohlc b { font-weight: 700; font-size: var(--fs-meta); color: var(--text); }
.vcp-ohlc span { color: var(--text3); }
.vcp-ohlc em { font-style: normal; }
.vcp-ohlc em.up { color: var(--green, #21d196); }
.vcp-ohlc em.down { color: var(--red); }
.vcp-lname { color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vcp-lval { color: var(--text); white-space: nowrap; }
.vcp-lerr { color: var(--red2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 260px; }
.vcp-lbtns {
  display: inline-flex; align-items: center; gap: 2px;
  opacity: 0; pointer-events: none; transition: opacity 150ms var(--ease-out);
}
.vcp-lrow:hover .vcp-lbtns { opacity: 1; pointer-events: auto; }
.vcp-lbtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; padding: 0; border: none; border-radius: var(--radius);
  background: none; color: var(--text3); cursor: pointer;
}
.vcp-lbtn:hover { color: var(--text); background: var(--bg4); }
.vcp-lbtn i, .vcp-lbtn svg { width: 12px; height: 12px; stroke-width: 1.8; }
@media (pointer: coarse) {
  .vcp-lbtns { opacity: 1; pointer-events: auto; }
  .vcp-lbtn { width: 24px; height: 24px; }
}

/* indicator picker + per-indicator settings popover */
.vcp-menu {
  position: absolute; top: calc(100% + 4px); right: 10px; z-index: 30;
  display: grid; grid-template-columns: repeat(2, minmax(230px, 1fr));
  gap: 1px 6px; padding: 6px; border: 1px solid var(--border2);
  border-radius: var(--radius-lg); background: var(--bg2);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
}
@media (max-width: 700px) {
  .vcp-menu { grid-template-columns: 1fr; max-height: 320px; overflow-y: auto; }
}
.vcp-mrow {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 6px 9px; border: none; border-radius: 4px; background: none;
  color: var(--text2); font: 500 var(--fs-data)/1.4 var(--mono); text-align: left; cursor: pointer;
}
.vcp-mrow:hover { background: var(--bg3); color: var(--text); }
.vcp-mrow.on { color: var(--text); }
.vcp-mrow i, .vcp-mrow svg { width: 13px; height: 13px; color: var(--teal); }
.vcp-pop {
  position: absolute; top: calc(100% + 2px); left: 0; z-index: 31;
  display: flex; flex-direction: column; gap: 7px; min-width: 220px;
  padding: 10px; border: 1px solid var(--border2); border-radius: var(--radius-lg);
  background: var(--bg2); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
}
.vcp-poprow { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: var(--fs-meta); color: var(--text2); }
.vcp-poprow select {
  padding: 3px 6px; border: 1px solid var(--border); border-radius: 4px;
  background: var(--bg3); color: var(--text); font: 500 var(--fs-meta)/1.4 var(--mono);
}
.vcp-popnote { color: var(--text3); font-size: var(--fs-meta); line-height: 1.5; max-width: 240px; }

/* ── Chart-page refinement pass (owner: TV-grade, sleek) ─────────────────
      The chart page hides the panel chrome — the engine carries its own
      toolbar; symbol button + search modal follow TradingView's pattern. */
.vpanel.vp-chartpro > .vpanel-head { display: none; }
.vpanel.vp-chartpro { border-radius: 6px; overflow: hidden; }
.vcp-bar { gap: 7px; padding: 4px 8px; }
.vcp-symbtn {
  display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px;
  border: none; border-radius: var(--radius); background: none; cursor: pointer;
  color: var(--text); font: 700 var(--fs-data)/1.3 var(--mono);
  transition: background 120ms;
}
.vcp-symbtn:hover { background: var(--bg3); }
.vcp-symbtn b { font-weight: 700; letter-spacing: 0.01em; }
.vcp-symq { color: var(--text3); font-size: var(--fs-meta); font-weight: 600; margin-left: -2px; }
.vcp-symbtn i, .vcp-symbtn svg { width: 13px; height: 13px; color: var(--text3); stroke-width: 2; }
.vcp-div { width: 1px; height: 18px; background: var(--border); }
.vcp-btn.vcp-ico { padding: 4px 6px; }
.vcp-btn.vcp-ico i, .vcp-btn.vcp-ico svg { margin: 0; }

/* heatmap opacity — appears only while the map is on. Two variants, one built
   per session by vchart.js: a range slider on a fine pointer, a stepped chip
   row on a coarse one (audit F-FORM). */
.vcp-alpha { display: none; align-items: center; }
.vcp-alpha.show { display: inline-flex; }
.vcp-alpha.vcp-astep { gap: 2px; }
.vcp-alpha input[type="range"] {
  width: 74px; height: 3px; appearance: none; -webkit-appearance: none;
  background: linear-gradient(90deg, #31188466, #f44ac6);
  border-radius: 2px; outline: none; cursor: pointer;
}
.vcp-alpha input[type="range"]::-webkit-slider-thumb {
  appearance: none; -webkit-appearance: none; width: 11px; height: 11px;
  border-radius: 50%; background: var(--text); border: 2px solid var(--bg2);
  cursor: grab;
}
.vcp-alpha input[type="range"]::-moz-range-thumb {
  width: 11px; height: 11px; border-radius: 50%; background: var(--text);
  border: 2px solid var(--bg2); cursor: grab;
}

/* symbol search modal — TradingView pattern: dim, centered card, live rows */
.vcp-search {
  position: absolute; inset: 0; z-index: 40; display: flex;
  align-items: flex-start; justify-content: center; padding-top: 42px;
  background: rgba(5, 4, 12, 0.62); backdrop-filter: blur(3px);
  animation: veilIn 160ms ease both;
}
@keyframes veilIn { from { opacity: 0; } }
.vcp-search-box {
  animation: vcpPop 150ms var(--ease-out) both;
  transform-origin: top center;
  width: min(480px, 92%); max-height: min(430px, 82%); display: flex;
  flex-direction: column; border: 1px solid var(--border2); border-radius: var(--radius-lg);
  background: var(--bg2); box-shadow: 0 24px 64px rgba(0, 0, 0, 0.55);
  overflow: hidden;
}
.vcp-search-inp {
  padding: 12px 14px; border: none; border-bottom: 1px solid var(--border);
  background: none; color: var(--text); font: 500 var(--fs-data)/1.4 var(--mono);
}
.vcp-search-inp:focus { outline: none; }
.vcp-search-list { overflow-y: auto; padding: 4px; }
.vcp-srow {
  display: grid; grid-template-columns: 26px 64px 1fr auto auto; gap: 10px;
  align-items: center; width: 100%; padding: 7px 10px; border: none;
  border-radius: var(--radius); background: none; text-align: left; cursor: pointer;
  color: var(--text2); font: 500 var(--fs-meta)/1.4 var(--mono);
}
.vcp-srow:hover, .vcp-srow.sel { background: var(--bg3); color: var(--text); }
.vcp-srow b { color: var(--text); font-weight: 700; }
.vcp-sic { display: flex; align-items: center; justify-content: center; }
.vcp-sic img { width: 20px; height: 20px; border-radius: 50%; }
.vcp-sletter {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 50%; background: var(--bg4);
  color: var(--text2); font-size: var(--fs-meta); font-weight: 700;
}
.vcp-sname { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--text3); font-size: var(--fs-meta); }
.vcp-spx { color: var(--text); font-variant-numeric: tabular-nums; }
.vcp-schg { font-variant-numeric: tabular-nums; font-size: var(--fs-meta); }
.vcp-schg.up { color: var(--green, #21d196); }
.vcp-schg.down { color: var(--red); }
.vcp-snone { padding: 18px; text-align: center; color: var(--text3); font-size: var(--fs-meta); }

/* ── Velo-style screener (vScreener rebuild) ────────────────────────── */
.vscr2 { position: absolute; inset: 0; display: flex; flex-direction: column; }
.vscr2-bar {
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  padding: 5px 10px; border-bottom: 1px solid var(--border);
}
/* `.vscr2-chk` (a 12×12 OS checkbox + label) is GONE — the screener's two
   filters are .vpage-chip toggles now, for the reasons in hub.js's mkChk
   (audit F-FORM). Rules deleted rather than left to rot. */
.vscr2-sel {
  appearance: none; -webkit-appearance: none;
  padding: 2px 18px 2px 7px; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg2); color: var(--text2); font: 500 var(--fs-meta)/1.5 var(--mono);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='9' viewBox='0 0 24 24' fill='none' stroke='%238b85a3' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 6px center;
  cursor: pointer;
}
.vscr2-sel:hover { color: var(--text); border-color: var(--border2); }
.vscr2-tabs {
  display: inline-flex; gap: 1px; margin-left: auto; padding: 1px;
  border: 1px solid var(--border); border-radius: var(--radius);
}
.vscr2-tab {
  padding: 2px 9px; border: none; border-radius: var(--radius); background: none;
  color: var(--text3); font: 600 var(--fs-meta)/1.5 var(--mono); cursor: pointer;
}
.vscr2-tab.on { background: var(--bg4); color: var(--text); }
.vscr2-search {
  display: flex; align-items: center; gap: 7px; padding: 4px 10px;
  border-bottom: 1px solid var(--border); color: var(--text3);
}
.vscr2-search i, .vscr2-search svg { width: 12px; height: 12px; }
.vscr2-search input {
  flex: 1; border: none; background: none; color: var(--text);
  font: 500 var(--fs-meta)/1.6 var(--mono);
}
.vscr2-search input:focus { outline: none; }
.vscr2 .vscr2-scroll { position: relative; inset: auto; flex: 1; min-height: 0; }
.vscr2-r {
  display: grid; grid-template-columns: 30px minmax(150px, 1fr) 110px 90px 110px;
  gap: 8px; align-items: center; padding: 4px 10px;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
  font-size: var(--fs-meta); color: var(--text2);
}
.vscr2-r.clickable { cursor: pointer; }
.vscr2-r.clickable:hover { background: var(--bg3); }
.vscr2-h {
  position: sticky; top: 0; z-index: 2; background: var(--bg2);
  color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.05em;
  text-transform: uppercase;
}
.vscr2-c.fr { text-align: right; font-variant-numeric: tabular-nums; }
.vscr2-h .vscr2-c { cursor: pointer; user-select: none; }
.vscr2-h .vscr2-c:hover { color: var(--text); }
.vscr2-sym { display: inline-flex; align-items: center; gap: 8px; overflow: hidden; }
.vscr2-sym b { color: var(--text); font-weight: 700; }
.vscr2-sym i, .vscr2-sym svg { width: 16px; height: 16px; color: var(--text3); }
.vscr2-name {
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  color: var(--text3); font-size: var(--fs-meta);
}
.vscr2-logo { width: 18px; height: 18px; border-radius: 50%; flex: none; }
.vscr2-star {
  display: inline-flex; align-items: center; justify-content: center;
  border: none; background: none; padding: 8px; margin: -8px;
  color: var(--text3); opacity: 0.35; font-size: var(--fs-data); cursor: pointer; line-height: 1;
}
@media (hover: none) { .vscr2-star { opacity: 0.7; } }
.vscr2-r:hover .vscr2-star { opacity: 0.8; }
.vscr2-star.on { color: #e7b53a; opacity: 1; }
.vscr2-h .vscr2-star { cursor: default; opacity: 0; }
.vscr2-c.up { color: var(--green, #21d196); }
.vscr2-c.down { color: var(--red); }
.vscr2-none { padding: 22px; text-align: center; color: var(--text3); font-size: var(--fs-meta); }
.vscr2-news {
  display: grid; grid-template-columns: 92px 1fr auto; gap: 10px;
  align-items: baseline; padding: 8px 12px; text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
}
.vscr2-news:hover { background: var(--bg3); }
.vscr2-nsrc { color: var(--text3); font-size: var(--fs-meta); text-transform: uppercase; letter-spacing: 0.05em; }
.vscr2-nt { color: var(--text); font-size: var(--fs-meta); }
.vscr2-nago { color: var(--text3); font-size: var(--fs-meta); }
@media (max-width: 760px) {
  .vscr2-r { grid-template-columns: 26px minmax(110px, 1fr) 90px 70px; }
  .vscr2-r .cvol { display: none; }
  .vscr2-name { display: none; }
}

/* ── UX audit pass (owner: snappy/compact/polished) ──────────────────── */
/* the chart IS the page — fill the viewport instead of a fixed strip */
.vpanel.vp-chartpro { height: calc(100dvh - 114px); min-height: 460px; }
@media (max-width: 900px) { .vpanel.vp-chartpro { height: calc(100dvh - 110px); min-height: 420px; } }

/* (dashboard-only controls hide in sections via the v1.12.0 rule above) */

/* fetches dim the stage just enough to read as "working" */
.vcp-stage { transition: opacity 140ms var(--ease-out); }
.vcp.loading .vcp-stage { opacity: 0.55; }

/* pressables answer the press */
.vcp-chip:active, .vcp-symbtn:active, .vscr2-tab:active, .vscr2-sel:active,
.vcp-mrow:active, .vcp-srow:active { transform: scale(0.97); }
.vcp-chip, .vcp-symbtn, .vscr2-tab, .vcp-mrow, .vcp-srow {
  transition: color 120ms var(--ease-out), background 120ms var(--ease-out),
    border-color 120ms var(--ease-out), transform 120ms var(--ease-out);
}

/* popovers scale in from their trigger, quick and origin-aware */
.vcp-menu {
  transform-origin: top right;
  animation: vcpPop 140ms var(--ease-out) both;
}
.vcp-pop {
  transform-origin: top left;
  animation: vcpPop 140ms var(--ease-out) both;
}
@keyframes vcpPop {
  from { opacity: 0; transform: scale(0.96); }
  to { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .vcp-menu, .vcp-pop { animation: none; }
}

/* screener rows: press feedback + calmer hover */
.vscr2-r.clickable { transition: background 100ms var(--ease-out); }
.vscr2-r.clickable:active { background: var(--bg4); }

/* mid-width toolbars: one scrollable row beats a two-line wrap */
@media (max-width: 760px) {
  .vcp-bar { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
  .vcp-bar::-webkit-scrollbar { display: none; }
  .vscr2-bar { gap: 8px; }
  .vscr2-tabs { margin-left: 0; }
}


/* ── audit round 2: focus rings, section menu, containers, safe areas ── */
.vcp-chip:focus-visible, .vcp-btn:focus-visible, .vcp-symbtn:focus-visible,
.vscr2-tab:focus-visible, .vscr2-sel:focus-visible, .vcp-lbtn:focus-visible,
.vscr2-star:focus-visible {
  outline: 2px solid var(--teal); outline-offset: 1px;
}
.vcp-input:focus { border-color: var(--teal); }

/* section pages: layout management is dashboard business — the ⋮ menu keeps
   only the suite links (Reset/Delete on a chart page begged for misclicks) */
body.in-section .hub-menu button[data-menu],
body.in-section .hub-menu label,
body.in-section .hub-menu .sep { display: none; }

/* the chart toolbar + menu + screener adapt to their CONTAINER — page media
   queries lied inside narrow pinned widgets */
@container (max-width: 560px) {
  .vcp-btn span { display: none; }
  .vcp-btn { padding: 4px 7px; }
  .vcp-div { display: none; }
  .vcp-alpha input[type="range"] { width: 52px; }
  .vcp-menu { grid-template-columns: 1fr; max-width: calc(100% - 16px); max-height: 300px; overflow-y: auto; }
  .vscr2-r { grid-template-columns: 26px minmax(100px, 1fr) 88px 68px; }
  .vscr2-r .cvol { display: none; }
  .vscr2-name { display: none; }
}

/* iOS: sub-16px inputs auto-zoom the page on focus. This rule USED TO LIVE HERE
   and was silently undone 376 lines later by `.vgal-search input { font: … }` —
   same specificity, later in the file, and the `font:` shorthand resets
   font-size. It now lives at the very END of this file so nothing can outrank
   it by source order (audit F-ZOOM). Do not move it back up. */

/* viewport-fit=cover is declared — consume the insets it exposes */
.hub-bar {
  padding-left: max(16px, env(safe-area-inset-left));
  padding-right: max(16px, env(safe-area-inset-right));
}
.hub-main {
  padding-left: max(16px, env(safe-area-inset-left));
  padding-right: max(16px, env(safe-area-inset-right));
  padding-bottom: max(40px, env(safe-area-inset-bottom));
}


@keyframes hubToastIn { from { opacity: 0; transform: translateX(-50%) translateY(8px) scale(0.985); } }

/* overflow menu scales in from its trigger corner */
.hub-menu.open { animation: vcpPop 150ms var(--ease-out) both; transform-origin: top right; }

/* modal + palette veils fade instead of popping */
.hub-modal-veil, .hub-palette-veil { animation: veilIn 160ms ease both; }
@media (prefers-reduced-motion: reduce) {
  .hub-menu.open, .hub-modal-veil, .hub-palette-veil, .vcp-search, .vcp-search-box { animation: none; }
}

/* wordmark is a link home to the dashboard — and stays NEUTRAL (owner:
   "remove the blue"); affordance is the hover surface, not a color */
a.hub-bar-title, a.hub-bar-title strong, a.hub-bar-title:hover,
a.hub-bar-title:hover strong, a.hub-bar-title:visited { color: var(--text); }
a.hub-bar-title { text-decoration: none; border-radius: var(--radius); padding: 2px 5px; margin: -2px -5px; }
a.hub-bar-title:hover { background: var(--bg3); }

/* legend rows shrink-wrap — full-width rows were eating crosshair hovers */
.vcp-legend { align-items: flex-start; }
.vcp-lrow { width: max-content; max-width: 100%; }


/* chromeless widgets (ticker tape): the strip IS the content — no title bar
   in view mode; edit mode brings it back for drag/settings/remove */
.hw-chromeless .hw-head { display: none; }
body.editing .hw-chromeless .hw-head { display: flex; }

/* legend settings tag — which EMA / venue / window this row is */
.vcp-ltag { color: var(--text3); font-size: var(--fs-meta); white-space: nowrap; }
.vcp-ltag:empty { display: none; }

/* indicator menu: active rows show check + a small "+" for a 2nd instance */
.vcp-mact { display: inline-flex; align-items: center; gap: 6px; }
.vcp-madd {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border-radius: var(--radius);
  background: var(--bg4); color: var(--text2); font-size: var(--fs-meta); line-height: 1;
  opacity: 0; transition: opacity 120ms var(--ease-out), color 120ms;
}
.vcp-mrow:hover .vcp-madd { opacity: 1; }
.vcp-madd:hover { color: var(--teal); }
@media (pointer: coarse) { .vcp-madd { opacity: 1; } }

/* ── timeframe menu: grouped list, starrable, custom interval ── */
.vcp-menu.vcp-tfmenu {
  left: 110px; right: auto; grid-template-columns: 1fr; min-width: 180px;
  max-width: 220px; transform-origin: top left;
}
.vcp-tfcat {
  padding: 6px 9px 3px; color: var(--text3); font-size: var(--fs-meta);
  letter-spacing: 0.08em; font-weight: 600;
}
.vcp-tfrow { display: flex; align-items: center; justify-content: space-between; }
.vcp-tfrow.on > span:first-child { color: var(--teal); font-weight: 700; }
.vcp-tfstar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; margin: -4px -4px -4px 0; border-radius: var(--radius);
  color: var(--text3); opacity: 0.25; font-size: var(--fs-meta);
  transition: opacity 120ms var(--ease-out), color 120ms;
}
.vcp-tfrow:hover .vcp-tfstar { opacity: 0.75; }
.vcp-tfstar.on { color: #e7b53a; opacity: 1; }
.vcp-tfstar:hover { color: #e7b53a; }
@media (pointer: coarse) { .vcp-tfstar { opacity: 0.7; } }
.vcp-tfcustom { display: flex; gap: 5px; padding: 4px 6px 6px; }
.vcp-tfcustom input {
  flex: 1; min-width: 0; padding: 4px 8px; border: 1px solid var(--border);
  border-radius: var(--radius); background: var(--bg3); color: var(--text);
  font: 500 var(--fs-meta)/1.4 var(--mono);
}
.vcp-tfcustom input:focus { outline: none; border-color: var(--teal); }
.vcp-tfcustom input.bad { border-color: var(--red); }
.vcp-tfgo {
  padding: 4px 10px; border: none; border-radius: var(--radius);
  background: var(--bg4); color: var(--text); font: 600 var(--fs-meta)/1.5 var(--mono);
  cursor: pointer;
}
.vcp-tfgo:hover { background: var(--border2); }

/* live stock hits in the Focus search carry their TV logos */
.fs-logo { width: 16px; height: 16px; border-radius: 50%; flex: none; }

/* ── drawing tools rail + overlay ── */
.vcp-rail {
  position: absolute; left: 0; top: 0; bottom: 0; z-index: 4;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  width: 34px; padding: 6px 0; border-right: 1px solid var(--border);
  background: color-mix(in srgb, var(--bg2) 92%, transparent);
}
.vcp-box { left: 34px; }
.vcp-legend { left: 44px; }
/* width/height are NOT redundant with inset:0. A canvas is a REPLACED element,
   and for an absolutely-positioned replaced box `width:auto` resolves to the
   INTRINSIC width — the canvas.width attribute — after which `right` is simply
   ignored as over-constrained (CSS 2.1 §10.3.8). paintDrawings() sets that
   attribute to `stage.clientWidth * devicePixelRatio` for a crisp backing
   store, so on any DPR>1 display (every phone, every Retina Mac) the canvas
   was LAID OUT at twice the stage width while ctx.setTransform(dpr) also
   scaled the coordinates — drawings landed at double position and size, and
   only .vpanel's overflow kept the excess off the page. Pinning the CSS box to
   the stage is the other half of the standard HiDPI canvas pattern: the
   attribute sizes the bitmap, this sizes the element. */
.vcp-draw { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 2; pointer-events: none; }
.vcp-tool {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border: none; border-radius: var(--radius);
  background: none; color: var(--text3); cursor: pointer;
  transition: color 120ms var(--ease-out), background 120ms var(--ease-out), transform 120ms var(--ease-out);
}
.vcp-tool i, .vcp-tool svg { width: 14px; height: 14px; stroke-width: 1.7; }
.vcp-tool:hover { color: var(--text); background: var(--bg3); }
.vcp-tool:active { transform: scale(0.94); }
.vcp-tool.on { color: var(--teal); background: var(--bg4); }
.vcp-tool:focus-visible { outline: 2px solid var(--teal); outline-offset: -1px; }
.vcp-rail-sep { width: 16px; height: 1px; margin: 4px 0; background: var(--border); }
.vcp-drawtext {
  position: absolute; z-index: 5; width: 160px; padding: 3px 7px;
  border: 1px solid var(--teal); border-radius: var(--radius);
  background: var(--bg2); color: var(--text); font: 500 var(--fs-meta)/1.4 var(--mono);
}
.vcp-drawtext:focus { outline: none; }
/* tiny pinned widgets: no room for the rail */
@container (max-width: 560px) {
  .vcp-rail { display: none; }
  .vcp-box { left: 0; }
  .vcp-legend { left: 10px; }
}

/* ── v1.27.0: audit P0/P1 pass — on-widget quick controls, geometry lock,
   coach-mark, in-page maximize, §3.3 polish ───────────────────────────── */

/* the widget itself is a container, so face controls can hide on narrow blocks
   (.hw-body stays the nearest container for everything inside the body) */
.grid-stack-item-content.hw { container-type: inline-size; }

/* face pills (audit C1): daily flips — timeframe, window, currency — live on
   the widget face; the settings modal keeps identity. Same segmented-switch
   language as the screener tabs, shrunk to head scale. */
.hw-ctl {
  display: inline-flex; flex: none; gap: 1px;
  padding: 1px; border: 1px solid var(--border); border-radius: var(--radius);
}
.hw-ctl-pill {
  padding: 0 6px; border: none; border-radius: calc(var(--radius) - 1px);
  background: none; color: var(--text3);
  font: 600 var(--fs-ui)/1.5 var(--mono); letter-spacing: 0.02em; cursor: pointer;
  transition: color 120ms var(--ease-out), background 120ms var(--ease-out), transform 120ms var(--ease-out);
}
.hw-ctl-pill:hover { color: var(--text); }
.hw-ctl-pill:active { transform: scale(0.94); }
.hw-ctl-pill.on { background: var(--bg4); color: var(--text); }
.hw-ctl-pill:focus-visible { outline: 2px solid var(--teal); outline-offset: -1px; }
@container (max-width: 340px) { .hw-head .hw-ctl { display: none; } }

/* header title: the NAME half gives way first — "· SOLUSD" beats "Mini Ch…"
   (audit M4); full text lives in the title tooltip. The name keeps a 3ch
   floor and the separator lives in ::before so it can never orphan. */
.hw-title { display: flex; align-items: baseline; gap: 5px; min-width: 0; }
.hw-t1 { flex: 0 50 auto; min-width: 3ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hw-t2 { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hw-t2::before { content: '· '; }

/* geometry lock (audit M3): view mode is for reading, Customize is for moving */
body:not(.editing) .hw-head { cursor: default; }
body:not(.editing) .hw-head:active { cursor: default; }
body.editing .grid-stack-item > .ui-resizable-handle { opacity: 0.4; }

/* in-page maximize — the fallback when requestFullscreen is denied (B5) */
.grid-stack-item-content.hw.hw-max,
.vpanel.hw-max {
  position: fixed; inset: 10px; z-index: var(--z-max); width: auto; height: auto;
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.7);
}
body.hub-maxed { overflow: hidden; }

/* first-visit coach-mark */
.hub-coach {
  display: flex; align-items: center; gap: 9px;
  margin: 0 0 10px; padding: 8px 12px;
  border: 1px dashed var(--border2); border-radius: var(--radius-lg);
  color: var(--text2); font-size: var(--fs-meta);
  animation: hubPageIn 220ms var(--ease-out) backwards;
}
.hub-coach i, .hub-coach svg { width: 14px; height: 14px; flex: none; color: var(--teal); stroke-width: 1.5; }
.hub-coach b { color: var(--text); font-weight: 600; }
.hub-coach-x {
  margin-left: auto; display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; flex: none; border: none; border-radius: var(--radius);
  background: none; color: var(--text3); cursor: pointer;
  transition: color 120ms var(--ease-out), background 120ms var(--ease-out);
}
.hub-coach-x:hover { color: var(--text); background: var(--bg3); }
.hub-coach-x i, .hub-coach-x svg { width: 12px; height: 12px; color: currentColor; }

/* ⋮ menu: passive backup-age line under the export group */
.hub-menu-hint { padding: 3px 10px 6px; color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.03em; }
body.in-section .hub-menu .hub-menu-hint { display: none; }
/* the coach-mark talks about the dashboard — section pages don't need it */
body.in-section .hub-coach { display: none; }

/* §3.3 session clocks: rows share the block's height instead of clipping the
   second row (Macro preset also grew h 4→6 via migration) */
.vclocks { align-content: stretch; grid-auto-rows: minmax(0, 1fr); gap: 6px; padding: 8px 10px; overflow: hidden; }
.vclock { display: flex; flex-direction: column; justify-content: center; min-height: 0; padding: 4px 6px; }
.vclock .time { margin: 1px 0 2px; font-size: clamp(13px, 2.1vh, 18px); }

/* §3.3 candle close: auto-margins center when there's room, squeeze + scroll
   when there isn't (fixed 12px gaps used to cut the 1W row) */
.vcd { gap: 0; padding: 6px 14px; overflow-y: auto; justify-content: flex-start; }
.vcd-clock { margin-top: auto; padding-bottom: clamp(3px, 0.9vh, 8px); margin-bottom: clamp(1px, 0.5vh, 4px); }
.vcd-clock .local { margin-top: 2px; }
.vcd-row { flex: none; padding: clamp(2px, 0.9vh, 6px) 0; }
.vcd-row:last-child { margin-bottom: auto; }

/* §3.3 funding table: short venue codes in the header + a slimmer column
   floor — DERIBIT no longer clips to DERIE at the preset width */
.vrow .fr { min-width: 56px; }

/* §3.3 alert inputs: placeholders must read as hints, not values */
.valerts-add input::placeholder { color: var(--text3); opacity: 0.75; }

/* §3.3 TV embeds: stay invisible until their document loads — no white flash
   against the dark theme (tvEmbed adds .tv-ready on iframe load, 4s failsafe) */
.hw-body > .tv-wrap, .vpanel-body > .tv-wrap { opacity: 0; transition: opacity 240ms var(--ease-out); }
.tv-wrap.tv-ready { opacity: 1; }

/* duplicate lives with the other layout tools — Customize only */
body:not(.editing) .hw-btn[data-act="dup"] { display: none; }

/* ── unified News (audit C3): tabs + read-state + density ─────────────── */
.vnews2 .vtabs { gap: 3px; }
.vnews a.seen { color: var(--text3); }
.vnews a.seen .meta { opacity: 0.75; }
.vnews2.compact a { padding: 5px 12px 4px; font-size: var(--fs-meta); line-height: 1.35; }
.vnews2.compact a .meta { margin-top: 1px; font-size: var(--fs-meta); }

/* ── any-edge resize (Customize): the hovered edge lights up ──────────── */
/* NOTE: no z-index here on purpose. Gridstack's script writes an inline
   `z-index: 100` on every handle, which no stylesheet rule can outrank — the
   `z-index: 5` that used to sit here was dead code. The chrome ladder at the
   top of this file is based ABOVE 100 instead, so handles now sit under every
   overlay without fighting the inline style (audit F-Z). */
.editing .grid-stack-item > .ui-resizable-handle {
  background-repeat: no-repeat;
  transition: opacity 140ms var(--ease-out);
}
.editing .grid-stack-item > .ui-resizable-handle:hover { opacity: 1; }
.editing .grid-stack-item > .ui-resizable-n:hover,
.editing .grid-stack-item > .ui-resizable-s:hover {
  background-image: linear-gradient(var(--teal), var(--teal));
  background-size: calc(100% - 10px) 2px; background-position: center;
  filter: drop-shadow(0 0 6px var(--teal-glow, rgba(0, 212, 212, 0.45)));
}
.editing .grid-stack-item > .ui-resizable-e:hover,
.editing .grid-stack-item > .ui-resizable-w:hover {
  background-image: linear-gradient(var(--teal), var(--teal));
  background-size: 2px calc(100% - 10px); background-position: center;
  filter: drop-shadow(0 0 6px var(--teal-glow, rgba(0, 212, 212, 0.45)));
}
.editing .grid-stack-item > .ui-resizable-ne:hover,
.editing .grid-stack-item > .ui-resizable-nw:hover,
.editing .grid-stack-item > .ui-resizable-sw:hover,
.editing .grid-stack-item > .ui-resizable-se:hover {
  background-image: radial-gradient(circle, var(--teal) 0 3px, transparent 3.5px);
  background-position: center;
  filter: drop-shadow(0 0 6px var(--teal-glow, rgba(0, 212, 212, 0.45)));
}
/* edge handles exist only in Customize — a locked grid shows none */
body:not(.editing) .grid-stack-item > .ui-resizable-handle { display: none; }

/* (the ≤640px "only n/s handles" rule lives further down, next to the coarse
   block that grows them — it has to outrank `body.editing … { display: block }`
   there, which is one specificity step above a bare `.editing` selector) */

/* Customize: hovering a block lights its WHOLE frame — the element is
   grabbable everywhere, the hovered edge adds the brighter grab line */
body.editing .grid-stack-item:hover > .grid-stack-item-content.hw {
  border-color: color-mix(in srgb, var(--teal) 70%, var(--border));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025), 0 3px 12px rgba(0, 0, 0, 0.22),
    0 0 0 1px color-mix(in srgb, var(--teal) 40%, transparent),
    0 0 18px color-mix(in srgb, var(--teal) 20%, transparent);
}

/* ── Element Gallery section — the storefront ─────────────────────────── */
.vgal { padding-top: 8px; animation: hubPageIn 220ms var(--ease-out) backwards; }

/* v3 storefront: the rail is an INDEX — jump + scroll-spy, never a filter */
.vgal-cols { display: grid; grid-template-columns: 188px minmax(0, 1fr); gap: 26px; align-items: start; }
.vgal-nav {
  position: sticky; top: 56px;
  display: flex; flex-direction: column; gap: 1px;
  padding: 2px 16px 6px 0; border-right: 1px solid var(--border);
  max-height: calc(100dvh - 76px); overflow-y: auto; scrollbar-width: none;
}
.vgal-nav::-webkit-scrollbar { display: none; }
.vgal-navlb { padding: 4px 10px 8px; color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.14em; text-transform: uppercase; }
.vgal-navi {
  position: relative; display: flex; align-items: center; gap: 9px;
  padding: 7px 10px; background: none; border: 0; border-radius: var(--radius);
  color: var(--text3); font: 500 var(--fs-ui)/1.3 var(--mono); letter-spacing: 0.01em;
  cursor: pointer; text-align: left; white-space: nowrap;
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
  transition: color 120ms var(--ease-out), background 120ms var(--ease-out);
}
.vgal-navi i, .vgal-navi svg { width: 14px; height: 14px; stroke-width: 1.5; flex: none; opacity: 0.75; }
.vgal-navi:hover { color: var(--text); background: var(--bg3); }
.vgal-navi:active { transform: scale(0.98); }
.vgal-navi.on { color: var(--text); background: color-mix(in srgb, var(--teal) 7%, transparent); }
.vgal-navi.on::before {
  content: ''; position: absolute; left: -1px; top: 22%; bottom: 22%;
  width: 2px; border-radius: 2px; background: var(--teal);
}
.vgal-navi.on i, .vgal-navi.on svg { color: var(--teal); opacity: 1; }
.vgal-navi.dim { opacity: 0.35; }
.vgal-navi:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }
.vgal-navi .n { margin-left: auto; padding: 1px 6px; border-radius: 999px; background: var(--bg3); color: var(--text3); font-size: var(--fs-meta); font-variant-numeric: tabular-nums; }
.vgal-navi.on .n { background: color-mix(in srgb, var(--teal) 15%, transparent); color: var(--teal); }
.vgal-maincol { min-width: 0; }
.vgal-toolbar {
  position: sticky; top: 40px; z-index: 5;
  display: flex; align-items: center; gap: 12px;
  padding: 10px 0; margin-bottom: 4px;
  background: var(--bg);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
}
.vgal2 .vgal-search { flex: 0 1 300px; }
.vgal-ctx { margin-left: auto; color: var(--text3); font-size: var(--fs-meta); font-variant-numeric: tabular-nums; white-space: nowrap; }
.vgal-mnav { display: none; }
.vgal-sec { scroll-margin-top: 104px; }
.vgal2 .vgal-cat { display: flex; align-items: center; gap: 8px; margin: 30px 2px 12px; color: var(--text); font-size: var(--fs-data); font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
.vgal2 .vgal-sec:first-child .vgal-cat { margin-top: 4px; }
.vgal2 .vgal-cat i, .vgal2 .vgal-cat svg { width: 14px; height: 14px; stroke-width: 1.6; color: var(--teal); opacity: 0.9; }
.vgal2 .vgal-cat span { color: var(--text3); font-weight: 500; font-size: var(--fs-meta); }
.vgal2 .vgal-cat::after { content: ''; flex: 1; height: 1px; background: color-mix(in srgb, var(--border) 80%, transparent); }
.vgal2 .vgal-card:hover { border-color: color-mix(in srgb, var(--teal) 40%, var(--border2)); }
@media (max-width: 1120px) {
  .vgal-cols { grid-template-columns: 164px minmax(0, 1fr); gap: 18px; }
}
@media (max-width: 860px) {
  .vgal-cols { grid-template-columns: 1fr; gap: 0; }
  .vgal-nav { display: none; }
  .vgal2 .vgal-toolbar { flex-wrap: wrap; row-gap: 6px; }
  .vgal2 .vgal-search { flex: 1 1 220px; }
  .vgal-mnav { display: flex; gap: 2px; overflow-x: auto; scrollbar-width: none; flex-basis: 100%; }
  .vgal-mnav::-webkit-scrollbar { display: none; }
  .vgal-mnav .vgal-navi { flex: none; padding: 6px 10px; border-radius: 999px; }
  .vgal-mnav .vgal-navi.on { background: color-mix(in srgb, var(--teal) 10%, transparent); color: var(--text); }
  .vgal-mnav .vgal-navi.on::before { display: none; }
  .vgal-mnav .vgal-navi .n { display: none; }
  .vgal-ctx { display: none; }
}

.vgal-intro { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin: 6px 2px 14px; color: var(--text3); font-size: var(--fs-meta); }
.vgal-intro b { color: var(--text); font-size: var(--fs-ui); font-weight: 700; letter-spacing: 0.01em; }
.vgal-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; }
.vgal-search {
  display: flex; align-items: center; gap: 8px; flex: 0 1 240px; min-width: 170px;
  height: 32px; padding: 0 13px;
  border: 1px solid color-mix(in srgb, var(--border2) 80%, transparent); border-radius: 999px;
  background: color-mix(in srgb, var(--bg2) 78%, transparent); color: var(--text3);
  transition: border-color 140ms var(--ease-out), background 140ms var(--ease-out);
}
.vgal-search:hover { background: var(--bg2); }
.vgal-search i, .vgal-search svg { width: 13px; height: 13px; flex: none; }
/* longhand, never the `font:` shorthand — the shorthand resets font-size and
   this rule is what used to silently undo the 16px iOS floor (audit F-ZOOM) */
.vgal-search input { flex: 1; min-width: 0; border: none; background: none; color: var(--text); font-family: var(--mono); font-weight: 500; font-size: var(--fs-meta); line-height: 1.5; }
.vgal-search input:focus { outline: none; }
.vgal-search:focus-within { border-color: var(--teal); }
.vgal-chips { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
.vgal-cat {
  display: flex; align-items: baseline; gap: 8px;
  margin: 22px 2px 10px; color: var(--text2);
  font-size: var(--fs-meta); font-weight: 600; letter-spacing: 0.09em; text-transform: uppercase;
}
.vgal-list > .vgal-cat:first-child { margin-top: 4px; }
.vgal-cat span { color: var(--text3); font-weight: 500; letter-spacing: 0.02em; }
.vgal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 10px; }
.vgal-card {
  display: flex; align-items: flex-start; gap: 12px; text-align: left;
  padding: 11px 12px; border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--bg2); color: inherit; font: inherit; cursor: pointer;
  transition: border-color 150ms var(--ease-out), transform 150ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .vgal-card:hover { border-color: var(--border3); transform: translateY(-1px); }
  .vgal-card:hover .vgal-thumb { color: var(--text2); }
}
.vgal-card:active { transform: scale(0.99); }
.vgal-card:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
.vgal-thumb {
  width: 56px; height: 38px; flex: none; margin-top: 2px;
  fill: none; stroke-linejoin: round; opacity: 0.9;
  color: var(--text3); transition: color 150ms var(--ease-out);
}
.vgal-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
.vgal-head { display: flex; align-items: flex-start; gap: 7px; min-width: 0; }
/* Two lines, then ellipsis. Titles measure up to 219px in a 163px column, so a
   single nowrap line could only ever end in "Cumulative Return By…" — and the
   name is the one thing on this card that cannot be guessed from the rest. */
.vgal-head b {
  flex: 1 1 auto; min-width: 0; color: var(--text);
  font-size: var(--fs-data); font-weight: 600; line-height: 1.25;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere;
}
/* state + provenance, demoted out of the title row */
.vgal-meta { display: flex; align-items: center; gap: 6px; min-width: 0; margin-top: 1px; }
.vgal-meta .vgal-badge { margin-left: 0; }
.vgal-meta .vgal-on { display: inline-flex; align-items: center; gap: 3px; min-width: 0; }
.vgal-meta .vgal-on i { width: 11px; height: 11px; flex: none; }
.vgal-meta .vgal-on svg { width: 11px; height: 11px; }
.vgal-on {
  flex: none; padding: 0 6px; border-radius: 999px;
  background: color-mix(in srgb, var(--teal) 14%, transparent);
  color: var(--teal); font-size: var(--fs-meta); letter-spacing: 0.05em; line-height: 1.7;
}
.vgal-badge {
  margin-left: auto; flex: none; padding: 0 6px;
  border: 1px solid var(--border2); border-radius: 999px;
  color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.07em; text-transform: uppercase; line-height: 1.8;
}
.vgal-badge.vice { color: var(--teal); border-color: color-mix(in srgb, var(--teal) 40%, transparent); }
.vgal-desc {
  color: var(--text3); font-size: var(--fs-meta); line-height: 1.45;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.vgal-add {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 24px; height: 24px; margin: 6px 0 0 -2px; border-radius: var(--radius);
  color: var(--text3); cursor: pointer; align-self: center;
  transition: color 130ms var(--ease-out), background 130ms var(--ease-out), transform 120ms var(--ease-out);
}
.vgal-add i, .vgal-add svg { width: 14px; height: 14px; }
.vgal-add:hover { color: var(--teal); background: color-mix(in srgb, var(--teal) 12%, transparent); }
.vgal-add:active { transform: scale(0.9); }
.vgal-none { padding: 34px; text-align: center; color: var(--text3); font-size: var(--fs-data); }

/* inspector drawer: the element's live twin, on glass */
.vgal-veil {
  position: fixed; inset: 0; z-index: var(--z-veil);
  background: rgba(4, 3, 9, 0.45);
  animation: veilIn 160ms ease both;
}
.vgal-drawer {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: min(560px, 94vw);
  display: flex; flex-direction: column; gap: 12px;
  padding: 14px 18px 16px;
  background: color-mix(in srgb, var(--bg2) 78%, transparent);
  -webkit-backdrop-filter: blur(22px) saturate(1.5);
  backdrop-filter: blur(22px) saturate(1.5);
  border-left: 1px solid rgba(255, 255, 255, 0.09);
  box-shadow: -24px 0 70px rgba(0, 0, 0, 0.5);
  overflow-y: auto;
  animation: vgalDrawerIn 220ms var(--ease-out) both;
}
@keyframes vgalDrawerIn { from { transform: translateX(24px); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .vgal-drawer { animation: none; } .vgal-veil { animation: none; } }
.vgal-d-head { display: flex; align-items: center; gap: 9px; min-width: 0; }
.vgal-d-head i, .vgal-d-head svg { width: 15px; height: 15px; flex: none; color: var(--teal); stroke-width: 1.5; }
.vgal-d-head b { color: var(--text); font-size: var(--fs-ui); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vgal-d-head .vgal-badge { margin-left: 2px; }
.vgal-d-head .hw-btn { margin-left: auto; }
.vgal-d-prev { flex: none; }
.vgal-d-desc { color: var(--text2); font-size: var(--fs-data); line-height: 1.55; }
.vgal-d-meta { display: flex; flex-direction: column; gap: 7px; }
.vgal-d-meta span { display: flex; align-items: baseline; gap: 10px; color: var(--text2); font-size: var(--fs-meta); }
.vgal-d-meta label { flex: none; width: 84px; color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.09em; text-transform: uppercase; }
.vgal-d-foot { margin-top: auto; padding-top: 6px; }
.vgal-btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 12px; border: 1px solid var(--border2); border-radius: var(--radius);
  background: none; color: var(--text2); font: 600 var(--fs-data)/1.5 var(--mono); cursor: pointer;
  transition: color 130ms var(--ease-out), border-color 130ms var(--ease-out), background 130ms var(--ease-out), transform 120ms var(--ease-out);
}
.vgal-btn i, .vgal-btn svg { width: 13px; height: 13px; }
.vgal-btn:hover { color: var(--text); border-color: var(--border3); }
.vgal-btn:active { transform: scale(0.97); }
.vgal-btn.primary { border-color: color-mix(in srgb, var(--teal) 55%, transparent); color: var(--teal); }
.vgal-btn.primary:hover { background: color-mix(in srgb, var(--teal) 12%, transparent); }
.vgal-btn.big { width: 100%; justify-content: center; padding: 9px 12px; font-size: var(--fs-data); }
.vgal-btn:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
.vgal-preview {
  position: relative; height: 320px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg); overflow: hidden; container-type: inline-size;
}
.vgal-preview > .tv-wrap {
  position: absolute; top: 0; left: 0; width: 125%; height: 125%;
  transform: scale(0.8); transform-origin: 0 0;
  display: flex; flex-direction: column; justify-content: center;
  opacity: 0; transition: opacity 240ms var(--ease-out);
}
.vgal-preview > .tv-wrap.tv-ready { opacity: 1; }
@media (max-width: 700px) { .vgal-grid { grid-template-columns: 1fr; } }

/* ── v1.28.0: the phenomenal pass — quiet chips, Scanner desk, Tracked
   wallets, HL Positioning, slimmer Gallery. Hairlines carry the structure;
   color only ever means something. ────────────────────────────────────── */

/* chips, quieted globally: text first, pill only when active */
/* NOTE: this is the SECOND .vpage-chip rule in this file (the first is ~line
   589) and it wins by source order. Both must carry the control floor or the
   pass silently misses — found during the Phase 5 calibration. */
.vpage-chip {
  border: 1px solid transparent; background: none; color: var(--text3);
  padding: 5px 11px; border-radius: 999px; font-size: var(--fs-ui);
}
.vpage-chip:hover { color: var(--text); background: var(--bg3); border-color: transparent; }
.vpage-chip.on { color: var(--text); background: var(--bg4); border-color: transparent; font-weight: 600; }

/* ── Vice Scanner — the desk ──────────────────────────────────────────── */
.vsc { padding-top: 8px; animation: hubPageIn 220ms var(--ease-out) backwards; }
.vsc [hidden] { display: none !important; } /* the hidden attr must ALWAYS beat class display in here */

/* status header: what the Scanner is looking at right now */
.vsc-head { margin-bottom: 12px; }
.vsc-title { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.vsc-title b, .vsc-title h1 { white-space: nowrap; }
.vsc-title > i, .vsc-title > svg {
  width: 17px; height: 17px; color: var(--teal); stroke-width: 1.75; flex: none;
}
/* section titles are real <h1>s for document structure; the <b> selector
   stays for any older markup that still uses it (audit 2026-07-30) */
.vsc-title b, .vsc-title h1 {
  color: var(--text); font-size: var(--fs-h2); font-weight: 700; letter-spacing: -0.3px;
}
.vsc-title h1 { margin: 0; line-height: inherit; }
.vsc-live { display: inline-flex; align-items: center; gap: 6px; margin-left: var(--sp-2); color: var(--text3); font-size: var(--fs-meta); font-variant-numeric: tabular-nums; white-space: nowrap; }
.vsc-live .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green, #21d196); box-shadow: 0 0 7px color-mix(in srgb, var(--green, #21d196) 55%, transparent); flex: none; }
.vsc-live.syncing .dot { background: var(--gold, #e7b53a); box-shadow: none; animation: vscPulse 1.1s ease-in-out infinite; }
@keyframes vscPulse { 50% { opacity: 0.3; } }
.vsc-resync.spin i, .vsc-resync.spin svg { animation: vscSpin 900ms linear infinite; }
@keyframes vscSpin { to { transform: rotate(360deg); } }
/* Lede and disclosure share one row. They were stacked, costing ~73px of
   permanent chrome above the fold (UI audit 2026-07-30). */
.vsc-intro { margin: var(--sp-2) 0 0; max-width: 78ch; }
.vsc-lead { display: inline; color: var(--text2); font-size: var(--fs-title); line-height: 1.6; }
/* the summary trails the sentence like a continuation rather than floating
   beside a wrapped paragraph; opened, it drops to its own block below */
.vsc-method { display: inline-block; vertical-align: baseline; margin-left: 10px; }
.vsc-method[open] { display: block; margin: 9px 0 0; }

.vsc-bar { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; margin: 0 0 var(--sp-3); }
/* The global bar sits ABOVE the tablist because it scopes every tab. It reads
   as a control surface — its own faint panel — rather than as another naked
   row, which is what let it blur into the tabs directly beneath it. No bottom
   border: the tabs carry their own rule and two together made a double line. */
.vsc-gbar {
  margin: var(--sp-3) 0 0; padding: 7px var(--sp-3);
  gap: var(--sp-2);
  border: 1px solid var(--border); border-radius: var(--radius);
  background: color-mix(in srgb, var(--bg3) 40%, transparent);
}
/* Rhythm inside the bar: 4px between chips of one control, 20px between
   controls, and the depth/window pair pushed to the far end. Every gap being
   equal is what made six controls read as one undifferentiated strip. */
.vsc-cluster {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-4); min-width: 0;
}
.vsc-cluster:last-child { gap: var(--sp-1) var(--sp-3); flex: none; }
/* The search was `flex: 1 1 260px` and grew until cluster A wrapped, turning a
   one-row control bar into three. It takes the slack rather than demanding it. */
.vsc-gbar .vsc-search { flex: 1 1 190px; max-width: 340px; min-width: 150px; }
/* "All books / In a position" is two chips; letting them wrap inside a 178px
   box turned the whole cluster into two rows and the bar into three.
   Scoped to .vsc-acts, NOT to every .vgal-chips in the bar: as a blanket rule
   it also pinned the six depth chips to one 357px line, which overflowed a
   360px viewport and pushed the whole document sideways. */
.vsc-gbar .vsc-acts { flex-wrap: nowrap; flex: none; }
@media (min-width: 1181px) { .vsc-gbar { flex-wrap: nowrap; } .vsc-cluster { flex-wrap: nowrap; } }
/* Below that the clustering stops earning its keep: two nested flex boxes plus
   a spacer meant cluster A wrapped internally AND cluster B took a line of its
   own — three rows for six controls. `display: contents` dissolves the
   grouping so all six flow as one wrap, which fills two rows instead of three.
   The clusters exist to create rhythm at width; when there is no width to
   spare, packing wins. */
@media (max-width: 1180px) {
  .vsc-cluster { display: contents; }
  .vsc-gbar > .vsc-sp { display: none; }
  .vsc-gbar { gap: var(--sp-1) var(--sp-3); }
  .vsc-gbar .vsc-search { max-width: 260px; }
}
.vsc-gbar + .vsc-cohort + .vsc-views { margin-top: var(--sp-3); }
/* the per-view bar leads its panel now instead of trailing the cards */
.vsc-panel > .vsc-bar { margin: var(--sp-3) 0 0; }
.vsc-panel > .vsc-bar[hidden] { display: none; }
.vsc-sp { flex: 1; }
/* room for the intent hint (audit B1) without squeezing the field itself */
.vsc-search { flex: 1 1 260px; max-width: 420px; }
.vsc-flowctl { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.vsc-fl { color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.09em; text-transform: uppercase; margin: 0 2px 0 8px; }
.vsc-flowctl .vsc-fl:first-child { margin-left: 0; }
/* cohort depth — sits with the window chips, reads as one control group */
.vsc-sizes { display: flex; align-items: center; gap: 4px; }
.vsc-sizes .vsc-fl { margin-left: 0; }
.vsc-sizes[hidden] { display: none; }
.vsc-none { padding: 10px 12px; color: var(--text3); font-size: var(--fs-data); }
.vsc-drawer .vgal-d-head a.hw-btn { text-decoration: none; }

/* view tabs: Leaderboard · Positioning · Flow · Tracked */
.vsc-views { display: flex; align-items: center; gap: var(--sp-1); margin: var(--sp-5) 0 0; border-bottom: 1px solid var(--border); overflow-x: auto; scrollbar-width: none; }
.vsc-views::-webkit-scrollbar { display: none; }
.vsc-view {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  padding: 8px 12px 9px; margin-bottom: -1px;
  background: none; border: 0; border-bottom: 2px solid transparent;
  color: var(--text3); font: 600 var(--fs-ui)/1 var(--mono); letter-spacing: 0.02em;
  cursor: pointer; transition: color 130ms var(--ease-out);
}
.vsc-view i, .vsc-view svg { width: 13px; height: 13px; stroke-width: 1.6; }
.vsc-view:hover { color: var(--text2); }
.vsc-view.on { color: var(--text); border-bottom-color: var(--teal); }
.vsc-view .n {
  min-width: 16px; padding: 1px 5px; border-radius: 999px; text-align: center;
  background: var(--bg3); color: var(--text3); font-size: var(--fs-meta);
}
.vsc-view.on .n { background: color-mix(in srgb, var(--teal) 18%, transparent); color: var(--teal); }
.vsc-view:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }

/* the other shared-trade groups are buttons that re-point the drawer at
   themselves — a row shape, but a real button so it is keyboard-reachable */
.vsc-sharedalt {
  width: 100%; background: none; border: 0; text-align: left; cursor: pointer;
  font: inherit; color: inherit;
  transition: background 120ms var(--ease-out);
}
.vsc-sharedalt:hover { background: color-mix(in srgb, var(--teal) 9%, transparent); }
.vsc-sharedalt:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }

/* Top Movers states the cut it made. A gainers board that has quietly dropped
   200 coins should say so, or a short list reads as a thin market. */
.vmv-foot {
  padding: 7px 10px 2px; color: var(--text3); font-size: var(--fs-meta);
  line-height: 1.4; border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
  margin-top: 4px;
}

/* ── venue strip ────────────────────────────────────────────────────────────
   One line of exchange-wide context above the cohort. Hairline separators
   rather than boxes: this is a caption for the page, not a fifth card row. */
/* Shares the lede's row: prose left, exchange figures right. As its own
   full-width band it was one of seven stacked rows of near-identical weight,
   which is what made the header read as clutter rather than as a hierarchy.
   No box — a box here would be a third bordered rectangle competing with the
   filter bar and the cards for the same attention. */
.vsc-venue {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 var(--sp-3);
  margin-left: auto; padding: 0;
}
.vsc-vlab {
  display: inline-flex; align-items: center; gap: 5px;
  color: var(--teal); font-size: var(--fs-meta); font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase; white-space: nowrap;
}
.vsc-vlab > i, .vsc-vlab > svg { width: 12px; height: 12px; flex: none; stroke-width: 2; }
.vsc-vs {
  display: inline-flex; align-items: baseline; gap: 6px; min-width: 0;
  padding-left: var(--sp-3); border-left: 1px solid var(--border);
  white-space: nowrap;
}
.vsc-venue > .vsc-vlab + .vsc-vs { border-left: 0; padding-left: 0; }
.vsc-vs label {
  color: var(--text3); font-size: var(--fs-meta); font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase;
}
.vsc-vs b { font-size: var(--fs-data); font-weight: 700; font-variant-numeric: tabular-nums; }
.vsc-vs b.up { color: var(--green, #21d196); }
.vsc-vs b.down { color: var(--red); }
.vsc-vs b.t3 { color: var(--text3); font-weight: 500; }
/* qualifiers live in the stat's title attribute — see vstat() in hub.js */
.vsc-vs { cursor: help; }
.vsc-vs:not([title]) { cursor: default; }
.vsc-vs.wait { border-left: 0; padding-left: 0; color: var(--text3); font-size: var(--fs-meta); }

/* The hinge between exchange-wide and cohort, sitting under the controls that
   set the cohort. Prose, not a stat: it is the one thing on this page a reader
   has to actually read once, so it gets a sentence rather than a chip. */
.vsc-cohort {
  /* one sentence, one line — no prose measure, because a 68ch cap wrapped a
     single caption into two lines and bought back the band it was saving */
  display: flex; align-items: center; gap: 7px;
  margin: 6px 0 0; padding: 0;
  color: var(--text3); font-size: var(--fs-meta); line-height: 1.5;
}
.vsc-cohort[hidden] { display: none; }
.vsc-cohort > i, .vsc-cohort > svg {
  width: 13px; height: 13px; flex: none;
  color: var(--teal); stroke-width: 2;
}
.vsc-cohort b { color: var(--text2); font-weight: 700; }
@media (max-width: 700px) {
  .vsc-venue { gap: 0 var(--sp-2); }
  .vsc-vs { padding-left: var(--sp-2); }
}

/* ── cohort exposure strip ──────────────────────────────────────────────────
   Four blocks: position, margin, unrealised pnl, funding — each a total with
   its long/short split. Deliberately shorter than a .vsc-card: this is the
   headline the section opens with, and the audit's standing complaint about
   this page is permanent chrome above the fold, so it buys its row with about
   64px rather than a second card row's 108px. */
.vsc-expo {
  /* SAME track algorithm and gap as .vsc-agg below it. They stack, and two
     four-across rows computed from different minmax values land their column
     edges ~4px apart — which reads as a wobble, not as a grid. */
  display: grid; grid-template-columns: repeat(auto-fit, minmax(196px, 1fr));
  gap: var(--sp-2); margin: var(--sp-3) 0 0;
}
.vsc-expo:empty, .vsc-expo[hidden] { display: none; }
.vsc-ex {
  display: grid; grid-template-rows: auto auto auto; gap: 5px;
  padding: 9px 12px 8px; min-width: 0; text-align: left;
  /* A <button> does NOT inherit colour — it resolves to the UA's `buttontext`,
     which is black. Every other child here sets its own colour (label, the
     up/down arms, the note), so the only casualty was the unsigned total:
     position and margin rendered black-on-black while open pnl and funding,
     which carry .up/.down, looked fine. Set it on the button, not on the <b>,
     so anything added inside later inherits something legible. */
  color: var(--text);
}
.vsc-ex-h { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.vsc-ex-h > i, .vsc-ex-h > svg {
  width: 12px; height: 12px; flex: none; align-self: center;
  color: var(--caccent, var(--teal)); opacity: 0.85; stroke-width: 2;
}
.vsc-ex-h label {
  color: var(--text3); font-size: var(--fs-meta); font-weight: 500;
  letter-spacing: 0.1em; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
/* The exposure strip is the section's headline and the cards below it are
   secondary reads. They were set at the same weight, which is precisely why
   nine boxes in two rows read as a wall instead of a hierarchy: the squint
   test found no primary. The strip's figures step up; the cards step down. */
.vsc-ex-h b {
  margin-left: auto; font-size: var(--fs-h3); font-weight: 700;
  white-space: nowrap; letter-spacing: -0.02em;
}
.vsc-ex-h b.up { color: var(--green, #21d196); }
.vsc-ex-h b.down { color: var(--red); }
/* share bar: two arms filling one track, so the widths ARE the percentages */
.vsc-ex-bar {
  display: flex; height: 4px; border-radius: 2px; overflow: hidden;
  background: var(--bg4); font-size: 0;
}
.vsc-ex-bar .l { background: var(--green, #21d196); transition: width 400ms var(--ease-out); }
.vsc-ex-bar .s { background: var(--red); transition: width 400ms var(--ease-out); }
/* diverging bar: both arms grow OUT from a centre rule, so the two sides are
   compared by magnitude and the sign is carried by colour AND by the label.
   A share bar here would read "longs hold 81% of the pnl", which is nonsense
   when one side is negative. */
.vsc-ex-bar.div {
  /* two equal half-tracks with the zero rule welded to the boundary. As a flex
     run the arms centred as a GROUP, so the zero line drifted with them and a
     -$32.8M / +$59.3M pair drew its centre a third of the way across. */
  display: grid; grid-template-columns: 1fr 1fr; background: transparent; position: relative;
}
.vsc-ex-bar.div::before {
  content: ''; position: absolute; inset: 0; border-radius: 2px;
  background: var(--bg4);
}
.vsc-ex-bar.div::after {
  content: ''; position: absolute; top: -1px; bottom: -1px; left: 50%;
  width: 1px; background: var(--border2); z-index: 2;
}
.vsc-ex-bar.div .l, .vsc-ex-bar.div .s { position: relative; z-index: 1; height: 100%; }
.vsc-ex-bar.div .l { justify-self: end; border-radius: 2px 0 0 2px; }
.vsc-ex-bar.div .s { justify-self: start; border-radius: 0 2px 2px 0; }
.vsc-ex-bar.div .pos { background: var(--green, #21d196); }
.vsc-ex-bar.div .neg { background: var(--red); }
.vsc-ex-f {
  /* wraps rather than clips: at 4-across on a 1024 viewport the pair does not
     fit one line, and a truncated "$905.2…" is a worse answer than a second
     line. `.down` keeps margin-left:auto so it stays right-set either way. */
  display: flex; align-items: baseline; gap: var(--sp-2); min-width: 0;
  flex-wrap: wrap; font-size: var(--fs-meta); line-height: 1.3;
}
.vsc-ex-f .up { color: var(--green, #21d196); }
.vsc-ex-f .down { color: var(--red); margin-left: auto; text-align: right; }
.vsc-ex-f em { font-style: normal; color: var(--text3); margin: 0 4px; }
/* Under ~1360px the four blocks are narrow enough that the value pair no
   longer fits one line. The percentage is the part that goes: the bar already
   draws the split and the drawer states it exactly, whereas the dollar figures
   exist nowhere else in the strip. */
@media (max-width: 1360px) { .vsc-ex-f em { display: none; } }
.vsc-ex-f .vsc-nodata { font-size: var(--fs-meta); }
.vsc-ex-n {
  grid-row: 4; color: var(--text3); font-size: var(--fs-meta); line-height: 1.3;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vsc-ex:has(.vsc-ex-n) { grid-template-rows: auto auto auto auto; }
/* the whole block is the button — no nested interactive (design system §5) */
.vsc-ex.tap { cursor: pointer; transition: border-color 140ms var(--ease-out); }
.vsc-ex.tap:hover { border-color: color-mix(in srgb, var(--teal) 40%, var(--border2)); }
.vsc-ex.tap:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }

/* aggregate strip */
.vsc-agg {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(196px, 1fr));
  gap: var(--sp-3); margin: var(--sp-4) 0;
}
.vsc-agg:empty { display: none; }
.vsc-card {
  /* label · value · sub · rail — one shape for every card, so a row of four
     reads as a row and not as four unrelated widgets (UI audit 2026-07-30) */
  display: grid; grid-template-rows: auto auto 1fr auto; gap: var(--sp-1); min-width: 0;
  padding: var(--sp-4) var(--sp-4) var(--sp-3);
}
.vsc-card label {
  color: var(--text3); font-size: var(--fs-meta); font-weight: 500;
  letter-spacing: 0.1em; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* the value is the payload — it was 15px against a 12px label with 0.1em
   tracking, so the label read nearly as loud as the number it introduced */
.vsc-card .v {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  color: var(--text); font-size: 21px; font-weight: 700;
  font-variant-numeric: tabular-nums; line-height: 1.15; letter-spacing: -0.01em;
  min-width: 0;
}
.vsc-card .v > b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vsc-card .v .vsc-coins { flex: none; }
.vsc-card .rail { display: flex; align-items: center; margin-top: 2px; min-height: 3px; }
/* a ratio is a proportion, not an alert — 3px of hairline, no text inside,
   in a UI that prefers hairlines to filled boxes */
.vposn-meter.hair { height: 3px; border-radius: 2px; width: 100%; font-size: 0; }
.vposn-meter.hair .l, .vposn-meter.hair .s { padding: 0; min-width: 0; }
.vsc-card .v b.up { color: var(--green, #21d196); }
.vsc-card .v b.down { color: var(--red); }
.vsc-card .v .wrow { display: inline-flex; align-items: center; gap: 7px; min-width: 0; }
.vsc-card .v .wrow .vic, .vsc-card .v .wrow .vic-f { width: 17px; height: 17px; font-size: var(--fs-meta); flex: none; }
/* NOTE: the only .vsc-card .s rule — it used to be declared twice, and the
   second one's white-space:nowrap silently truncated three of the four cards.
   Grep before adding another. */
.vsc-card .s {
  color: var(--text3); font-size: var(--fs-meta); line-height: 1.45;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
.vsc-card .s b { font-weight: 600; }
.vsc-card .s b.up { color: var(--green, #21d196); }
.vsc-card .s b.down { color: var(--red); }

/* the table shell — rows are grids; the header row sticks under the hub bar */
.vsc-scroll {
  /* NO overflow:hidden here — the column header is position:sticky and a
     clipping ancestor kills it (it ate the first row when I tried it for
     rounded corners). The last row rounds its own corners instead. */
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--bg2);
}
.vsc-r {
  display: grid; align-items: center; gap: 10px;
  /* Every numeric column was a fixed px width and only TRADER carried an fr,
     so 100% of the slack at wide viewports piled into that one column —
     ~500px of dead space between the name and the first figure at 1440.
     Slack now distributes proportionally across the data columns. */
  grid-template-columns:
    26px 30px minmax(168px, 1.7fr)
    minmax(92px, 1fr) minmax(74px, 0.85fr) minmax(100px, 1.05fr)
    minmax(104px, 1.1fr) minmax(104px, 1.1fr) minmax(84px, 0.9fr)
    minmax(92px, 1.15fr);
  padding: 7px var(--sp-3); border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
  color: var(--text2); font-size: var(--fs-data);
}
.vsc-r:last-child { border-bottom: 0; border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
/* The row's primary action is a real button stretched across it, so the star
   is a SIBLING rather than an interactive nested inside a role="button" row
   (invalid ARIA + keyboard trap — audit 2026-07-30 F-06). It is out of flow,
   so it claims no grid track; the cells sit above it and still take clicks,
   which bubble to the row's existing delegation. */
.vsc-r.clickable { position: relative; }
.vsc-open {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; margin: 0; padding: 0;
  background: none; border: 0; color: transparent; font: inherit;
  cursor: pointer; appearance: none;
}
/* .vsc-opencell is the row-action button's ARIA cell (audit A4) — like the
   button it wraps, it is out of flow and claims no grid track, so it must be
   excluded from the rule that positions the real cells above it. */
.vsc-r.clickable > .vsc-opencell { position: absolute; inset: 0; z-index: 0; display: block; }
.vsc-r.clickable > *:not(.vsc-open):not(.vsc-opencell) { position: relative; z-index: 1; }
.vsc-open:focus-visible {
  outline: 2px solid var(--teal); outline-offset: -2px; border-radius: var(--radius);
}
.vsc-h {
  position: sticky; top: 40px; z-index: 6;
  background: color-mix(in srgb, var(--bg2) 88%, var(--bg));
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  border-bottom: 1px solid var(--border);
  color: var(--text3); font-size: var(--fs-data); letter-spacing: 0.08em; text-transform: uppercase;
  padding-top: 8px; padding-bottom: 8px;
}
.vsc-h button {
  background: none; border: 0; padding: 0; color: inherit; font: inherit;
  letter-spacing: inherit; text-transform: inherit; text-align: right;
}
.vsc-h .sortable { cursor: pointer; user-select: none; transition: color 120ms var(--ease-out); }
.vsc-h .sortable:hover { color: var(--text); }
.vsc-h .sortable:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; border-radius: 3px; }
.vsc-h .bias, .vsc-h .cen, .vsc-h .cbig, .vsc-h .act, .vsc-h .mkt { text-align: left; }
.vsc-r .rk { color: var(--text3); font-size: var(--fs-meta); font-variant-numeric: tabular-nums; }
.vsc-r .rk.pod { color: var(--teal); font-weight: 700; }
.vsc-r .who { display: flex; flex-direction: column; gap: 1px; min-width: 0; line-height: 1.35; }
.vsc-r .who b { color: var(--text); font-weight: 600; font-size: var(--fs-data); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* block, not flex: the position text is a bare text node, and an anonymous
   flex item can never be ellipsised — on a phone it clipped mid-word
   ("4.7x gr"). As a block the ellipsis applies (audit 2026-07-30). */
.vsc-r .who .sub {
  display: block; color: var(--text3); font-size: var(--fs-meta);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vsc-r .who .sub > .vsc-mini { margin-right: 6px; vertical-align: middle; }
.vsc-r .who .sub.up, .vsc-r .who .sub .up { color: var(--green, #21d196); }
.vsc-r .who .sub.down, .vsc-r .who .sub .down { color: var(--red); }
.vsc-r .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.vsc-r .num.up { color: var(--green, #21d196); }
.vsc-r .num.down { color: var(--red); }
.vsc-r .num.t3, .vsc-r .t3 { color: var(--text3); }
/* inside a nowrap .num cell, so a long fill line ("551.82 xyz:SKHY · 12
   fills") ran past the column edge and was cut mid-word — pre-existing,
   found while measuring the Flow view (audit 2026-07-30) */
.vsc-r .sub2 {
  display: block; color: var(--text3); font-size: var(--fs-meta); font-weight: 400;
  letter-spacing: 0; text-transform: none; line-height: 1.4;
  overflow: hidden; text-overflow: ellipsis;
}
.vsc-r .cls { display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; }
.vsc-r .cls i {
  font-style: normal; font-size: var(--fs-meta); font-weight: 700; letter-spacing: 0.08em;
  padding: 2px 6px; border-radius: 999px; border: 1px solid var(--border2); color: var(--text3);
}
.vsc-r .cls i.crypto { color: var(--teal); border-color: color-mix(in srgb, var(--teal) 40%, transparent); }
.vsc-r .cls i.tradfi { color: var(--gold, #e7b53a); border-color: color-mix(in srgb, var(--gold, #e7b53a) 40%, transparent); }
.vsc-r.clickable { cursor: pointer; transition: background 100ms var(--ease-out); }
.vsc-r.clickable:hover { background: var(--bg3); }
.vsc-r.clickable:active { background: var(--bg4); }
.vsc-r.clickable:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }

/* the coin rail: one tap answers "who is long/short X" */
/* NO overflow here. This used to be a scrolling row of chips, and the
   leftover overflow-x clipped the dropdown's absolutely-positioned panel — the
   menu opened correctly and was simply invisible. */
.vsc-coinrail {
  display: flex; align-items: center; gap: var(--sp-2);
  margin: 0; flex: none;
}
.vsc-coinrail:empty { display: none; }
.vsc-coinrail .lb { flex: none; margin-right: 4px; color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.1em; text-transform: uppercase; }
.vsc-crc {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  padding: 5px 9px 5px 6px; border: 1px solid transparent; border-radius: 999px;
  background: none; color: var(--text3); font: 600 var(--fs-data)/1.2 var(--mono);
  cursor: pointer; transition: color 120ms var(--ease-out), border-color 120ms var(--ease-out), background 120ms var(--ease-out);
}
.vsc-crc .vic, .vsc-crc .vic-f { width: 16px; height: 16px; font-size: var(--fs-meta); flex: none; }
.vsc-crc:hover { color: var(--text); border-color: var(--border3, var(--border2)); }
.vsc-crc:active { transform: scale(0.96); }
.vsc-crc.on {
  color: var(--teal); border-color: color-mix(in srgb, var(--teal) 50%, transparent);
  background: color-mix(in srgb, var(--teal) 9%, transparent);
}
@media (pointer: coarse) { .vsc-crc { padding-top: 7px; padding-bottom: 7px; } }

/* the tiny long/short meter that lives inside rows and card footers */
.vsc-mini { display: inline-flex; width: 46px; height: 4px; border-radius: 2px; overflow: hidden; background: var(--bg4); flex: none; }
.vsc-mini .l { display: block; background: color-mix(in srgb, var(--green, #21d196) 72%, transparent); }
.vsc-mini .s { display: block; background: color-mix(in srgb, var(--red) 66%, transparent); }

/* star = track */
/* Tracking is the Scanner's headline feature, but the star used to rest at
   opacity 0 — invisible until you happened to hover a row, so nobody found it
   (audit 2026-07-30). It now rests faint-but-present: the column reads as an
   affordance without shouting over the numbers. */
.vsc-star {
  background: none; border: 0; padding: 2px; cursor: pointer;
  color: var(--text3); font-size: var(--fs-data); line-height: 1; opacity: 0.28;
  transition: color 120ms var(--ease-out), opacity 120ms var(--ease-out), transform 120ms var(--ease-out);
}
.vsc-r:hover .vsc-star, .vsc-star:focus-visible, .vsc-tw .vsc-star, .vsc-drawer .vsc-star { opacity: 0.75; }
.vsc-star:hover { color: #e7b53a; opacity: 1; }
.vsc-star:active { transform: scale(0.85); }
.vsc-star.on { color: #e7b53a; opacity: 1; }
.vsc-star.lg { font-size: var(--fs-prose); }
.vsc-h .vsc-star { display: none; }
@media (hover: none) { .vsc-star { opacity: 0.7; } }

/* loading shimmer */
.vsc-skel { grid-template-columns: 26px 30px minmax(150px, 1fr) 110px 120px 130px; }
.vsc-skel span {
  display: block; height: 11px; border-radius: 4px;
  background: linear-gradient(100deg, var(--bg3) 40%, var(--bg4) 50%, var(--bg3) 60%) 0 0 / 240% 100%;
  animation: vscShimmer 1.3s linear infinite;
}
.vsc-skel span:first-child { width: 14px; }
@keyframes vscShimmer { to { background-position: -140% 0; } }
@media (prefers-reduced-motion: reduce) {
  .vsc-skel span, .vsc-live.syncing .dot, .vsc-resync.spin i, .vsc-resync.spin svg { animation: none; }
}

/* action badges: tape directions + order kinds */
.vsc-badge {
  display: inline-flex; align-items: center; max-width: 100%;
  padding: 2.5px 8px; border-radius: 999px; border: 1px solid var(--border2);
  background: var(--bg3); color: var(--text2);
  font-style: normal; font-size: var(--fs-meta); font-weight: 600; letter-spacing: 0.04em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vsc-badge.ol, .vsc-badge.tp { color: var(--green, #21d196); border-color: color-mix(in srgb, var(--green, #21d196) 42%, transparent); background: color-mix(in srgb, var(--green, #21d196) 11%, transparent); }
.vsc-badge.os, .vsc-badge.sl { color: var(--red); border-color: color-mix(in srgb, var(--red) 42%, transparent); background: color-mix(in srgb, var(--red) 11%, transparent); }
.vsc-badge.cl { color: color-mix(in srgb, var(--green, #21d196) 68%, var(--text3)); background: none; }
.vsc-badge.cs { color: color-mix(in srgb, var(--red) 68%, var(--text3)); background: none; }
.vsc-badge.fl { color: #b47aff; border-color: color-mix(in srgb, #b47aff 40%, transparent); background: color-mix(in srgb, #b47aff 10%, transparent); }
.vsc-badge.liq { color: var(--gold, #e7b53a); border-color: color-mix(in srgb, var(--gold, #e7b53a) 45%, transparent); background: color-mix(in srgb, var(--gold, #e7b53a) 12%, transparent); }

/* Positioning view rows */
.vsc-pr { grid-template-columns: minmax(118px, 1fr) 168px 64px 132px 100px 148px minmax(150px, 1.1fr); }
.vsc-pr .bias { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.vsc-pr .bias .vposn-meter { width: 100%; }
.vsc-pr .bias .bt { font-size: var(--fs-meta); font-weight: 700; }
/* the denominator rides with the percentage — see the cell's comment in hub.js */
.vsc-pr .bias .bt em {
  font-style: normal; font-weight: 400; color: var(--text3); margin-left: 5px;
}
.vsc-pr .bias .bt.up { color: var(--green, #21d196); }
.vsc-pr .bias .bt.down { color: var(--red); }
.vsc-pr .cen { display: flex; flex-direction: column; gap: 2px; align-items: flex-end; font-variant-numeric: tabular-nums; }
.vsc-pr .cen .en { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.vsc-pr .cen .en b { font-size: var(--fs-meta); font-weight: 700; }
.vsc-pr .cen .en b.up { color: var(--green, #21d196); }
.vsc-pr .cen .en b.down { color: var(--red); }
.vsc-pr .cbig { display: flex; flex-direction: column; gap: 1px; min-width: 0; text-align: right; }
.vsc-pr .cbig b { font-size: var(--fs-meta); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vsc-pr .cbig b.up { color: var(--green, #21d196); }
.vsc-pr .cbig b.down { color: var(--red); }
/* the foot says what the next page costs AND that the rows it is not showing
   still count — the whole point of choosing a 1000-deep cohort */
.vsc-footof {
  display: block; margin-top: 3px; color: var(--text3);
  font-size: var(--fs-meta); font-weight: 400; letter-spacing: 0;
}
.vsc-foot {
  display: block; width: 100%; padding: 9px 12px; background: none; border: 0;
  border-top: 1px solid var(--border); border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  color: var(--teal); font: 600 var(--fs-meta)/1 var(--mono); cursor: pointer; text-align: center;
  transition: background 120ms var(--ease-out);
}
.vsc-foot:hover { background: var(--bg3); }

/* Flow view rows — the tape */
.vsc-fr { grid-template-columns: 56px minmax(110px, 1fr) minmax(124px, 150px) 116px 112px 96px 96px; }
.vsc-fr .tm { color: var(--text3); font-size: var(--fs-meta); font-variant-numeric: tabular-nums; white-space: nowrap; }
.vsc-fr .mkt { min-width: 0; }
.vsc-fr .act { min-width: 0; }
.vsc-tapefoot {
  padding: 8px 12px; border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
  color: var(--text3); font-size: var(--fs-meta); line-height: 1.5;
}

/* tracked view */
.vsc-tracked[hidden] { display: none; }
.vsc-cols { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 14px; align-items: start; }
@media (max-width: 1000px) { .vsc-cols { grid-template-columns: 1fr; } }
.vsc-col-main { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 10px; align-items: start; }
.vsc-tw { border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--bg2); overflow: hidden; }
.vsc-tw-head {
  display: flex; align-items: center; gap: 8px; padding: 8px 10px 8px 12px;
  border-bottom: 1px solid var(--border);
}
.vsc-tw-head b { color: var(--text); font-size: var(--fs-data); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vsc-tw-head .eq { margin-left: auto; color: var(--text); font-size: var(--fs-meta); font-variant-numeric: tabular-nums; }
.vsc-tw-head .upl { flex: none; font-size: var(--fs-meta); font-variant-numeric: tabular-nums; }
.vsc-tw-head .upl.up { color: var(--green, #21d196); }
.vsc-tw-head .upl.down { color: var(--red); }
.vsc-tw-foot {
  display: flex; align-items: center; gap: 8px; padding: 7px 12px;
  border-top: 1px dashed color-mix(in srgb, var(--border) 70%, transparent);
  color: var(--text3); font-size: var(--fs-meta); font-variant-numeric: tabular-nums;
}
.vsc-col-side { position: sticky; top: 56px; }
.vsc-feed {
  border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--bg2);
  max-height: 60vh; overflow-y: auto;
}
.vsc-al {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  padding: 8px 11px; background: none; border: 0; border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
  color: var(--text2); font-size: var(--fs-meta); cursor: pointer;
}
.vsc-al:last-child { border-bottom: 0; }
.vsc-al:hover { background: var(--bg3); }
.vsc-al .vic, .vsc-al .vic-f { width: 15px; height: 15px; font-size: var(--fs-meta); flex: none; }
.vsc-al .txt { flex: 1; min-width: 0; line-height: 1.4; }
.vsc-al .txt b { color: var(--text); font-weight: 600; }
.vsc-al .ago { flex: none; color: var(--text3); font-size: var(--fs-meta); }
.vsc-clear {
  margin-left: auto; background: none; border: 0; padding: 2px 4px;
  color: var(--text3); font: 500 var(--fs-meta)/1 var(--mono); letter-spacing: 0.06em; cursor: pointer;
  position: relative; z-index: 1;
}
.vsc-clear:hover { color: var(--text); }
.vsc-teach {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 44px 20px; text-align: center; border: 1px dashed var(--border2); border-radius: var(--radius-lg);
}
.vsc-teach i, .vsc-teach svg { width: 22px; height: 22px; stroke-width: 1.25; opacity: 0.6; }
.vsc-teach p { margin: 0; color: var(--text2); font-size: var(--fs-data); }
.vsc-teach p.d { color: var(--text3); font-size: var(--fs-data); max-width: 380px; line-height: 1.55; }

/* ── Positions table — one row per open position ───────────────────────────
   Nine tracks: rank, trader, market, position, entry, liq, margin, funding,
   pnl. Minimums sum to ~840px + gaps, so the whole table still fits 1024
   without a horizontal scrollbar; the fr weights hand the slack to the two
   text columns rather than to the numbers. */
.vsc-bk {
  grid-template-columns:
    30px minmax(122px, 1.35fr) minmax(118px, 1.2fr)
    minmax(104px, 1.05fr) minmax(96px, 1fr) minmax(96px, 1fr)
    minmax(84px, 0.9fr) minmax(88px, 0.92fr) minmax(104px, 1.1fr);
}
/* Aster publishes no margin, no funding and no liquidation price, so with
   Aster alone those three columns would be a wall of "not shared". Same
   re-cut the Leaderboard makes with .vsc-lite. */
.vsc-bklite .vsc-bk {
  grid-template-columns:
    30px minmax(140px, 1.6fr) minmax(130px, 1.3fr)
    minmax(116px, 1.15fr) minmax(104px, 1.05fr) minmax(116px, 1.2fr);
}
/* Peel columns in value order, the same ladder .vsc-br / .vsc-pr / .vsc-fr
   already walk. Order of loss: margin first (the only figure here recoverable
   from two others on the same row — position ÷ leverage), then funding, then
   entry, then the liquidation price, then the rank. Trader, market, position
   and pnl survive to the narrowest width, because without those four the row
   has stopped being a position.

   These minimums are measured, not guessed: nine tracks force the document
   wider than the viewport below ~900px, which is a horizontal page scroll on
   a table that is supposed to be readable on a laptop. */
@media (max-width: 1180px) {
  .vsc-bk {
    grid-template-columns:
      30px minmax(112px, 1.3fr) minmax(112px, 1.2fr)
      minmax(100px, 1.05fr) minmax(92px, 1fr) minmax(92px, 1fr)
      minmax(88px, 0.92fr) minmax(104px, 1.1fr);
  }
  .vsc-bk > .cmgn { display: none; }
}
@media (max-width: 1080px) {
  .vsc-bk {
    grid-template-columns:
      28px minmax(104px, 1.3fr) minmax(104px, 1.2fr)
      minmax(94px, 1.05fr) minmax(86px, 1fr) minmax(86px, 1fr)
      minmax(96px, 1.1fr);
  }
  .vsc-bk > .cfnd { display: none; }
}
@media (max-width: 920px) {
  .vsc-bk {
    grid-template-columns:
      26px minmax(94px, 1.3fr) minmax(94px, 1.2fr)
      minmax(86px, 1.05fr) minmax(82px, 1fr) minmax(90px, 1.1fr);
  }
  .vsc-bk > .cent { display: none; }
}
@media (max-width: 760px) {
  .vsc-bk {
    grid-template-columns:
      24px minmax(88px, 1.2fr) minmax(84px, 1.1fr)
      minmax(80px, 1fr) minmax(84px, 1.05fr);
  }
  .vsc-bk > .cliq { display: none; }
}
@media (max-width: 600px) {
  .vsc-bk {
    grid-template-columns:
      minmax(76px, 1.2fr) minmax(72px, 1fr) minmax(70px, 1fr) minmax(74px, 1fr);
  }
  .vsc-bk > .rk { display: none; }
}
@media (max-width: 430px) {
  .vsc-bk { grid-template-columns: minmax(64px, 1.3fr) minmax(60px, 1fr) minmax(66px, 1.05fr); }
  .vsc-bk > .cpos { display: none; }
}
/* Below ~600px the market cell cannot hold icon + ticker + side badge on one
   line. The badge wraps under rather than being dropped — side is the one
   thing a position row must never stop saying. */
@media (max-width: 600px) {
  .vsc-bk .mkt .wrow { flex-wrap: wrap; row-gap: 2px; }
  .vsc-bk .mkt .vsc-badge { flex: none; }
}
.vsc-bk .rk {
  color: var(--text3); font-size: var(--fs-meta); font-variant-numeric: tabular-nums;
  text-align: right;
}
/* The market cell holds three things in ~112px: token icon, ticker, side.
   As a bordered-and-filled pill the side badge read as a third object of equal
   weight and the cell looked cluttered — that chrome is tuned for the Flow
   tape, where the badge is the row's whole subject. Here it is one attribute
   of a position, so it keeps the colour and the word and drops the box.
   Spacing is fixed rather than inherited: 7px across the row, 2px down. */
.vsc-bk .mkt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.vsc-bk .mkt .wrow { display: inline-flex; align-items: center; gap: 7px; min-width: 0; }
.vsc-bk .mkt .wrow > b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vsc-bk .mkt .vsc-badge {
  padding: 0; border: 0; background: none; border-radius: 0;
  font-size: var(--fs-meta); font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase;
}
.vsc-bk .mkt .sub2 { line-height: 1.3; }
.vsc-bk .who { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.vsc-bk .who > b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* long/short is stated in a badge, not carried by colour alone */
.vsc-badge.long {
  color: var(--green, #21d196); border-color: color-mix(in srgb, var(--green, #21d196) 42%, transparent);
  background: color-mix(in srgb, var(--green, #21d196) 11%, transparent);
}
.vsc-badge.short {
  color: var(--red); border-color: color-mix(in srgb, var(--red) 42%, transparent);
  background: color-mix(in srgb, var(--red) 11%, transparent);
}
.vsc-bk .num .sub2.down { color: var(--red); }

/* ── flow tape: the row carries the direction ──────────────────────────────
   Side sets the hue, open-vs-close sets the strength, so a new position reads
   louder than an exit. Kept faint on purpose — 250 tinted rows at card
   opacity would be a stripe pattern, not a tape. The badge still states the
   direction in words; this never carries it alone. */
.vsc-fr.k-ol { background: color-mix(in srgb, var(--green, #21d196) 9%, transparent); }
.vsc-fr.k-cl { background: color-mix(in srgb, var(--green, #21d196) 4%, transparent); }
.vsc-fr.k-os { background: color-mix(in srgb, var(--red) 9%, transparent); }
.vsc-fr.k-cs { background: color-mix(in srgb, var(--red) 4%, transparent); }
.vsc-fr.k-liq { background: color-mix(in srgb, var(--gold, #e7b53a) 11%, transparent); }
.vsc-fr.k-fl { background: color-mix(in srgb, #b47aff 7%, transparent); }
.vsc-fr.clickable:hover { background: color-mix(in srgb, var(--teal) 9%, var(--bg3)); }

/* ── wallet drawer: pnl by window ──────────────────────────────────────── */
.vsc-pnlrow {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
  gap: var(--sp-2);
}
.vsc-pw {
  display: flex; flex-direction: column; gap: 3px; min-width: 0;
  padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius);
  background: color-mix(in srgb, var(--bg3) 55%, transparent);
}
.vsc-pw label {
  display: flex; align-items: center; gap: 4px;
  color: var(--text3); font-size: var(--fs-meta); font-weight: 500;
  letter-spacing: 0.1em; text-transform: uppercase;
}
.vsc-pw b {
  font-size: var(--fs-title); font-weight: 700; letter-spacing: -0.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vsc-pw b.up { color: var(--green, #21d196); }
.vsc-pw b.down { color: var(--red); }
.vsc-pw b.t3 { color: var(--text3); font-size: var(--fs-data); font-weight: 500; }

/* ── wallet drawer: donuts ─────────────────────────────────────────────── */
.vsc-donuts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--sp-2);
}
.vsc-donut {
  display: flex; flex-direction: column; min-width: 0;
  padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius);
  background: color-mix(in srgb, var(--bg3) 55%, transparent);
}
.vsc-donuthead { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.vsc-donuthead label {
  color: var(--text3); font-size: var(--fs-meta); font-weight: 500;
  letter-spacing: 0.1em; text-transform: uppercase;
}
.vsc-donuthead b { font-size: var(--fs-title); font-weight: 700; white-space: nowrap; }
.vsc-donuthead span {
  color: var(--text3); font-size: var(--fs-meta); line-height: 1.35;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* the ring needs a real height — chartMount's echarts box has none of its own */
.vsc-donutbox { position: relative; height: 116px; margin-top: 6px; }
.vsc-donutbox .vchart { position: absolute; inset: 0; }

/* the liquidation map's market picker rides in the chart's section heading */
.vsc-liqpick { display: inline-flex; flex-wrap: wrap; gap: 4px; margin-right: var(--sp-2); }

/* wallet inspector drawer */
/* The Scanner's drawers carry dense numbers and charts. The gallery's glass
   treatment (78% surface + backdrop blur) let the leaderboard read straight
   through them — and where the blur failed to composite it was plain
   see-through. Legibility beats the glass here: these are opaque. */
.vsc-drawer {
  width: min(760px, 96vw); gap: 10px;
  background: var(--bg2);
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border-left: 1px solid var(--border2);
}
/* and the veil goes darker so the page behind stops competing */
.vsc-veil { background: rgba(4, 3, 9, 0.78); }
.vsc-addr {
  display: inline-flex; align-items: center; gap: 5px; flex: none;
  padding: 3px 9px; border: 1px solid var(--border2); border-radius: 999px;
  background: var(--bg3); color: var(--text3); font: 500 var(--fs-meta)/1.4 var(--mono); cursor: pointer;
  transition: color 120ms var(--ease-out), border-color 120ms var(--ease-out);
}
.vsc-addr:hover { color: var(--text); border-color: var(--teal); }
.vsc-addr i, .vsc-addr svg { width: 11px; height: 11px; }
.vsc-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); gap: 10px 16px; padding: 2px 0 4px; }
.vsc-stats span { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.vsc-stats label { color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.1em; text-transform: uppercase; white-space: nowrap; }
.vsc-stats b { color: var(--text); font-size: var(--fs-data); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vsc-stats b.up { color: var(--green, #21d196); }
.vsc-stats b.down { color: var(--red); }
.vsc-sec {
  display: flex; align-items: center; gap: 8px; margin-top: 4px;
  color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.1em; text-transform: uppercase;
}
.vsc-sec .vgal-chips { margin-left: auto; }
.vsc-sec .vgal-chips + .vgal-chips { margin-left: 0; }
.vsc-eq { height: 170px; flex: none; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); }
.vsc-pos { grid-template-columns: minmax(90px, 1fr) 110px 120px minmax(110px, 1fr); padding: 6px 10px; border-radius: var(--radius); }
.vsc-pos:nth-child(odd) { background: color-mix(in srgb, var(--bg3) 55%, transparent); }
.vsc-pos5 { grid-template-columns: minmax(92px, 1fr) 96px 96px 112px 96px 122px; padding: 6px 10px; border-radius: var(--radius); }
.vsc-pos5:nth-child(odd) { background: color-mix(in srgb, var(--bg3) 55%, transparent); }
.vsc-ord { grid-template-columns: minmax(96px, 1fr) 118px 112px 104px 104px; padding: 6px 10px; border-radius: var(--radius); }
.vsc-ord:nth-child(odd) { background: color-mix(in srgb, var(--bg3) 55%, transparent); }
.vsc-pos, .vsc-pos5, .vsc-ord { border-bottom: 0; }
.vsc-rhy { display: flex; align-items: flex-end; gap: 12px; padding: 4px 2px 0; }
.vsc-rhy svg { width: min(420px, 100%); height: 56px; }
.vsc-rhy rect { fill: color-mix(in srgb, var(--teal) 52%, var(--bg4)); }
.vsc-rhy rect.pk { fill: var(--teal); }
.vsc-rhy rect.z { fill: var(--bg4); }
.vsc-rhy text { fill: var(--text3); font-size: var(--fs-meta); font-family: var(--mono, monospace); }
.vsc-rhylab { color: var(--text3); font-size: var(--fs-meta); white-space: nowrap; padding-bottom: 12px; }

/* responsive: peel columns off in value order, per row type */
@media (max-width: 1240px) {
  .vsc-br { grid-template-columns: 26px 30px minmax(160px, 1.4fr) 92px 74px 100px 104px 84px minmax(92px, 126px); }
  .vsc-br .cvol { display: none; }
}
@media (max-width: 1080px) {
  .vsc-br { grid-template-columns: 26px 30px minmax(150px, 1.4fr) 90px 70px 98px 102px minmax(80px, 116px); }
  .vsc-br .cupct { display: none; }
  .vsc-pr { grid-template-columns: minmax(110px, 1fr) 150px 60px 124px 96px minmax(140px, 1.1fr); }
  .vsc-pr .cen { display: none; }
}
@media (max-width: 920px) {
  .vsc-br { grid-template-columns: 26px 26px minmax(140px, 1.4fr) 88px 96px 100px; }
  .vsc-br .croi, .vsc-br .cls { display: none; }
  .vsc-pr { grid-template-columns: minmax(104px, 1fr) minmax(130px, 1.2fr) 56px 116px 92px; }
  .vsc-pr .cbig { display: none; }
  .vsc-fr { grid-template-columns: 52px minmax(100px, 1fr) minmax(116px, 140px) 108px 104px 92px; }
  .vsc-fr .cpx { display: none; }
}
@media (max-width: 768px) {
  /* the mobile hub bar is two rows tall — a fixed sticky offset can't know it */
  .vsc-h { position: static; }
}
@media (max-width: 680px) {
  .vsc-br { grid-template-columns: 24px 24px minmax(112px, 1fr) 90px 96px; }
  .vsc-br .ceq { display: none; }
  .vsc-pr { grid-template-columns: minmax(96px, 1fr) minmax(110px, 1.2fr) 100px 88px; }
  .vsc-pr .ch2 { display: none; }
  .vsc-fr { grid-template-columns: 48px minmax(104px, 1.2fr) minmax(92px, 1fr) 92px; }
  .vsc-fr .who, .vsc-fr .cpnl { display: none; }
  .vsc-pos5 { grid-template-columns: minmax(90px, 1fr) 88px 100px 118px; }
  .vsc-pos5 .cliq, .vsc-pos5 .cfund { display: none; }
  .vsc-ord { grid-template-columns: minmax(90px, 1fr) 112px 100px 100px; }
  .vsc-ord .cdist { display: none; }
  .vsc-stats { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); }
}
@media (max-width: 480px) {
  .vsc-r { padding: 7px 10px; gap: 8px; }
  .vsc-br { grid-template-columns: 22px 22px minmax(96px, 1fr) 88px; }
  .vsc-br .cupl { display: none; }
  .vsc-pr { grid-template-columns: minmax(88px, 1fr) minmax(92px, 1.1fr) 92px; }
  .vsc-pr .cupl { display: none; }
  .vsc-skel { grid-template-columns: 22px 22px minmax(96px, 1fr) 88px; }
  .vsc-skel span:nth-child(n+5) { display: none; }
}

/* coin icons: what each wallet is trading — overlapping stack on the board,
   inline next to the ticker in the drawer rows */
.vsc-coins { display: inline-flex; align-items: center; vertical-align: middle; }
.vsc-coins .vic, .vsc-coins .vic-f {
  width: 15px; height: 15px; font-size: var(--fs-meta);
  border: 1.5px solid var(--bg2); box-sizing: content-box;
}
.vsc-coins > :not(:first-child) { margin-left: -5px; }
.vsc-coins.big .vic, .vsc-coins.big .vic-f { width: 19px; height: 19px; font-size: var(--fs-meta); }
.vsc-r .wrow { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.vsc-r .wrow .vic, .vsc-r .wrow .vic-f { width: 15px; height: 15px; font-size: var(--fs-meta); flex: none; }

/* ── Chart desk: strip + full-bleed hero + context band ───────────────── */
.vdesk { padding-top: 6px; animation: hubPageIn 220ms var(--ease-out) backwards; }
.vdesk [hidden] { display: none !important; } /* the hidden attr must ALWAYS beat class display in here */

/* the strip: one contained bar — symbol identity, live numbers, actions */
.vdesk-strip {
  display: flex; align-items: center; flex-wrap: wrap; row-gap: 10px;
  background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: 9px 14px; margin-bottom: 10px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
}
.vdesk-sym { display: flex; align-items: center; gap: 10px; padding-right: 16px; min-width: 0; }
.vdesk-sym .vic, .vdesk-sym .vic-f { width: 26px; height: 26px; font-size: var(--fs-meta); flex: none; }
/* `.id` is an <h1> — the chart desk's only heading (design §8). Chrome, not a
   document title, so it resets the UA's heading type the same way `.hw-title`
   does; the visual is unchanged. */
.vdesk-sym .id {
  display: flex; flex-direction: column; gap: 1px; min-width: 0;
  font-size: inherit; font-weight: inherit; line-height: inherit; margin: 0;
}
.vdesk-sym .id b { color: var(--text); font-size: var(--fs-ui); font-weight: 700; letter-spacing: 0.01em; white-space: nowrap; }
.vdesk-sym .id .nm { color: var(--text3); font-size: var(--fs-meta); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 160px; }
.vdesk-stats { display: flex; align-items: stretch; flex-wrap: wrap; row-gap: 10px; min-width: 0; }
.vdesk-stats .grp {
  display: flex; flex-direction: column; justify-content: center; gap: 3px;
  padding: 0 16px; border-left: 1px solid var(--border); min-width: 0;
}
.vdesk-stats .grp.gpx { flex-direction: row; align-items: baseline; gap: 9px; }
.vdesk-stats .px { color: var(--text); font-size: var(--fs-title); font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.vdesk-stats .px.pop-up { animation: vdeskPopUp 500ms var(--ease-out); }
.vdesk-stats .px.pop-dn { animation: vdeskPopDn 500ms var(--ease-out); }
@keyframes vdeskPopUp { 30% { color: var(--green, #21d196); } }
@keyframes vdeskPopDn { 30% { color: var(--red); } }
.vdesk-stats .chg { font-size: var(--fs-meta); font-weight: 600; font-variant-numeric: tabular-nums; }
.vdesk-stats .chg.up, .vdesk-stats b.up { color: var(--green, #21d196); }
.vdesk-stats .chg.down, .vdesk-stats b.down { color: var(--red); }
.vdesk-stats .grp label { color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.1em; text-transform: uppercase; white-space: nowrap; }
.vdesk-stats .grp b { color: var(--text); font-size: var(--fs-meta); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.vdesk-stats .grp .cd { font-style: normal; color: var(--text3); font-size: var(--fs-meta); font-variant-numeric: tabular-nums; }
.vdesk-stats .crowd { display: inline-flex; align-items: center; gap: 8px; }
.vdesk-stats .crowd .vsc-mini.big { width: 88px; height: 5px; border-radius: 2.5px; }
.vdesk-stats .crowd b { font-size: var(--fs-meta); }
.vdesk-stats .grp label .cd { font-style: normal; letter-spacing: 0.04em; }
.vdesk-stats .offhl { color: var(--text3); font-size: var(--fs-meta); }
.vdesk-acts { margin-left: auto; display: flex; align-items: center; gap: 10px; padding-left: 16px; }
.vdesk-acts .vsc-star { opacity: 0.75; }
.vdesk-acts .vsc-star[disabled] { opacity: 0.25; cursor: default; }

/* the hero: full width, the first viewport is the chart */
.vdesk-main > .vdesk-hero { height: calc(100dvh - 128px); min-height: 460px; }

/* a STATIC page — panels carry none of the dashboard's edit chrome */
.vdesk .vpanel .hw-btns { display: none; }

/* symbol context band, under the screener: positioning · news, funding wide */
.vdesk-ctx { padding-top: 10px; }
.vdesk-slot { min-width: 0; }
.vdesk-slot:empty { display: none; }
.vdesk-slot.span2 { grid-column: 1 / -1; }
.vdesk-slot .vpanel { min-height: 0; height: 100%; }
.vdesk-slot .vpanel.vdesk-posn, .vdesk-slot .vpanel.vdesk-news { height: 460px; }
.vdesk-slot .vpanel.vdesk-fund { height: 190px; }

/* engine switcher: Vice · TV · HL, one quiet segmented pill */
.vdesk-eng { display: inline-flex; align-items: center; border: 1px solid var(--border2); border-radius: 999px; background: var(--bg3); overflow: hidden; }
.vdesk-eng button {
  appearance: none; background: none; border: 0; padding: 5px 11px;
  color: var(--text3); font: 600 var(--fs-meta)/1.2 var(--mono); letter-spacing: 0.04em; cursor: pointer;
  transition: color 120ms var(--ease-out), background 120ms var(--ease-out);
}
.vdesk-eng button + button { border-left: 1px solid var(--border2); }
.vdesk-eng button:hover { color: var(--text); }
.vdesk-eng button.on { background: var(--bg4); color: var(--text); }


/* the TV hero rides chrome-less — its own toolbar is the chrome */
.vdesk-hero.vdesk-tv > .vpanel-head { display: none; }

/* the strip holds ONE row: peel numbers off by value, never wrap ragged */
@media (max-width: 1280px) { .vdesk-stats [data-gvol] { display: none !important; } }
@media (max-width: 1120px) { .vdesk-stats [data-goi] { display: none !important; } }
@media (max-width: 930px) { .vdesk-stats [data-gcrowd] { display: none !important; } }
/* funding is the last number to peel — under this the strip is identity, price
   and the controls, and that still holds ONE row down to a phone */
@media (max-width: 730px) { .vdesk-stats [data-gfund] { display: none !important; } }
@media (max-width: 900px) {
  .vdesk-main > .vdesk-hero { height: 520px; min-height: 420px; }
  .vdesk-slot .vpanel.vdesk-posn, .vdesk-slot .vpanel.vdesk-news { height: 430px; }
}
/* Below this, identity + price + controls genuinely cannot share a line — no
   amount of peeling gets 382px of content into 356px. So the strip stops being
   a row and becomes a small two-line CARD: identity across the top, then price
   and controls sharing the second line.

   Letting it simply wrap was the wrong answer twice over. The controls ended up
   alone on a line with the whole left half empty, which reads as a broken box
   rather than a compact one — the strip looked emptier the narrower it got,
   which is exactly backwards. A grid fills both lines instead. */
@media (max-width: 500px) {
  .vdesk-strip {
    display: grid; grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: 'sym sym' 'stats acts';
    align-items: center; column-gap: 10px; row-gap: 8px;
  }
  .vdesk-sym { grid-area: sym; padding-right: 0; }
  .vdesk-stats { grid-area: stats; }
  .vdesk-acts { grid-area: acts; margin-left: 0; padding-left: 0; }
}
/* wallpaper is a display MODE — the least useful control on a phone, and the
   one whose absence buys the other three a comfortable line */
/* superseded by the deliberate 640px rule in the item-15 block below — an
   accidental 400px cliff meant a 393px iPhone could not see Wallpaper Mode
   exists while a 430px one could (audit F-WALL). */
@media (max-width: 640px) {
  .vdesk-strip { padding: 8px 10px; }
  .vdesk-sym { padding-right: 10px; }
  .vdesk-stats .grp { padding: 0 10px; }
  .vdesk-stats .px { font-size: var(--fs-prose); }
  .vdesk-sym .id .nm { max-width: 110px; }
  .vdesk-acts { padding-left: 10px; gap: 8px; }
  .vdesk-scan span { display: none; }
}

/* ── HL Positioning widget: the crowd for one symbol ──────────────────── */
.vposn { position: absolute; inset: 0; display: flex; flex-direction: column; gap: 9px; padding: 10px 13px; overflow-y: auto; }
.vposn-lead { color: var(--text2); font-size: var(--fs-meta); line-height: 1.45; }
.vposn-lead em { font-style: normal; font-weight: 700; }
.vposn-lead em.up { color: var(--green, #21d196); }
.vposn-lead em.down { color: var(--red); }
.vposn-meter {
  display: flex; flex: none; height: 15px; border-radius: 4px; overflow: hidden;
  font-size: var(--fs-meta); font-weight: 700; letter-spacing: 0.05em;
}
.vposn-meter .l {
  display: flex; align-items: center; padding: 0 6px;
  background: color-mix(in srgb, var(--green, #21d196) 26%, var(--bg3));
  color: var(--green, #21d196); min-width: 0; overflow: hidden;
  transition: width 400ms var(--ease-out);
}
.vposn-meter .s {
  display: flex; align-items: center; justify-content: flex-end; padding: 0 6px;
  background: color-mix(in srgb, var(--red) 26%, var(--bg3));
  color: var(--red); min-width: 0; overflow: hidden;
  transition: width 400ms var(--ease-out);
}
.vposn-meter.slim { height: 6px; border-radius: 3px; }
.vposn-meter.slim .l, .vposn-meter.slim .s { padding: 0; }
.vposn-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); gap: 7px 14px; }
.vposn-grid span { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.vposn-grid label { color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.09em; text-transform: uppercase; }
.vposn-grid b { color: var(--text); font-size: var(--fs-meta); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vposn-grid b i { font-style: normal; }
.vposn-grid b i.up { color: var(--green, #21d196); }
.vposn-grid b i.down { color: var(--red); }
.vposn-top { display: flex; flex-direction: column; margin-top: auto; border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent); }
.vposn-row {
  display: grid; grid-template-columns: 74px minmax(90px, 1fr) minmax(120px, auto) 90px;
  gap: 10px; align-items: center; width: 100%; text-align: left;
  padding: 5px 2px; border: none; border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
  background: none; color: var(--text2); font: inherit; font-size: var(--fs-meta); cursor: pointer;
  transition: background 110ms var(--ease-out);
}
.vposn-row:last-child { border-bottom: 0; }
.vposn-row:hover { background: var(--bg3); }
.vposn-row b { color: var(--text); font-weight: 600; }
.vposn-row .side { font-size: var(--fs-meta); font-weight: 700; letter-spacing: 0.06em; }
.vposn-row .side.up { color: var(--green, #21d196); }
.vposn-row .side.down { color: var(--red); }
.vposn-row .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.vposn-row .num.up { color: var(--green, #21d196); }
.vposn-row .num.down { color: var(--red); }
@container (max-width: 400px) { .vposn-row { grid-template-columns: 64px 1fr 90px; } .vposn-row .num:nth-child(3) { display: none; } }

/* ── Gallery, slimmed: more elements per glance, one line each ────────── */
.vgal-grid { grid-template-columns: repeat(auto-fill, minmax(272px, 1fr)); gap: 8px; }
.vgal-card { padding: 9px 11px; gap: 10px; }
.vgal-thumb { width: 46px; height: 32px; margin-top: 3px; }
.vgal-desc { -webkit-line-clamp: 1; font-size: var(--fs-meta); }
.vgal-badge { border: none; background: var(--bg3); }
.vgal-badge.vice { background: color-mix(in srgb, var(--teal) 12%, transparent); border: none; }
.vgal-cat::after { content: ''; flex: 1; height: 1px; background: color-mix(in srgb, var(--border) 80%, transparent); }
.vgal-cat { margin: 18px 2px 9px; }
.vgal-intro { margin-bottom: 12px; }

/* hidden must always win — display:grid/flex on the class beat the attribute */
.vsc-agg[hidden], .vsc-bar[hidden], .vsc-scroll[hidden], .vsc-views[hidden] { display: none; }

/* tracked cards are narrower than the inspector — tighter position columns */
.vsc-col-main { grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); }
.vsc-tw .vsc-pos {
  grid-template-columns: minmax(78px, 1fr) 80px minmax(84px, auto) 88px;
  gap: 7px; padding: 5px 10px; font-size: var(--fs-meta);
}
/* narrow boards: keep the coin stacks, let the class badge yield */
@media (max-width: 1100px) { .vsc-scroll .vsc-r .cls i { display: none; } }

/* ── adversarial-review fixes ─────────────────────────────────────────── */
/* sort headers are real buttons now — strip the chrome, keep the header voice */
/* The header CELL now carries aria-sort and this stays a button (audit A2), so
   it fills its cell and takes the cell's own alignment rather than forcing
   right on columns the header deliberately sets left. */
.vsc-h button.sortable {
  display: block; width: 100%;
  border: none; background: none; padding: 0; margin: 0;
  font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
  text-align: inherit; cursor: pointer;
}
/* stars: keyboard users get the ring and the reveal */
.vsc-star:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; opacity: 1; }
.vsc-r:focus-within .vsc-star { opacity: 0.75; }
.vsc-r.clickable:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }
/* phones: position rows flex instead of overflowing their fixed tracks */
@media (max-width: 640px) {
  .vsc-pos, .vsc-tw .vsc-pos {
    grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
    gap: 7px;
  }
  .vsc-pos .num { overflow: hidden; text-overflow: ellipsis; }
}
/* tiny positioning widgets: drop the size column before it forces a scroll */
@container (max-width: 300px) {
  .vposn-row { grid-template-columns: 56px 1fr 82px; }
}
/* meter count labels: lift toward white for AA on the tinted bands */
.vposn-meter .l { color: color-mix(in srgb, var(--green, #21d196) 65%, white); }
.vposn-meter .s { color: color-mix(in srgb, var(--red) 60%, white); }
/* the new section entrances respect reduced motion like everything else */
@media (prefers-reduced-motion: reduce) {
  .vsc, .vgal, .vsc-drawer { animation: none; }
  .vposn-meter .l, .vposn-meter .s { transition: none; }
}

/* ── v1.29.0: site-wide audit pass — the two raw surfaces dressed, smart
   menu anchoring, honest live dot, in-UI dialogs, shortcuts sheet ──────── */

/* add-tray search + Gallery link: were emitted unstyled (raw input, default
   browser button) — the storefront's front door now speaks the house language */
.hub-tray-search {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 14px; padding: 7px 11px;
  border: 1px solid var(--border2); border-radius: var(--radius);
  background: var(--bg3); color: var(--text3);
  transition: border-color 140ms var(--ease-out);
}
.hub-tray-search:focus-within { border-color: var(--teal); }
.hub-tray-search i, .hub-tray-search svg { width: 13px; height: 13px; flex: none; }
.hub-tray-search input {
  flex: 1; min-width: 0; border: none; background: none;
  color: var(--text); font: 500 var(--fs-data)/1.5 var(--mono);
}
.hub-tray-search input:focus { outline: none; }
.hub-tray-search input::placeholder { color: var(--text3); }
.hub-tray-gallery {
  display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%;
  margin-top: 16px; padding: 9px 11px;
  border: 1px dashed var(--border2); border-radius: var(--radius);
  background: none; color: var(--text2); font: 600 var(--fs-meta)/1.5 var(--mono); cursor: pointer;
  transition: color 140ms var(--ease-out), border-color 140ms var(--ease-out);
}
.hub-tray-gallery:hover { color: var(--teal); border-color: color-mix(in srgb, var(--teal) 45%, transparent); }
.hub-tray-gallery:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
.hub-tray-gallery i, .hub-tray-gallery svg { width: 14px; height: 14px; flex: none; }

/* focus-search internals: category headers + symbol/name typography (were
   emitted unstyled — headers rendered as body text, names ran into symbols) */
.hub-palette-cat {
  padding: 8px 10px 3px; color: var(--text3);
  font-size: var(--fs-meta); font-weight: 600; letter-spacing: 0.09em; text-transform: uppercase;
}
.hub-palette-item .fs-l { flex: none; color: var(--text); font-weight: 600; }
.hub-palette-item .fs-sub {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text3); font-size: var(--fs-meta);
}
.hub-palette-item .vic, .hub-palette-item .vic-f { width: 16px; height: 16px; flex: none; }

/* ⋮ menu flips its anchor when the button sits near the viewport's left edge
   (wrapped bar rows) — right-anchoring used to push it off-screen */
.hub-menu.flip { right: auto; left: 0; }
.hub-menu.open.flip { transform-origin: top left; }

/* the live dot only glows when the feed actually ticks — stale feeds gray out */
.hub-live .dot.stale { background: var(--text3); animation: none; }

/* destructive confirm button (in-UI dialogs replaced prompt/confirm) */
.hub-btn.danger-solid { background: var(--red); color: #fff; font-weight: 600; }
.hub-btn.danger-solid:hover { filter: brightness(1.08); color: #fff; background: var(--red); }

/* keyboard shortcuts sheet */
.hub-keys { display: grid; grid-template-columns: auto 1fr; gap: 9px 18px; align-items: center; }
.hub-keys kbd {
  justify-self: start; padding: 2px 8px;
  border: 1px solid var(--border2); border-radius: var(--radius-sm);
  background: var(--bg3); color: var(--text); font: 600 var(--fs-meta)/1.6 var(--mono);
  white-space: nowrap;
}
.hub-keys span { color: var(--text2); font-size: var(--fs-data); }

/* mobile: the wrapped bar breathes a little less */
@media (max-width: 768px) { .hub-bar { row-gap: 2px; } }

/* an .hw-note that flows with content instead of blanketing it — the alerts
   empty note used to sit over the fired-history list and eat its clicks */
.hw-note.hw-note-flow { position: static; inset: auto; padding: 14px 18px 10px; }

/* reduced-motion coverage for the last animated stragglers */
@media (prefers-reduced-motion: reduce) {
  .hub-palette, .hub-coach { animation: none; }
  .vrow.flash { animation: none; }
}

/* the shortcuts sheet is help, not layout management — it stays in sections */
body.in-section .hub-menu button[data-menu="keys"] { display: flex; }

/* screener sort headers are real buttons now (keyboard-reachable, matching
   the Scanner's pattern) — strip the button chrome, keep the header voice */
.vscr2-h button.vscr2-c {
  border: none; background: none; padding: 0; margin: 0;
  font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
  text-align: right; cursor: pointer;
}
.vscr2-h button.vscr2-c:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }

/* ── v1.30.0: the elevation wave — skeletons, settle-in, staleness dots,
   the Tier-2 inspector drawer, a tighter mobile bar ─────────────────────── */

/* chart skeleton: shimmer bars hold the space until the first draw settles */
.hw-skel {
  position: absolute; inset: 0; z-index: 2;
  display: flex; flex-direction: column; justify-content: flex-end; gap: 10px;
  padding: 18px 16px 22px; pointer-events: none;
}
.hw-skel span {
  height: 10px; border-radius: 4px;
  background: linear-gradient(90deg, var(--bg3) 25%, var(--bg4) 50%, var(--bg3) 75%);
  background-size: 200% 100%;
  animation: vscShimmer 1.3s linear infinite;
}
.hw-skel span:nth-child(1) { width: 34%; }
.hw-skel span:nth-child(2) { width: 82%; }
.hw-skel span:nth-child(3) { width: 58%; }
.hw-skel span:nth-child(4) { width: 71%; }
@media (prefers-reduced-motion: reduce) { .hw-skel span { animation: none; } }

/* widget content settles in instead of popping (opacity only — cheap) */
.hw-body > * { animation: hubBodyIn 220ms var(--ease-out); }
@keyframes hubBodyIn { from { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .hw-body > * { animation: none; } }

/* staleness: a chart whose feed has gone quiet tints its header icon amber
   (a title-::after dot clipped away under ellipsized titles) */
.hw-stale .hw-head > i, .hw-stale .hw-head > svg,
.vpanel.hw-stale .vpanel-head > i, .vpanel.hw-stale .vpanel-head > svg {
  color: var(--gold); opacity: 1;
}
/* The state machine's other two visible states. Connecting is deliberately
   quiet — a pulse, not a spinner, because a wall of spinners on a live market
   board reads as breakage. Error borrows the stale treatment in red. */
.hw-error .hw-head > i, .hw-error .hw-head > svg,
.vpanel.hw-error .vpanel-head > i, .vpanel.hw-error .vpanel-head > svg {
  color: var(--red); opacity: 1;
}
.hw[data-state="connecting"] .hw-head > i, .hw[data-state="connecting"] .hw-head > svg {
  animation: hwPulse 1.6s ease-in-out infinite;
}
@keyframes hwPulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 0.9; } }
@media (prefers-reduced-motion: reduce) {
  .hw[data-state="connecting"] .hw-head > i, .hw[data-state="connecting"] .hw-head > svg {
    animation: none; opacity: 0.6;
  }
}

/* error notes always sit above a lingering skeleton */
.hw-note { z-index: 3; }

/* ── Tier-2 inspector drawer (reuses the glass drawer shell) ──────────── */
.hub-insp-veil { z-index: var(--z-drawer); background: rgba(4, 3, 9, 0.3); }
.vgal-drawer.hub-insp { width: min(400px, 94vw); gap: 10px; }
.hub-insp-sec {
  margin-top: 6px; color: var(--text3);
  font-size: var(--fs-meta); font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase;
}
.hub-insp .hf-field { margin-bottom: 11px; }
.hub-insp-row { display: flex; gap: 7px; flex-wrap: wrap; }
.hub-insp-row .vgal-btn { flex: 1; justify-content: center; }
.hub-insp-danger { color: var(--red); border-color: color-mix(in srgb, var(--red) 45%, transparent); }
.hub-insp-danger:hover { color: var(--red2); border-color: var(--red); background: color-mix(in srgb, var(--red) 10%, transparent); }
/* the inspected block glows so the drawer's target is never ambiguous */
.grid-stack-item-content.hw.hw-inspected {
  border-color: color-mix(in srgb, var(--teal) 70%, var(--border));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025), 0 3px 12px rgba(0, 0, 0, 0.22),
    0 0 0 1px color-mix(in srgb, var(--teal) 40%, transparent),
    0 0 18px color-mix(in srgb, var(--teal) 20%, transparent);
}

/* mobile bar: two tidy rows — the % change yields, the select stays compact */
@media (max-width: 768px) {
  .hp-txt em { display: none; }
  /* the layout name IS the control's content — it survives the icon-only bar */
  .hub-bar .hub-layout-btn span { display: inline; }
}

/* ── v1.31.0: navigation overhaul — the layout switcher becomes a designed
   menu (the last OS-styled control in the bar), family mark on the footer ── */
.hub-layout-btn { color: var(--text2); font-weight: 600; }
.hub-layout-btn:hover { color: var(--text); }
.hub-layout-btn > span {
  max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.hub-layout-btn .chev {
  width: 11px !important; height: 11px !important; color: var(--text3);
  transition: transform 160ms var(--ease-out);
}
.hub-layout-btn[aria-expanded="true"] .chev { transform: rotate(180deg); }
#hub-layout-menu { left: 0; right: auto; min-width: 200px; max-height: min(60vh, 420px); overflow-y: auto; }
#hub-layout-menu.open { transform-origin: top left; }
/* near the viewport's right edge the menu flips to right-anchored */
#hub-layout-menu.flip { left: auto; right: 0; }
#hub-layout-menu.open.flip { transform-origin: top right; }
#hub-layout-menu .lay-n { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#hub-layout-menu [aria-checked="true"] { color: var(--text); }
#hub-layout-menu [aria-checked="true"] i, #hub-layout-menu [aria-checked="true"] svg { color: var(--teal); }
.hub-lay-kbd {
  flex: none; padding: 0 5px;
  border: 1px solid var(--border2); border-radius: var(--radius-sm);
  color: var(--text3); font: 600 var(--fs-meta)/1.6 var(--mono);
}
@media (max-width: 768px) { .hub-layout-btn > span { max-width: 92px; } }

/* Scanner symbol scope: Focus on ETH → ETH traders. The chip names the
   filter and clears it in one click. */
.vsc-scopewrap:empty { display: none; }
.vsc-scope {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 4px 5px 4px 10px;
  border: 1px solid color-mix(in srgb, var(--teal) 45%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--teal) 9%, transparent);
  color: var(--text2); font-size: var(--fs-data);
}
.vsc-scope b { color: var(--text); font-weight: 700; }
.vsc-scope .vic, .vsc-scope .vic-f { width: 15px; height: 15px; font-size: var(--fs-meta); flex: none; }
.vsc-scope button {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border: none; border-radius: 50%;
  background: none; color: var(--text3); cursor: pointer;
  transition: color 120ms var(--ease-out), background 120ms var(--ease-out);
}
.vsc-scope button:hover { color: var(--text); background: color-mix(in srgb, var(--teal) 18%, transparent); }
.vsc-scope button:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
.vsc-scope button i, .vsc-scope button svg { width: 11px; height: 11px; }

/* CME gap tracker: unfilled gap bands carry a light label */
.hw-body .vgap-note { position: absolute; left: 10px; top: 6px; z-index: 3; color: var(--text3); font-size: var(--fs-meta); pointer-events: none; }

/* ── iOS-native round 3 (2026-07-28 responsive audit): the hub on a phone
   must read as a native app — tap targets, safe areas, sheets, press
   feedback. Desktop is untouched: everything gates on pointer/width. ── */

/* the sticky bar owns the notch strip in standalone/home-screen installs */
.hub-bar { padding-top: max(6px, env(safe-area-inset-top)); }

/* floating surfaces respect the home indicator + sensor housings */
.hub-toast { bottom: calc(22px + env(safe-area-inset-bottom)); }
.hw-max {
  inset: max(10px, env(safe-area-inset-top)) max(10px, env(safe-area-inset-right))
    max(10px, env(safe-area-inset-bottom)) max(10px, env(safe-area-inset-left));
}
.vgal-drawer {
  padding-right: max(18px, env(safe-area-inset-right));
  padding-bottom: max(16px, env(safe-area-inset-bottom));
}
.vgal-veil { overscroll-behavior: contain; }

/* the ⋮ menu can outgrow a landscape phone — let it scroll like its sibling */
.hub-menu { max-height: calc(100dvh - 64px); overflow-y: auto; overscroll-behavior: contain; }

/* the palette must clear short viewports and the raised keyboard */
.hub-palette-veil { padding: min(14vh, 8dvh) 16px; }
.hub-palette-list { max-height: min(320px, calc(100dvh - 8dvh - 120px)); }

/* chart toolbar menus escape the ≤760 scrolling-bar clip via the widget
   container (container-typed bodies anchor position:fixed descendants) */
@media (max-width: 760px) {
  .vcp-menu {
    position: fixed; left: 10px; right: 10px; top: 54px; width: auto;
    max-height: min(65dvh, 440px); overflow-y: auto; overscroll-behavior: contain;
  }
  /* Same specificity as `.vcp-menu.vcp-tfmenu` (hub.css:1132), which pins
     `left: 110px; max-width: 220px` and therefore OUTRANKED the rule above no
     matter where it sat. On a 320px screen that put the timeframe menu's right
     edge at 330px — 10px off-screen. Pre-existing, but the `⋯` chip added this
     phase is what makes the menu reachable on a phone at all, so it is ours. */
  .vcp-menu.vcp-tfmenu { left: 10px; right: 10px; max-width: none; }
}

/* Flow tape peels once more on small phones (408px of columns at 390 before) */
@media (max-width: 480px) {
  .vsc-fr { grid-template-columns: 44px minmax(96px, 1.2fr) minmax(84px, 1fr) 82px; gap: 8px; }
}

/* touch: press answers, targets grow to the iOS floor — visuals unchanged */
.vrow.clickable:active, .vnews a:active, .hub-menu button:active,
.hub-menu label:active, .hub-menu a:active, .hub-palette-item:active,
.vposn-row:active, .vsc-al:active, .vscr2-news:active, .vsuite a:active,
.vsc-foot:active, .vdesk-eng button:active { background: var(--bg4); }
@media (pointer: coarse) {
  .hub-nav a { padding: 10px 13px; }
  #hub-focus, #hub-pal, .hub-bar .hub-btn { padding: 11px 10px; }
  .hub-menu button, .hub-menu label, .hub-menu a, .hub-palette-item { padding: 11px 12px; }
  .vtab, .vcp-chip, .vscr2-tab, .vdesk-eng button, .vpage-chip, .vgal-btn { padding-top: 7px; padding-bottom: 7px; }
  .vsc-view { padding-top: 12px; padding-bottom: 13px; }
  .hw-ctl-pill { padding: 5px 8px; }
  .hw-head { min-height: 40px; }
  .hw-btn { width: 30px; height: 30px; }
  /* invisible slop: the star owns its square before the row hears the tap */
  .vsc-clear::after { content: ''; position: absolute; inset: -12px; }
  .vsc-star, .valert-row .del, .vcp-lbtn, .vgal-add, .hub-coach-x, .vsc-scope button {
    position: relative; z-index: 1; padding: 10px; margin: -10px;
  }
  .vgal-d-head .hw-btn, .vsc-tw-head .hw-btn { position: relative; z-index: 1; }
  .vgal-d-head .hw-btn::after, .vsc-tw-head .hw-btn::after { content: ''; position: absolute; inset: -8px; }
  /* hover-revealed widget chrome must simply be there on touch */
  body:not(.editing) .hw-btns { opacity: 0.9; }

  /* …but "there, permanently" and "destructive" must not be the same button.
     On a fine pointer the ✕ is one deliberate hover away and the mouse is
     precise. On touch it was a 30px destructive control sitting 1px from
     Fullscreen (`.hw-btns` gap is 1px), always visible, on ten widgets at once.
     Removal is layout editing, so it goes where the rest of layout editing
     already lives: Customize. The touch header outside Customize is
     REFRESH + FULLSCREEN, nothing else — which also drops the header from
     three controls to two and takes the neighbour-miss share down with it
     (audit F3; §4.3 note 5 — the cure for a crowded row is fewer buttons,
     not bigger ones). Removal keeps its undo either way. */
  body:not(.editing) .hw-btn[data-act="remove"] { display: none; }
}

/* phones: drawers become bottom sheets — full width, rounded top, grabber */
@media (max-width: 640px) {
  .vgal-drawer {
    top: auto; left: 0; right: 0; bottom: 0; width: 100%;
    max-height: calc(100dvh - 52px);
    border-left: 0; border-top: 1px solid rgba(255, 255, 255, 0.09);
    border-radius: 16px 16px 0 0;
    padding-bottom: max(18px, env(safe-area-inset-bottom));
    animation: vgalSheetIn 260ms var(--ease-out) both;
  }
  .vgal-drawer::before {
    content: ''; display: block; flex: none;
    width: 36px; height: 4px; margin: -4px auto 2px; border-radius: 2px;
    background: rgba(255, 255, 255, 0.18);
  }
}
/* keyframes live at top level — nested inside the media block they never
   resolved and the sheet froze at its from-state (28px low, invisible-ish) */
@keyframes vgalSheetIn {
  from { transform: translateY(28px); opacity: 0; }
  to { transform: none; opacity: 1; }
}

/* Customize on touch: gridstack's stock 10px grips are blind targets — grow
   them and keep them visible (pairs with alwaysShowResizeHandle:'mobile') */
@media (pointer: coarse) {
  body.editing .grid-stack-item > .ui-resizable-handle { opacity: 0.7; display: block; }
  body.editing .grid-stack-item > .ui-resizable-e,
  body.editing .grid-stack-item > .ui-resizable-w { width: 22px; }
  body.editing .grid-stack-item > .ui-resizable-n,
  body.editing .grid-stack-item > .ui-resizable-s { height: 22px; }
  body.editing .grid-stack-item > .ui-resizable-se,
  body.editing .grid-stack-item > .ui-resizable-sw,
  body.editing .grid-stack-item > .ui-resizable-ne,
  body.editing .grid-stack-item > .ui-resizable-nw { width: 36px; height: 36px; }
}

/* …but on a COLLAPSED grid 6 of those 8 handles cannot do anything: the column
   is the full width of the screen, so there is nowhere for an east/west or
   corner handle to drag to. They are also the ones doing the damage — at 51×51
   on touch, the top-left corner lands on the widget's title and the bottom-right
   one on its ✕. Ten widgets × 8 = 80 handles; this leaves the 20 that resize
   height. Must sit AFTER the block above and carry the same `body.editing`
   weight, or `display: block` up there wins (audit F-Z, second defect). */
@media (max-width: 640px) {
  body.editing .grid-stack-item > .ui-resizable-e,
  body.editing .grid-stack-item > .ui-resizable-w,
  body.editing .grid-stack-item > .ui-resizable-ne,
  body.editing .grid-stack-item > .ui-resizable-nw,
  body.editing .grid-stack-item > .ui-resizable-se,
  body.editing .grid-stack-item > .ui-resizable-sw { display: none; }
}

/* ── Wallpaper Mode: the chart desk as a full-dot-matrix VFD, for a TV ── */
.vwall { position: fixed; inset: 0; z-index: var(--z-wall); background: #010108; cursor: none; }
/* DIRECT child only. As `.vwall canvas` this also claimed every preview
   canvas inside the panel — same ancestor — and outranked `.vwall-tprev`
   on specificity, so each card's miniature was absolutely positioned and
   stretched to the full panel. */
.vwall > canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.vwall.awake { cursor: default; }
.vwall-chrome {
  position: absolute; top: 20px; right: 20px; z-index: 2;
  display: flex; align-items: center; gap: 8px;
  opacity: 0; pointer-events: none; transform: translateY(-4px);
  transition: opacity 180ms var(--ease-out), transform 180ms var(--ease-out);
}
.vwall.awake .vwall-chrome { opacity: 1; pointer-events: auto; transform: none; }
.vwall-chip {
  display: inline-flex; align-items: center; gap: 7px;
  appearance: none; border: 1px solid rgba(126, 165, 235, 0.3); border-radius: 999px;
  background: rgba(4, 9, 24, 0.78); padding: 8px 15px; cursor: pointer;
  color: #d5e6ff; font: 600 var(--fs-meta)/1.2 var(--mono); letter-spacing: 0.05em;
}
.vwall-chip svg { width: 13px; height: 13px; }
.vwall-chip:hover { border-color: rgba(126, 165, 235, 0.55); }

/* THE DESIGN PANEL — grouped Design / Market / Overlays.
   It keeps its dark glass on every theme, INCLUDING the light ones: it is
   chrome floating in front of the display, not part of it, and a panel
   that restyles itself per theme would read as a second, competing
   surface. It scrolls because a TV mode can afford the height. */
.vwall-panel {
  position: absolute; top: 64px; right: 20px; z-index: 2; width: 322px;
  display: none; flex-direction: column; gap: 6px;
  max-height: calc(100vh - 96px); overflow-y: auto; overscroll-behavior: contain;
  border: 1px solid rgba(126, 165, 235, 0.3); border-radius: 14px;
  background: rgba(4, 9, 24, 0.9); backdrop-filter: blur(10px); padding: 14px 15px 15px;
}
.vwall-panel.open { display: flex; }
/* The panel is a column flexbox that now overflows its own max-height, and a
   column flex item SHRINKS by default — so once the seven theme previews
   pushed the content past the cap, every segment control was squashed toward
   zero height and simply vanished. Nothing in here may shrink; it scrolls. */
.vwall-panel > * { flex-shrink: 0; }
.vwall-panel h4 {
  margin: 12px 0 2px; padding-top: 11px;
  border-top: 1px solid rgba(126, 165, 235, 0.18);
  color: rgba(213, 230, 255, 0.85); font: 700 var(--fs-meta)/1 var(--mono);
  letter-spacing: 0.18em; text-transform: uppercase;
}
.vwall-panel h4:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.vwall-panel label {
  color: rgba(213, 230, 255, 0.55); font: 600 var(--fs-meta)/1 var(--mono);
  letter-spacing: 0.12em; text-transform: uppercase; margin-top: 5px;
}
.vwall-panel label:first-child { margin-top: 0; }

/* Theme cards. The swatch is a real sample of the theme: its own ground,
   its own five palette roles, its own dot shape. Two columns keeps five
   themes to three rows without shrinking the sample below legible. */
.vwall-themes { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
/* An odd count leaves a hole in a two-up grid. Rather than pad it, let the
   last card take the full width — it earns a wider preview, and the row
   reads as a deliberate close instead of a gap. */
.vwall-themes > .vwall-theme:last-child:nth-child(odd) { grid-column: 1 / -1; }
.vwall-theme {
  appearance: none; display: flex; flex-direction: column; gap: 5px;
  border: 1px solid rgba(126, 165, 235, 0.24); border-radius: 10px;
  background: none; padding: 7px 7px 8px; cursor: pointer; text-align: left;
  transition: border-color 130ms var(--ease-out), background 130ms var(--ease-out);
}
.vwall-theme:hover { border-color: rgba(126, 165, 235, 0.5); background: rgba(126, 165, 235, 0.06); }
.vwall-theme.on { border-color: rgba(65, 227, 255, 0.7); background: rgba(65, 227, 255, 0.1); }
.vwall-theme:focus-visible { outline: 1px solid rgba(65, 227, 255, 0.8); outline-offset: 2px; }
.vwall-theme b {
  color: #eef8ff; font: 700 var(--fs-meta)/1 var(--mono); letter-spacing: 0.04em;
}
.vwall-theme em {
  color: rgba(213, 230, 255, 0.45); font: 400 var(--fs-meta)/1.15 var(--mono);
  font-style: normal; letter-spacing: 0.02em;
  /* two lines' worth, always — a one-line note next to a two-line one made
     the grid rows step, which reads as a bug rather than as copy */
  min-height: calc(var(--fs-meta) * 2.3);
}
/* The preview paints itself from the theme record — see paintChip(). Height
   is fixed so every card measures the same before paint; width is fluid so
   the full-width last card gets a wider sample of the same display. */
.vwall-tprev {
  display: block; width: 100%; height: 42px;
  border-radius: 5px; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.07);
}
.vwall-themes > .vwall-theme:last-child:nth-child(odd) .vwall-tprev { height: 38px; }

/* What "Auto" resolved to. Sits directly under its segment, quiet enough to
   read as a footnote to the control rather than another control. */
.vwall-hint {
  margin: -2px 0 0; min-height: 11px;
  color: rgba(213, 230, 255, 0.4);
  font: 400 var(--fs-meta)/1.1 var(--mono); letter-spacing: 0.02em;
}
.vwall-hint:empty { min-height: 0; }

/* Palette chips — the secondary choice, sized like one. A row of swatches
   plus a name, wrapping, rather than a second grid of preview cards. */
.vwall-pals { display: flex; flex-wrap: wrap; gap: 5px; }
.vwall-pal {
  appearance: none; display: flex; align-items: center; gap: 6px;
  border: 1px solid rgba(126, 165, 235, 0.24); border-radius: 8px;
  background: none; padding: 5px 8px 5px 6px; cursor: pointer;
  transition: border-color 130ms var(--ease-out), background 130ms var(--ease-out);
}
.vwall-pal:hover { border-color: rgba(126, 165, 235, 0.5); background: rgba(126, 165, 235, 0.06); }
.vwall-pal.on { border-color: rgba(65, 227, 255, 0.7); background: rgba(65, 227, 255, 0.1); }
.vwall-pal:focus-visible { outline: 1px solid rgba(65, 227, 255, 0.8); outline-offset: 2px; }
.vwall-pal b {
  color: rgba(238, 248, 255, 0.9); font: 600 var(--fs-meta)/1 var(--mono); letter-spacing: 0.03em;
}
.vwall-pal.on b { color: #eef8ff; }
.vwall-pswatch {
  display: flex; border-radius: 3px; overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
.vwall-pswatch i { width: 5px; height: 14px; }

/* Overlay toggles: pressed state is the fill, not a checkbox glyph. */
.vwall-togs { display: flex; gap: 6px; }
.vwall-tog {
  appearance: none; flex: 1; border: 1px solid rgba(126, 165, 235, 0.28); border-radius: 999px;
  background: none; padding: 6px 0; cursor: pointer;
  color: rgba(213, 230, 255, 0.55); font: 600 var(--fs-meta)/1.2 var(--mono); letter-spacing: 0.04em;
  transition: color 130ms var(--ease-out), background 130ms var(--ease-out), border-color 130ms var(--ease-out);
}
.vwall-tog:hover { color: #eef8ff; border-color: rgba(126, 165, 235, 0.5); }
.vwall-tog.on { background: rgba(65, 227, 255, 0.16); border-color: rgba(65, 227, 255, 0.45); color: #9feeff; }
.vwall-tog:focus-visible { outline: 1px solid rgba(65, 227, 255, 0.8); outline-offset: 2px; }
.vwall-sym {
  appearance: none; width: 100%; border: 1px solid rgba(126, 165, 235, 0.3); border-radius: 8px;
  background: rgba(10, 18, 40, 0.6); padding: 7px 10px; outline: none;
  color: #eef8ff; font: 700 var(--fs-data)/1.2 var(--mono); letter-spacing: 0.06em; text-transform: uppercase;
}
.vwall-sym:focus { border-color: rgba(65, 227, 255, 0.55); }
.vwall-sym.bad { border-color: rgba(255, 59, 87, 0.8); color: #ff8fa3; }
.vwall-seg { display: flex; border: 1px solid rgba(126, 165, 235, 0.3); border-radius: 999px; overflow: hidden; }
.vwall-seg button {
  appearance: none; flex: 1; border: 0; background: none; padding: 6px 0; cursor: pointer;
  color: rgba(213, 230, 255, 0.6); font: 600 var(--fs-meta)/1.2 var(--mono); letter-spacing: 0.04em;
  transition: color 120ms var(--ease-out), background 120ms var(--ease-out);
}
.vwall-seg button + button { border-left: 1px solid rgba(126, 165, 235, 0.22); }
.vwall-seg button:hover { color: #eef8ff; }
.vwall-seg button.on { background: rgba(65, 227, 255, 0.16); color: #9feeff; }

/* ── Scanner: how-the-board-is-built disclosure ──────────────
   Native <details> so it is keyboard- and AT-correct for free.
   Collapsed by default: the trust claim is available, not loud. */
.vsc-method { max-width: 660px; }
.vsc-method > summary {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-data); color: var(--text3);
  border-bottom: 1px dashed var(--border3);
  padding-bottom: 1px; cursor: pointer; list-style: none;
  transition: color 150ms ease, border-color 150ms ease;
}
.vsc-method > summary::-webkit-details-marker { display: none; }
.vsc-method > summary::before {
  content: '+'; font-weight: 700; line-height: 1;
}
.vsc-method[open] > summary::before { content: '−'; }
.vsc-method > summary:hover { color: var(--text2); border-bottom-color: var(--text3); }
.vsc-method > summary:focus-visible { outline: 1px solid var(--teal); outline-offset: 3px; }
.vsc-methodbody {
  margin-top: 10px; padding-left: 12px;
  border-left: 1px solid var(--border);
}
.vsc-methodbody p {
  margin: 0 0 8px; font-size: var(--fs-data); line-height: 1.65; color: var(--text3);
}
.vsc-methodbody p:last-child { margin-bottom: 0; }
.vsc-methodbody b { color: var(--text2); font-weight: 700; }
/* Fine print, but the link inside it is still a link — control floor applies. */
.vsc-methodfoot a { color: var(--teal); font-size: var(--fs-ui); }
.vsc-methodfoot a:hover { text-decoration: underline; }

/* Visually hidden, still announced. Backs the single polite live region. */
.hub-sr-only {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip: rect(0 0 0 0); clip-path: inset(50%);
  overflow: hidden; white-space: nowrap;
}

/* ══ Phase 5 (audit/2026-07) — target size + prose legibility ═══════════════
   Two rules the type scale alone can't express. */

/* 1 — TOUCH TARGETS.
   The brief's 44px floor applies to touch, not to a mouse at 1440px, and the
   dense terminal look depends on small visual controls. So the HIT AREA grows
   on coarse pointers while the drawn box stays exactly as designed.

   THE Z-INDEX (audit F-TAP). This pseudo used to carry `z-index: -1`, on the
   reasoning that it should sit beneath its own control's content. A negative-z
   child paints above its stacking context's own background but BELOW every
   in-flow descendant of that context — and hit-testing follows paint order. So
   the slop only ever worked where nothing between the control and its stacking
   context painted: it was live on `.hub-bar .hub-btn` (100% self, a direct child
   of the sticky bar) and COMPLETELY INERT everywhere else. Measured proof: with
   the pseudo deleted outright, `#hub-nav a` still scored 47% — identical to
   shipped. `z-index: 0` is what makes it real; `min-width/min-height: 100%`
   still guarantees it can never shrink a control that is already bigger.

   Slop alone is not enough where an ancestor clips (`.hub-nav` is
   `overflow-x: auto`, which forces overflow-y to auto and crops the 44px patch
   back to the capsule's height) or where the control has a fixed border-box size
   that swallows padding. Those get a real min-height below. */
@media (pointer: coarse), (max-width: 768px) {
  .hw-btn, .hw-ctl-pill, .vsc-star, .vtab, .vpage-chip, .hub-btn,
  .vgal-navi, .vsc-view, .hub-nav a, .vsc-h .sortable, .hub-coach-x,
  /* the vchart toolbar — five of the twelve worst dead zones in the app, and
     the only major control surface with no coarse treatment at all (F-VCP) */
  .vcp-btn, .vcp-tool, .vcp-lbtn, .vcp-symbtn, .vcp-chip,
  .vsc-czseg, .vsc-addr, .vgal-add, .vscr2-tab, .vscr2-sel {
    position: relative;
  }
  .hw-btn::after, .hw-ctl-pill::after, .vsc-star::after, .vtab::after,
  .vpage-chip::after, .hub-btn::after, .vgal-navi::after, .vsc-view::after,
  .hub-nav a::after, .vsc-h .sortable::after, .hub-coach-x::after,
  .vcp-btn::after, .vcp-tool::after, .vcp-lbtn::after, .vcp-symbtn::after,
  .vcp-chip::after, .vsc-czseg::after, .vsc-addr::after, .vgal-add::after,
  .vscr2-tab::after {
    content: '';
    position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: var(--tap-min); height: var(--tap-min);
    min-width: 100%; min-height: 100%;
    z-index: 0;
  }
  /* The Scanner star sits above the row's full-bleed .vsc-open button, so its
     expanded area has to stay above that too or the row swallows the tap. */
  .vsc-r.clickable .st { z-index: 2; }
  /* Now that the slop paints at z-index 0 instead of behind everything, a
     control's own glyph has to be lifted over it — otherwise the pseudo covers
     the icon it belongs to. Defensive: the measured numbers match the audit's
     fix-B run with or without this, but it keeps a control's content and its
     hit area from disagreeing as the slop list grows. */
  .hw-btn > i, .hw-btn > svg, .hub-btn > i, .hub-btn > svg,
  .hub-nav a > i, .hub-nav a > svg, .hub-nav a > span,
  .vcp-btn > i, .vcp-btn > svg, .vcp-btn > span,
  .vcp-tool > i, .vcp-tool > svg, .vcp-lbtn > i, .vcp-lbtn > svg,
  .vcp-symbtn > b, .vcp-symbtn > span, .vcp-symbtn > i, .vcp-symbtn > svg {
    position: relative; z-index: 1;
  }
}

/* REAL height where slop cannot reach. The drawn box stays as designed
   horizontally; only the row gains height. Verified by tap-experiment.mjs to
   take the widget-control dead zones to 0% and the nav's to 9% (audit F-TAP
   fix B — the measured one). `.hub-nav` must stretch or the link cannot use the
   height it is given. */
@media (pointer: coarse) {
  .hub-nav { align-items: stretch; }
  .hub-nav a, .hw-head .hw-btn, .hw-ctl-pill,
  .vcp-btn, .vcp-symbtn, .vcp-chip, .vcp-tool, .vsc-czseg,
  /* These three sit in horizontal strips that scroll (`.vsc-views` and
     `.vgal-navi.m` are overflow-x:auto, which forces overflow-y to auto too),
     so the strip CROPS the 44px slop back to its own height and the pseudo
     alone cannot reach the floor. The Scanner's view tabs were the single
     biggest remaining offender in the sweep — 145 hits at 43% dead — and they
     are that section's primary control. Real height is the only fix. */
  .vsc-view, .vgal-navi, .vpage-chip, .vscr2-tab,
  .vscr2-sel { min-height: var(--tap-min); }
  /* The two most-used text inputs in the app sit in a 32px pill (28px inside
     the Scanner) whose <input> has no height of its own, so the field itself
     measured 18–26px — under any touch floor, and still 57% dead after the
     16px type fix. F-ZOOM calls this out in its own words: "An 18px-tall
     search box is separately below any touch floor." The pill takes the
     height and the input stretches into it; the type size is already handled
     by the LAST WORD block at the end of this file.
     `.hub-page` is again load-bearing: the Scanner's own 28px height rule sits
     ~430 lines BELOW this block at the same (0,2,0) specificity, so without the
     prefix source order hands the Scanner its 28px back and only the Gallery
     field gets fixed. Measured: that is exactly what happened on the first
     attempt (the search input's dead-zone count fell 44 to 29, not 44 to 0). */
  .hub-page .vgal-search, .hub-page .vsc .vsc-search { height: var(--tap-min); }
  .hub-page .vgal-search > input { align-self: stretch; min-width: 0; }
  /* the chart legend is an OVERLAY on the canvas — giving its buttons 44px of
     height would wall off the top-left of every chart. Slop only, no min-height. */
}

/* 2 — PROSE.
   Geist Mono stays site-wide (owner decision 2026-07-03; audit D-004). Mono is
   harder to read than a sans at the same size, so explanatory copy compensates
   with size, leading and a bounded measure instead of a second typeface. */
.hf-help, .vgal-d-desc, .vsc-teach, .vsc-sub, .hub-coach p, .hub-menu-hint {
  font-size: var(--fs-data);
  line-height: var(--lh-prose);
  max-width: var(--measure-prose);
}

/* ══ Scanner stats band ══════════════════════════════════════════════════
   One hairline-divided strip, not four cards. The dividers stop at the
   band's edges so the row reads as a single object. */
.vsc-card .rail { margin-top: var(--sp-1); }
/* only the ACTIVE coin carries weight — the rest are quiet until hovered */
.vsc-crc:hover { color: var(--text); background: var(--bg3); }
.vsc-crc.on {
  color: var(--teal); border-color: color-mix(in srgb, var(--teal) 38%, transparent);
  background: color-mix(in srgb, var(--teal) 10%, transparent);
}
/* the sticky header is now the section's only tinted surface, so it has to
   carry the separation the removed table frame used to provide */

/* the section mark sits on the site's badge primitive; the title needs a
   touch more air beside a 32px badge than it did beside a bare glyph */
.vsc-title > .v-mark { margin-right: var(--sp-1); }

/* ══ Scanner coin filter — menu, not a chip row ══════════════════════════
   Nine equal-weight pills were nine competing targets for a filter that holds
   one value at a time. The trigger states the current scope; the menu holds
   the alternatives. Built on the Hub's existing .hub-menu primitive so it
   matches the layout and overflow menus rather than inventing a third. */
.vsc-coinwrap { flex: none; }
.vsc-coinbtn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 10px 6px 8px;
  border: 1px solid var(--border2); border-radius: var(--radius);
  background: var(--bg2); color: var(--text2);
  font: 600 var(--fs-ui)/1.2 var(--mono); cursor: pointer;
  transition: color 130ms var(--ease-out), border-color 130ms var(--ease-out);
}
.vsc-coinbtn:hover { color: var(--text); border-color: var(--border3); }
.vsc-coinbtn.on { color: var(--teal); border-color: color-mix(in srgb, var(--teal) 42%, transparent); }
.vsc-coinbtn > i:last-child, .vsc-coinbtn > svg:last-child {
  width: 13px; height: 13px; opacity: 0.7; margin-left: 2px;
}
.vsc-coinbtn:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
/* left-anchored: the trigger sits at the start of its row, not the end */
.vsc-coinmenu { right: auto; left: 0; min-width: 208px; max-height: 340px; overflow-y: auto; }
.vsc-coinmenu button { justify-content: flex-start; }
.vsc-coinmenu button.on { color: var(--teal); background: color-mix(in srgb, var(--teal) 9%, transparent); }
.vsc-coinmenu button .n { margin-left: auto; color: var(--text3); font-size: var(--fs-meta); }
.vsc-coinmenu .vic, .vsc-coinmenu .vic-f { width: 15px; height: 15px; flex: none; }

/* ══ Stat cards — tappable, and their breakdown drawer ═══════════════════ */
.vsc-card.tap {
  cursor: pointer; text-align: left; font: inherit;
  transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .vsc-card.tap:hover {
    border-color: var(--caccent);
    transform: translateY(-2px);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04),
                0 10px 26px -12px color-mix(in srgb, var(--caccent) 55%, transparent),
                0 0 0 1px color-mix(in srgb, var(--caccent) 22%, transparent);
  }
  .vsc-card.tap:hover .vsc-card-go { opacity: 1; transform: none; }
}
.vsc-card.tap:active { transform: translateY(-1px) scale(0.99); }
.vsc-card.tap:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
/* the affordance stays out of the way until the card is hovered */
.vsc-card-go {
  position: absolute; top: var(--sp-3); right: var(--sp-3);
  width: 13px; height: 13px; color: var(--caccent);
  opacity: 0; transform: translate(-2px, 2px);
  transition: opacity 160ms var(--ease-out), transform 160ms var(--ease-out);
}
.vsc-card[disabled] { cursor: default; }

.vsc-statdrawer { display: flex; flex-direction: column; gap: var(--sp-3); }
.vsc-statgrid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); gap: var(--sp-2);
}
.vsc-kpi {
  display: flex; flex-direction: column; gap: 2px; min-width: 0;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg2);
}
.vsc-kpi label {
  display: flex; align-items: center; gap: 5px;
  color: var(--text3); font-size: var(--fs-meta); letter-spacing: 0.08em; text-transform: uppercase;
  line-height: 1.3;
  /* two lines, then clamp — "MARKETS ON THE TAPE" was losing its last word to
     an ellipsis, which reads as broken rather than terse */
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}
.vsc-kpi b {
  color: var(--text); font-size: var(--fs-title); font-weight: 700;
  font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vsc-kpi b.up { color: var(--green); }
.vsc-kpi b.down { color: var(--red); }
.vsc-statchart { position: relative; flex: none; }
.vsc-statchart .vchart { width: 100%; height: 100%; }
.vsc-statfoot { color: var(--text3); font-size: var(--fs-meta); }

/* The drawer takes focus programmatically so screen readers land inside it.
   That is not a user-navigated focus, so it must not paint the UA's focus
   ring — every control INSIDE the drawer keeps its own. */
.vgal-drawer:focus, .vgal-drawer:focus-visible { outline: none; }

/* KPI supporting line — the number alone rarely answers "compared to what" */
.vsc-kpi .note {
  color: var(--text3); font-size: var(--fs-meta); line-height: 1.4;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.vsc-hint { color: var(--text3); font-weight: 400; text-transform: none; letter-spacing: 0; }
/* the drawer is a flex column — without flex:none a long list below simply
   shrinks the chart to nothing (it collapsed to ~40px behind the crowd list) */
.vsc-statchart { flex: none; height: 300px; }
/* the coin drill-down mounts VChartPro, which carries its own toolbar and is
   meant to be zoomed — give it real height, and let the drawer scroll */
.vsc-statchart.chart-pro { height: min(62vh, 560px); }
.vsc-statchart.chart-pro .vcp { height: 100%; }

/* the crowd list — every market, each row a way into that coin's chart */
.vsc-crowdlist { display: flex; flex-direction: column; }
.vsc-crowdrow {
  display: grid; align-items: center; gap: var(--sp-3);
  grid-template-columns: minmax(84px, 1.1fr) 56px minmax(72px, 0.9fr) 52px minmax(72px, 0.9fr);
  width: 100%; padding: 8px var(--sp-2); text-align: left;
  background: none; border: 0; border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
  color: var(--text2); font: inherit; font-size: var(--fs-data); cursor: pointer;
  transition: background 120ms var(--ease-out), color 120ms var(--ease-out);
}
.vsc-crowdrow:last-child { border-bottom: 0; }
.vsc-crowdrow:hover { background: var(--bg3); color: var(--text); }
.vsc-crowdrow:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }
.vsc-crowdrow .num { text-align: right; font-variant-numeric: tabular-nums; }
.vsc-crowdrow .num.t3 { color: var(--text3); }
.vsc-crowdrow .vposn-meter.hair { width: 56px; }

/* breadcrumb back out of a coin drill-down */
.vsc-backlink { color: var(--text3); cursor: pointer; font-weight: 600; }
.vsc-backlink:hover { color: var(--teal); }
.vsc-backlink:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; border-radius: var(--radius-sm); }
.vsc-crumb { color: var(--text3); margin: 0 6px; }

/* .wrow's gap is scoped to .vsc-r, so the crowd list's icons sat flush against
   their ticker. Same spacing, applied where the row is not a .vsc-r. */
.vsc-crowdrow .wrow { display: inline-flex; align-items: center; gap: 9px; min-width: 0; }
.vsc-crowdrow .wrow b { font-weight: 700; }
.vsc-crowdrow .vic, .vsc-crowdrow .vic-f { width: 17px; height: 17px; flex: none; }
/* realized pnl on a closing alert */
.vsc-al .alpnl { margin-left: auto; padding-left: var(--sp-2); font-variant-numeric: tabular-nums; font-weight: 600; }
.vsc-al .alpnl.up { color: var(--green); }
.vsc-al .alpnl.down { color: var(--red); }
.vsc-al .ago { flex: none; color: var(--text3); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Direction chip. A crowd bet without a side is not a bet — and the side is
   carried by an arrow AND a word, never by colour alone (the green/red pair
   fails deutan separation, so colour cannot be the only signal). */
.vsc-lean {
  display: inline-flex; align-items: center; gap: 3px;
  font-weight: 600; white-space: nowrap;
}
.vsc-lean i, .vsc-lean svg { width: 11px; height: 11px; stroke-width: 2.2; }
.vsc-lean.up { color: var(--green); }
.vsc-lean.down { color: var(--red); }
.vsc-lean.t3 { color: var(--text3); }
.vsc-crowdrow .lean { font-size: var(--fs-meta); font-weight: 600; white-space: nowrap; }
.vsc-crowdrow .lean.up { color: var(--green); }
.vsc-crowdrow .lean.down { color: var(--red); }
.vsc-crowdrow .lean.t3 { color: var(--text3); }

/* Card identity mark. Four unlabelled numbers in a row are hard to tell apart
   at a glance; a glyph beside the label anchors each one. */
.vsc-card label {
  display: flex; align-items: center; gap: 6px;
}
.vsc-card label > i, .vsc-card label > svg {
  width: 12px; height: 12px; flex: none; color: var(--caccent, var(--teal));
  opacity: 0.85; stroke-width: 2;
}
.vsc-card label > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the value's token icon sits with the number, not crammed against it */
.vsc-card .v .wrow { display: inline-flex; align-items: center; gap: 9px; min-width: 0; }
.vsc-card .v .wrow .vic, .vsc-card .v .wrow .vic-f { width: 20px; height: 20px; flex: none; }
/* non-crypto tickers (equities on the tape) have no icon in the cache — the
   letter fallback should look deliberate rather than broken */
.vic-f {
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%; background: var(--bg4); color: var(--text2);
  font-weight: 700; letter-spacing: 0; text-transform: uppercase;
  border: 1px solid var(--border2); box-sizing: border-box;
}

/* The live-marked KPI. A pulsing dot says the number is moving, so a reader
   knows it is a live mark and not a snapshot. */
.vsc-kpi.live { border-color: color-mix(in srgb, var(--teal) 30%, var(--border)); }
.vsc-livedot {
  display: inline-block; width: 5px; height: 5px; border-radius: 50%;
  background: var(--teal); box-shadow: 0 0 6px var(--teal-glow);
  animation: vscLive 2.4s ease-in-out infinite; flex: none;
}
@keyframes vscLive { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .vsc-livedot { animation: none; opacity: 0.9; } }
.vsc-kpi.live b { transition: color 220ms var(--ease-out); }

/* the picker's leading filter glyph — states what the control does now that
   the "POSITIONS IN" label is gone */
.vsc-coinbtn > i.lead, .vsc-coinbtn > svg.lead {
  width: 13px; height: 13px; opacity: 0.75; margin-right: 1px;
}
.vsc-coinbtn.on > i.lead, .vsc-coinbtn.on > svg.lead { opacity: 1; }

/* ══ Scanner UX remediation ═══════════════════════════════════════════════
   Every rule below answers a numbered finding in audit/SCANNER-UX.md
   (2026-07-30). Nothing here mints a new visual language: the pills reuse
   .vpage-chip's shape, the buttons reuse the site's border/bg tokens, and the
   type is --fs-* throughout. */

/* B1/B4 — the field is three tools in one coat. The hint names the tool the
   current text will use; the clear makes Escape discoverable. */
.vsc-qhint {
  flex: none; color: var(--teal); font-size: var(--fs-meta); font-weight: 600;
  letter-spacing: 0.02em; white-space: nowrap;
}
.vsc-qclear {
  flex: none; position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 19px; height: 19px; padding: 0; margin-right: -5px;
  background: none; border: 0; border-radius: 50%;
  color: var(--text3); cursor: pointer;
  transition: color 120ms var(--ease-out), background 120ms var(--ease-out);
}
.vsc-qclear:hover { color: var(--text); background: var(--bg4); }
.vsc-qclear:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
.vsc-qclear i, .vsc-qclear svg { width: 11px; height: 11px; }

/* B5 — the primary filter measured 18px tall, the most cramped control on the
   page. It now fills the pill it sits in. */
.vsc-search > input { align-self: stretch; min-width: 0; padding: 0; }

/* C2/B2 — an empty result offers the way out of whatever emptied it, and when
   the text is a live market it offers the scope rather than a dead end. */
.vsc-empty > [role="cell"] { display: block; }
.vsc-empty .lead { display: block; }
.vsc-emptyacts {
  display: flex; align-items: center; justify-content: center;
  gap: var(--sp-2); flex-wrap: wrap; margin-top: var(--sp-3);
}
.vsc-emptybtn {
  padding: 6px 13px; border-radius: 999px;
  border: 1px solid var(--border2); background: var(--bg3);
  color: var(--text2); font: 600 var(--fs-meta)/1.4 var(--mono); cursor: pointer;
  transition: color 120ms var(--ease-out), background 120ms var(--ease-out), border-color 120ms var(--ease-out);
}
.vsc-emptybtn:hover { color: var(--text); background: var(--bg4); border-color: var(--border3, var(--border2)); }
.vsc-emptybtn:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.vsc-emptybtn.primary {
  color: var(--teal); border-color: color-mix(in srgb, var(--teal) 42%, transparent);
  background: color-mix(in srgb, var(--teal) 10%, transparent);
}
.vsc-emptybtn.primary:hover { background: color-mix(in srgb, var(--teal) 17%, transparent); }

/* A4 — the foot is a row with one cell so role=table owns nothing but rows. */
.vsc-footrow, .vsc-footrow > [role="cell"] { display: block; }

/* A2 — the header cell carries aria-sort; the button inside it fills it. */
.vsc-h [role="columnheader"] > .sortable { display: block; width: 100%; text-align: inherit; }

/* D — TOUCH TARGETS, FINE POINTER.
   §1 above grows these hit areas on coarse pointers only. Measured with a
   mouse they are 16–21px on the short edge, which is small for a mouse too.
   Scoped to the exact complement of that media query, so the 44px touch
   targets are left exactly as they are.

   Every expansion here is WIDTH-NEUTRAL — it grows downward and outward into
   space the element's own cell already owns, never past it. A pseudo-element
   with a negative horizontal inset reads as real overflow to the audit's own
   scrollWidth probe, which is the correct reading: it IS content outside the
   box. So the controls take the room they are entitled to instead. */
@media (pointer: fine) and (min-width: 769px) {
  /* headers: 21px short edge → 35px, across the column's full width */
  .vsc-h .sortable { position: relative; }
  .vsc-h .sortable::after { content: ''; position: absolute; inset: -7px 0; }
  /* the star drew 17px inside a 26px cell — it now claims the cell it is in */
  .vsc-r .st { display: flex; align-items: center; }
  .vsc-r .st > .vsc-star {
    flex: 1; min-width: 0; padding: 6px 2px;
    display: inline-flex; align-items: center; justify-content: center;
  }
  /* the alert-feed clear drew 16px tall; the negative margin keeps the section
     header's baseline exactly where it was */
  .vsc-clear { padding: 7px 4px; margin-top: -5px; margin-bottom: -5px; }
  /* the search clear fills the pill's height rather than floating in it */
  .vsc-qclear { align-self: stretch; width: 24px; height: auto; border-radius: var(--radius-sm, 4px); }
}

/* ══ Scanner · two venues ═════════════════════════════════════════════════
   The Scanner reads Hyperliquid and Aster. They publish different facts, so
   the table is not fixed — see CAPS in hub.js. Styling reuses the coin
   filter's menu and the site's chip shapes; nothing new is invented here. */

/* the venue picker leads the filter bar — same button as the coin filter */
.vsc-srcwrap { flex: none; }
.vsc-srcmenu { right: auto; left: 0; min-width: 232px; }
.vsc-srcmenu button { justify-content: flex-start; }
.vsc-srcmenu button.on { color: var(--teal); }
.vsc-srcmenu button .n {
  margin-left: auto; color: var(--text3); font-size: var(--fs-meta);
  letter-spacing: 0.06em; text-transform: uppercase;
}
.vsc-srcmenu .vic, .vsc-srcmenu i, .vsc-srcmenu svg { width: 14px; height: 14px; flex: none; }
.vsc-srcmenu .hub-menu-hint { padding: 8px 12px 9px; color: var(--text3); line-height: 1.45; }

/* which venue a row came from — only rendered when more than one is on.
   .who is a flex COLUMN, so an inline-block badge stretches the full column
   width; the name and its badge get their own row inside it instead. */
.vsc-r .who .nm { display: flex; align-items: center; gap: 6px; min-width: 0; }
.vsc-r .who .nm b { min-width: 0; }
/* The mark alone, ringed in the venue's colour — at 100 rows the spelled-out
   name was a word repeated down the whole column. The name is still in the
   accessibility tree (.hub-sr-only inside the badge), so nothing is lost by
   dropping the glyph to an icon. */
.vsc-srctag {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 17px; height: 17px; border-radius: 50%;
}
.vsc-srctag .vic, .vsc-srctag .vic-f {
  width: 17px; height: 17px; border-radius: 50%; font-size: var(--fs-meta);
}
.vsc-srctag.hl { box-shadow: 0 0 0 1px color-mix(in srgb, var(--teal) 50%, transparent); }
.vsc-srctag.aster { box-shadow: 0 0 0 1px color-mix(in srgb, var(--pink, #ff2e88) 50%, transparent); }

/* dropdown: checkbox · venue mark · name */
.vsc-srcmenu .box { flex: none; }
.vsc-srcmenu .mark { display: inline-flex; flex: none; margin-left: 2px; }
.vsc-srcmenu .mark .vic, .vsc-srcmenu .mark .vic-f {
  width: 17px; height: 17px; border-radius: 50%; font-size: var(--fs-meta);
}

/* A value the venue does not publish. Deliberately NOT a dash: a dash in a
   numeric column reads as zero, or as a failed fetch. This reads as a fact
   about the source, and carries the venue name in its title. */
.vsc-nodata {
  font-style: normal; color: var(--text3); font-size: var(--fs-meta);
  opacity: 0.75; letter-spacing: 0.02em; cursor: help;
  border-bottom: 1px dotted color-mix(in srgb, var(--text3) 55%, transparent);
}

/* Aster on its own publishes neither equity nor roi, so both columns come out
   and the row re-flows onto the remaining eight tracks. This is the ONLY
   reduced set the two venues can produce — Aster is missing exactly this pair. */
.vsc-lite .ceq, .vsc-lite .croi { display: none; }
.vsc-lite .vsc-br {
  grid-template-columns:
    26px 30px minmax(168px, 1.7fr)
    minmax(110px, 1.3fr) minmax(120px, 1.3fr)
    minmax(116px, 1.25fr) minmax(96px, 1fr)
    minmax(92px, 1.15fr);
}
/* an unavailable window/venue still shows, so its absence is legible */
.vpage-chip[disabled] { opacity: 0.38; cursor: not-allowed; }
.vpage-chip[disabled]:hover { color: var(--text3); }
.vsc-star[disabled] { opacity: 0.2; cursor: not-allowed; }

/* a window only SOME selected DEXs run a board for: still clickable, but
   marked, because an empty column with no explanation reads as a bug */
.vpage-chip.partial { padding-right: 6px; }
/* A drawn circle around a 12px letter — the type floor forbids going smaller —
   left the "i" touching its own border and reading as a crude bullet. It is a
   real icon now, so it is sized like one and owes the floor nothing.
   Muted rather than gold: gold is the warning colour in this palette, and this
   marker means "there is a note", not "something is wrong". */
.vsc-chipinfo {
  display: inline-flex; align-items: center; justify-content: center;
  width: 12px; height: 12px; margin-left: 5px; flex: none;
  color: var(--text3); opacity: 0.75; cursor: help;
  transition: color 120ms var(--ease-out), opacity 120ms var(--ease-out);
}
.vsc-chipinfo > svg { width: 12px; height: 12px; stroke-width: 2.25; }
.vpage-chip:hover .vsc-chipinfo, .vsc-pw:hover .vsc-chipinfo { color: var(--teal); opacity: 1; }
.vpage-chip.on .vsc-chipinfo { color: currentColor; opacity: 0.8; }

/* These sit in 74–130px numeric tracks. Without a hard clip a long reason
   spills over the neighbouring columns instead of truncating — the cell must
   fade to an ellipsis and keep the full sentence in its title. */
.vsc-r .num > .vsc-nodata, .vsc-r .num .vsc-nodata {
  display: inline-block; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  vertical-align: bottom;
}
.vsc-r .num { overflow: hidden; }

/* ENS identity — the name a wallet chose for itself, where the venue gave none */
.vsc-id { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.vsc-ensav {
  width: 16px; height: 16px; border-radius: 50%; flex: none;
  background: var(--bg4); object-fit: cover;
}
.vsc-ensname {
  color: var(--text); font-weight: 600; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* scoped: a bare <img> inside the row picked up no intrinsic box from the
   generic rule, so it loaded correctly and laid out at 0×0 */
.vsc-r .vsc-id .vsc-ensav, .vsc-id > .vsc-ensav {
  width: 16px; height: 16px; min-width: 16px; min-height: 16px;
  border-radius: 50%; flex: none; object-fit: cover; display: inline-block;
}

/* ══ Scanner density ══════════════════════════════════════════════════════
   Measured 2026-07-30: 477px of a 969px viewport was chrome before the first
   row — 49%, the very number the original audit flagged, crept back by the
   venue picker and a second control row. The filter bar had wrapped to two
   rows at 81px.

   Every rule is scoped to .vsc so the Dashboard, Chart desk and Gallery keep
   the proportions they already have. Nothing here changes a type token: the
   12px floor and the coarse-pointer tap targets are untouched, and the height
   comes out of PADDING and GAPS, which is where the slack actually was. */

/* one row, tighter rhythm */
.vsc .vsc-bar { gap: var(--sp-2); margin-bottom: var(--sp-2); }
.vsc .vgal-chips { gap: 4px; }
.vsc .vpage-chip { padding: 4px 10px; line-height: 1.3; }
.vsc .vsc-search { height: 28px; padding: 0 11px; flex: 1 1 220px; max-width: 340px; }
.vsc .vsc-srcbtn, .vsc .vsc-coinbtn { padding: 4px 9px 4px 7px; }
.vsc .vsc-fl { margin: 0 2px 0 6px; }

/* view tabs: the underline needs less room than a button does */
.vsc .vsc-views { margin-top: var(--sp-3); }
.vsc .vsc-view { padding: 6px 11px 7px; }

/* summary cards — the label/value/sub stack had 16px of padding around a
   three-line block, which is a card's worth of air for a number */
/* secondary to the exposure strip: tighter, quieter, and sitting close enough
   to read as its supporting row rather than as a second competing band */
.vsc .vsc-agg { gap: var(--sp-2); margin: var(--sp-2) 0 var(--sp-2); }
.vsc .vsc-card { padding: 9px 12px 8px; background: color-mix(in srgb, var(--bg3) 40%, transparent); }
.vsc .vsc-card .v { font-size: var(--fs-title); }
.vsc .vsc-card label { margin-bottom: 3px; }
.vsc .vsc-card .s { margin-top: 2px; line-height: 1.35; }

/* the lede and its disclosure sit closer to the title */
.vsc .vsc-head { margin-bottom: var(--sp-2); }
.vsc .vsc-intro { margin-top: 4px; }
.vsc .vsc-lead { line-height: 1.45; }

/* rows: 50px per row meant ~13 rows a screen; the sub-line does not need
   7px above AND below the name it belongs to */
.vsc .vsc-scroll .vsc-r { padding-top: 5px; padding-bottom: 5px; }
.vsc .vsc-r .who { line-height: 1.3; }

/* ── Collateral view ──────────────────────────────────────────────────────
   Segregation by collateralization. The rail is the segmentation itself made
   operable: a share bar whose segments are buttons that filter the table.

   The ramp runs hot → neutral and carries NO GREEN. Green means pnl-positive
   everywhere else on this page, and a green "safe collateral" band sitting on
   the same screen as `open pnl` would be two different claims in one colour.
   Bands are named by their RANGE in the UI; the adjective lives in the legend
   and the aria-label, because a range is a fact and "critical" in a data cell
   is an editorial claim. */
.vsc-cz { margin: var(--sp-2) 0 10px; }
.vsc-czhead {
  display: flex; align-items: baseline; gap: 10px;
  flex-wrap: wrap; margin-bottom: 6px;
}
.vsc-czhead label {
  font-size: var(--fs-meta); color: var(--text3);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.vsc-czhead b { font-size: var(--fs-title); color: var(--text); }
.vsc-czhead .vsc-hint { font-size: var(--fs-meta); color: var(--text3); }

.vsc-czbar {
  display: flex; width: 100%; height: 26px; gap: 2px;
  border-radius: 3px; overflow: hidden;
}
.vsc-czseg {
  border: 0; padding: 0; min-width: 0; cursor: pointer;
  font: inherit; font-size: 11px; color: #0b0a12;
  transition: filter 120ms ease, opacity 120ms ease;
  overflow: hidden; white-space: nowrap;
}
.vsc-czseg:hover { filter: brightness(1.15); }
.vsc-czseg.on { outline: 1px solid var(--text); outline-offset: -1px; }
/* dimmed, not hidden: the unselected bands still carry the shape of the cohort */
.vsc-czbar:has(.on) .vsc-czseg:not(.on) { opacity: 0.4; }
.vsc-czseg.b0 { background: var(--red); }
.vsc-czseg.b1 { background: color-mix(in srgb, var(--red) 55%, var(--gold)); }
.vsc-czseg.b2 { background: var(--gold); }
.vsc-czseg.b3 { background: color-mix(in srgb, var(--gold) 40%, var(--text3)); }
.vsc-czseg.b4 { background: var(--text3); }

.vsc-czkey {
  display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 7px;
  align-items: center;
}
.vsc-czkey .k {
  display: inline-flex; align-items: center; gap: 5px;
  background: none; border: 0; padding: 2px 0; cursor: pointer;
  font: inherit; font-size: var(--fs-meta); color: var(--text2);
}
.vsc-czkey .k i {
  width: 9px; height: 9px; border-radius: 2px; flex: none;
}
.vsc-czkey .k.b0 i { background: var(--red); }
.vsc-czkey .k.b1 i { background: color-mix(in srgb, var(--red) 55%, var(--gold)); }
.vsc-czkey .k.b2 i { background: var(--gold); }
.vsc-czkey .k.b3 i { background: color-mix(in srgb, var(--gold) 40%, var(--text3)); }
.vsc-czkey .k.b4 i { background: var(--text3); }
.vsc-czkey .k em { font-style: normal; color: var(--text3); }
.vsc-czkey .k b { color: var(--text); font-weight: 600; }
.vsc-czkey .k span { color: var(--text3); }
.vsc-czkey .k.on { color: var(--text); }
.vsc-czkey .k.on b { text-decoration: underline; text-underline-offset: 2px; }
.vsc-czclear {
  background: none; border: 0; cursor: pointer; font: inherit;
  font-size: var(--fs-meta); color: var(--teal); padding: 2px 0;
}

/* the row: one ACCOUNT, not one position */
.vsc-cz2 {
  grid-template-columns:
    30px minmax(128px, 1.4fr) minmax(112px, 1.15fr) minmax(122px, 1.25fr)
    minmax(82px, 0.85fr) minmax(104px, 1fr) minmax(100px, 1fr) minmax(108px, 1.05fr);
}
.vsc-cz2 .czb { font-weight: 600; }
.vsc-cz2 .czb.b0 { color: var(--red); }
.vsc-cz2 .czb.b1 { color: color-mix(in srgb, var(--red) 55%, var(--gold)); }
.vsc-cz2 .czb.b2 { color: var(--gold); }
.vsc-cz2 .czb.b3 { color: var(--text2); }
.vsc-cz2 .czb.b4 { color: var(--text3); }
/* a hairline, not a filled meter — the number is the subject, this is its echo */
.vsc-czmini {
  display: block; width: 100%; max-width: 74px; height: 2px;
  margin: 3px 0 0 auto; background: var(--border2); border-radius: 1px;
}
.vsc-czmini i { display: block; height: 100%; border-radius: 1px; }
.vsc-czmini i.b0 { background: var(--red); }
.vsc-czmini i.b1 { background: color-mix(in srgb, var(--red) 55%, var(--gold)); }
.vsc-czmini i.b2 { background: var(--gold); }
.vsc-czmini i.b3 { background: color-mix(in srgb, var(--gold) 40%, var(--text3)); }
.vsc-czmini i.b4 { background: var(--text3); }

/* Peel in value order. Buffer and the trader survive to the narrowest width:
   without those two the row has stopped being a statement about collateral. */
@media (max-width: 1180px) {
  .vsc-cz2 {
    grid-template-columns:
      30px minmax(120px, 1.4fr) minmax(108px, 1.15fr) minmax(118px, 1.25fr)
      minmax(80px, 0.85fr) minmax(100px, 1fr) minmax(104px, 1.05fr);
  }
  .vsc-cz2 > .cutil { display: none; }
}
@media (max-width: 1040px) {
  .vsc-cz2 {
    grid-template-columns:
      28px minmax(112px, 1.4fr) minmax(104px, 1.15fr) minmax(112px, 1.2fr)
      minmax(78px, 0.85fr) minmax(98px, 1fr);
  }
  .vsc-cz2 > .ceq { display: none; }
}
@media (max-width: 880px) {
  .vsc-cz2 {
    grid-template-columns:
      minmax(104px, 1.4fr) minmax(96px, 1.15fr) minmax(104px, 1.2fr) minmax(76px, 0.85fr);
  }
  .vsc-cz2 > .rk, .vsc-cz2 > .cntl { display: none; }
}
@media (max-width: 620px) {
  .vsc-cz2 { grid-template-columns: minmax(96px, 1.3fr) minmax(88px, 1fr) minmax(96px, 1.1fr); }
  .vsc-cz2 > .clev { display: none; }
  .vsc-czkey { gap: 3px 10px; }
}
@media (max-width: 460px) {
  .vsc-cz2 { grid-template-columns: minmax(90px, 1.3fr) minmax(84px, 1fr); }
  .vsc-cz2 > .cnear { display: none; }
  .vsc-czhead .vsc-hint { display: none; }
  .vsc-czbar { height: 22px; }
}

/* ── narrow: the summary is a rail, not a stack ───────────────────────────
   Both rows are `repeat(auto-fit, minmax(196px, 1fr))`, so under ~460px of
   content only one track fits and four exposure blocks plus five cards become
   927px of vertical stack — nearly two phone screens of chrome before the
   first row of data. Swiping a rail is the honest trade: every block is still
   reachable, none of them is a scroll obstacle.

   `:not([hidden])` matters. The `[hidden] { display: none }` rules sit EARLIER
   in this file, so a bare `display: flex` here would out-cascade them and
   un-hide the card row on the Tracked view. */
@media (max-width: 560px) {
  .vsc-expo:not([hidden]):not(:empty),
  .vsc-agg:not([hidden]):not(:empty) {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .vsc-expo::-webkit-scrollbar,
  .vsc-agg::-webkit-scrollbar { display: none; }
  /* 78% leaves a deliberate sliver of the next block visible — the affordance
     that says "this scrolls" without adding a control to say it */
  .vsc-expo > *, .vsc-agg > * { flex: 0 0 78%; scroll-snap-align: start; }
}
/* The depth chips wrap at this width and orphan `1000` onto a line of its own.
   They cannot simply be `nowrap` — a previous pass tried exactly that and the
   six chips pinned to one 357px line pushed the document sideways at 360px.
   A scroller keeps them on one line AND keeps the page from widening. */
@media (max-width: 560px) {
  .vsc-gbar .vsc-sizes {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    max-width: 100%;
  }
  .vsc-gbar .vsc-sizes::-webkit-scrollbar { display: none; }
}


/* ── Portfolio (#/portfolio) — SPOT holdings ──────────────────────────────
   This page REUSES the section chrome the Scanner established rather than
   restating it: .vsc-title / .vsc-live / .vsc-intro / .vsc-lead / .vsc-method
   for the header, .vsc-views / .vsc-view for the underline tabs, .vsc-gbar +
   .vsc-cluster for the control bar, .vsc-scroll for the table surface, and
   .hub-btn for every button. Those selectors are top-level and section-neutral;
   the `vsc-` prefix is historical, not ownership. Two sections that answer
   "what do I own" and "what do they own" must not disagree about what a tab is.

   What is genuinely portfolio-specific lives under .vpf-*, and matches the
   Scanner's shipped metrics exactly where it parallels one: rows are 7px /
   var(--sp-3) with a 55%-opacity bottom hairline and --fs-data in --text2, the
   same as .vsc-r; cards carry the compact `color-mix(--bg3 40%)` surface that
   `.vsc .vsc-card` uses.

   Every size is a token. All class names grepped against hub.css AND vice.css
   before minting (the .vcd / .top collision precedent). */
.vpf { display: flex; flex-direction: column; }

/* ── the summary ──────────────────────────────────────────────────────────
   ONE panel, not five boxes: the headline figure on the left, the supporting
   stats as a content-sized tape on the right, divided by hairlines.

   Why not a grid of cards: a stretched grid inflates its tracks with the
   viewport, and a card whose label and figure sit at opposite ends then grows
   a hole between them — measured at 140px at 1920px, and widening. A stat is a
   label bound to a figure; the two have to stay adjacent at every width. Here
   the pair is stacked, and surplus width becomes trailing whitespace. */
.vpf-stats:not([hidden]) {
  display: flex; flex-wrap: wrap; align-items: stretch;
  /* Sized to its content, not to the viewport. Stretched full-width it left
     767px of empty panel to the right of the last stat at 1920px — an
     unfinished-looking half-bar. A summary is a fixed amount of information;
     it should occupy a fixed amount of space and let the page grow around it. */
  width: fit-content; max-width: 100%;
  margin: var(--sp-4) 0 0;
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--bg2); overflow: hidden;
}
.vpf-hero {
  display: flex; flex-direction: column; justify-content: center; gap: 3px;
  padding: var(--sp-4) var(--sp-5) var(--sp-4) var(--sp-4);
  min-width: 0; flex: 0 1 auto;
  border-right: 1px solid var(--border);
  background: linear-gradient(150deg, color-mix(in srgb, var(--teal) 8%, transparent) 0%, transparent 70%);
}
.vpf-hero > label {
  display: flex; align-items: center; gap: 6px;
  color: var(--text3); font-size: var(--fs-meta); font-weight: 500;
  letter-spacing: 0.1em; text-transform: uppercase;
}
.vpf-hero > label i, .vpf-hero > label svg {
  width: 12px; height: 12px; color: var(--teal); opacity: 0.85; stroke-width: 2;
}
.vpf-net {
  color: var(--text); font-size: var(--fs-display); font-weight: 700;
  font-variant-numeric: tabular-nums; line-height: 1.08; letter-spacing: -0.025em;
  overflow: hidden; text-overflow: ellipsis;
}
/* an unpriced or still-loading total is a WORD, not a number — it must not
   wear the display scale as if it were one */
.vpf-net.vpf-nonum { font-size: var(--fs-h2); color: var(--text2); font-weight: 600; }

/* the tape: content-sized stats, hairline-divided, wrapping rather than
   inflating — the .vsc-vs idiom from the Scanner's venue strip */
.vpf-tape {
  display: flex; flex-wrap: wrap; align-items: center;
  flex: 1 1 auto; min-width: 0; padding: 0;
}
.vpf-ts {
  display: flex; flex-direction: column; gap: 2px; min-width: 0;
  padding: var(--sp-3) var(--sp-5);
  border-right: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}
.vpf-ts:last-child { border-right: 0; }
.vpf-ts > label {
  display: flex; align-items: center; gap: 5px;
  color: var(--text3); font-size: var(--fs-meta); font-weight: 500;
  letter-spacing: 0.1em; text-transform: uppercase; white-space: nowrap;
}
.vpf-ts > label i, .vpf-ts > label svg {
  width: 11px; height: 11px; flex: none; color: var(--text3); opacity: 0.8; stroke-width: 2;
}
.vpf-ts > b {
  color: var(--text); font-size: var(--fs-h3); font-weight: 700;
  font-variant-numeric: tabular-nums; letter-spacing: -0.02em; white-space: nowrap;
}
.vpf-ts.up > b { color: var(--green); }
.vpf-ts.down > b { color: var(--red2); }
.vpf-tsub { color: var(--text3); font-size: var(--fs-meta); white-space: nowrap; }
.vpf-ks { color: var(--text3); font-size: var(--fs-meta); line-height: 1.45; }
.vpf-warn { color: var(--gold); }

/* ── control bar ─────────────────────────────────────────────────────────
   .vsc-gbar supplies the faint panel; only the portfolio-specific bits here. */
.vpf-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--text3); font-size: var(--fs-ui); cursor: pointer; white-space: nowrap;
}
.vpf-toggle input { accent-color: var(--teal); }
.vpf-fresh {
  color: var(--text3); font-size: var(--fs-meta);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* ── holdings table — .vsc-r's metrics, portfolio's columns ─────────────── */
.vpf-tbl { border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--bg2); }
.vpf-hr, .vpf-r {
  display: grid; align-items: center; gap: 10px;
  grid-template-columns:
    minmax(150px, 1.75fr) minmax(80px, 0.95fr) minmax(92px, 1fr)
    minmax(88px, 0.95fr) minmax(100px, 1.05fr) minmax(72px, 0.75fr)
    minmax(100px, 1.05fr) 34px;
  padding: 7px var(--sp-3);
}
/* Row-wide link to the Chart desk. Per design §5 it is a SIBLING overlay, not
   a wrapper: the row must not become an interactive containing the edit button
   (invalid ARIA + keyboard trap). It claims no grid track, and the real cells
   sit above it and still take clicks that bubble to the row's delegation. */
.vpf-r { position: relative; }
.vpf-open {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; margin: 0; padding: 0;
  background: none; border: 0; color: transparent; font: inherit;
  cursor: pointer; appearance: none; overflow: hidden;
}
.vpf-open:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; border-radius: var(--radius); }
.vpf-r > .vpf-c { position: relative; z-index: 1; }
/* the edit button must out-stack the overlay on touch or the overlay eats it */
.vpf-acts { z-index: 2; }
/* footing: a rule above it, not a fill — it is a summary of the rows, not
   another row of data competing with them */
.vpf-total {
  border-top: 1px solid var(--border2); border-bottom: 0;
  background: color-mix(in srgb, var(--bg3) 30%, transparent);
  color: var(--text3);
}
.vpf-total:hover { background: color-mix(in srgb, var(--bg3) 30%, transparent); }
.vpf-total .vpf-c > b { color: var(--text2); }
.vpf-total .vpf-c.up > b { color: var(--green); }
.vpf-total .vpf-c.down > b { color: var(--red2); }
.vpf-total .vpf-c.l > b {
  color: var(--text3); font-size: var(--fs-meta); font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase;
}
/* an exception flag earns a colour; the default source is simply not printed */
.vpf-flag { color: var(--gold); opacity: 0.85; }
.vpf-hr {
  border-bottom: 1px solid var(--border);
  background: color-mix(in srgb, var(--bg3) 55%, transparent);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.vpf-r {
  border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
  color: var(--text2); font-size: var(--fs-data);
}
.vpf-r:last-child { border-bottom: 0; border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
.vpf-r:hover { background: color-mix(in srgb, var(--bg3) 45%, transparent); }
.vpf-hc {
  display: inline-flex; align-items: center; gap: 4px;
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--text3); font: 600 var(--fs-data)/1.2 var(--mono);
  text-transform: uppercase; letter-spacing: 0.08em;
  transition: color 130ms var(--ease-out);
}
.vpf-hc:hover { color: var(--text2); }
.vpf-hc.on { color: var(--teal); }
.vpf-hc i, .vpf-hc svg { width: 11px; height: 11px; stroke-width: 2; opacity: 0.75; }
.vpf-hc.r, .vpf-c.r { justify-content: flex-end; text-align: right; }
.vpf-c { display: flex; flex-direction: column; gap: 1px; min-width: 0; font-variant-numeric: tabular-nums; }
.vpf-c > b { color: var(--text); font-weight: 600; }
.vpf-c.up > b { color: var(--green); }
.vpf-c.down > b { color: var(--red2); }

/* asset cell: icon · symbol over name — the one identity column */
.vpf-asset { flex-direction: row; align-items: center; gap: var(--sp-2); }
.vpf-an { display: flex; flex-direction: column; gap: 0; min-width: 0; }
.vpf-an > b { color: var(--text); font-size: var(--fs-data); font-weight: 700; letter-spacing: 0.01em; }
.vpf-sub {
  color: var(--text3); font-size: var(--fs-meta); line-height: 1.35;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vpf-none { color: var(--text3); font-size: var(--fs-meta); font-style: italic; }
/* provenance marks: hairline glyphs, never filled badges */
.vpf-src { width: 12px; height: 12px; color: var(--text3); flex: none; opacity: 0.8; }
.vpf-src.warn { color: var(--gold); opacity: 1; }
/* weight: a proportion, so a 3px hairline — not a filled bar (design §3) */
.vpf-wbar {
  display: block; height: 3px; width: 100%; margin-top: 3px;
  border-radius: 2px; background: color-mix(in srgb, var(--border2) 70%, transparent);
  overflow: hidden;
}
.vpf-wbar::after { content: ''; display: block; height: 100%; width: var(--w, 0%); background: var(--teal); opacity: 0.75; }
.vpf-acts { flex-direction: row; justify-content: flex-end; }
.vpf-mini {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; background: transparent;
  border: 0; border-radius: var(--radius-sm);
  color: var(--text3); cursor: pointer;
  transition: color 130ms var(--ease-out), background 130ms var(--ease-out);
}
.vpf-mini:hover { color: var(--text); background: var(--bg3); }
.vpf-mini.danger:hover { color: var(--red2); }
.vpf-mini:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
.vpf-mini i, .vpf-mini svg { width: 13px; height: 13px; }
.vpf-foot {
  margin: var(--sp-3) 0 0; color: var(--text3);
  font-size: var(--fs-meta); line-height: var(--lh-prose); max-width: var(--measure-prose);
}

/* ── empty / loading states ─────────────────────────────────────────────── */
.vpf-empty {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
  padding: var(--sp-7) var(--sp-4); text-align: center;
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--bg2);
}
.vpf-empty > i, .vpf-empty > svg { width: 22px; height: 22px; color: var(--text3); stroke-width: 1.5; }
.vpf-eh { margin: 0; color: var(--text); font-size: var(--fs-title); font-weight: 600; }
.vpf-ep { margin: 0; color: var(--text2); font-size: var(--fs-data); line-height: var(--lh-prose); max-width: 54ch; }
.vpf-eacts { display: flex; gap: var(--sp-2); margin-top: var(--sp-3); flex-wrap: wrap; justify-content: center; }
.vpf-empty .hub-btn { padding: 7px 13px; font-size: var(--fs-ui); }
.vpf-empty .hub-btn:not(.primary) { border: 1px solid var(--border2); }
@keyframes vpfSpin { to { transform: rotate(360deg); } }
.vpf-empty > i.vpf-spin, .vpf-empty > svg.vpf-spin { animation: vpfSpin 1.4s linear infinite; color: var(--teal); }

/* ── allocation ─────────────────────────────────────────────────────────── */
.vpf-mix {
  display: grid; grid-template-columns: minmax(220px, 0.85fr) minmax(240px, 1fr);
  gap: var(--sp-5); align-items: center;
  padding: var(--sp-4); border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--bg2);
}
/* chartMount's .vchart is position:absolute; inset:0 — its box MUST establish
   a containing block or the canvas escapes and sizes to the document */
.vpf-mixchart { position: relative; height: 288px; min-width: 0; }
.vpf-legend { display: flex; flex-direction: column; }
.vpf-lg {
  display: grid; grid-template-columns: 8px minmax(52px, auto) 1fr auto;
  gap: var(--sp-2); align-items: center;
  padding: 5px 0; border-bottom: 1px solid color-mix(in srgb, var(--border) 45%, transparent);
  font-size: var(--fs-data); font-variant-numeric: tabular-nums;
}
.vpf-lg:last-child { border-bottom: 0; }
.vpf-lg i { width: 8px; height: 8px; border-radius: 2px; }
.vpf-lg b { color: var(--text); font-weight: 700; }
.vpf-lg > span:nth-of-type(1) { color: var(--text2); text-align: right; }
.vpf-lg > span:nth-of-type(2) { color: var(--text3); font-size: var(--fs-meta); min-width: 44px; text-align: right; }

/* ── curve ──────────────────────────────────────────────────────────────── */
.vpf-cwrap { border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--bg2); padding: var(--sp-3); }
.vpf-chips { display: flex; gap: var(--sp-1); margin-bottom: var(--sp-2); }
.vpf-chip {
  padding: 4px 11px; background: transparent;
  border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text3); font: 500 var(--fs-data)/1.35 var(--mono); cursor: pointer;
  transition: color 130ms var(--ease-out), border-color 130ms var(--ease-out);
}
.vpf-chip:hover { color: var(--text2); border-color: var(--border2); }
.vpf-chip.on { color: var(--teal); border-color: var(--teal); background: color-mix(in srgb, var(--teal) 9%, transparent); }
.vpf-curve { position: relative; height: 320px; }
.vpf-clegend {
  display: flex; flex-direction: column; gap: 5px;
  margin-top: var(--sp-3); padding-top: var(--sp-3);
  border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
}
.vpf-lgi { display: block; color: var(--text3); font-size: var(--fs-meta); line-height: var(--lh-prose); max-width: var(--measure-prose); }
.vpf-lgi b { color: var(--text2); }
.vpf-swatch { display: inline-block; width: 16px; height: 0; margin-right: 7px; vertical-align: middle; }
.vpf-swatch.solid { border-top: 2px solid var(--teal); }
.vpf-swatch.dash { border-top: 2px dashed var(--purple); }

/* ── dialogs ────────────────────────────────────────────────────────────── */
.vpf-form { display: flex; flex-direction: column; gap: var(--sp-3); min-width: min(468px, 78vw); }
.vpf-f { display: flex; flex-direction: column; gap: 4px; }
.vpf-f.grow { flex: 1 1 220px; }
.vpf-f > span {
  color: var(--text3); font-size: var(--fs-meta); font-weight: 500;
  letter-spacing: 0.1em; text-transform: uppercase;
}
.vpf-frow { display: flex; gap: var(--sp-3); flex-wrap: wrap; }
.vpf-frow > .vpf-f { flex: 1 1 156px; }
.vpf-in {
  width: 100%; padding: 7px 10px; background: var(--bg);
  border: 1px solid var(--border2); border-radius: var(--radius);
  color: var(--text); font: 400 var(--fs-ui)/1.4 var(--mono);
  transition: border-color 130ms var(--ease-out);
}
.vpf-in:hover { border-color: var(--border3); }
.vpf-in:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; border-color: var(--teal); }
.vpf-code { resize: vertical; word-break: break-all; font-size: var(--fs-meta); line-height: 1.5; }
.vpf-hint { margin: 0; color: var(--text3); font-size: var(--fs-meta); line-height: var(--lh-prose); }
.vpf-results { display: flex; flex-direction: column; max-height: 232px; overflow-y: auto; }
.vpf-res {
  display: grid; grid-template-columns: 18px auto 1fr auto; gap: var(--sp-2);
  align-items: center; padding: 6px 8px; background: transparent;
  border: 0; border-radius: var(--radius);
  color: var(--text2); font: 400 var(--fs-data)/1.35 var(--mono); cursor: pointer; text-align: left;
}
.vpf-res:hover { background: var(--bg3); color: var(--text); }
.vpf-res:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }
.vpf-res b { color: var(--text); font-weight: 700; }
.vpf-rank { color: var(--text3); font-size: var(--fs-meta); }
.vpf-picked:not([hidden]) {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 7px 10px; background: color-mix(in srgb, var(--teal) 8%, transparent);
  border: 1px solid var(--teal); border-radius: var(--radius); font-size: var(--fs-data);
}
.vpf-picked b { color: var(--text); font-weight: 700; }
.vpf-picked .vpf-mini { margin-left: auto; }
.vpf-lots, .vpf-wlist { display: flex; flex-direction: column; gap: var(--sp-2); }
.vpf-lot { display: grid; grid-template-columns: auto 1fr 1fr auto; gap: var(--sp-2); align-items: end; }
.vpf-w {
  display: grid; grid-template-columns: minmax(80px, auto) auto 1fr auto;
  gap: var(--sp-2); align-items: center; padding: 7px 10px;
  background: color-mix(in srgb, var(--bg3) 45%, transparent);
  border: 1px solid var(--border); border-radius: var(--radius);
  font-size: var(--fs-data);
}
.vpf-w > b { color: var(--text); font-weight: 600; }
.vpf-addr { color: var(--text2); font-size: var(--fs-meta); }

/* ── narrow ───────────────────────────────────────────────────────────────
   Seven tracks at their stated minimums need ~700px, so below 900px the row
   becomes a labelled two-column card. The labels move INTO the cells rather
   than being dropped — an unlabelled number is worse than a taller row. */
@media (max-width: 900px) {
  .vpf-hero { flex: 1 1 100%; border-right: 0; border-bottom: 1px solid var(--border); }
  .vpf-mix { grid-template-columns: 1fr; gap: var(--sp-3); }
  .vpf-hr { display: none; }
  .vpf-r {
    grid-template-columns: 1fr 1fr; gap: var(--sp-2) var(--sp-3);
    padding: var(--sp-3); align-items: start;
  }
  .vpf-r .vpf-c.l { grid-column: 1 / -1; }
  .vpf-r .vpf-c.r { align-items: flex-start; text-align: left; }
  .vpf-r .vpf-c.r::before {
    content: attr(data-l); color: var(--text3);
    font-size: var(--fs-meta); font-weight: 500;
    letter-spacing: 0.1em; text-transform: uppercase;
  }
  .vpf-acts { grid-column: 1 / -1; flex-direction: row; justify-content: flex-start; }
  .vpf-acts::before { content: none; }
}
@media (max-width: 560px) {
  /* Stats stacked one-per-row would put the first holding most of a screen
     down — the tape becomes a snap rail instead, the same treatment (and the
     same measured reason) as the Scanner's exposure strip. */
  .vpf-tape {
    flex-wrap: nowrap; overflow-x: auto;
    scroll-snap-type: x mandatory; scrollbar-width: none;
  }
  .vpf-tape::-webkit-scrollbar { display: none; }
  .vpf-ts { flex: 0 0 auto; scroll-snap-align: start; min-width: 42%; }
  .vpf-hero { flex: 1 1 100%; border-right: 0; border-bottom: 1px solid var(--border); }
  .vpf-form { min-width: 0; }
  .vpf-lot { grid-template-columns: 1fr 1fr auto; }
  .vpf-lot > .vpf-sub { grid-column: 1 / -1; }
  .vpf-net { font-size: var(--fs-h1); }
}

/* touch targets: grow the hit area, leave the drawn box alone (design §7).
   z-index 0, not -1 — same reason as the main slop block (audit F-TAP). */
@media (pointer: coarse), (max-width: 768px) {
  .vpf-mini, .vpf-chip { position: relative; }
  .vpf-mini::after, .vpf-chip::after {
    content: ''; position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: var(--tap-min); height: var(--tap-min);
    min-width: 100%; min-height: 100%; z-index: 0;
  }
  .vpf-mini > *, .vpf-chip > * { position: relative; z-index: 1; }
}

/* record-a-sale block inside the edit dialog */
.vpf-sellbox {
  display: flex; flex-direction: column; gap: var(--sp-2);
  margin-top: var(--sp-2); padding-top: var(--sp-3);
  border-top: 1px solid var(--border);
}
.vpf-selltitle { display: flex; align-items: center; gap: 6px; color: var(--text); font-size: var(--fs-data); }
.vpf-selltitle i, .vpf-selltitle svg { width: 13px; height: 13px; color: var(--teal); }
.vpf-sellbox .hub-btn { align-self: flex-start; border: 1px solid var(--border2); padding: 6px 11px; font-size: var(--fs-ui); }
.vpf-up { color: var(--green); }
.vpf-down { color: var(--red2); }

/* allocation risk read — concentration + dry powder, the two things a donut
   cannot say */
.vpf-risk {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
  gap: var(--sp-2); margin-top: var(--sp-3);
}
.vpf-rstat {
  display: flex; flex-direction: column; gap: 2px; min-width: 0;
  padding: 9px 12px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: color-mix(in srgb, var(--bg3) 40%, transparent);
}
.vpf-rl {
  color: var(--text3); font-size: var(--fs-meta); font-weight: 500;
  letter-spacing: 0.1em; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vpf-rstat > b {
  color: var(--text); font-size: var(--fs-title); font-weight: 700;
  font-variant-numeric: tabular-nums; letter-spacing: -0.01em;
}
.vpf-rstat > b.vpf-warn { color: var(--gold); }
.vpf-btnrow { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.vpf-btnrow .hub-btn { border: 1px solid var(--border2); padding: 6px 11px; font-size: var(--fs-ui); }
.vpf-ddrow { margin: 0 0 var(--sp-2); grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }

/* ══════════════════════════════════════════════════════════════════════════
   PHASE A — phone chrome and overlays (audit items 8 + 9)
   ══════════════════════════════════════════════════════════════════════════

   ITEM 8 · ONE 52px ROW.
   The bar was 13–28% of a phone screen (28.4% at 320px, 24.5% in Customize)
   and it wrapped to three or four ragged rows, stranding `.hub-div` hairlines
   at line ends and leaving a row that was 75% empty. A native iOS nav bar is
   44pt ≈ 7%. Target here: 52px, ≤9%, never wrapped, on every route AND in
   Customize.

   What leaves the bar on a phone:
     · the "Vice Hub" wordmark — decoration inside an app you already opened,
       and it cost an entire row;
     · the Academy link — an EXTERNAL destination the owner has descoped, and
       it is already in the ⋮ menu. Dropping it is also what makes C1's maths
       work: the audit sized C1 for FIVE sections, not six;
     · layout switcher · Add · Tidy · Customize · Focus — all into ⋮, which
       becomes a bottom sheet (it was a 596px dropdown running 54px past the
       fold);
     · every `.hub-div` — a divider only means something inside one unwrapped
       row, and there is now only one group per side.

   Section navigation follows the owner's answer to §6 Q1: the icon capsule
   stays on the row at ≥390px (C1), and below that the section title becomes a
   button opening a switcher sheet (C2). 390 is not arbitrary — it is the
   iPhone 12/13/14 width, and the first width where five 40px pills plus the
   flamingo, search and ⋮ fit inside 12px gutters. */
/* The height clause is why a 734×343 landscape phone is included: it is 734
   WIDE, so a width-only rule handed it the desktop bar — 67px on a 343px-tall
   screen, and 104px WRAPPED once you opened Customize (30.3% of everything the
   user can see). The 950px cap keeps a short-but-wide DESKTOP window out of the
   phone layout; `.vsheet` deliberately has no such cap, because a bottom sheet
   on a short desktop window is fine and a hidden layout switcher is not. */
@media (max-width: 640px), (max-height: 500px) and (max-width: 950px) {
  .hub-bar {
    flex-wrap: nowrap; gap: 4px;
    min-height: 52px; padding: 0 12px;
    padding-top: max(0px, env(safe-area-inset-top));
    margin: 0 -12px 10px;
  }
  .hub-bar-title { display: none; }
  .hub-bar .hub-div { display: none; }
  #hub-layout-wrap, #hub-add, #hub-tidy, #hub-edit, #hub-focus { display: none; }
  /* Academy is external and already in ⋮; the capsule is for THIS app */
  #hub-nav a:not([data-sec]) { display: none; }
  /* Five 36px pills + the flamingo + search + ⋮ measure 312px inside 12px
     gutters, so 390px leaves ~78px of slack and 430px leaves ~118px. The
     drawn pill stays small on purpose — `min-height: var(--tap-min)` in the
     coarse block already gives the ROW its 44px, which is where the audit
     put the floor (§4.3: grow the hit area, leave the box alone). */
  .hub-nav { margin-left: 6px; padding: 2px; gap: 1px; min-width: 0; }
  .hub-nav a { padding: 6px 11px; }
  .hub-brand .vt-flamingo { width: 22px; height: 22px; }

  /* Customize is still a canvas mode here (item 12 replaces it): the bar
     swaps navigation for the one control that gets you out. Same 52px. */
  body.editing .hub-nav, body.editing #hub-secbtn, body.editing #hub-pal { display: none; }
  body.editing #hub-edit, body.editing #hub-add { display: inline-flex; }
  body.editing #hub-edit span, body.editing #hub-add span { display: inline; }
  body.editing #hub-edit { font-size: var(--fs-meta); }
}

/* On a 568px-tall iPhone SE, 52px IS 9.2% — the row height and the ≤9% budget
   are not simultaneously satisfiable there. 48px still clears a 44px control
   with room, and takes the shortest phone in the sweep to 8.5%. */
@media (max-width: 640px) and (max-height: 600px), (max-height: 420px) and (max-width: 950px) {
  .hub-bar { min-height: 48px; }
}

/* TABLETS — not a tablet breakpoint set (audit §4.8 argues against one), just
   the same two pieces of decoration removed. A 712×1138 Galaxy Tab ran the
   dashboard bar to 104px across TWO rows with two dividers stranded at the
   line end: with coarse padding applied, 12 controls plus a 79px wordmark do
   not fit 712px. The wordmark is decoration inside an app the user already
   opened, and a divider only means something inside one unwrapped row. */
@media (max-width: 900px) {
  .hub-bar-title { display: none; }
  .hub-bar .hub-div { display: none; }
}

/* C1 ≥390px: the capsule rides the row, the title button stands down.
   C2 ≤389px: the reverse. Written as two explicit arms rather than one
   `display:none` default so neither can leak onto a desktop. */
#hub-secbtn { display: none; }
@media (max-width: 389px) {
  .hub-bar #hub-nav { display: none; }
  .hub-bar #hub-secbtn { display: inline-flex; }
}
.hub-secbtn {
  align-items: center; gap: 7px; flex: 0 1 auto; min-width: 0;
  background: none; border: 0; border-radius: var(--radius);
  color: var(--text); font: 600 var(--fs-ui)/1.2 var(--mono);
  padding: 6px 8px; margin-left: 4px; cursor: pointer;
  transition: background 140ms var(--ease-out), transform 140ms var(--ease-out);
}
.hub-secbtn .hub-secbtn-l { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hub-secbtn .hub-secbtn-i { display: inline-flex; flex: none; color: var(--teal); }
.hub-secbtn i, .hub-secbtn svg { width: 14px; height: 14px; }
.hub-secbtn .chev { width: 12px; height: 12px; flex: none; color: var(--text3); }
.hub-secbtn:active { transform: scale(0.97); background: var(--bg3); }
.hub-secbtn:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
@media (pointer: coarse) { .hub-secbtn { min-height: var(--tap-min); } }

/* F6 — `⌘K` is meaningless on a device with no ⌘ key. The width rule at ≤860
   already hid it; this makes it a property of the INPUT, not of the screen,
   so a 1024px tablet stops advertising a shortcut it cannot type either. */
@media (pointer: coarse) { .hub-btn .kbd { display: none; } }

/* ══ ITEM 9 · BOTTOM SHEETS — one overlay grammar on phones ═══════════════
   `.vsheet` is the primitive: `.hub-modal`, `.hub-menu` and the new switcher
   all become one of these below 640px wide OR 500px tall. The height clause is
   the fix for a 734×343 landscape phone, which is 734 WIDE and so kept getting
   desktop side-drawers on a 343px-tall screen (audit §3.6).

   `position: fixed` here works only because sheet() mounts to document.body:
   `.hub-bar` has `backdrop-filter`, which makes it a containing block for
   fixed descendants, so a sheet left inside `.hub-menu-wrap` would anchor to
   the toolbar. */
.vsheet-veil {
  position: fixed; inset: 0; z-index: var(--z-modal);
  background: rgba(4, 3, 9, 0.62);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  display: flex; align-items: flex-end; justify-content: center;
  animation: veilIn 160ms ease both;
  overscroll-behavior: contain;
}
.vsheet-veil.out { animation: veilOut 180ms ease both; }
@keyframes veilOut { to { opacity: 0; } }
.vsheet {
  display: flex; flex-direction: column;
  width: 100%; max-width: 560px; max-height: min(86dvh, 720px);
  padding-bottom: max(10px, env(safe-area-inset-bottom));
  background: var(--bg2);
  border: 1px solid var(--border2); border-bottom: 0;
  border-radius: 18px 18px 0 0;
  box-shadow: 0 -18px 60px rgba(0, 0, 0, 0.6);
  /* iOS drawer curve; transitions (not keyframes) so a drag can interrupt it */
  animation: vsheetIn 300ms var(--ease-drawer) both;
  transition: transform 260ms var(--ease-drawer);
  overscroll-behavior: contain;
}
@keyframes vsheetIn { from { transform: translateY(100%); } }
.vsheet-veil.out .vsheet { transform: translateY(100%); transition: transform 180ms var(--ease-out); }
/* while a finger owns it, the sheet tracks the finger with no easing at all */
.vsheet.drag { transition: none; }
.vsheet-grab {
  flex: none; width: 40px; height: 4px; margin: 9px auto 3px;
  border-radius: 2px; background: rgba(255, 255, 255, 0.2);
  touch-action: none;
}
.vsheet-head {
  flex: none; display: flex; align-items: center; gap: 9px;
  padding: 6px 14px 11px;
  color: var(--text); font-weight: 600; font-size: var(--fs-title);
  touch-action: none;
}
.vsheet-head > i, .vsheet-head > svg { width: 15px; height: 15px; color: var(--teal); flex: none; }
.vsheet-head .vsheet-t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vsheet-head .x { margin-left: auto; flex: none; }
.vsheet-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
  padding: 0 12px 4px;
}
/* primary full-width at the bottom, secondary as text ABOVE it — replaces the
   29px Add/Save/Cancel row the audit measured (F-PF-BTN) */
.vsheet-foot {
  flex: none; display: flex; flex-direction: column-reverse; gap: 6px;
  padding: 10px 12px 2px; border-top: 1px solid var(--border);
}
.vsheet-foot > * { width: 100%; justify-content: center; min-height: 48px; font-size: var(--fs-ui); }
.vsheet-foot > .hub-btn:not(.primary) { background: none; color: var(--text2); }
.vsheet-foot > .hub-btn:not(.primary):active { background: var(--bg3); }

/* list + action rows inside a sheet */
.vsheet-list { display: flex; flex-direction: column; padding-bottom: 6px; }
.vsheet-item {
  display: flex; align-items: center; gap: 11px; width: 100%;
  min-height: 52px; padding: 8px 10px;
  background: none; border: 0; border-radius: var(--radius);
  color: var(--text2); font: inherit; font-size: var(--fs-ui); text-align: left; cursor: pointer;
  transition: background 140ms var(--ease-out);
}
.vsheet-item > i, .vsheet-item > svg { width: 16px; height: 16px; flex: none; opacity: 0.8; }
.vsheet-il { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.vsheet-il b { color: var(--text); font-weight: 600; }
.vsheet-il em { font-style: normal; color: var(--text3); font-size: var(--fs-meta); }
.vsheet-item:active { background: var(--bg3); }
.vsheet-item.on { background: var(--bg4); }
.vsheet-item.on > i, .vsheet-item.on > svg { color: var(--teal); opacity: 1; }
.vsheet-item:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }
.vsheet-item.danger { color: var(--red); }
.vsheet-item.danger > i, .vsheet-item.danger > svg { color: var(--red); opacity: 1; }
.vsheet-item.danger b { color: var(--red); }
.vsheet-item.danger:active { background: color-mix(in srgb, var(--red) 12%, transparent); }
/* the four evicted bar actions, as a 2-up grid at the top of ⋮ */
.vsheet-acts { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding: 2px 0 8px; }
.vsheet-act {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 50px; padding: 8px 10px;
  background: var(--bg3); border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text); font: 600 var(--fs-ui)/1.2 var(--mono); cursor: pointer;
  transition: background 140ms var(--ease-out), transform 140ms var(--ease-out);
}
.vsheet-act > i, .vsheet-act > svg { width: 15px; height: 15px; flex: none; color: var(--teal); }
.vsheet-act:active { transform: scale(0.97); background: var(--bg4); }
.vsheet-act:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
/* a lone action has no partner to sit beside — it takes the row */
.vsheet-acts > :only-child { grid-column: 1 / -1; }
.vsheet-lab {
  padding: 8px 2px 4px; color: var(--text3);
  font-size: var(--fs-meta); font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase;
}

/* The ⋮ and layout menus are lent to a real sheet by JS. Every OTHER
   `.hub-menu` in the app — the Scanner's coin filter and DEX picker — is an
   anchored dropdown built inline by its own section, so it gets the sheet
   GEOMETRY here instead: bottom-anchored, full width, thumb-reachable, and
   incapable of rendering off the bottom of a short screen. No veil, because
   these are single-choice popovers and the document-level click handler that
   already closes them is the correct dismissal. */
@media (max-width: 640px), (max-height: 500px) {
  .hub-menu.open:not(.in-sheet) {
    position: fixed; inset: auto 0 0 0; top: auto;
    width: auto; min-width: 0;
    max-height: min(70dvh, 520px);
    padding: 6px 10px max(10px, env(safe-area-inset-bottom));
    border-radius: 18px 18px 0 0; border-bottom: 0;
    box-shadow: 0 -18px 60px rgba(0, 0, 0, 0.6);
    animation: vsheetIn 260ms var(--ease-drawer) both;
  }
  .hub-menu.open:not(.in-sheet)::before {
    content: ''; display: block;
    width: 40px; height: 4px; margin: 3px auto 7px;
    border-radius: 2px; background: rgba(255, 255, 255, 0.2);
  }
  .hub-menu.open:not(.in-sheet) button,
  .hub-menu.open:not(.in-sheet) label,
  .hub-menu.open:not(.in-sheet) a { min-height: 50px; font-size: var(--fs-ui); }
  /* the anchoring flip is meaningless once the menu spans the screen */
  .hub-menu.open.flip:not(.in-sheet) { inset: auto 0 0 0; }
}

/* a `.hub-menu` lent to a sheet stops being an anchored dropdown. It keeps its
   own click delegation — sheet() moves the live node rather than cloning it. */
.hub-menu.in-sheet {
  display: block; position: static;
  min-width: 0; max-height: none; padding: 0;
  background: none; border: 0; border-radius: 0; box-shadow: none;
}
.hub-menu.in-sheet button, .hub-menu.in-sheet label, .hub-menu.in-sheet a { min-height: 50px; font-size: var(--fs-ui); }
.hub-menu.in-sheet button i, .hub-menu.in-sheet button svg,
.hub-menu.in-sheet label i, .hub-menu.in-sheet label svg,
.hub-menu.in-sheet a i, .hub-menu.in-sheet a svg { width: 15px; height: 15px; }
.hub-menu.in-sheet .hub-menu-hint { padding: 4px 10px 8px; }

/* ── `.hub-modal` IS a sheet on a phone ─────────────────────────────────
   Everything else transient on a phone already arrives from the bottom; a
   top-anchored box floating at y=53 with 300px of dimmed dead space below it
   was the odd one out (F-PF-MODAL). */
@media (max-width: 640px), (max-height: 500px) {
  .hub-modal-veil {
    align-items: flex-end; padding: 0;
    overflow: hidden; overscroll-behavior: contain;
  }
  .hub-modal, .hub-modal.wide {
    width: 100%; max-width: none; max-height: min(88dvh, 760px);
    display: flex; flex-direction: column;
    border: 1px solid var(--border2); border-bottom: 0;
    border-radius: 18px 18px 0 0;
    padding-bottom: max(10px, env(safe-area-inset-bottom));
    animation: vsheetIn 300ms var(--ease-drawer) both;
  }
  .hub-modal::before {
    content: ''; display: block; flex: none;
    width: 40px; height: 4px; margin: 9px auto 1px;
    border-radius: 2px; background: rgba(255, 255, 255, 0.2);
  }
  .hub-modal-head { flex: none; border-bottom: 0; padding: 6px 15px 10px; font-size: var(--fs-title); }
  .hub-modal-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 0 14px 6px; }
  /* primary full-width and ≥48px at the bottom, secondary as text above it.
     Measured before: Add 40×29, Save 47×29, Cancel 61×29, close ✕ 21×21. */
  .hub-modal-foot {
    flex: none; flex-direction: column-reverse; gap: 6px;
    justify-content: stretch; padding: 10px 14px 2px;
  }
  .hub-modal-foot > * { width: 100%; justify-content: center; min-height: 48px; font-size: var(--fs-ui); }
  .hub-modal-foot > .hub-btn:not(.primary) { background: none; color: var(--text2); }
  .hub-modal-foot > .hub-btn:not(.primary):active { background: var(--bg3); }
  .hub-modal-head .x { width: 40px; height: 40px; }
  /* one field height in the product: the Portfolio's own 36px input was the
     only survivor of the 44px pass (F-PF-IN) */
  .vpf-in, .hf-input, .hf-select, .hf-textarea { min-height: var(--tap-min); }
  /* …and a button in a dialog BODY is still a dialog's button. "Add address"
     measured 704×29 inside the wallet dialog: full width, and under the floor,
     because the foot rule above only reaches `.hub-modal-foot`. */
  .hub-modal-body .hub-btn, .vsheet-body .hub-btn { min-height: var(--tap-min); }
}

@media (prefers-reduced-motion: reduce) {
  .vsheet, .vsheet-veil, .vsheet-veil.out, .vsheet-veil.out .vsheet,
  .hub-modal { animation: none; transition: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   ITEM 10 — Scanner + Screener density
   ══════════════════════════════════════════════════════════════════════════ */

/* The phone bar is now exactly one row, so a sticky offset CAN know its height.
   Everything that needs to park under it reads this instead of guessing. */
.hub-page { --hub-barh: 0px; }
@media (max-width: 640px), (max-height: 500px) and (max-width: 950px) {
  .hub-page { --hub-barh: calc(52px + env(safe-area-inset-top)); }
}
@media (max-width: 640px) and (max-height: 600px), (max-height: 420px) and (max-width: 950px) {
  .hub-page { --hub-barh: calc(48px + env(safe-area-inset-top)); }
}

/* (a) SIX TABS BECOME ONE CHIP.
   706px of tabs in a 324px strip meant Markets, Flow and Tracked were
   off-screen with no fade and no chevron — invisible AND unadvertised. The
   tablist stays in the DOM above 640px (where it fits and scroll-snaps); below
   it the chip is the control and the sheet carries all six with descriptions. */
.vsc-vpick { display: none; }
.vsc-vpick .chev { margin-left: auto; }
@media (max-width: 640px) {
  /* `.vsc` scopes this to the SCANNER. The Portfolio's Holdings / Allocation /
     Curve tabs reuse the same `.vsc-views` class, and an unscoped hide took
     them out too — with no chip to replace them, so two of the three portfolio
     views became unreachable on every phone. Caught by the portfolio-with-data
     sweep (§Progress gap 1), which is the first thing that ever exercised them.
     Three tabs fit 375px comfortably; only six do not. */
  .vsc .vsc-views { display: none; }
  .vsc-vpick {
    display: inline-flex; align-items: center; gap: 8px; width: 100%;
    min-height: var(--tap-min); margin: 0 0 10px; padding: 8px 12px;
    background: var(--bg2); border: 1px solid var(--border2); border-radius: var(--radius);
    color: var(--text); font: 600 var(--fs-ui)/1.2 var(--mono); cursor: pointer;
    transition: border-color 140ms var(--ease-out), transform 140ms var(--ease-out);
  }
  .vsc-vpick > i, .vsc-vpick > svg { width: 15px; height: 15px; flex: none; color: var(--teal); }
  .vsc-vpick .chev { width: 13px; height: 13px; color: var(--text3); }
  .vsc-vpick .n {
    padding: 1px 7px; border-radius: 999px; background: var(--bg4);
    color: var(--text2); font-size: var(--fs-meta);
  }
  .vsc-vpick:active { transform: scale(0.99); background: var(--bg3); }
  .vsc-vpick:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
}
.vsheet-n {
  margin-left: auto; flex: none; padding: 2px 8px; border-radius: 999px;
  background: var(--bg4); color: var(--text2); font-size: var(--fs-meta);
}
/* above the chip's range the strip survives — give it snap + an edge fade so
   "there is more to the right" is visible rather than inferred (F-TABS) */
@media (min-width: 641px) {
  .vsc-views { scroll-snap-type: x proximity; scroll-padding-inline: 24px; }
  .vsc-views > .vsc-view { scroll-snap-align: center; }
}

/* (b) THE PREAMBLE IS A LANDING STATE, NOT A PERMANENT ONE.
   The Scanner spends ~200px before the first data row and the Gallery ~254px,
   so 44% of the first screen is chrome. Deliberately NOT solved by hiding the
   preamble on scroll: removing 200px of laid-out content yanks the page under
   the reader's thumb. Instead the TITLE row sticks under the bar. The preamble
   scrolls away by itself, nothing is hidden, there is zero layout shift, and
   once you are into the data the chrome is bar + title ≈ 88px instead of 318. */
@media (max-width: 640px) {
  /* `display: contents` is doing the real work. A sticky element only sticks
     inside its PARENT's box, and `.vsc-head` is exactly as tall as the preamble
     — so the title unstuck and scrolled away with it the moment the preamble
     cleared the viewport (measured at y = −147 before this). Dissolving the
     head's box promotes the title to a child of `.vsc` / `.vgal`, which span
     the whole scrollable section, so it sticks for the length of the page. */
  .vsc-head { display: contents; }
  .vsc-head > :last-child { margin-bottom: 12px; }
  .vsc .vsc-title, .vgal .vsc-title {
    position: sticky; top: var(--hub-barh); z-index: 6;
    margin: 0 -12px; padding: 7px 12px;
    background: color-mix(in srgb, var(--bg) 88%, transparent);
    backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
    border-bottom: 1px solid transparent;
  }
  /* the hairline appears only once the title is actually doing sticky work,
     so the landing state stays clean. Driven by one passive scroll listener on
     <body>, not a per-section observer: `paintHead()` rebuilds `.vsc-title`'s
     innerHTML on every sync, which would silently drop an observer bound to it. */
  body.hub-scrolled .vsc .vsc-title,
  body.hub-scrolled .vgal .vsc-title { border-bottom-color: var(--border); }
  /* …and the table's own sticky header parks under it. The old rule turned
     this off on phones because "the mobile hub bar is two rows tall — a fixed
     sticky offset can't know it". It is one row now, and it publishes its
     height, so the column headers can come back. */
  .vsc-h { position: sticky; top: calc(var(--hub-barh) + 40px); }
}

/* (c) RECLAIM THE NESTED INSETS.
   54px of a 393px screen went to `.hub-main` 16 + `.hub-sec` ~10 + row 10, per
   side, before any data — the table grid got 86% of the width. On a phone
   there is ONE gutter, 12px, and the row's own padding is cell padding rather
   than a third frame. `.hub-bar`'s -12px margin is what makes the bar span the
   full width against this gutter — the two numbers must stay equal. */
@media (max-width: 640px) {
  .hub-main { padding: 0 12px 28px; }
  .hub-sec { padding-left: 0; padding-right: 0; }
}

/* (d) FOUR FILTER ROWS BECOME ONE CHIP.
   ~370px of controls — 56% of a 660px viewport — in front of a table most
   people never filter. The three face tabs stay (most-used, only three); the
   rest goes behind a chip that states how many filters are on. */
.vscr2-fbtn { display: none; }
.vscr2-fl > span { display: none; }
@media (max-width: 640px) {
  .vscr2-filters:not(.in-sheet) { display: none; }
  .vscr2-fbtn {
    display: inline-flex; align-items: center; gap: 7px;
    min-height: var(--tap-min); padding: 6px 12px;
    background: var(--bg2); border: 1px solid var(--border2); border-radius: var(--radius);
    color: var(--text2); font: 600 var(--fs-ui)/1.2 var(--mono); cursor: pointer;
    transition: color 140ms var(--ease-out), border-color 140ms var(--ease-out), transform 140ms var(--ease-out);
  }
  .vscr2-fbtn > i, .vscr2-fbtn > svg { width: 14px; height: 14px; flex: none; }
  .vscr2-fbtn.on { color: var(--teal); border-color: color-mix(in srgb, var(--teal) 42%, transparent); }
  .vscr2-fbtn .n {
    padding: 1px 7px; border-radius: 999px;
    background: var(--teal); color: var(--bg); font-size: var(--fs-meta); font-weight: 700;
  }
  .vscr2-fbtn:active { transform: scale(0.97); }
  .vscr2-fbtn:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
}
/* in the sheet the controls stack and their labels come back — a bare
   <select> with no visible label is fine in a dense bar and wrong in a list */
.vscr2-filters.in-sheet { display: flex; flex-direction: column; align-items: stretch; gap: 10px; padding-bottom: 4px; }
.vscr2-filters.in-sheet > .vpage-chip { justify-content: center; min-height: var(--tap-min); }
.vscr2-fl { display: flex; flex-direction: column; gap: 5px; }
.vscr2-filters.in-sheet .vscr2-fl > span {
  display: block; color: var(--text3);
  font-size: var(--fs-meta); font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase;
}
.vscr2-filters.in-sheet .vscr2-sel { width: 100%; min-height: var(--tap-min); }

/* ══════════════════════════════════════════════════════════════════════════
   ITEMS 11 + 12 — the card feed and the reorder sheet
   ══════════════════════════════════════════════════════════════════════════ */

/* Per-card fold. The feed is a column of full-width cards; without a way to
   skip one you scroll its whole height to get past it. Phone only — on a
   desktop canvas a card's height is already the user's own choice. */
.hw-fold { display: none; }
@media (max-width: 640px) {
  .hw-head { position: relative; }
  /* No negative margin. `margin-right: -6px` let the button paint 10px past
     the header's content box on every card — measured `inside: false` on all
     nine that have a header. A control that renders outside its own container
     is a control you cannot rely on hitting. */
  .hw-fold {
    position: relative;
    display: inline-flex; align-items: center; justify-content: center;
    flex: none; width: 32px; min-height: var(--tap-min);
    background: none; border: 0; color: var(--text3); cursor: pointer;
    transition: color 140ms var(--ease-out);
  }
  /* 32px wide against a 44px finger — the row already has the height, the slop
     gives it the width. Same instrument as everything else on this surface. */
  .hw-fold::after {
    content: ''; position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: var(--tap-min); height: var(--tap-min);
    min-width: 100%; min-height: 100%; z-index: 0;
  }
  .hw-fold > i, .hw-fold > svg { position: relative; z-index: 1; }
  .hw-fold i, .hw-fold svg { width: 15px; height: 15px; transition: transform 200ms var(--ease-out); }
  .hw-fold:active { color: var(--text); }
  .hw-fold:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }
  /* a folded card is its header and nothing else — the body is removed from
     the a11y tree too, not just hidden, so VoiceOver does not read a card the
     user has collapsed */
  .hw-folded .hw-body { display: none; }
  .hw-folded .hw-ctl-pill { display: none; }
}

/* the reorder sheet */
.varr { display: flex; flex-direction: column; gap: 2px; padding-bottom: 6px; }
.varr-row {
  display: flex; align-items: center; gap: 10px;
  min-height: 56px; padding: 6px 2px 6px 0;
  border-radius: var(--radius);
  background: var(--bg2);
  color: var(--text2); font-size: var(--fs-ui);
  touch-action: pan-y;
  transition: background 140ms var(--ease-out), box-shadow 160ms var(--ease-out);
}
.varr-grip {
  display: inline-flex; align-items: center; justify-content: center;
  flex: none; width: var(--tap-min); min-height: var(--tap-min);
  color: var(--text3); cursor: grab;
  touch-action: none; /* the grip owns the gesture; the list still scrolls */
}
.varr-grip i, .varr-grip svg { width: 16px; height: 16px; }
.varr-ic { width: 15px; height: 15px; flex: none; color: var(--teal); opacity: 0.85; }
.varr-t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.varr-x {
  display: inline-flex; align-items: center; justify-content: center;
  flex: none; width: var(--tap-min); min-height: var(--tap-min);
  background: none; border: 0; border-radius: var(--radius);
  color: var(--text3); cursor: pointer;
  transition: color 140ms var(--ease-out), background 140ms var(--ease-out);
}
.varr-x i, .varr-x svg { width: 15px; height: 15px; }
.varr-x:active { color: var(--red2); background: color-mix(in srgb, var(--red) 12%, transparent); }
.varr-x:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }
.varr-row.varr-dragging {
  background: var(--bg4); position: relative; z-index: 2;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45);
  transition: none; /* it must track the finger exactly, not lag behind it */
}
.varr-add {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; min-height: 52px; margin-top: 8px;
  background: none; border: 1px dashed var(--border2); border-radius: var(--radius);
  color: var(--text2); font: 600 var(--fs-ui)/1.2 var(--mono); cursor: pointer;
  transition: color 140ms var(--ease-out), border-color 140ms var(--ease-out), transform 140ms var(--ease-out);
}
.varr-add i, .varr-add svg { width: 15px; height: 15px; }
.varr-add:active { transform: scale(0.99); color: var(--text); border-color: var(--text3); }
.varr-add:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }

/* ══════════════════════════════════════════════════════════════════════════
   ITEM 13 — container-aware density
   ══════════════════════════════════════════════════════════════════════════ */

/* (c) THE WATCHLIST CANYON.
   At 361px the row rendered `[icon] SYM · · · · · · · [price] · · · [chg]`
   with price parked at ~68% and change at ~91% — two isolated columns with a
   canyon between them, because `.px` takes `margin-left: auto` and `.chg` a
   78px fixed track. Numbers that belong to the same row should read as one
   right-aligned pair; the NAME is what should absorb the slack. */
@media (max-width: 640px) {
  .vrow .name { display: none; } /* the symbol is the identity at this width */
  .vrow .sym { flex: 1; min-width: 0; }
  .vrow .px { margin-left: 0; text-align: right; }
  .vrow .chg { min-width: 62px; margin-left: 8px; }
}

/* ── F-STRIP: the chart desk stops DELETING its numbers on a phone ────────
   Measured at 393px: 5 of the 6 stat groups were `display: none !important` —
   the top-100 crowd meter, 24h volume, open interest, funding and the venue
   note — leaving identity + price. A phone is exactly where the user has no
   second screen to read funding or OI from, so peeling to nothing is the wrong
   response to narrow. The right one is to reflow: the stats become a
   scroll-snapped rail, the same primitive the Portfolio tape and the Scanner
   exposure strip already use. `!important` has to be answered in kind — the
   peel rules above carry it. */
@media (max-width: 640px) {
  .vdesk-stats {
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    scroll-snap-type: x proximity; overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
  }
  .vdesk-stats::-webkit-scrollbar { display: none; }
  /* `:not([hidden])` is LOAD-BEARING, not defensive. These same groups are also
     hidden by the ATTRIBUTE in paintStats() whenever the symbol is not on
     Hyperliquid — there is no volume, OI or funding to show for a TradingView
     symbol, and `[data-offhl]` explains that instead. `.vdesk [hidden]
     { display: none !important }` (hub.css:2488) is the same specificity as
     this rule AND also `!important`, so source order decides — and this block
     is ~2,300 lines later. Without the guard, un-peeling for narrow screens
     would force four empty stat slots onto every non-HL symbol, which is
     exactly the bare-`—` the design system forbids. */
  .vdesk-stats [data-gvol]:not([hidden]), .vdesk-stats [data-goi]:not([hidden]),
  .vdesk-stats [data-gcrowd]:not([hidden]), .vdesk-stats [data-gfund]:not([hidden]) { display: flex !important; }
  .vdesk-stats .grp { flex: 0 0 auto; scroll-snap-align: start; padding: 0 12px; }
  /* a divider only separates when there is something before it — as the first
     item in a scrolled rail it is an orphaned tick against the price */
  .vdesk-stats .grp:first-child { border-left: 0; padding-left: 0; }
  .vdesk-stats .crowd .vsc-mini.big { width: 64px; }
}

/* F-GAL-CHIP — the jump chips scroll 578px of content in a 369px strip with no
   sign of it. The 7px vertical clip the audit recorded is already gone (the
   44px min-height from items 1–7 fixed it; re-measured clipY: 0), but "no
   scroll affordance" was the other half and is still true. A mask fade, not an
   overlay — an overlay would sit on top of the last chip and eat its taps. */
/* ── one edge fade for every horizontal rail ──────────────────────────────
   A mask, never an overlay: an overlay would sit on top of the end chip and
   eat its taps. Two-sided, because centring the active chip cuts the LEFT edge
   too and a chip sliced with no fade reads as broken rather than scrollable.
   `.at-start` / `.at-end` come off when there is genuinely nothing more that
   way — a permanently dimmed end chip reads as disabled. */
.vgal-mnav, .vsc-views, .vcp-tfstrip, .vdesk-stats {
  --rail-l: 22px; --rail-r: 22px;
  mask-image: linear-gradient(to right, transparent 0, #000 var(--rail-l),
    #000 calc(100% - var(--rail-r)), transparent 100%);
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--rail-l),
    #000 calc(100% - var(--rail-r)), transparent 100%);
}
.vgal-mnav.at-start, .vsc-views.at-start, .vcp-tfstrip.at-start, .vdesk-stats.at-start { --rail-l: 0px; }
.vgal-mnav.at-end, .vsc-views.at-end, .vcp-tfstrip.at-end, .vdesk-stats.at-end { --rail-r: 0px; }

/* `.vsc-czseg` got `min-height: var(--tap-min)` in items 1–7, but its container
   `.vsc-czbar` is a FIXED 26px (22px when narrow) with `overflow: hidden` —
   which clips hit-testing, not just paint. So the segments laid out 44px tall
   and were tappable over 26 of them: a touch fix that did not fix, the same
   shape as F-TAP's inert slop. Measured as an 18–22px clip on the bar itself.
   The bar takes the height on touch. That is not "chunky" — a segmented control
   at the platform floor is what iOS ships; the audit gave `.vsc-view` a real
   44px for the identical reason (§4.3 fix B). */
@media (pointer: coarse) {
  .vsc-czbar { height: var(--tap-min); }
  /* the same filter's key rows are its list form — they need the floor too */
  .vsc-czkey .k { min-height: var(--tap-min); }
}

/* My own `⋯ all timeframes` chip is 26px wide inside a scrolling strip, and an
   `overflow-x: auto` ancestor crops the 44px slop pseudo back to the strip —
   the exact mechanism that forced `.vsc-view` to take a real height. Width is
   what is short here, so width is what it gets. */
@media (pointer: coarse) { .vcp-tfmore { min-width: var(--tap-min); } }

/* Column-header labels that peel instead of their column (audit item 15).
   `open interest ↓` clipped x +41px inside its own cell on landscape and both
   iPads, and the Markets view cannot drop that column — it IS the view. Both
   forms ship; only one renders. The button keeps the long form as its
   accessible name, so nothing is lost to a screen reader. */
.vsc-h .shl { display: none; }
@media (max-width: 1100px) {
  .vsc-h .lgl { display: none; }
  .vsc-h .shl { display: inline; }
}

/* The chart desk's own strip controls were never given a touch treatment — the
   engine switcher measured 53×28 and the TV button 38×28, with neither the slop
   pseudo nor a min-height, and they are the top non-artifact entries left in the
   sweep's dead-zone table. Un-peeling the stat rail above made them more
   prominent, not less, so they have to be fixed with it. Same instrument as
   §4.3: grow the hit area, leave the drawn box alone. */
@media (pointer: coarse), (max-width: 768px) {
  .vdesk-eng button { position: relative; }
  .vdesk-eng button::after {
    content: ''; position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: var(--tap-min); height: var(--tap-min);
    min-width: 100%; min-height: 100%; z-index: 0;
  }
  .vdesk-eng button > * { position: relative; z-index: 1; }
}
@media (pointer: coarse) {
  /* the capsule clips its children, so the pseudo alone cannot reach 44 — the
     row has to take the height, exactly like `.vsc-view` did */
  .vdesk-eng, .vdesk-eng button { min-height: var(--tap-min); }
}


/* (b) THE TIMEFRAME STRIP.
   One chip meant tap → menu → tap for the most common charting action, with the
   menu covering the chart you were reading. The strip scroll-snaps and fades at
   its right edge so "there is more" is visible rather than inferred. */
.vcp-tfstrip {
  display: flex; align-items: center; gap: 4px;
  min-width: 0; max-width: 100%;
  overflow-x: auto; scrollbar-width: none;
  scroll-snap-type: x proximity; scroll-padding-inline: 20px;
  -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain;
}
.vcp-tfstrip::-webkit-scrollbar { display: none; }
.vcp-tfstrip > .vcp-chip { flex: none; scroll-snap-align: center; }
.vcp-tfmore { font-weight: 700; letter-spacing: 0.06em; }

/* ══════════════════════════════════════════════════════════════════════════
   ITEM 14 — the native-feel layer (§4.7)
   None of this is a bug. Together it is most of the difference between a
   responsive site and an app.
   ══════════════════════════════════════════════════════════════════════════ */
@media (pointer: coarse) {
  /* 300ms tap delay, gone. `manipulation` keeps pan and pinch-zoom and drops
     only double-tap-to-zoom, which is what the delay exists to wait for. NOT
     on the sheet grabbers or the reorder grips — those declare `touch-action:
     none` themselves and must keep it. */
  .hub-page a, .hub-page button, .hub-page summary, .hub-page label,
  .hub-page [role="button"], .hub-page [role="tab"], .hub-page .clickable,
  .hub-page .vsc-r, .hub-page .vrow, .hub-page .vposn-row { touch-action: manipulation; }

  /* The grey flash is Safari's tap feedback. Removing it without a replacement
     leaves a control that answers nothing, so every removal below is paired
     with a real `:active`. */
  .hub-page { -webkit-tap-highlight-color: transparent; }
  .hub-btn:active, .hub-nav a:active, .hw-btn:active, .hw-fold:active,
  .vpage-chip:active, .vsc-view:active, .vgal-navi:active, .vcp-chip:active,
  .vcp-btn:active, .vcp-tool:active, .vsc-coinbtn:active, .vscr2-tab:active,
  .vsc-vpick:active, .vscr2-fbtn:active, .vsc-fbtn:active, .hub-secbtn:active,
  .vsheet-item:active, .vsheet-act:active, .varr-row:active {
    background: var(--bg4);
  }
  /* a pressable surface should also give a little (emil: 0.95–0.98) */
  .vsheet-item:active, .vsheet-act:active, .vgal-card:active { transform: scale(0.985); }
  .vsheet-item, .vsheet-act, .vgal-card { transition: transform 140ms var(--ease-out), background 140ms var(--ease-out); }
}

/* iOS inflates text in landscape unless told not to. Site-wide, not phone-only
   — it is a property of the engine, not the screen. */
.hub-page { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

/* No nested scroller may hand its overscroll to the page. `contain` on the
   scrollers that did not already declare it. */
.vw-scroll, .vsc-scroll, .vscr2-scroll, .hub-palette-list, .hub-tray-list,
.hub-insp, .vgal-drawer, .vsc-views, .vgal-navi.m, .varr, .vsheet-body {
  overscroll-behavior: contain;
}

/* Section changes repainted instantly, which reads as replacement rather than
   navigation. 200ms, ease-out, transform+opacity only. `backwards` NOT `both`
   — a retained transform keyframe makes the element a containing block for
   every fixed descendant, which is the bug this audit found in `hubPageIn`. */
@media (pointer: coarse) {
  .hub-sec > * { animation: hubSecIn 200ms var(--ease-out) backwards; }
}
@keyframes hubSecIn { from { opacity: 0; transform: translateY(6px); } }
@media (prefers-reduced-motion: reduce) {
  .hub-sec > * { animation: none; }
  /* motion goes; the press feedback that confirms a tap stays */
  .vsheet-item:active, .vsheet-act:active, .vgal-card:active { transform: none; }
}

/* (d) GALLERY DESCRIPTIONS — 2 lines on phones (owner's answer to §6 Q3).
   The unconditional 1-line clamp at 375px left `.vgal-desc` 212px wide ≈ 30
   characters ("Weekend CME futures gaps on…"), and the Gallery's whole job is
   choosing between 58 elements. Costs ~17px a card. This must sit AFTER the
   slimmed-Gallery block that sets `-webkit-line-clamp: 1`, or source order
   hands it back. */
@media (max-width: 640px) {
  .vgal-desc { -webkit-line-clamp: 2; }
}

/* the Scanner's own cohort bar gets the same treatment — 322px of stacked
   controls (source · search · books · depth · window) put the first data row
   944px down a 660px screen. Source and search stay; the rest folds. */
.vsc-fbtn { display: none; }
.vsc-fbtn[hidden] { display: none !important; }
@media (max-width: 640px) {
  /* `.vsc` scopes ALL THREE. The Portfolio builds its own bar as
     `vsc-bar vsc-gbar` (hub.js:13538) inside `.vpf`, so unscoped these hid its
     second control cluster — the dust filter — on every phone, with no chip to
     replace it. Exactly the same class-sharing trap as `.vsc-views`, in the
     same commit, which is why the scope is now stated three times rather than
     inherited from a wrapper. */
  .vsc .vsc-gbar > .vsc-sp { display: none; }
  .vsc .vsc-gbar > .vsc-cluster:last-of-type:not(.in-sheet),
  .vsc .vsc-gbar .vsc-acts:not(.in-sheet) { display: none; }
  .vsc-fbtn {
    display: inline-flex; align-items: center; gap: 7px; flex: none;
    min-height: var(--tap-min); padding: 6px 12px;
    background: var(--bg2); border: 1px solid var(--border2); border-radius: var(--radius);
    color: var(--text2); font: 600 var(--fs-ui)/1.2 var(--mono); cursor: pointer;
    transition: color 140ms var(--ease-out), border-color 140ms var(--ease-out), transform 140ms var(--ease-out);
  }
  .vsc-fbtn > i, .vsc-fbtn > svg { width: 14px; height: 14px; flex: none; }
  .vsc-fbtn.on { color: var(--teal); border-color: color-mix(in srgb, var(--teal) 42%, transparent); }
  .vsc-fbtn .n {
    padding: 1px 7px; border-radius: 999px;
    background: var(--teal); color: var(--bg); font-size: var(--fs-meta); font-weight: 700;
  }
  .vsc-fbtn:active { transform: scale(0.97); }
  .vsc-fbtn:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
}
/* lent nodes stack in the sheet; their own chip rows keep wrapping as designed */
/* ── a sheet must carry the `[hidden]` guard its origin had ────────────────
   `.vsc [hidden] { display: none !important }` (hub.css:1602) and its `.vdesk`
   twin exist because the hidden ATTRIBUTE loses to a class `display:`. But
   `lendToSheet()` moves the live node OUT of `.vsc` and into a sheet mounted on
   `document.body` — so the guard, which is scoped to an ancestor the node no
   longer has, silently stops applying. Measured in the Flow view: `.vsc-acts`
   read `hidden=true display=none` in the bar and `hidden=true display=flex`
   the moment it was lent to the Cohort sheet.
   Any container that adopts foreign nodes has to re-declare this. */
.vsheet [hidden], .vsheet-lent [hidden], .hub-modal [hidden] { display: none !important; }

/* Every arm carries `:not([hidden])`, and it is load-bearing in the same way as
   the chart strip's: the Scanner hides `actEl`, `winEl` and `sizeEl` BY
   ATTRIBUTE per view (`winEl.hidden = v === 'flow'` etc — the Flow tape has no
   depth or window to scope). `.vsc-sizes[hidden]` is the same specificity
   (0,2,0) and these rules are ~3,400 lines later, so without the guard the
   Cohort sheet would show depth and window chips in Flow — controls the view
   deliberately removed because they do nothing there. */
.vsheet-lent { display: flex; flex-direction: column; gap: 14px; padding-bottom: 4px; }
.vsheet-lent > .in-sheet:not([hidden]) { display: flex; flex-wrap: wrap; gap: 7px; }
.vsheet-lent .vsc-cluster.in-sheet:not([hidden]) { flex-direction: column; align-items: stretch; gap: 14px; }
.vsheet-lent .vgal-chips:not([hidden]) { display: flex; flex-wrap: wrap; gap: 7px; }

/* ── three controls the earlier touch pass genuinely missed ─────────────────
   Found by re-running the sweep after items 8–10, not assumed. All three are
   real controls under the floor, as distinct from the `label` / multi-line
   inline `a` rows §7 documents as probe artifacts and says to ignore.

   1. `a.hub-brand` — 99 hits, the single biggest remaining entry. It is a 20px
      logo and it was never in the slop list at all. It is a DIRECT child of
      `.hub-bar`, which is a stacking context, so slop works perfectly here —
      that is exactly the case the audit measured at 100% self-coverage.
   2. `.vsc-coinbtn` / `.vsc-srcbtn` — 59 hits, 153×27 and 137×27. Wide enough,
      27px tall. The Scanner's own filter triggers.
   3. `.vsc-method > summary` — 72 hits across the two disclosures, 193×23. A
      real disclosure toggle, and §7 lists it in the worst-dead table.
   4. footer links — 68 hits at 42×22. A link is never metadata (design §1). */
@media (pointer: coarse), (max-width: 768px) {
  .hub-brand, .foot-links a, .vsc-method > summary { position: relative; }
  .hub-brand::after, .foot-links a::after, .vsc-method > summary::after {
    content: ''; position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: var(--tap-min); height: var(--tap-min);
    min-width: 100%; min-height: 100%; z-index: 0;
  }
  .hub-brand > *, .foot-links a > * { position: relative; z-index: 1; }
}
@media (pointer: coarse) {
  .vsc-coinbtn, .vsc-method > summary { min-height: var(--tap-min); }
  /* an inline-flex summary can take a height; keep the "+" and label centred */
  .vsc-method > summary { align-items: center; }
}

/* The vchart symbol search is `padding-top: 42px` inside the chart stage, which
   on a 343px-tall landscape phone pushed its box 9px past the bottom of the
   screen. On a short viewport it centres instead of hanging from the top, and
   its list takes whatever height is left rather than a fixed one. */
@media (max-height: 500px) {
  .vcp-search { align-items: center; padding-top: 0; padding-bottom: 0; }
  .vcp-search-box { max-height: calc(100% - 16px); display: flex; flex-direction: column; }
  .vcp-search-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
}

/* ══ CONTRAST — what `contrast.mjs` found once it accounted for OPACITY and
   for PLACEHOLDERS (audit item 16). Both were invisible to the first version of
   that probe: it only ever compared opacity to 0, and it skipped replaced
   elements entirely, so every placeholder in the app went unmeasured.

   Measured on `--bg2` rgb(13,11,24):
     .vsc-star / .vscr2-star  ★   3.27-3.58 : 1
     input::placeholder           4.23 : 1   (needs 4.5)

   THE STARS ARE NOT FIXED HERE, deliberately. They carry `aria-label`, so the
   ★ is decorative presentation of a control — WCAG 1.4.11 (non-text, 3:1), not
   1.4.3 (text, 4.5:1) — and at 3.27-3.58 they pass that. An unstarred star is
   quiet on purpose (0.28, 0.7 on touch) so it does not compete with a starred
   one; forcing it opaque to satisfy a threshold that does not apply would
   destroy the affordance. The PROBE learned the distinction instead.

   What that did turn up is real and is fixed in hub.js: two stars had
   `aria-pressed` and `title` but no `aria-label`, so their accessible name fell
   back to the ★ character — which makes the glyph TEXT, and text at 3.58:1
   fails. `a11y-sweep.mjs` now rejects a glyph-only name too.

   Sets colour only, so the LAST WORD field-size block below is unaffected. */

/* Placeholders are text. The 117,117,117 measured at 4.23:1 is Chrome's UA
   default, applying wherever no rule set one; `--text3` is 5.55:1 on the same
   surface and is what every explicit placeholder rule in this file already
   uses. */
.hub-page input::placeholder,
.hub-page textarea::placeholder { color: var(--text3); opacity: 1; }

/* ══ LAST WORD — the iOS 16px field floor (audit F-ZOOM) ══════════════════
   Safari zooms the whole page when you focus a field under 16px, and the user
   has to pinch back out. It is the single most recognisable "this is a website,
   not an app" tell on iOS, and it was firing on the two most-used inputs in the
   product (Gallery "Search elements" and the Scanner's trader filter — both
   `.vgal-search input`, both measured at 12px).

   The rule used to sit at ~line 1023 and was undone 376 lines later by a `font:`
   shorthand at equal specificity. It lives HERE, at the end of the file, so
   source order can never outrank it again. KEEP IT LAST — anything appended
   below this block must not set a field font-size.

   Written as a catch-all rather than the old 14-name list so the invariant
   survives the next field somebody adds. Verified before shipping: no field in
   the app declares more than 14px, so this only ever raises a size. The
   exclusions are the controls iOS does NOT zoom for and would only be
   distorted by a text size — checkbox, radio, range, and the hidden file input.
   `mobile-sweep.mjs` asserts this: 0 fields under 16px.

   The `.hub-page` prefix is load-bearing, not decoration: a bare select is
   specificity (0,0,1) and loses to the one-class rule on .vscr2-sel — the first
   version of this rule shipped without the prefix and the sweep caught 27
   screener selects still sitting at 12px. `.hub-page` (on body) lifts every arm
   to (0,1,1), which outranks any single-class field rule and ties the two-class
   ones — and being last in the file wins those ties. */
@media (pointer: coarse) {
  .hub-page input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]),
  .hub-page select,
  .hub-page textarea { font-size: var(--fs-title); }
}

/* ══ Wallpaper studio (#/wallpaper) ═════════════════════════════════════
   The page version of the wallpaper's design panel. Control pills reuse the
   .vwall-seg / .vwall-tog / .vwall-sym language so the studio and the
   in-mode popover read as one instrument at two sizes. */
.vwp { display: flex; flex-direction: column; gap: 16px; }
/* FULL-WIDTH HERO. The renderers adapt to any box, so the preview takes the
   whole measure and a bounded height — a wide live strip, not a squashed
   square fighting a side rail for rows. Settings ride below as a band. */
.vwp-hero {
  position: relative; overflow: hidden; width: 100%;
  height: clamp(340px, 56vh, 720px);
  border: 1px solid var(--border2); border-radius: 14px; background: #05060a;
}
.vwp-hero canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
/* the Launch pill floats alone bottom-right: any caption here sat on top of
   whatever the live display was drawing in its own bottom band */
.vwp-herobar {
  position: absolute; right: 12px; bottom: 12px; z-index: 2;
  display: flex; align-items: center;
}
.vwp-launch {
  display: inline-flex; align-items: center; gap: 8px;
  appearance: none; border: 1px solid rgba(65, 227, 255, 0.45); border-radius: 999px;
  background: rgba(5, 10, 20, 0.72); backdrop-filter: blur(6px); padding: 9px 16px; cursor: pointer;
  color: #bff4ff; font: 700 var(--fs-data)/1.2 var(--mono); letter-spacing: 0.04em;
  transition: background 130ms var(--ease-out), border-color 130ms var(--ease-out);
}
.vwp-launch:hover { background: rgba(65, 227, 255, 0.2); border-color: rgba(65, 227, 255, 0.7); }
.vwp-launch svg { width: 15px; height: 15px; }
.vwp-launch kbd {
  border: 1px solid rgba(191, 244, 255, 0.35); border-radius: 5px;
  padding: 1px 6px; font: 600 var(--fs-meta)/1.2 var(--mono); color: rgba(191, 244, 255, 0.8);
}
.vwp-settings {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 12px;
  align-items: start;
}
.vwp-set {
  display: flex; flex-direction: column; gap: 7px;
  border: 1px solid var(--border); border-radius: 14px;
  background: var(--bg2); padding: 14px 15px 15px;
}
.vwp-set:empty { display: none; }
.vwp-set h4 {
  color: var(--text2); font: 700 var(--fs-meta)/1 var(--mono);
  letter-spacing: 0.18em; text-transform: uppercase; margin-bottom: 2px;
}
.vwp-set label {
  color: var(--text3); font: 600 var(--fs-meta)/1 var(--mono);
  letter-spacing: 0.12em; text-transform: uppercase; margin-top: 5px;
}
.vwp-h { display: flex; align-items: baseline; gap: 12px; margin-top: 6px; }
.vwp-h h3 { color: var(--text); font: 700 var(--fs-ui)/1.2 var(--mono); letter-spacing: 0.06em; text-transform: uppercase; }
.vwp-h p { color: var(--text3); font: 400 var(--fs-data)/1.4 var(--mono); }
.vwp-h p b { color: var(--text2); font-weight: 700; }
.vwp-themes { display: grid; grid-template-columns: repeat(auto-fill, minmax(228px, 1fr)); gap: 12px; }
.vwp-card {
  appearance: none; display: flex; flex-direction: column; gap: 8px;
  border: 1px solid var(--border2); border-radius: 12px;
  background: var(--bg2); padding: 9px 10px 10px; cursor: pointer; text-align: left;
  transition: border-color 130ms var(--ease-out), background 130ms var(--ease-out), transform 130ms var(--ease-out);
}
.vwp-card:hover { border-color: var(--border3); transform: translateY(-1px); }
.vwp-card.on { border-color: rgba(65, 227, 255, 0.7); background: rgba(65, 227, 255, 0.07); }
.vwp-card:focus-visible { outline: 1px solid var(--teal); outline-offset: 2px; }
.vwp-cprev {
  display: block; width: 100%; aspect-ratio: 16 / 9; height: auto;
  border-radius: 7px; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.07);
}
.vwp-cmeta { display: flex; flex-direction: column; gap: 2px; }
.vwp-cmeta b { color: var(--text); font: 700 var(--fs-data)/1.2 var(--mono); letter-spacing: 0.03em; }
.vwp-cmeta em { color: var(--text3); font: 400 var(--fs-meta)/1.25 var(--mono); font-style: normal; }
.vwp-palhead { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); }
.vwp-palhead .vwp-palfor { color: var(--text3); letter-spacing: 0.06em; }
.vwp-pals { display: flex; flex-wrap: wrap; gap: 6px; }
.vwp-pal {
  appearance: none; display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--border2); border-radius: 999px;
  background: var(--bg3); padding: 5px 11px 5px 7px; cursor: pointer;
  transition: border-color 130ms var(--ease-out), background 130ms var(--ease-out);
}
.vwp-pal:hover { border-color: var(--border3); }
.vwp-pal.on { border-color: rgba(65, 227, 255, 0.7); background: rgba(65, 227, 255, 0.09); }
.vwp-pal:focus-visible { outline: 1px solid var(--teal); outline-offset: 2px; }
.vwp-psw { display: inline-flex; border-radius: 4px; overflow: hidden; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12); }
.vwp-psw i { width: 9px; height: 14px; }
.vwp-pal b { color: var(--text); font: 700 var(--fs-meta)/1.2 var(--mono); letter-spacing: 0.03em; }
@media (max-width: 940px) {
  .vwp-hero { height: clamp(240px, 42vh, 420px); }
}
