:root {
  --bg: #0a0a0a;
  --bg-sidebar: #000000;
  --bg-elevated: #1a1a1a;
  --border: #2a2a2a;
  --text: #ffffff;
  --text-dim: #8a8a8a;
  --accent: #0052ff;
  --accent-rgb: 0, 82, 255;
  --accent-dim: #003bb8;
  --accent-text: #ffffff;
  --danger: #e5484d;
  --danger-bg: #3a1f20;
  --user-bubble: #1a1a1a;
  --radius: 14px;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --mono: "SF Mono", Consolas, Menlo, monospace;

  /* App-chrome customization hooks — set live via public/ui-prefs.js in
     response to chat commands ("make the sidebar green", "make the text
     bigger"). Kept separate from the trading-terminal panel theme below,
     which is untouched by this system. Deliberately literal colors, not
     var(--bg-sidebar) etc. — a custom-property reference re-resolves live,
     so "make the sidebar green" would otherwise silently repaint every
     region still defaulting to that same reference too. */
  --bg-dashboard: #000000;
  --bg-main: #0a0a0a;
  --bg-composer: #1a1a1a;
  --ui-font-scale: 1;
  --chat-width-scale: 1;
  --sidebar-w: 260px;

  /* Trading-terminal panel theme (widget cards / workspace tiles) */
  --panel-bg: #0a0a0a;
  --panel-header-bg: #111111;
  --panel-border: #262626;
  --panel-border-soft: #1c1c1c;
  --panel-text: #ffffff;
  --panel-text-rgb: 255, 255, 255;
  --panel-text-dim: #8a8a8a;
  /* Deliberately one fixed muted-slate value in both themes (not redefined
     under [data-theme="light"]) — column headers should read as a subdued
     dark label regardless of theme, not brighten along with the rest of the
     dark-mode palette the way --panel-text-dim does. */
  --panel-text-faint: #6b7280;
  --buy: #16c784;
  --buy-bg: rgba(22, 199, 132, 0.12);
  --sell: #ea3943;
  --sell-bg: rgba(234, 57, 67, 0.12);

  /* Status/semantic text colors (badges, pills, live indicators) — tuned
     bright so they pop on the near-black app/panel backgrounds above.
     Re-tuned darker under :root[data-theme="light"] below since these are
     read directly as text, often over a same-hue alpha-tinted pill
     background, and a bright pastel loses all contrast once that
     background turns pale instead of near-black. */
  --status-green: #7ee787;
  --status-blue: #8fb0ff;
  --status-amber: #e3b341;
  --status-teal: #2dd4bf;
  --status-purple: #c792ea;

  /* Semi-opaque chrome laid over embedded chart iframes (resize/interval
     notices, loading placeholders) — rgb triplet so it can feed rgba()
     alongside the fixed alpha each caller already had. */
  --overlay-rgb: 10, 10, 10;

  /* TradingView-style dark chrome (chart preferences / quick-start /
     indicator-library modals) — previously hardcoded literals that never
     lightened with the rest of the app. Defaults here match those old
     literals exactly, so dark mode is pixel-identical; only the
     :root[data-theme="light"] block below actually changes anything. */
  --tv-modal-bg: #090909;
  --tv-modal-border: #343434;
  --tv-modal-text: #f2f2f2;
  --tv-modal-text-dim: #d8d8d8;
  --tv-modal-divider: #2c2c2c;
  --tv-modal-input-bg: #0c0c0c;
  --tv-modal-input-border: #4d4d4d;
  --tv-modal-input-border-hover: #838383;
  --tv-modal-hover: #262626;
  --tv-modal-btn-bg: #161616;
}

/* Light theme, toggled via documentElement[data-theme="light"] from
   public/ui-prefs.js. --accent/--accent-rgb/--accent-text are intentionally
   absent here: a user-chosen accent is set as an inline style on
   documentElement regardless of theme, and inline styles always win over
   this attribute selector. */
:root[data-theme="light"] {
  --bg: #f5f6f8;
  --bg-sidebar: #ffffff;
  --bg-dashboard: #ffffff;
  --bg-main: #f5f6f8;
  --bg-composer: #eef0f3;
  --bg-elevated: #eef0f3;
  --border: #dde1e6;
  --text: #16181d;
  --text-dim: #6b7280;
  --user-bubble: #e7ecf3;
  --accent-dim: #0040cc;
  --danger: #d1373d;
  --danger-bg: #fbe4e4;

  --panel-bg: #ffffff;
  --panel-header-bg: #f0f2f5;
  --panel-border: #dde1e6;
  --panel-border-soft: #e6e9ee;
  --panel-text: #16181d;
  --panel-text-rgb: 22, 24, 29;
  --panel-text-dim: #6b7280;

  /* --buy/--sell double as button-chrome AND plain text (order book, ticker,
     confirm badges) — the dark-mode values above are vivid enough to pop as
     text on near-black but fall well under WCAG contrast on a white panel
     (the green in particular, ~2:1). Buttons stay legible either way since
     their own text is a fixed dark/white, so it's safe to darken these. */
  --buy: #157a3d;
  --sell: #d1373d;

  /* Darkened so each still reads as text at normal size against a white or
     pale-tinted pill background — the dark-mode values above were tuned to
     pop against near-black instead. */
  --status-green: #157a3d;
  --status-blue: #2454d6;
  --status-amber: #8a6d1f;
  --status-teal: #0f766e;
  --status-purple: #7e3fb4;

  --overlay-rgb: 245, 246, 248;

  --tv-modal-bg: #ffffff;
  --tv-modal-border: #dde1e6;
  --tv-modal-text: #16181d;
  --tv-modal-text-dim: #4b5563;
  --tv-modal-divider: #e6e9ee;
  --tv-modal-input-bg: #f5f6f8;
  --tv-modal-input-border: #c7ccd3;
  --tv-modal-input-border-hover: #9aa1ab;
  --tv-modal-hover: #e9ecf0;
  --tv-modal-btn-bg: #f0f2f5;
}

* { box-sizing: border-box; }

/* App-wide scrollbar styling — thin, theme-colored, no arrow buttons —
   instead of each browser/OS's default scrollbar. Uses var(--border) so it
   tracks the light/dark theme and any live "make X green"-style recolor
   (public/ui-prefs.js) automatically, same as everything else themed here. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
*::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
*::-webkit-scrollbar-button { display: none; height: 0; width: 0; }
*::-webkit-scrollbar-corner { background: transparent; }

html, body {
  height: 100%;
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: calc(15px * var(--ui-font-scale));
}

/* body stacks the ad banner above .app instead of .app claiming a flat
   100vh — that way closing the banner (display:none) lets .app's flex:1
   reclaim the space automatically, no JS-driven resize needed. */
body {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.app {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
  width: 100vw;
  overflow: hidden;
}

/* Global Coinzilla banner, above the fold, same on every page — see
   AD_VARIANTS/renderAdVariant in app.js for the per-tile A-ADS square and
   the sidebar Coinzilla slot below. min-height here is a pre-JS fallback;
   renderAdMountPlacements sets an inline *height* (not just min-height)
   matching the active variant's declared banner size, and overflow:hidden
   below is load-bearing: whatever the network's script actually injects
   (a differently-sized creative, an error state for an unrecognized zone,
   a placeholder while a real zone ID hasn't been created yet) gets clipped
   to that reserved footprint instead of being free to grow the banner —
   and the rest of the page down with it — to whatever size it wants. */
.top-ad-banner {
  position: relative;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 90px;
  overflow: hidden;
  background: var(--panel-bg);
  border-bottom: 1px solid var(--panel-border);
}
.top-ad-banner-mount { display: flex; align-items: center; justify-content: center; overflow: hidden; max-width: 100%; }
.top-ad-banner-close {
  position: absolute;
  top: 6px;
  right: 8px;
  width: 22px;
  height: 22px;
  line-height: 20px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--panel-border);
  background: var(--panel-bg);
  color: var(--panel-text);
  font-size: 14px;
  cursor: pointer;
}
.top-ad-banner-close:hover { background: var(--border); }

/* Same look/position as .top-ad-banner-close above, generalized for the
   sidebar and chat ad mounts (see addAdCloseButton in app.js) — every
   non-tile ad placement gets an equally obvious, always-visible way to
   dismiss it, not just the top banner. */
.ad-close-btn {
  position: absolute;
  top: 4px;
  right: 4px;
  z-index: 2;
  width: 22px;
  height: 22px;
  line-height: 20px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--panel-border);
  background: var(--panel-bg);
  color: var(--panel-text);
  font-size: 14px;
  cursor: pointer;
  opacity: 0.95;
}
.ad-close-btn:hover { background: var(--border); opacity: 1; }

/* Second Coinzilla placement some variants declare — see renderAdVariant in
   app.js. :empty collapses it to nothing when the active variant doesn't
   use a sidebar spot, so it never reserves dead space. */
.sidebar-ad-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px;
  border-top: 1px solid var(--panel-border);
  overflow: hidden;
  /* Flexbox quirk: overflow other than `visible` gives a flex item an
     automatic minimum size of 0, so without this the sidebar's flex
     column was free to silently squash this item down to whatever room
     was left instead of genuinely overflowing .sidebar — which is what
     actually triggers .sidebar's overflow-y:auto scrollbar. flex-shrink:0
     keeps its real content size so it overflows honestly and stays
     reachable by scrolling instead of getting quietly clipped. */
  flex-shrink: 0;
}
.sidebar-ad-slot:empty { display: none; padding: 0; border: 0; }

/* Coinzilla's own mount div (see renderCoinzillaUnit in app.js, which sets
   this element's width/height inline to match the placement spec) —
   overflow:hidden here is the actual clip boundary: whatever their script
   injects as a child is confined to the reserved box regardless of its own
   size, same reasoning as .top-ad-banner's overflow:hidden above. */
.coinzilla { overflow: hidden; }

/* A dashed outline + "Ad" tag around every populated non-tile mount
   (top banner / sidebar / chat), independent of whether the network's own
   creative actually loaded — an ad iframe that fails to fill (offline,
   adblock, or just this local dev/testing environment) is otherwise a
   blank, borderless box that's invisible against the dark theme and reads
   as "nothing rendered here" instead of "this is where an ad goes". The
   tile placement doesn't need this: its own tile chrome + "Advertisement"
   title already makes it visually distinct.
   Deliberately `outline` (not `border`) and a label positioned with
   *positive* offsets fully inside the box: outline never participates in
   layout sizing (border would add a couple px on top of the mount's exact
   width/height, which the parent's overflow:hidden — see .top-ad-banner —
   would then clip), and a label anchored outside the box (e.g. top:-8px)
   would get clipped by that same overflow:hidden. Both now live entirely
   within the exact footprint that was reserved, so nothing here can ever
   conflict with the hard size cap. */
.top-ad-banner-mount:not(:empty),
.sidebar-ad-slot:not(:empty),
.chat-ad-slot:not(:empty) {
  position: relative;
}
.top-ad-banner-mount:not(:empty) > *,
.sidebar-ad-slot:not(:empty) > *,
.chat-ad-slot:not(:empty) > * {
  outline: 1px dashed var(--panel-border);
  outline-offset: -1px;
}
.top-ad-banner-mount:not(:empty)::before,
.sidebar-ad-slot:not(:empty)::before,
.chat-ad-slot:not(:empty)::before {
  content: "Ad";
  position: absolute;
  top: 2px;
  left: 4px;
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
  background: var(--panel-bg);
  padding: 0 4px;
  border-radius: 3px;
  opacity: 0.9;
  pointer-events: none;
  z-index: 1;
}

/* Chat-pane placement — see #chatAdSlot in index.html and
   renderAdMountPlacements in app.js. Sits between the transcript and the
   composer; :empty collapses it when the active variant doesn't use it. */
.chat-ad-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
  padding: 8px 0;
  overflow: hidden;
  /* Same overflow-gives-automatic-min-0 flexbox quirk as .sidebar-ad-slot
     above — flex-shrink:0 keeps this at its real content size so its
     sibling .chat-stage (which already has min-height:0 and its own
     internal scroll on .chat) absorbs the squeeze instead, rather than
     both splitting it and silently cropping the ad. */
  flex-shrink: 0;
  /* Unlike the sidebar, there's no scroll container above this to fall
     back on — .chat-float-body doesn't scroll, and .composer-wrap (its
     sibling right below this) is the one thing that must always stay
     reachable. A tall chat placement (e.g. a 300x600 half-page spec) could
     otherwise claim more height than the window has room for and push the
     composer off-screen entirely with no way to scroll back to it. Capping
     here (rather than trusting .chat-stage's shrink-to-0 to always leave
     enough room) guarantees the composer stays visible regardless of what
     size a variant declares. */
  max-height: 320px;
}
.chat-ad-slot:empty { display: none; padding: 0; }

/* Manual A-B preview switcher, only ever mounted behind ?adtest=1 (see
   initAdTestSwitcher in app.js) — reuses the same floating-popover look as
   .workspace-save-popover so it doesn't need its own design language. */
.ad-test-switcher {
  position: fixed;
  right: 16px;
  bottom: 16px;
  width: 280px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  padding: 10px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  z-index: 2100;
}
.ad-test-switcher-title {
  margin: 0 0 2px;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.ad-test-switcher-option {
  text-align: left;
  padding: 7px 9px;
  border-radius: 7px;
  border: 1px solid var(--panel-border);
  background: var(--panel-bg-elevated, var(--panel-bg));
  color: var(--panel-text);
  font-size: 12.5px;
  cursor: pointer;
}
.ad-test-switcher-option:hover { border-color: var(--accent); }
.ad-test-switcher-option.active { border-color: var(--accent); color: var(--accent); font-weight: 600; }

/* Sidebar */
.sidebar {
  width: var(--sidebar-w, 260px);
  flex-shrink: 0;
  background: var(--bg-sidebar);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  padding: 16px 12px;
  position: relative;
  transition: width 0.15s ease;
  /* None of the sections below reserve their own scroll, so a sidebar
     that's grown taller than the viewport (e.g. the 300x600 half-page
     Coinzilla slot on a short window) previously just clipped everything
     past the fold with no way to reach it — body/.app both use
     overflow:hidden by design (see body's comment above), so the sidebar
     needs to be the one place that scrolls internally instead. */
  overflow-y: auto;
}
.sidebar.no-transition { transition: none; }

/* Mirrors .dashboard-resize (a draggable strip on the panel's inner edge) —
   the sidebar's inner edge is its right side rather than its left. */
.sidebar-resize {
  position: absolute;
  right: -3px;
  top: 0;
  bottom: 0;
  width: 6px;
  cursor: ew-resize;
  z-index: 5;
}
.sidebar-resize:hover, .sidebar-resize.active { background: var(--accent-dim); opacity: 0.5; }

/* Collapsed: mirrors .dashboard.collapsed's icon-rail treatment — shrink to
   a narrow strip holding just the toggle, hide everything else rather than
   unmounting it, so re-expanding needs no re-render. */
.sidebar.collapsed {
  width: 44px;
  min-width: 44px;
  padding: 16px 6px;
  overflow: hidden;
}
.sidebar.collapsed .brand-name,
.sidebar.collapsed .new-chat-btn,
.sidebar.collapsed .language-picker,
.sidebar.collapsed .theme-picker-btn,
.sidebar.collapsed .sidebar-section,
.sidebar.collapsed .sidebar-resize {
  display: none;
}
.sidebar.collapsed .sidebar-header {
  border-bottom: none;
  padding-bottom: 0;
  margin-bottom: 0;
}
.sidebar.collapsed .brand { justify-content: center; }
.sidebar.collapsed .sidebar-collapse-btn svg { transform: rotate(180deg); }

.sidebar-header {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 16px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 600;
  font-size: calc(15px * var(--ui-font-scale));
  padding: 4px;
}
.brand-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sidebar-collapse-btn {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-dim);
  width: 22px;
  height: 22px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
}
.sidebar-collapse-btn:hover { color: var(--text); background: var(--bg-elevated); }
.sidebar-collapse-btn svg { transition: transform 0.15s ease; }

.brand-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 8px var(--accent);
}

.new-chat-btn {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 10px;
  padding: 10px 12px;
  font-size: calc(13px * var(--ui-font-scale));
  cursor: pointer;
  text-align: left;
  transition: background 0.15s;
}
.new-chat-btn:hover { background: var(--border); }

.language-picker {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  color: var(--text-dim);
  font-size: 12px;
  padding: 0 4px;
}
.language-picker select {
  min-width: 112px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--bg-elevated);
  color: var(--text);
  padding: 5px 7px;
  font: inherit;
}

.theme-picker-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-elevated);
  color: var(--text);
  padding: 8px 12px;
  font-size: calc(12.5px * var(--ui-font-scale));
  cursor: pointer;
}
.theme-picker-btn:hover { border-color: var(--accent); }

.sidebar-section { display: flex; flex-direction: column; gap: 4px; }

.sidebar-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-size: calc(11px * var(--ui-font-scale));
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  padding: 8px 8px 4px;
}

.quick-actions-more {
  background: transparent;
  border: none;
  color: var(--text-dim);
  font-size: 11px;
  text-transform: none;
  letter-spacing: normal;
  padding: 2px 6px;
  border-radius: 6px;
  cursor: pointer;
}
.quick-actions-more:hover { background: var(--bg-elevated); color: var(--text); }

.sidebar-label-add {
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  line-height: 1;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--text-dim);
  font-size: 14px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.sidebar-label-add:hover { background: var(--bg-elevated); color: var(--text); }

/* Saved dashboards otherwise reads as just another plain sidebar list next
   to Chats/Quick actions — a light accent-tinted card (plus the accent label
   + icon below) sets it apart as its own, cross-chat feature rather than
   another list of items. Margin keeps it from butting up against Chats/Quick
   actions; padding is deliberately roomier than a plain .sidebar-section so
   the card doesn't collapse into a cramped pill when the list is short (or,
   with nothing saved yet, empty — see .saved-dashboards-empty). */
.saved-dashboards-section {
  background: rgba(var(--accent-rgb), 0.05);
  border: 1px solid rgba(var(--accent-rgb), 0.22);
  border-radius: 10px;
  padding: 6px 6px 8px;
  margin: 6px 0;
}
.sidebar-label-accent { color: var(--accent); padding: 2px 2px 6px; }
/* A <button> (not the old plain <span>) so the whole row toggles the
   section's collapsed state — sibling to .sidebar-label-add, not nested
   inside it, so the two never fight over the same click. */
.sidebar-label-title {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  flex: 1;
  border: none;
  background: transparent;
  padding: 0;
  font: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  color: inherit;
  cursor: pointer;
}
.sidebar-label-title span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sidebar-label-icon { flex-shrink: 0; color: var(--accent); }
.sidebar-label-chevron { flex-shrink: 0; margin-left: auto; color: var(--accent); opacity: .7; transition: transform .15s; }
.saved-dashboards-section.collapsed .sidebar-label-chevron { transform: rotate(-90deg); }
.saved-dashboards-section.collapsed .saved-dashboards-list { display: none; }
.saved-dashboards-section .sidebar-label-add { color: var(--accent); opacity: .75; }
.saved-dashboards-section .sidebar-label-add:hover { background: rgba(var(--accent-rgb), 0.16); color: var(--accent); opacity: 1; }
.saved-dashboards-empty {
  padding: 6px 8px;
  font-size: calc(12.5px * var(--ui-font-scale));
  color: var(--text-dim);
  line-height: 1.4;
}

.quick-actions-list { display: flex; flex-direction: column; gap: 4px; }
.quick-action-group-label {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  opacity: 0.7;
  padding: 6px 10px 2px;
}
.quick-action-group-label:first-child { padding-top: 0; }
.quick-action-row { display: flex; align-items: center; gap: 2px; }
.quick-action-row .quick-action { flex: 1; min-width: 0; }
.quick-action-remove, .quick-action-edit {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  border-radius: 6px;
  border: none;
  background: transparent;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  display: none;
  align-items: center;
  justify-content: center;
}
.quick-action-edit { font-size: 11px; }
.quick-action-row:hover .quick-action-remove, .quick-action-row:hover .quick-action-edit { display: flex; }
.quick-action-remove:hover { color: var(--danger); background: var(--danger-bg); }
.quick-action-edit:hover { color: var(--text); background: var(--bg-elevated); }

.saved-dashboards-list { display: flex; flex-direction: column; gap: 2px; }
.saved-dashboard-item {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 8px 6px 8px 10px;
  border-radius: 8px;
  cursor: pointer;
  color: var(--text-dim);
}
.saved-dashboard-item:hover { background: var(--bg-elevated); }
.saved-dashboard-item.active { background: var(--bg-elevated); color: var(--text); }
.saved-dashboard-item-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.saved-dashboard-item-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: calc(13.5px * var(--ui-font-scale));
}
.saved-dashboard-item-linked-chat {
  align-self: flex-start;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: calc(11px * var(--ui-font-scale));
  color: var(--text-dim);
  background: transparent;
  border: none;
  padding: 0;
  cursor: pointer;
}
.saved-dashboard-item-linked-chat:hover { color: var(--accent); text-decoration: underline; }
.saved-dashboard-item-linked-chat:disabled { cursor: default; text-decoration: none; opacity: .6; }
.saved-dashboard-item-linked-chat:disabled:hover { color: var(--text-dim); }
.saved-dashboard-item-folder,
.saved-dashboard-item-link,
.saved-dashboard-item-share,
.saved-dashboard-item-close {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  border-radius: 6px;
  border: none;
  background: transparent;
  color: var(--text-dim);
  font-size: 11px;
  line-height: 1;
  cursor: pointer;
  display: none;
  align-items: center;
  justify-content: center;
}
.saved-dashboard-item-folder { filter: grayscale(1); opacity: .7; }
.saved-dashboard-item-folder.in-folder { display: flex; filter: none; opacity: 1; color: var(--accent); }
.saved-dashboard-item-link { filter: grayscale(1); opacity: .7; }
.saved-dashboard-item-link.has-link { display: flex; }
.saved-dashboard-item-link.linked { filter: none; opacity: 1; color: var(--accent); }
.saved-dashboard-item:hover .saved-dashboard-item-folder,
.saved-dashboard-item:hover .saved-dashboard-item-link,
.saved-dashboard-item:hover .saved-dashboard-item-share,
.saved-dashboard-item:hover .saved-dashboard-item-close,
.saved-dashboard-item.active .saved-dashboard-item-folder,
.saved-dashboard-item.active .saved-dashboard-item-link { display: flex; }
.saved-dashboard-item-folder:hover { background: var(--bg-elevated); opacity: 1; }
.saved-dashboard-item-link:hover { background: var(--bg-elevated); opacity: 1; }
.saved-dashboard-item-share:hover { background: var(--bg-elevated); color: var(--text); }
.saved-dashboard-item-close:hover { color: var(--danger); background: var(--danger-bg); }

/* Shared by both kinds of collapsible group in the unified "Saved" list (see
   buildSavedGroupDetails in app.js): a dashboard folder, and — under the
   "All" filter — a Chat/Dashboard/Wireframe/Theme type section. Plain
   <details>/<summary> — free keyboard toggling and no JS needed for the
   open/closed visual state itself, just persisting the choice (the 'toggle'
   listener). Marker removed and replaced by a chevron that rotates the same
   way the section-level one does, so both read as the same affordance. */
.saved-group { margin-top: 2px; }
.saved-group-summary {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 6px 6px 4px;
  border-radius: 6px;
  cursor: pointer;
  color: var(--text-dim);
  font-size: calc(12px * var(--ui-font-scale));
  list-style: none;
}
.saved-group-summary::-webkit-details-marker { display: none; }
.saved-group-summary:hover { background: var(--bg-elevated); }
.saved-group-summary::before {
  content: '';
  flex-shrink: 0;
  width: 8px;
  height: 8px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform .15s;
  opacity: .6;
}
.saved-group[open] > .saved-group-summary::before { transform: rotate(45deg); }
.saved-group-icon {
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-dim);
}
.saved-group-icon svg { width: 12px; height: 12px; }
.saved-group-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.saved-group-count { flex-shrink: 0; opacity: .6; font-variant-numeric: tabular-nums; }
.saved-group-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-left: 14px;
  border-left: 1px solid rgba(var(--accent-rgb), 0.18);
  margin-left: 8px;
}

/* "All" plus one chip per saved-item type (see renderSavedItemFilters) —
   lets someone narrow the list to just their saved themes, wireframes, etc. */
.saved-item-filters { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 2px 6px; }
.saved-item-filter-chip {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  border-radius: 999px;
  border: 1px solid rgba(var(--accent-rgb), 0.22);
  background: transparent;
  color: var(--text-dim);
  font-size: calc(11px * var(--ui-font-scale));
  cursor: pointer;
}
.saved-item-filter-chip:hover { background: rgba(var(--accent-rgb), 0.1); }
.saved-item-filter-chip.active { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
/* Type chips (Chat/Dashboard/Wireframe/Theme) drop their label and go
   icon-only + circular so all four fit on one row next to "All" instead of
   wrapping to a second line; the label survives as the chip's title/
   aria-label tooltip. */
.saved-item-filter-chip.icon-only { width: 24px; height: 24px; padding: 0; justify-content: center; border-radius: 50%; }
.saved-item-filter-icon { flex-shrink: 0; display: flex; align-items: center; justify-content: center; width: 12px; height: 12px; }
.saved-item-filter-icon svg { width: 11px; height: 11px; }

/* Wireframe/theme/chat rows in the unified "Saved" list — a lighter-weight
   sibling of .saved-dashboard-item (which keeps its own bespoke
   folder/link/share/close controls) for the three types that only need a
   click-to-apply action plus, for wireframes/themes, a couple of icon
   buttons. */
.saved-item-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 6px 7px 8px;
  border-radius: 8px;
  cursor: pointer;
  color: var(--text-dim);
}
.saved-item-row:hover { background: var(--bg-elevated); }
.saved-item-row.active { background: var(--bg-elevated); color: var(--text); }
.saved-item-row-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: calc(13.5px * var(--ui-font-scale));
}
.saved-item-row-action {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  border-radius: 6px;
  border: none;
  background: transparent;
  color: var(--text-dim);
  font-size: 11px;
  line-height: 1;
  cursor: pointer;
  display: none;
  align-items: center;
  justify-content: center;
}
.saved-item-row:hover .saved-item-row-action { display: flex; }
.saved-item-row-action:hover { background: var(--bg-elevated); color: var(--text); }
.saved-item-row-action.danger:hover { color: var(--danger); background: var(--danger-bg); }

.chat-list-section { max-height: 40vh; overflow-y: auto; }
.chat-list { display: flex; flex-direction: column; gap: 2px; }
.chat-list-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 6px 8px 10px;
  border-radius: 8px;
  cursor: pointer;
  color: var(--text-dim);
}
.chat-list-item:hover { background: var(--bg-elevated); }
.chat-list-item.active { background: var(--bg-elevated); color: var(--text); }
.chat-list-item-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: calc(13.5px * var(--ui-font-scale));
}
.chat-list-item-pin,
.chat-list-item-clear,
.chat-list-item-close {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  border-radius: 6px;
  border: none;
  background: transparent;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  display: none;
  align-items: center;
  justify-content: center;
}
.chat-list-item-pin {
  font-size: 11px;
  filter: grayscale(1);
  opacity: .7;
}
.chat-list-item-clear {
  font-size: 11px;
  filter: grayscale(1);
  opacity: .7;
}
.chat-list-item-pin.pinned { display: flex; filter: none; opacity: 1; }
.chat-list-item:hover .chat-list-item-pin,
.chat-list-item:hover .chat-list-item-clear,
.chat-list-item:hover .chat-list-item-close,
.chat-list-item.active .chat-list-item-pin { display: flex; }
.chat-list-item-pin:hover { background: var(--bg-elevated); opacity: 1; }
.chat-list-item-clear:hover { background: var(--bg-elevated); opacity: 1; filter: none; }
.chat-list-item-close:hover { color: var(--danger); background: var(--danger-bg); }

.quick-action {
  background: transparent;
  border: none;
  color: var(--text);
  text-align: left;
  padding: 6px 10px;
  border-radius: 8px;
  font-size: calc(12.5px * var(--ui-font-scale));
  cursor: pointer;
  transition: background 0.12s;
}
.quick-action:hover { background: var(--bg-elevated); }

.quick-menu {
  position: fixed;
  display: none;
  flex-direction: column;
  gap: 2px;
  min-width: 200px;
  max-width: 280px;
  max-height: 70vh;
  overflow-y: auto;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 6px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
  z-index: 60;
}
.quick-menu.show { display: flex; }
.quick-menu-item {
  background: transparent;
  border: none;
  color: var(--text);
  text-align: left;
  padding: 8px 10px;
  border-radius: 8px;
  font-size: 13.5px;
  cursor: pointer;
}
.quick-menu-item:hover { background: var(--border); }
.quick-menu-note {
  padding: 6px 10px 4px;
  font-size: 12px;
  color: var(--text-dim);
  max-width: 240px;
}

.sidebar-footer {
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.env-pill {
  display: inline-block;
  font-size: 11.5px;
  color: var(--text-dim);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  padding: 5px 10px;
  border-radius: 999px;
}
.env-pill.live { color: var(--status-green); border-color: rgba(126, 231, 135, 0.35); }
.env-pill.err { color: var(--danger); border-color: var(--danger-bg); }

/* Workspace: a tmux-like tiling canvas. Pinned widgets and extra chat panes
   live here as freely draggable/resizable tiles, independent of the main
   chat's own scroll/clearing. The aside itself can also be dragged narrower
   or wider from its left edge. */
.dashboard {
  /* 720px (was 560px) so a fresh session opens with the workspace already
     favored over chat — only the fallback for sessions with no saved
     dashboardWidth pref; a manual drag (ui-prefs.js's setDashboardWidth)
     always overrides it. */
  width: var(--workspace-w, 720px);
  min-width: 320px;
  max-width: 92vw;
  flex-shrink: 0;
  background: var(--bg-dashboard);
  border-left: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  position: relative;
  transition: width 0.15s ease;
  overflow: hidden;
}
.dashboard.no-transition { transition: none; }
.dashboard.collapsed {
  width: 44px;
  min-width: 44px;
}
.dashboard.collapsed .dashboard-title,
.dashboard.collapsed .dashboard-fullscreen-btn,
.dashboard.collapsed .workspace-save-menu,
.dashboard.collapsed .dashboard-tradingview-widgets,
.dashboard.collapsed .workspace-grid-picker,
.dashboard.collapsed .dashboard-more-btn,
.dashboard.collapsed .dashboard-header-secondary,
.dashboard.collapsed .dashboard-body,
.dashboard.collapsed .dashboard-resize {
  display: none;
}
.dashboard.collapsed .dashboard-toggle svg {
  transform: rotate(180deg);
}
.dashboard-resize {
  position: absolute;
  left: -3px;
  top: 0;
  bottom: 0;
  width: 6px;
  cursor: ew-resize;
  z-index: 5;
}
.dashboard-resize:hover, .dashboard-resize.active { background: var(--accent-dim); opacity: 0.5; }

/* Fullscreen workspace: native Fullscreen API on the aside itself when
   available (mirrors the per-tile fullscreen button), falling back to a
   fixed-position overlay class when the browser rejects the request. */
.dashboard:fullscreen {
  width: 100vw !important;
  height: 100vh !important;
  border: 0;
}
.dashboard:fullscreen .dashboard-resize { display: none; }
.dashboard.workspace-maximized {
  position: fixed;
  inset: 0;
  width: 100vw !important;
  height: 100vh !important;
  max-width: 100vw;
  z-index: 1000;
  border: 0;
}
.dashboard.workspace-maximized .dashboard-resize { display: none; }
.icon-btn .icon-compress { display: none; }
body.workspace-fullscreen-active .icon-expand { display: none; }
body.workspace-fullscreen-active .icon-compress { display: inline; }
/* Collapsing the workspace while it IS the fullscreen surface just hides
   its contents behind a still-fullscreen blank pane — not a useful state.
   The floating chat handle (above) already covers "get chat back". */
body.workspace-fullscreen-active .dashboard-toggle { display: none; }

/* Fullscreen chat: same native-Fullscreen-API-with-CSS-fallback pattern as
   the workspace above, targeting #mainChat instead of #dashboard. */
.main:fullscreen {
  width: 100vw !important;
  height: 100vh !important;
  border: 0;
}
.main:fullscreen .chat-float-resize { display: none; }
.main.chat-maximized {
  position: fixed;
  inset: 0;
  width: 100vw !important;
  height: 100vh !important;
  max-width: 100vw;
  z-index: 1000;
  border: 0;
}
.main.chat-maximized .chat-float-resize { display: none; }

/* Chat as a floating overlay while the workspace is fullscreen. #mainChat is
   reparented inside #dashboard for this (see syncChatFloatState in app.js) —
   these rules only take effect once it's actually there, since a
   position:fixed descendant of the fullscreen element renders relative to
   its bounds (i.e. the full viewport), same as the workspace-maximized
   fallback which is itself inset:0. */
.chat-float-handle { display: none; }
body.workspace-fullscreen-active #mainChat {
  position: fixed;
  top: 16px;
  left: auto;
  right: 16px;
  width: min(400px, calc(100vw - 32px));
  height: min(720px, calc(100vh - 32px));
  min-width: 0;
  z-index: 1001;
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
  overflow: hidden;
  /* Smooth collapse/expand and drag/resize settle — turned off below while
     a drag or resize is actively in progress, where it would only fight the
     pointermove updates and make the panel feel laggy. */
  transition: width 0.2s ease, height 0.2s ease, left 0.2s ease, top 0.2s ease, right 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
/* Collapsed chat doesn't float as a pill anymore — its composer is moved
   into the workspace header instead (see #dashboardChatBar and
   toggleChatFloatCollapse in app.js), so the panel itself just disappears. */
body.workspace-fullscreen-active #mainChat.chat-float-collapsed { display: none; }
/* Reparented into #dashboard while fullscreen is active (see
   syncWidgetPickerFullscreenParent in app.js) — needs a z-index above the
   floating chat panel (1001) so it can still cover the whole workspace,
   chat included, same as it does outside fullscreen. */
body.workspace-fullscreen-active #tradingViewWidgetsBackdrop,
body.workspace-fullscreen-active #chartPreferencesBackdrop,
body.workspace-fullscreen-active #newsWidgetPreferencesBackdrop,
body.workspace-fullscreen-active #cryptoPanicPreferencesBackdrop,
body.workspace-fullscreen-active #applyDefaultsBackdrop,
body.workspace-fullscreen-active #rapidMoversBackdrop,
body.workspace-fullscreen-active #rapidMoversStopBackdrop {
  z-index: 1002;
}
body.workspace-fullscreen-active #mainChat.chat-float-dragging {
  transition: none;
  user-select: none;
}
body.workspace-fullscreen-active .chat-float-handle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  background: var(--bg-elevated);
  flex-shrink: 0;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-dim);
  cursor: grab;
  touch-action: none;
  transition: background 0.2s ease, color 0.2s ease;
}
body.workspace-fullscreen-active #mainChat.chat-float-dragging .chat-float-handle { cursor: grabbing; }
.chat-float-handle #chatFloatCollapseBtn svg { transition: transform 0.15s ease; }
.chat-float-body {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
  /* Safety net, not the primary fix — #chatAdSlot's own max-height (see
     styles.css) is what's supposed to guarantee .composer-wrap always has
     room. This is a second line of defense for anything that still
     doesn't leave enough (a narrow window, browser zoom, subpixel
     rounding on some displays): rather than the composer becoming
     permanently unreachable with nothing to scroll, the whole column
     becomes scrollable so it can always be reached. .chat's own
     overflow-y:auto (the message transcript) still handles normal
     scrolling on its own — this only engages when even *that* shrinking
     to fit isn't enough. */
  overflow-y: auto;
}
.chat-float-resize {
  display: none;
  position: absolute;
  right: 0;
  bottom: 0;
  width: 18px;
  height: 18px;
  cursor: nwse-resize;
  touch-action: none;
  z-index: 2;
}
.chat-float-resize::after {
  content: '';
  position: absolute;
  right: 4px;
  bottom: 4px;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--border);
  border-bottom: 2px solid var(--border);
  border-radius: 0 0 2px 0;
}
.chat-float-resize:hover::after { border-color: var(--accent); }
body.workspace-fullscreen-active #mainChat:not(.chat-float-collapsed) .chat-float-resize { display: block; }

.dashboard-header {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 6px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  position: relative;
}
.dashboard-toggle {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-dim);
  width: 26px;
  height: 26px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
}
.dashboard-toggle:hover { color: var(--text); background: var(--bg-elevated); }
.dashboard-toggle svg { transition: transform 0.15s ease; }
.dashboard-title {
  font-weight: 600;
  font-size: 13.5px;
  flex: 1 1 auto;
  min-width: 30px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Collapsed-chat composer, relocated into the workspace header instead of
   floating over the grid (see toggleChatFloatCollapse in app.js, which
   reparents the real #composerWrap node here) — lets the workspace be
   redirected without reopening the full chat panel. container-type/name
   mirror .main so the composer's own @container chat-column rules (hides
   the history hint, shrinks the chat/workspace toggle) still kick in at
   this width. */
.dashboard-chat-bar {
  display: none;
  align-items: center;
  gap: 6px;
  flex: 1 1 220px;
  min-width: 0;
  container-type: inline-size;
  container-name: chat-column;
}
.dashboard-chat-bar.active { display: flex; }
.dashboard-header.chat-bar-active .dashboard-title {
  flex: 0 1 auto;
  max-width: 34%;
}
.dashboard-chat-expand-btn { flex-shrink: 0; }
.dashboard-chat-bar .composer-wrap {
  max-width: none;
  width: 100%;
  margin: 0;
}
.dashboard-chat-bar .composer {
  padding: 4px 6px 4px 14px;
  border-radius: 10px;
}
/* Suggestion/history popovers normally open upward above the composer —
   flipped to open downward here since this composer now sits at the very
   top of the screen instead of the bottom. */
.dashboard-chat-bar .ac-panel,
.dashboard-chat-bar .history-panel {
  top: 100%;
  bottom: auto;
  margin-top: 8px;
  margin-bottom: 0;
  z-index: 42;
}

/* Mirrors the follow-up chips renderFollowupSuggestions just built (app.js)
   into a slim strip under the workspace header whenever the chat panel is
   docked/collapsed in fullscreen — those chips normally render inside
   #mainChat's .bubble, which sits display:none while docked (see the
   body.workspace-fullscreen-active #mainChat.chat-float-collapsed rule
   above), so without this mirror they'd be fully built but unreachable.
   syncDockedFollowupBar in app.js owns showing/hiding it. Scoped chip styling
   below follows the same pattern as #themeSyncChartsPrompt further down this
   file — .chart-prompt-btn is otherwise only styled under .bubble. */
.dashboard-followup-bar {
  display: none;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-bottom: 1px solid var(--border);
  background: var(--bg-sidebar);
}
.dashboard-followup-bar.active { display: flex; }
.dashboard-followup-label {
  flex-shrink: 0;
  font-size: 11.5px;
  color: var(--text-dim);
  white-space: nowrap;
}
.dashboard-followup-chips {
  display: flex;
  gap: 6px;
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
}
.dashboard-followup-chips .chart-prompt-btn {
  flex-shrink: 0;
  border-radius: 999px;
  padding: 4px 12px;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  white-space: nowrap;
  transition: background 0.15s, border-color 0.15s;
}
.dashboard-followup-chips .chart-prompt-btn:hover { filter: brightness(1.12); }
.dashboard-followup-dismiss { flex-shrink: 0; }

/* Mirrors showClassicMigrationWelcome's in-chat card (app.js) into the same
   slim strip .dashboard-followup-bar uses, for the same reason: while the
   chat panel is docked/collapsed in fullscreen, the real card sits
   display:none inside #mainChat and would otherwise be silently invisible
   right when a classic migration lands. Unlike the followup bar (arbitrary
   chip list), this one's shape is fixed — message + Back to classic + Got
   it — so it's its own element rather than a generic content slot. */
.dashboard-notice-bar {
  display: none;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-bottom: 1px solid var(--border);
  background: var(--bg-sidebar);
}
.dashboard-notice-bar.active { display: flex; }
.dashboard-notice-text {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 12px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dashboard-notice-bar .chart-prompt-btn {
  flex-shrink: 0;
  border-radius: 999px;
  padding: 4px 12px;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  white-space: nowrap;
  text-decoration: none;
  transition: background 0.15s, border-color 0.15s;
}
.dashboard-notice-bar .chart-prompt-btn.yes { background: var(--accent-dim); border-color: var(--accent); color: #fff; }
.dashboard-notice-bar .chart-prompt-btn:hover { filter: brightness(1.12); }

/* Small square icon-only buttons used throughout the workspace header and
   its overflow menu — symbols instead of text keep the header a single
   compact row no matter how many controls are available. */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  flex-shrink: 0;
}
.icon-btn:hover:not(:disabled) { color: var(--text); background: var(--bg-elevated); }
.icon-btn:disabled { opacity: 0.4; cursor: default; }
.icon-btn.active,
.icon-btn[aria-pressed="true"] { background: var(--accent-dim); color: #fff; border-color: var(--accent-dim); }
.icon-btn-success { background: var(--accent-dim) !important; color: #fff !important; border-color: var(--accent-dim) !important; }

/* Classic mode: only shown once workspaceClassicViewBtn is toggled on (see
   applyActiveViewMode in app.js) — a compact text button next to it rather
   than another icon glyph, since "Save now" needs to read unambiguously as
   an overwrite, not a new-snapshot save like the header Save button next to it. */
.dashboard-classic-save-now-btn {
  flex-shrink: 0;
  border: 1px solid var(--border);
  border-radius: 7px;
  padding: 0 10px;
  height: 28px;
  background: var(--accent-dim);
  color: #fff;
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.dashboard-classic-save-now-btn:hover { filter: brightness(1.08); }

/* Classic mode's escape hatch (see applyActiveViewMode) — a plain outline
   link, not filled like Save now, since it navigates away rather than acting
   on the current grid. Always available while in Classic mode so nobody who
   lands here feels stuck. */
.dashboard-classic-back-link {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--border);
  border-radius: 7px;
  padding: 0 10px;
  height: 28px;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 12px;
  text-decoration: none;
}
.dashboard-classic-back-link:hover { color: var(--text); background: var(--bg-elevated); }
/* Author `display` beats the UA stylesheet's `[hidden] { display: none }` at
   equal specificity, so the inline-flex rule above needs its own [hidden]
   override or the link stays visible even with the hidden attribute set. */
.dashboard-classic-back-link[hidden] { display: none; }
.workspace-view-delete:hover:not(:disabled) { color: var(--danger); background: var(--danger-bg); }

/* Everything besides the always-visible controls in the header row lives
   behind this "More" popover so the header never wraps to multiple rows,
   regardless of viewport width. */
.dashboard-more-btn { display: inline-flex; }
.dashboard-header-secondary {
  display: none;
  position: absolute;
  top: 100%;
  right: 10px;
  width: 260px;
  max-width: calc(100vw - 20px);
  flex-direction: column;
  align-items: stretch;
  gap: 3px;
  margin-top: 6px;
  padding: 8px;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  z-index: 42;
}
.dashboard-header-secondary.open { display: flex; }
.dashboard-menu-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 7px 8px;
  border: none;
  border-radius: 7px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
  text-align: left;
}
button.dashboard-menu-row:hover:not(:disabled) { background: var(--bg-elevated); }
button.dashboard-menu-row[aria-pressed="true"] { background: var(--accent-dim); color: #fff; }
.dashboard-menu-row-danger:hover { color: var(--danger); background: var(--danger-bg); }
.dashboard-menu-row-label { flex: 1 1 auto; }
label.dashboard-menu-row { cursor: default; color: var(--text-dim); }
.dashboard-menu-row select {
  flex: 0 0 auto;
  max-width: 130px;
  border: 1px solid var(--border);
  border-radius: 7px;
  padding: 5px 6px;
  background: var(--bg-elevated);
  color: var(--text);
  font: inherit;
  font-size: 11.5px;
}
.workspace-grid-picker {
  position: relative;
}
.workspace-grid-toggle {
  display: flex;
  align-items: center;
  gap: 5px;
  border: 1px solid var(--border);
  border-radius: 7px;
  padding: 5px 8px;
  background: var(--bg-elevated);
  color: var(--text);
  font: inherit;
  font-size: 11.5px;
  white-space: nowrap;
  cursor: pointer;
}
.workspace-grid-toggle:hover,
.workspace-grid-toggle[aria-expanded="true"] { border-color: var(--accent); outline: none; }
.workspace-grid-popover {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  width: 196px;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  padding: 10px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  z-index: 40;
}
/* Zoom in/out/reset for the workspace canvas itself (see .dashboard-widgets'
   var(--workspace-zoom) transform) — lets a grid that's too tall/wide for
   the viewport (e.g. a locked 3x3) shrink to fit without resorting to the
   browser's own page zoom, which would also shrink the sidebar and chat. */
.workspace-zoom-controls {
  display: flex;
  align-items: center;
  gap: 2px;
  border: 1px solid var(--border);
  border-radius: 7px;
  padding: 2px;
  background: var(--bg-elevated);
  flex-shrink: 0;
}
.workspace-zoom-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
}
.workspace-zoom-btn:hover:not(:disabled) { color: var(--text); background: var(--panel-bg); }
.workspace-zoom-btn:disabled { opacity: 0.35; cursor: default; }
.workspace-zoom-label {
  border: 0;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 10.5px;
  font-family: var(--mono);
  padding: 0 4px;
  cursor: pointer;
  white-space: nowrap;
  min-width: 34px;
  text-align: center;
}
.workspace-zoom-label:hover { color: var(--text); }
.workspace-save-menu {
  position: relative;
}
.workspace-save-popover {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  width: 248px;
  flex-direction: column;
  gap: 7px;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  padding: 10px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  z-index: 40;
}
.workspace-save-popover:not([hidden]) { display: flex; }
/* Sidebar's "+" sits near the sidebar's right inner edge, so the popover
   hangs from its right edge (like .chat-share-popover) rather than
   overflowing out past the sidebar with the header button's left:0. */
.sidebar-save-popover { left: auto; right: 0; }
.workspace-save-popover-title {
  margin: 0;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* Shared by the sidebar's Save popover (#sidebarSaveOptions) and the "save
   this" chat command's inline picker (see renderSaveOptionRows in app.js) —
   one builder, two places it renders into, so picking what to save always
   looks and behaves the same regardless of where it was triggered from.
   Each row carries a live icon/swatch/mini-wireframe plus a detail line (chat
   name, tile count, theme mode, ...) instead of a bare label, so it's never
   ambiguous what a checked box actually captures. */
.save-option-rows { display: flex; flex-direction: column; gap: 2px; }
.save-option-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 6px;
  border-radius: 7px;
  cursor: pointer;
  color: var(--text);
}
.save-option-row:hover { background: var(--bg-elevated); }
.save-option-row input[type="checkbox"] { flex-shrink: 0; cursor: pointer; }
.save-option-icon {
  flex-shrink: 0;
  width: 28px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-dim);
}
.save-option-icon svg { width: 15px; height: 15px; }
.save-option-icon .grid-preset-icon { width: 26px; height: 18px; }
.save-option-theme-swatch {
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.25);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
}
.save-option-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  gap: 1px;
}
.save-option-label { font-size: 12.5px; }
.save-option-detail {
  font-size: 11px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.save-option-actions { margin-top: 2px; }

.workspace-save-confirm {
  margin-top: 2px;
  border: none;
  border-radius: 7px;
  padding: 7px 8px;
  background: var(--accent-dim);
  color: #fff;
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
}
.workspace-save-confirm:hover { filter: brightness(1.08); }
.workspace-grid-presets {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
}
.grid-preset-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 7px;
  background: transparent;
  cursor: pointer;
}
.grid-preset-btn:hover,
.grid-preset-btn:focus-visible { border-color: var(--accent); outline: none; }
.grid-preset-btn.active {
  border-color: var(--accent-dim);
  background: color-mix(in srgb, var(--accent-dim) 18%, transparent);
}
.grid-preset-icon {
  display: grid;
  gap: 2px;
  width: 28px;
  height: 20px;
}
.grid-preset-icon i {
  display: block;
  font-style: normal;
  border-radius: 1px;
  background: var(--text-dim);
}
.grid-preset-btn.active .grid-preset-icon i { background: var(--accent); }
.grid-preset-free-btn {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 6px 7px;
  font-size: 11px;
  color: var(--text-dim);
}
.grid-preset-free-btn.active {
  color: var(--accent);
  border-color: var(--accent-dim);
  background: color-mix(in srgb, var(--accent-dim) 18%, transparent);
}
.grid-preset-free-icon { width: 16px; height: 16px; flex: none; }
.workspace-grid-custom-layouts {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--panel-border-soft, var(--border));
}
.grid-preset-custom-btn { position: relative; }
.grid-preset-icon-custom { position: relative; }
.grid-preset-icon-custom i {
  position: absolute;
  box-sizing: border-box;
  border: 1px solid var(--bg-elevated);
}
.grid-preset-add-icon { width: 14px; height: 14px; color: var(--text-dim); }
.grid-preset-custom-delete {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--panel-bg);
  color: var(--text-dim);
  font-size: 11px;
  line-height: 1;
  cursor: pointer;
  display: none;
  align-items: center;
  justify-content: center;
}
.grid-preset-custom-btn:hover .grid-preset-custom-delete { display: flex; }
.grid-preset-custom-delete:hover { color: var(--danger); background: var(--danger-bg); border-color: var(--danger); }
.workspace-grid-custom {
  display: flex;
  gap: 8px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--panel-border-soft, var(--border));
}
.workspace-grid-custom label {
  display: flex;
  flex-direction: column;
  gap: 3px;
  flex: 1;
  color: var(--text-dim);
  font-size: 10.5px;
}
.workspace-grid-custom input {
  width: 100%;
  min-width: 0;
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 4px 3px;
  background: var(--bg-elevated);
  color: var(--text);
  font: inherit;
  text-align: center;
}
.workspace-grid-gap {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--panel-border-soft, var(--border));
}
.workspace-grid-gap-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 8px;
  color: var(--text-dim);
  font-size: 10.5px;
}
.workspace-grid-gap-value {
  font-variant-numeric: tabular-nums;
  color: var(--text);
  font-weight: 600;
}
.workspace-grid-gap-control {
  display: flex;
  align-items: center;
  gap: 8px;
}
.workspace-grid-gap-step {
  flex: none;
  width: 26px;
  height: 26px;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--bg-elevated);
  color: var(--text);
  font-size: 15px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.workspace-grid-gap-step:hover { border-color: var(--accent); color: var(--accent); }
.workspace-grid-gap-step:active { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
/* Custom track/thumb so the slider matches app theming in both engines
   (Chromium and Firefox style range inputs via entirely separate
   pseudo-elements — there's no shorthand that covers both). Thumb is
   sized well past the 24px touch-target minimum for mobile dragging. */
.workspace-grid-gap-slider {
  flex: 1;
  min-width: 0;
  height: 28px;
  margin: 0;
  background: transparent;
  -webkit-appearance: none;
  appearance: none;
}
.workspace-grid-gap-slider::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: var(--border);
}
.workspace-grid-gap-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  margin-top: -10px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--accent-text, #fff);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  cursor: pointer;
}
.workspace-grid-gap-slider::-moz-range-track {
  height: 4px;
  border-radius: 2px;
  background: var(--border);
}
.workspace-grid-gap-slider::-moz-range-thumb {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--accent-text, #fff);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  cursor: pointer;
}
.workspace-grid-gap-slider:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--accent); outline-offset: 2px; }
.workspace-grid-gap-slider:focus-visible::-moz-range-thumb { outline: 2px solid var(--accent); outline-offset: 2px; }
.workspace-grid-fit-mode {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--panel-border-soft, var(--border));
}
.workspace-grid-fit-mode-label {
  display: block;
  margin-bottom: 6px;
  color: var(--text-dim);
  font-size: 10.5px;
}
/* Each option is a small icon + label chip (mirrors .grid-preset-btn) rather
   than a plain <select>, so the effect of each overflow mode reads visually
   instead of needing to parse six similar sentences of dropdown text. */
.grid-fit-options {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
}
.grid-fit-option {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 3px;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  text-align: center;
}
.grid-fit-option:hover,
.grid-fit-option:focus-visible { border-color: var(--accent); outline: none; }
.grid-fit-option.active {
  color: var(--accent);
  border-color: var(--accent-dim);
  background: color-mix(in srgb, var(--accent-dim) 18%, transparent);
}
.grid-fit-option svg { width: 18px; height: 18px; flex: none; }
.grid-fit-option span {
  font-size: 9px;
  line-height: 1.15;
}
/* Live one-line explanation of whichever chip is active, so the icon's
   meaning is always spelled out in words too — not everyone reads scroll
   direction glyphs the same way at a glance. */
.grid-fit-hint {
  margin: 6px 0 0;
  color: var(--text-dim);
  font-size: 10px;
  line-height: 1.35;
}
.dashboard-body {
  flex: 1;
  min-height: 0;
  overflow: auto;
  /* Reserve the vertical scrollbar's width up front, whether or not it's
     currently showing. Without this, Grid mode's layoutGrid() (workspace.js)
     measures canvas.clientWidth, sizes/positions tiles to fill it exactly,
     and only *then* sets canvas.style.minHeight for the row count — which
     can newly overflow this container and pop a scrollbar into existence
     that steals a few px of width nothing accounted for. layoutGrid never
     re-measures after that, so the rightmost column stays sized for the
     wider pre-scrollbar canvas and its content (e.g. an investing.com
     chart's price axis, hugging that column's own right edge) renders
     partly behind/past the now-narrower visible area. */
  scrollbar-gutter: stable;
  position: relative;
  scrollbar-width: thin;
  scrollbar-color: var(--panel-border) transparent;
}
.dashboard-body::-webkit-scrollbar { width: 10px; height: 10px; }
.dashboard-body::-webkit-scrollbar-track { background: transparent; }
.dashboard-body::-webkit-scrollbar-thumb {
  background: var(--panel-border);
  border-radius: 6px;
  border: 2px solid var(--bg);
  background-clip: padding-box;
}
.dashboard-body::-webkit-scrollbar-thumb:hover { background: var(--text-dim); background-clip: padding-box; }

/* Grid mode's "when it doesn't fit" preference, reflected onto the scroll
   container by app.js's updateDashboardOverflowMode() (see the
   workspaceGridFitOptions chips + workspace.onLayout wiring). Only applied
   while the active chat is actually in grid mode — free/auto mode always
   keeps the plain scroll-both default above. */
.dashboard-body.grid-fit-scroll-h { overflow-x: auto; overflow-y: hidden; }
.dashboard-body.grid-fit-scroll-v { overflow-x: hidden; overflow-y: auto; }
.dashboard-body.grid-fit-page { overflow: hidden; }
/* Auto-resize owns the complete grid geometry in workspace.js, so the outer
   workspace never turns an old tile/canvas height into a second scrollbar.
   Individual panels retain their own compact scroll areas where needed. */
.dashboard-body.grid-fit-auto { overflow: hidden; }
/* A selected wireframe remains the viewport-sized layout. Extra tiles are
   intentionally placed in subsequent rows, so restore vertical reachability
   without allowing a horizontal scrollbar. */
.dashboard-body.grid-fit-auto.grid-fit-auto-surplus { overflow-x: hidden; overflow-y: auto; }

/* Prev/next controls for 'page' fit mode, overlaid on the dashboard body
   rather than scrolling to the edge of whatever's currently paged in. Shown
   only while workspace.getGridPaging() reports an axis with more than one
   page (see updateGridPageArrows in app.js); each pair is disabled at its
   first/last page instead of hidden so the layout doesn't jump. */
.grid-page-arrow {
  position: absolute;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--text);
  cursor: pointer;
  opacity: 0.85;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
}
.grid-page-arrow:hover:not(:disabled) { opacity: 1; border-color: var(--accent); color: var(--accent); }
.grid-page-arrow:disabled { opacity: 0.3; cursor: default; }
.grid-page-arrow[hidden] { display: none; }
.grid-page-arrow-left { left: 10px; top: 50%; transform: translateY(-50%); }
.grid-page-arrow-right { right: 10px; top: 50%; transform: translateY(-50%); }
.grid-page-arrow-up { top: 10px; left: 50%; transform: translateX(-50%); }
.grid-page-arrow-down { bottom: 10px; left: 50%; transform: translateX(-50%); }

/* One .dashboard-pane per chat lives in the stage; only the active chat's
   pane is shown, so each chat keeps its own independent set of pinned tiles
   instead of sharing one workspace across every chat. */
.dashboard-stage {
  position: relative;
  height: 100%;
}
.dashboard-pane {
  display: none;
  position: relative;
  height: 100%;
}
.dashboard-pane.active { display: block; }

.dashboard-empty {
  color: var(--text-dim);
  font-size: 12.5px;
  line-height: 1.6;
  text-align: center;
  padding: 24px 10px;
  position: absolute;
  top: 50%; left: 0; right: 0;
  transform: translateY(-50%);
}
.dashboard-empty-icon {
  vertical-align: -2px;
  margin-right: 2px;
  opacity: 0.8;
}

/* The permanent "add widget" tile (see ensureAddWidgetTile/
   renderAddWidgetTileBody in app.js) — a real Workspace tile, so its
   header/body/close button are all the normal .tile-header/.tile-body/
   .widget-close-btn machinery; only the body content below is custom, a
   condensed version of the full #tradingViewWidgetsBackdrop picker so
   adding a widget never needs the extra click into that modal. Kept
   deliberately muted (no filled accent backgrounds) rather than the bright
   blue .icon-btn.active/accent-dim treatment used elsewhere. */
.add-widget-picker {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
}
/* attachSymbolChangeButton (app.js) normally switches between a persistent
   bar and a compact ⇄-button+popover based on an ancestor .tile-header's own
   measured width (see the @container tile-header rules further up) — outside
   a real tile-header, as here, neither variant's rule ever matches and it
   falls back to the compact button. Force the bar on unconditionally instead
   of relying on width: this tile has its own dedicated row for it, not a
   header shared with other controls, so there's no narrow case to fall back
   for. */
/* .tile-header-search .symbol-search-panel below is `position: absolute;
   top: 100%` — meant to drop the dropdown immediately under the search bar,
   anchored to .tile-header's own `position: relative` (set earlier). Without
   that ancestor here, the nearest positioned one is .tile-body itself, so
   `top: 100%` would resolve against the *whole tile's* height instead —
   the dropdown jumping to the bottom of the tile rather than sitting right
   under the input it belongs to. This same `position: relative` restores
   the correct anchor. */
.add-widget-picker-search { padding: 6px 6px 2px; position: relative; }
/* Both halves of the original @container rule's wide-bar case are needed,
   not just `display: block` — .tile-symbol-search is the flex container and
   .tile-header-search is the item that has to grow inside it; dropping
   either `flex: 1 1 auto` left the bar sized to its own min-width/content
   instead of the tile's full width. */
.add-widget-picker-search .tile-symbol-search { flex: 1 1 auto; }
.add-widget-picker-search .tile-header-search { display: block; flex: 1 1 auto; }
.add-widget-picker-search .tile-symbol-search > .tile-symbol-btn { display: none; }
.add-widget-picker-search .tile-header-search-input { font-size: 14px; padding: 7px 9px; }
/* Picked tickers, kept visible as removable chips instead of vanishing back
   into the search input on pick — lets more than one be queued up so a
   single "Uses market" row (see WIDGETS_USING_MARKET) can fan out into one
   tile per chip. */
.add-widget-picker-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 6px 6px 0;
}
.add-widget-picker-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--mono);
  font-size: 12.5px;
  color: var(--panel-text);
  background: var(--bg-elevated);
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  padding: 3px 5px 3px 9px;
}
.add-widget-picker-chip button {
  border: 0;
  background: transparent;
  color: var(--panel-text-dim);
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  padding: 0 2px;
}
.add-widget-picker-chip button:hover { color: var(--sell); }
/* A <button> (click-to-collapse), styled to still read as the plain section
   label the full modal's own <h3>+logo uses — no border/button chrome. */
.add-widget-picker-group-title {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  border: 0;
  background: transparent;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--panel-text-dim);
  opacity: 0.8;
  padding: 10px 6px 4px;
}
.add-widget-picker-group-title:hover { opacity: 1; }
.add-widget-picker-group-chevron { flex-shrink: 0; font-size: 10px; width: 10px; }
.add-widget-picker-group-title .widget-provider-logo { width: 16px; height: 16px; }
/* Stands in for .widget-provider-logo on the Favorites group's heading — a
   starred entry can come from any provider, so there's no single logo to show. */
.add-widget-picker-group-star { width: 16px; flex-shrink: 0; text-align: center; color: var(--accent); font-size: 13px; }
.add-widget-picker-group:first-child .add-widget-picker-group-title { padding-top: 2px; }
/* Only "Uses market" rows (Advanced chart, Symbol overview, Technical
   analysis) read the search bar's typed symbol — the rest are fixed/global
   widgets it has no effect on. Mirrors the full modal's own
   .widget-param-badge, shrunk to fit a condensed row. */
.add-widget-picker-badge {
  flex-shrink: 0;
  font-size: 10.5px;
  color: var(--panel-text-dim);
  border: 1px solid var(--panel-border);
  border-radius: 4px;
  padding: 2px 5px;
  opacity: 0.75;
}
.add-widget-picker-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 4px 6px;
}
.add-widget-picker-row {
  display: flex;
  align-items: center;
  gap: 2px;
}
.add-widget-picker-item {
  flex: 1;
  text-align: left;
  border: 0;
  background: transparent;
  color: var(--panel-text-dim);
  font-family: var(--mono);
  font-size: 14px;
  padding: 7px 6px;
  border-radius: 5px;
  cursor: pointer;
}
.add-widget-picker-item:hover { color: var(--panel-text); background: var(--bg-elevated); }
/* Advanced chart, always the first TradingView row — the old single-widget
   MultiCoinCharts app's default, so it reads as the primary action. */
.add-widget-picker-row.primary .add-widget-picker-item {
  font-weight: 600;
  color: var(--panel-text);
}
.add-widget-picker-star {
  flex-shrink: 0;
  border: 0;
  background: transparent;
  color: var(--panel-text-dim);
  cursor: pointer;
  font-size: 16px;
  padding: 2px 5px;
  opacity: 0.55;
}
.add-widget-picker-star:hover, .add-widget-picker-star.active { opacity: 1; color: var(--accent); }
.add-widget-picker-more {
  flex-shrink: 0;
  border: 0;
  border-top: 1px solid var(--panel-border);
  background: transparent;
  color: var(--panel-text-dim);
  font-size: 13px;
  padding: 8px 6px;
  cursor: pointer;
}
.add-widget-picker-more:hover { color: var(--panel-text); background: var(--bg-elevated); }
/* Twitch/YouTube groups' own row — free text (a channel/video URL or ID) has
   no ticker chip equivalent to read from, so unlike every other row here
   these need a real input instead of a plain click target. */
.add-widget-picker-input-row { display: flex; gap: 4px; padding: 2px 6px 6px; }
.add-widget-picker-input-row input {
  flex: 1;
  min-width: 0;
  background: var(--bg-elevated);
  border: 1px solid var(--panel-border);
  color: var(--panel-text);
  border-radius: 5px;
  padding: 6px 8px;
  font-size: 12.5px;
  font-family: var(--mono);
}
.add-widget-picker-input-row button {
  flex-shrink: 0;
  border: 1px solid var(--panel-border);
  background: var(--bg-elevated);
  color: var(--panel-text-dim);
  border-radius: 5px;
  padding: 0 10px;
  font-size: 12.5px;
  cursor: pointer;
}
.add-widget-picker-input-row button:hover { border-color: var(--accent); color: var(--panel-text); }
.add-widget-picker-input-error { margin: -2px 6px 6px; font-size: 11px; color: var(--danger); }
/* Shared by attachTwitchChannelSearch and attachYouTubeSearch (app.js) —
   wraps whichever plain text input either is attached to (the full modal's
   own field, or the condensed picker's row) so the dropdown can anchor under
   it regardless of which context it's used from. */
.search-suggest-wrap { position: relative; flex: 1; min-width: 0; }
.search-suggest-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 5;
  margin-top: 4px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 7px;
  overflow: hidden;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}
.search-suggest-result {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  border: 0;
  background: transparent;
  color: var(--text);
  font-size: 12.5px;
  text-align: left;
  padding: 7px 10px;
  cursor: pointer;
}
.search-suggest-result:hover, .search-suggest-result.active { background: var(--bg-elevated); }
.search-suggest-result-main { display: flex; flex-direction: column; gap: 1px; overflow: hidden; min-width: 0; }
.search-suggest-result-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search-suggest-result-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 10.5px; color: var(--text-dim); }
.search-suggest-result-tag { flex-shrink: 0; font-size: 10.5px; color: var(--text-dim); }
.search-suggest-result-tag.live { color: var(--sell); font-weight: 600; text-transform: uppercase; }

/* The tiling canvas itself — dot-grid background evokes a tmux/tiling-WM
   surface; tiles inside are absolutely positioned and free-dragged/resized. */
.dashboard-widgets {
  position: relative;
  min-height: 100%;
  width: 100%;
  /* Explicit rather than left to inherit --text (pure white/near-black
     depending on theme) — any bare, unclassed label text dropped directly
     in the workspace canvas (not inside a .tile, which sets its own
     --panel-text-scoped color) should read as a muted, subdued color like
     the markets-table headers, not the app's brightest text tone. */
  color: var(--panel-text-faint);
  background-image: radial-gradient(circle, var(--panel-border-soft) 1px, transparent 1px);
  background-size: 16px 16px;
  transform: scale(var(--workspace-zoom, 1));
  transform-origin: top left;
}

/* Layout decisions are contextual: they appear beside the workspace result
   in chat instead of occupying the header all the time. */
.workspace-layout-choices {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 12px;
}
.workspace-layout-choice {
  border: 1px solid var(--border);
  border-radius: 7px;
  padding: 5px 8px;
  background: var(--bg-elevated);
  color: var(--text-dim);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.workspace-layout-choice:hover,
.workspace-layout-choice:focus-visible {
  color: var(--text);
  border-color: var(--accent);
  outline: none;
}
.workspace-layout-choice.active {
  color: #fff;
  background: var(--accent-dim);
  border-color: var(--accent-dim);
}
.dashboard-widgets.grid-mode .tile {
  transition: left 0.16s ease, top 0.16s ease, width 0.16s ease, height 0.16s ease;
}
.dashboard-widgets.grid-mode .tile-header { cursor: grab; }
.dashboard-widgets.grid-mode .tile-resize-handle { display: none; }
.dashboard-widgets.auto-fit-mode .tile {
  transition: left 0.16s ease, top 0.16s ease, width 0.16s ease, height 0.16s ease;
}
.dashboard-widgets.auto-fit-mode .tile-header { cursor: grab; }
.dashboard-widgets.auto-fit-mode .tile-resize-handle { display: none; }
.dashboard-widgets.auto-fit-mode .tile.chart-grid-tile { min-width: 0; }
/* Grid mode's "Auto-resize" fit preference (layoutGrid's shrinkToFit) computes
   a per-tile pixel size that can land below chart-grid-tile's natural
   min-width/min-height floor below — without this, the CSS floor would win
   and re-widen/re-heighten tiles past the smaller spacing layoutGrid already
   used to position them, so they'd overlap their neighbors instead of
   actually shrinking. .tile.widget-group (styles.css, .tile.widget-group
   below) carries the same kind of hard floor — a combined chart/orderbook/
   trades tile, or a single-chart "N charts on different timeframes" tile
   (still built via the group-tile path so it gets the same exchange-picker
   header), needs the same override or it overlaps its neighbors exactly
   like chart-grid-tile did before this rule existed. */
.dashboard-widgets.grid-mode.grid-shrink-fit .tile.chart-grid-tile,
/* `!important` is intentional: .tile.widget-group's readable free-layout
   floor appears later in this stylesheet. During grid auto-fit, the grid's
   computed cell height is the source of truth; if that floor wins, the next
   row is positioned at the smaller computed height and tiles overlap. */
.dashboard-widgets.grid-mode.grid-shrink-fit .tile.widget-group { min-width: 0 !important; min-height: 0 !important; }

/* Below 1280px the dashboard is auto-collapsed to its icon rail by app.js
   (reusing the existing .collapsed state toggled by #dashboardToggle)
   rather than hidden outright, so it stays reachable instead of just
   vanishing with no way back. */

/* --- Tiles: draggable/resizable panes hosting pinned widgets or chat panes --- */
.tile {
  position: absolute;
  display: flex;
  flex-direction: column;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
  min-width: 220px;
  min-height: 120px;
}
/* Explicit chart grids retain enough canvas for price action, volume, and
   labels to remain readable. */
.tile.chart-grid-tile {
  min-width: 300px;
  min-height: 280px;
}
.tile.dragging, .tile.resizing {
  opacity: 0.94;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
  z-index: 999 !important;
  user-select: none;
}
.tile-header {
  cursor: grab;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  /* Trimmed from 6px/6px — TradingView's own toolbar (inside the iframe
     below) isn't ours to restyle, so this outer wrapper's own padding is
     the one lever available for reclaiming vertical chart space; classic's
     tighter-looking chrome comes from a header this thin, not from having
     fewer rows (it keeps TradingView's native toolbar too — same shape as
     here, just less padding around it). */
  padding: 3px 8px 3px 10px;
  background: var(--panel-header-bg);
  border-bottom: 1px solid var(--panel-border);
  font-family: var(--mono);
  font-size: 11px;
  color: var(--panel-text-dim);
  user-select: none;
  flex-shrink: 0;
  /* Anchors .tile-symbol-popover (below) to the header's own full width
     rather than the small ⇄ button that opens it — see that rule's comment. */
  position: relative;
  /* Lets .tile-symbol-search below react to this header's own rendered
     width (not the viewport's) via @container tile-header — a tile can be
     resized/zoomed independently of the window. */
  container-type: inline-size;
  container-name: tile-header;
}
.tile-header:active { cursor: grabbing; }
.tile-settings-btn {
  border: 0;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  padding: 2px 4px;
  border-radius: 5px;
  font-size: 13px;
  line-height: 1;
}
.tile.grid-dragging {
  transition: none !important;
}
.tile.grid-drop-target {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 45%, transparent), 0 10px 24px rgba(0, 0, 0, 0.38);
}
/* A compatible tile is deliberately a two-step target: first it tells the
   person to hold, then turns green only once releasing will combine. This
   keeps a quick drag past an overlapping widget from changing the workspace.
   The label is a pseudo-element so it never changes the live widget's DOM. */
.tile.tile-combine-holding,
.tile.tile-combine-target {
  position: relative;
}
.tile.tile-combine-holding {
  border-color: var(--status-amber);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--status-amber) 42%, transparent), 0 10px 24px rgba(0, 0, 0, 0.38);
  animation: combine-hold-pulse 650ms linear forwards;
}
.tile.tile-combine-target {
  border-color: var(--buy);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--buy) 45%, transparent), 0 10px 24px rgba(0, 0, 0, 0.38);
}
.tile.tile-combine-holding::after,
.tile.tile-combine-target::after {
  content: attr(data-combine-hint);
  position: absolute;
  z-index: 30;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 7px 10px;
  border: 1px solid currentColor;
  border-radius: 999px;
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  color: var(--status-amber);
  font-size: 11px;
  font-weight: 700;
  pointer-events: none;
  white-space: nowrap;
}
.tile.tile-combine-target::after { color: var(--buy); }
@keyframes combine-hold-pulse {
  from { outline: 0 solid color-mix(in srgb, var(--status-amber) 50%, transparent); }
  to { outline: 8px solid transparent; }
}
.tile-settings-btn:hover { color: var(--text); background: var(--bg-elevated); }
.tile-panels-btn {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  min-height: 22px;
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 3px 5px;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
}
.tile-panels-btn .combine-layout-icon { width: 17px; height: 12px; gap: 1px; }
.tile-panels-btn .combine-layout-icon i { border-radius: 1px; }
.tile-panels-btn:hover { color: var(--text); border-color: var(--accent); background: var(--bg-elevated); }
.tile-panels-btn:disabled { cursor: default; opacity: .55; }
.tile-group-customize-menu { position: relative; flex-shrink: 0; }
.tile-group-layout-toggle {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  min-height: 22px;
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 3px 5px;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
}
.tile-group-layout-toggle .combine-layout-icon { width: 17px; height: 12px; gap: 1px; }
.tile-group-layout-toggle .combine-layout-icon i { border-radius: 1px; }
.tile-group-layout-chevron { font-size: 12px; line-height: 9px; transform: translateY(-1px); }
.tile-group-layout-toggle:hover,
.tile-group-layout-toggle[aria-expanded="true"] { color: var(--text); border-color: var(--accent); background: var(--bg-elevated); }
.tile-group-layout-toggle:disabled { cursor: default; opacity: .55; }
/* A compact counterpart to the global Workspace grid popover: the same
   wireframe-first choice, but with the group-specific panel checkboxes in
   the same menu rather than a separate configuration screen. */
.tile-group-layout-popover {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  width: 236px;
  padding: 10px;
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  background: var(--panel-bg);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  z-index: 45;
}
.floating-panel-picker { position: fixed; }
/* Tiles normally clip their live content to the rounded frame. Let just this
   anchored configuration menu escape that frame while it is open, exactly as
   the Workspace header's grid popover does. */
.tile:has(.tile-group-layout-popover:not([hidden])) { overflow: visible; }
.tile-group-layout-section-label {
  margin: 0 0 6px;
  color: var(--text-dim);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .03em;
  text-transform: uppercase;
}
.tile-group-panel-options { display: flex; gap: 5px; margin-bottom: 10px; }
.tile-group-panel-option {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
  padding: 5px 6px;
  border: 1px solid var(--border);
  border-radius: 5px;
  color: var(--text-dim);
  font-size: 10.5px;
  cursor: pointer;
}
.tile-group-panel-option:hover { color: var(--text); border-color: var(--accent); }
.tile-group-panel-option input { margin: 0; accent-color: var(--accent); }
.tile-group-layout-options {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
}
.tile-group-layout-option {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 43px;
  padding: 6px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
}
.tile-group-layout-option:hover,
.tile-group-layout-option:focus-visible { border-color: var(--accent); outline: none; }
.tile-group-layout-option.active { border-color: color-mix(in srgb, var(--accent) 65%, var(--border)); background: color-mix(in srgb, var(--accent) 12%, transparent); }
.tile-group-layout-option .combine-layout-icon { width: 34px; height: 25px; }
.tile-group-layout-option.active .combine-layout-icon i { background: var(--accent); }
.tile-group-layout-hint { min-height: 15px; margin: 8px 0 0; color: var(--text-dim); font-size: 10.5px; }
.tile-group-layout-actions { display: flex; justify-content: flex-end; gap: 6px; margin-top: 8px; }
.tile-group-layout-actions button {
  border-radius: 5px;
  padding: 5px 8px;
  font: inherit;
  font-size: 10.5px;
  cursor: pointer;
}
.tile-group-layout-cancel { border: 1px solid var(--border); background: transparent; color: var(--text-dim); }
.tile-group-layout-cancel:hover { color: var(--text); border-color: var(--accent); }
.tile-group-layout-apply { border: 1px solid var(--accent); background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
.tile-group-layout-apply:hover { color: var(--text); background: color-mix(in srgb, var(--accent) 28%, transparent); }
.tile-group-layout-apply:disabled { cursor: default; opacity: .45; }
.saved-group-preset-item .saved-dashboard-item-name { color: var(--buy); }
.tile-exchange-select {
  border: 1px solid var(--border);
  border-radius: 5px;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 10.5px;
  padding: 2px 4px;
  max-width: 84px;
  cursor: pointer;
}
.tile-exchange-select:hover { color: var(--text); background: var(--bg-elevated); }
/* Disabled exchanges are intentionally a clear availability state, not a
   faded version of a selectable option. Native selects honor this color in
   Chromium's dropdown; the matching option title explains the reason. */
.tile-exchange-select option:disabled { color: var(--danger); opacity: 1; }
.tile-exchange-select-error { border-color: var(--danger); color: var(--danger); }
/* Persistent (not a timed flash like -error above) — marks a widget that
   silently fell back to Coinbase because the exchange it was actually
   requested on couldn't be reached, so it stays visible after the rest of
   a multi-widget plan finishes building. */
.tile-exchange-select-unavailable { border-color: var(--status-amber); color: var(--status-amber); }
.tile-interval-quick {
  position: relative;
  display: flex;
  align-items: center;
  gap: 3px;
  flex-shrink: 0;
}
/* The chart panel hides this redundant row while TradingView's top toolbar
   is visible. Its component-level display:flex otherwise overrides the
   browser's default [hidden] styling. */
.tile-interval-quick[hidden] { display: none; }
.tile-interval-pinned {
  display: flex;
  align-items: center;
  gap: 1px;
}
.tile-interval-quick-btn {
  border: 0;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  padding: 2px 5px;
  border-radius: 4px;
  font-size: 10.5px;
  line-height: 1.4;
  white-space: nowrap;
}
.tile-interval-quick-btn:hover { color: var(--text); background: var(--bg-elevated); }
.tile-interval-quick-btn[aria-pressed="true"] { color: var(--text); background: var(--accent-dim); }
/* BONUS_INTERVAL_CODE's pill (app.js) — hidden below the same 400px point
   .tile-title disappears at, so the header's genuinely cramped buckets
   (down to the ~276-295px floor documented further below) render exactly
   as before; a tile with real spare room gets one extra timeframe instead
   of the dead space that room would otherwise sit as. */
.tile-interval-quick-btn-bonus { display: none; }
@container tile-header (min-width: 400px) {
  .tile-interval-quick-btn-bonus { display: inline-block; }
}
/* The "<active> ▾" toggle carries the .tile-interval-quick-btn class too, so
   it scales identically to the pinned pills at every @container breakpoint
   below; only its "active but unpinned" highlight is specific to it. */
.tile-interval-more-btn[aria-pressed="true"] { color: var(--text); background: var(--accent-dim); }
/* Anchored to .tile-interval-quick (position: relative, set above), same
   "wrap has no layout role of its own but the popover positions off the
   nearest positioned ancestor" pattern as .tile-symbol-popover — except this
   one anchors to its own small wrap instead of spanning the full header,
   since a 6-row timeframe list is short and narrow enough not to need it. */
.tile-interval-popover {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 6;
  margin-top: 4px;
  min-width: 140px;
  max-height: 220px;
  overflow-y: auto;
  padding: 4px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}
.tile-interval-popover-group {
  padding: 6px 6px 2px;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.tile-interval-popover-group:first-child { padding-top: 2px; }
.tile-interval-popover-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 4px;
}
.tile-interval-popover-pick {
  flex: 1 1 auto;
  text-align: left;
  border: 0;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  padding: 4px 6px;
  border-radius: 4px;
  font-size: 12px;
}
.tile-interval-popover-pick:hover { color: var(--text); background: var(--bg); }
.tile-interval-popover-pick[aria-pressed="true"] { color: var(--text); background: var(--accent-dim); }
.tile-interval-pin-btn {
  flex-shrink: 0;
  border: 0;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  padding: 2px 5px;
  border-radius: 4px;
  font-size: 12px;
  line-height: 1;
}
.tile-interval-pin-btn:hover { color: var(--text); background: var(--bg); }
.tile-interval-pin-btn[aria-pressed="true"] { color: var(--status-amber); }
/* attachSymbolChangeButton's wrapper (app.js) — sits between .tile-title and
   .tile-controls (see workspace.js's headerMiddle). Default/narrow state:
   sized to its content (the small ⇄ button below); once .tile-header is wide
   enough the @container rule further down swaps that for the persistent
   .tile-header-search bar and lets this wrapper flex-grow to fill the middle
   of the header, same layout multicoincharts.com's/TradingView's own tile
   headers use. */
.tile-symbol-search {
  display: flex;
  align-items: center;
  min-width: 0;
}
.tile-header-search {
  display: none;
  min-width: 60px;
}
.tile-header-search-input {
  width: 100%;
  font: inherit;
  font-size: 11px;
  padding: 3px 8px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg);
  color: var(--text-dim);
}
.tile-header-search-input:hover { color: var(--text); }
.tile-header-search-input:focus { color: var(--text); outline: none; border-color: var(--accent); }
/* The search input can be narrow in a busy chart toolbar, but its results
   need a complete readable window. Anchor that window to the full header,
   bounded by the tile's own inset, rather than squeezing it into the input's
   remaining flex space. */
.tile-header-search .symbol-search-panel {
  position: absolute;
  top: 100%;
  left: 8px;
  right: 8px;
  width: auto;
  z-index: 5;
}
@container tile-header (min-width: 400px) {
  .tile-symbol-search { flex: 1 1 auto; }
  .tile-header-search { display: block; flex: 1 1 auto; }
  .tile-symbol-search > .tile-symbol-btn { display: none; }
}
/* Chart tiles carry quick-interval pills in the same header. Keep the compact
   fallback only while the tile is genuinely too narrow for both those pills
   and a useful search field; at normal two-column widths the input remains
   visible, matching TradingView's chart toolbar. Unlike the icon-only ⇄
   fallback elsewhere, this one carries the current symbol as a label (see
   .tile-symbol-btn-label below) — it's flex:1 1 auto, not 0 0 auto, so it
   soaks up the space .tile-header's justify-content: space-between would
   otherwise leave as a dead gap between the pills and the icon cluster.
   The button itself is width:100% with justify-content: space-between
   (label left, arrow right) rather than left- or right-aligned within the
   grown wrap — aligning the button any one way just relocates whatever
   width is left over to the opposite side as a dead gap (tried flex-end
   first; on a header crowded with an exchange picker + alert bell, the
   bonus pill often still doesn't fit, so that leftover width is real and
   variable, not a fixed few px an alignment choice can hide). Stretching
   the button to fill it removes the gap regardless of exactly how much is
   left. */
@container tile-header (max-width: 579px) {
  .tile-header:has(.tile-interval-quick) .tile-symbol-search { flex: 1 1 auto; min-width: 0; }
  .tile-header:has(.tile-interval-quick) .tile-header-search { display: none; }
  .tile-header:has(.tile-interval-quick) .tile-symbol-search > .tile-symbol-btn { display: inline-flex; width: 100%; justify-content: space-between; }
}
/* Dense chart grids (such as 4×3) need every useful control to fit within
   the tile, not merely a clipped desktop header. Scale the compact controls
   together so the timeframe strip, market picker, settings, and close button
   remain available at the chart's actual width. */
@container tile-header (max-width: 399px) {
  .tile-header:has(.tile-interval-quick) { gap: 3px; padding: 1px 4px; }
  .tile-header:has(.tile-interval-quick) .tile-title { display: none; }
  .tile-header:has(.tile-interval-quick) .tile-interval-quick { gap: 0; }
  .tile-header:has(.tile-interval-quick) .tile-interval-pinned { gap: 0; }
  .tile-header:has(.tile-interval-quick) .tile-interval-quick-btn { padding: 1px 3px; font-size: 9.5px; }
  .tile-header:has(.tile-interval-quick) .tile-symbol-btn { padding: 1px 5px; font-size: 11px; }
  .tile-header:has(.tile-interval-quick) .tile-controls { gap: 2px; }
  .tile-header:has(.tile-interval-quick) .tile-exchange-select { width: 62px; max-width: 62px; padding: 1px 2px; font-size: 9px; }
  .tile-header:has(.tile-interval-quick) .tile-panels-btn { min-height: 18px; padding: 2px 3px; }
  .tile-header:has(.tile-interval-quick) .tile-settings-btn,
  .tile-header:has(.tile-interval-quick) .tile-share-btn,
  .tile-header:has(.tile-interval-quick) .tile-alert-btn { padding: 1px 2px; font-size: 11px; }
  .tile-header:has(.tile-interval-quick) .tile-fullscreen-btn { padding: 1px 2px; font-size: 12px; }
  .tile-header:has(.tile-interval-quick) .widget-close-btn { width: 17px; height: 17px; border-radius: 4px; font-size: 12px; }
}
/* On the narrowest grid cells, preserve the controls that operate the chart
   itself. Sharing and fullscreen remain available from a wider tile, but are
   the first optional actions to yield space instead of clipping the header.
   276px, not the old 339px: condensing the interval row down to its default
   two pinned pills + a "<active> ▾" dropdown (see attachQuickIntervalButtons
   in app.js) freed enough width that a chart-grid-tile at its actual
   rendered floor (~295px of header, verified live: at that width the header's
   own scrollWidth matched its clientWidth with fullscreen+share forced
   visible, i.e. no clipping) now has room for them. The number itself sits
   well below 295 because `@container` measures this axis off the *content*
   box, and .tile-header's own ~18px horizontal padding (`.tile-header`'s own
   padding rule, further down this same container-query chain) never actually
   shrinks: a container query can't restyle the size of the very element that
   defines it, so that self-referencing padding rule silently no-ops and the
   full padding always applies. 276px content-box + that fixed ~18px padding
   lands the real trigger point right at the ~295px floor. This threshold
   still yields to a workspace whose tiles genuinely go narrower than that
   floor (e.g. a non-grid free-form tile, whose .tile min-width is only
   220px), and to anyone who's pinned back closer to all six timeframes,
   which needs roughly the old 339px worth of room again. */
@container tile-header (max-width: 276px) {
  .tile-header:has(.tile-interval-quick) .tile-share-btn,
  .tile-header:has(.tile-interval-quick) .tile-fullscreen-btn,
  .tile-header:has(.tile-interval-quick) .group-save-btn,
  .tile-header:has(.tile-interval-quick) .tile-group-customize-menu { display: none; }
}
/* Four-column workspaces can make a chart tile roughly 230px wide. Keep the
   whole operational header on one line at that size rather than clipping the
   final controls: this is a denser version of the same toolbar, not a
   different interaction model. */
@container tile-header (max-width: 259px) {
  .tile-header:has(.tile-interval-quick) { gap: 1px; padding: 1px 2px; }
  .tile-header:has(.tile-interval-quick) .tile-interval-quick-btn { padding: 1px; font-size: 8.5px; }
  .tile-header:has(.tile-interval-quick) .tile-exchange-select { width: 52px; max-width: 52px; padding: 0 1px; font-size: 8.5px; }
  .tile-header:has(.tile-interval-quick) .tile-panels-btn { width: 20px; min-height: 16px; padding: 0 1px; overflow: hidden; }
  .tile-header:has(.tile-interval-quick) .tile-panels-btn .combine-layout-icon { width: 16px; }
  .tile-header:has(.tile-interval-quick) .tile-settings-btn,
  .tile-header:has(.tile-interval-quick) .tile-share-btn,
  .tile-header:has(.tile-interval-quick) .tile-alert-btn { padding: 1px; font-size: 10px; }
  .tile-header:has(.tile-interval-quick) .tile-fullscreen-btn { padding: 1px; font-size: 11px; }
  .tile-header:has(.tile-interval-quick) .widget-close-btn { width: 16px; height: 16px; font-size: 11px; }
}
.tile-symbol-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  max-width: 100%;
  border: 0;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  padding: 2px 4px;
  border-radius: 5px;
  font-size: 13px;
  line-height: 1;
}
.tile-symbol-btn:hover { color: var(--text); background: var(--bg-elevated); }
/* Current symbol, shown only where attachSymbolChangeButton's showLabel
   option is on (app.js) — fills the header's otherwise-dead middle instead
   of leaving the ⇄ icon floating alone in it. */
.tile-symbol-btn-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text);
  min-width: 0;
}
.tile-symbol-btn-arrow { flex-shrink: 0; }
/* Anchored to .tile-header (position: relative, set above) and spans its
   full width — not a fixed pixel width hung off the small ⇄ button's own
   right edge. That earlier layout put the popover whenever the button sat
   more than ~320px from the tile's left edge, which is most of the time on
   a narrow grid tile: the tile itself clips overflow (see `.tile`), so most
   of the popover — the actual search input and results — was silently cut
   away, leaving a strip of it adrift with nothing else on screen tying it
   to the ⇄ button that opened it. Spanning the header's own width instead
   is the same "fill the available toolbar" pattern TradingView's own
   symbol-search widget falls back to on a narrow embed, and it can never
   be wider than the tile that clips it. */
.tile-symbol-popover {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 5;
  margin: 4px 8px 0;
  padding: 4px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}
.tile-symbol-popover-input {
  width: 100%;
  font: inherit;
  font-size: 12px;
  padding: 4px 6px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg);
  color: var(--text);
  box-sizing: border-box;
}
.tile-symbol-popover .symbol-search-panel { width: 100%; }
.tile-fullscreen-btn {
  border: 0;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  padding: 2px 4px;
  border-radius: 5px;
  font-size: 15px;
  line-height: 1;
}
.tile-fullscreen-btn:hover,
.tile-fullscreen-btn[aria-pressed="true"] { color: var(--text); background: var(--bg-elevated); }
.tile-alert-btn {
  border: 0;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  padding: 2px 4px;
  border-radius: 5px;
  font-size: 13px;
  line-height: 1;
}
.tile-alert-btn:hover { color: var(--text); background: var(--bg-elevated); }
.tile-share-btn {
  border: 0;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  padding: 2px 4px;
  border-radius: 5px;
  font-size: 13px;
  line-height: 1;
}
.tile-share-btn:hover { color: var(--text); background: var(--bg-elevated); }
.tile-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--panel-text);
  font-weight: 600;
  letter-spacing: 0.01em;
}
.tile-controls {
  display: flex;
  gap: 6px;
  align-items: center;
  flex-shrink: 0;
}
.tile-body {
  flex: 1;
  overflow: auto;
  position: relative;
  min-height: 0;
}
/* A tile's scrollable content (e.g. a 60-row trade tape, see TRADE_ROWS_MAX
   in app.js) is usually taller than the tile, so a scrollbar is unavoidable
   at basically any per-tile zoom level — but it doesn't need to sit there
   permanently. Keep the thumb invisible until the tile is actually being
   interacted with, same idea as macOS overlay scrollbars: still scrollable
   the whole time, just not visually "on" by default. */
.tile-body,
.trades-widget .trades-rows {
  scrollbar-color: transparent transparent;
  transition: scrollbar-color 0.15s ease;
}
.tile-body:hover, .tile-body:focus-within,
.trades-widget .trades-rows:hover, .trades-widget .trades-rows:focus-within {
  scrollbar-color: var(--panel-border) transparent;
}
.tile-body::-webkit-scrollbar-thumb,
.trades-widget .trades-rows::-webkit-scrollbar-thumb {
  background: transparent;
}
.tile-body:hover::-webkit-scrollbar-thumb, .tile-body:focus-within::-webkit-scrollbar-thumb,
.trades-widget .trades-rows:hover::-webkit-scrollbar-thumb, .trades-widget .trades-rows:focus-within::-webkit-scrollbar-thumb {
  background: var(--panel-border);
}
.tile-resize-handle {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 16px;
  height: 16px;
  cursor: nwse-resize;
  z-index: 2;
}
.tile-resize-handle::after {
  content: "";
  position: absolute;
  right: 4px;
  bottom: 4px;
  width: 7px;
  height: 7px;
  border-right: 2px solid var(--panel-border);
  border-bottom: 2px solid var(--panel-border);
}
.tile:hover .tile-resize-handle::after { border-color: var(--panel-text-dim); }
.tile:fullscreen {
  width: 100vw !important;
  height: 100vh !important;
  border: 0;
  border-radius: 0;
}
.tile:fullscreen .tile-resize-handle { display: none; }
.tile::backdrop { background: #08090b; }
.tile.tile-maximized {
  position: fixed;
  inset: 0;
  width: 100vw !important;
  height: 100vh !important;
  z-index: 2000 !important;
  border: 0;
  border-radius: 0;
}
.tile.tile-maximized .tile-resize-handle { display: none; }
.tile .widget-card { margin-top: 0; border: none; border-radius: 0; height: 100%; }
.tile .widget-card-header { display: none; } /* the tile's own header replaces it */

/* Price-only queries (such as `products get BTC-USD --jq '.price'`) use a
   compact market card instead of a mostly empty, generic terminal pane, and
   a price tile upgrades to the fuller Ticker card the moment it goes live
   (see addDashboardWidget's liveKind flip) — both hold fixed, non-scrolling
   content rather than a list, so center that content in the tile at any
   size (a small default pin, a full 1x1 grid cell, anything resized between)
   instead of pinning it to the top-left with dead space around it.
   `flex: none` on every direct child neutralizes each card's own legacy
   `flex: 1` (written back when centering was this rule's job alone) so
   sizing comes from width/max-width here instead of two rules fighting.
   `safe center` (not plain `center`) matters once a tile gets short enough
   that the card + status line no longer both fit: plain centering overflows
   equally past both edges, clipping the top of the price *and* the bottom
   of the status line in a scrolling box with no visible cue why — `safe`
   falls back to start-alignment instead, so it scrolls from a normal top
   edge like everything else once there isn't room to center. */
.tile.widget-price .tile-body,
.tile.widget-ticker .tile-body {
  display: flex;
  flex-direction: column;
  justify-content: safe center;
  align-items: safe center;
}
.tile.widget-price .tile-body > *,
.tile.widget-ticker .tile-body > * {
  flex: none;
  width: 100%;
  max-width: 440px;
}
.tile.widget-price .widget-live-status,
.tile.widget-ticker .widget-live-status {
  text-align: center;
}

/* The order book ladder's row count is fit to the tile body's height (see
   fitLevels() in buildOrderbookUpdater), but that height previously came
   from `.widget-body-orderbook { height: 100% }` — the *entire* tile body,
   leaving no room for the live-status footer appended after it. The footer
   still needs its own space, so its text ended up painted directly over the
   ladder's last row instead of below it. Making tile-body a flex column and
   letting the ladder flex to fill what's left (rather than claim 100% of
   the parent outright) reserves the footer's space up front, and since
   fitLevels() measures the ladder's own actual height, it now naturally
   fits one fewer row instead of overfilling. `overflow: hidden` is a
   safety clip in case a transient state (e.g. the footer's display
   toggling) still leaves the ladder briefly oversized. */
.tile.widget-orderbook .tile-body {
  display: flex;
  flex-direction: column;
}
.tile.widget-orderbook .widget-body-orderbook {
  flex: 1;
  min-height: 0;
  /* Undo the base rule's inline-chat cap (see its comment) — a tile has its
     own definite height from its own geometry, which fitLevels() already
     measures and fits the row count to, so this should never be the thing
     that clips it. */
  max-height: none;
  overflow: hidden;
  /* fitLevels() (app.js) floors row-count/rowHeight to however many *real*
     levels fit — by design it won't fabricate a partial row to fill the
     last bit of space, which otherwise left a dead gap below the last row.
     Making the ladder itself flex lets those genuine rows stretch to
     absorb that remainder evenly instead, so the ladder always fills the
     tile exactly with no fabricated or missing data. */
  display: flex;
  flex-direction: column;
}
.tile.widget-orderbook .ob-header-row,
.tile.widget-orderbook .ob-mid-row {
  flex: none;
}
.tile.widget-orderbook .ob-side {
  flex: 1;
  min-height: 0;
}
.tile.widget-orderbook .ob-row {
  /* No min-height: 0 here (unlike .ob-side above) — rows are free to grow
     and absorb the leftover-space remainder, but never shrink below their
     natural readable size even in an edge case where fitLevels() briefly
     overcounts; .widget-body-orderbook's overflow: hidden is the backstop
     for that, same as before. */
  flex: 1 1 auto;
}
.tile.widget-orderbook .widget-live-status {
  flex: none;
}
.dashboard-price-body {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 16px;
}
.dashboard-price-label {
  color: var(--panel-text-dim);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.dashboard-price-value {
  margin-top: 5px;
  color: var(--panel-text);
  font-family: var(--mono);
  font-size: 27px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.04em;
}
.dashboard-price-market {
  margin-top: 8px;
  color: var(--panel-text-dim);
  font-size: 12px;
}

/* Mini chat pane hosted inside a tile */
.tile-chat { display: flex; flex-direction: column; height: 100%; }
.tile-chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 10px 10px 4px;
}
.tile-chat-messages .msg-row { max-width: none; padding: 4px 0; }
.tile-chat-messages .empty-state { margin: 20% auto 0; }
.tile-chat-messages .empty-title { font-size: 15px; }
.tile-chat-messages .empty-sub { font-size: 12px; }
.tile-chat-composer {
  border-top: 1px solid var(--panel-border);
  padding: 8px;
  display: flex;
  align-items: flex-end;
  gap: 6px;
  flex-shrink: 0;
}
.tile-chat-input {
  flex: 1;
  background: var(--panel-header-bg);
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  color: var(--panel-text);
  font-family: var(--font);
  font-size: 12.5px;
  padding: 6px 9px;
  resize: none;
  max-height: 90px;
  outline: none;
}
.tile-chat-input:focus { border-color: var(--accent-dim); }
.tile-chat-send {
  background: var(--accent);
  border: none;
  color: var(--accent-text);
  width: 26px;
  height: 26px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
}

/* Main */
.main {
  flex: 1;
  display: flex;
  flex-direction: column;
  /* Keeps the chat column usable when the dashboard panel is dragged wide —
     matches the MIN_CHAT_WIDTH the dashboard's own width is clamped against
     in public/ui-prefs.js, so the two never fight each other. */
  min-width: 380px;
  background: var(--bg-main);
  /* Lets the composer react to .main's own width, not just the viewport's —
     the floating chat panel (see body.workspace-fullscreen-active #mainChat
     below) can be dragged/resized much narrower than any real viewport ever
     gets, and the composer needs the same "drop the decorations" treatment
     the @media(max-width:720px) phone layout already gives it. */
  container-type: inline-size;
  container-name: chat-column;
  position: relative;
  transition: flex-basis 0.15s ease, width 0.15s ease, min-width 0.15s ease;
}
.main.no-transition { transition: none; }

/* Split-view chat collapse: dragging #dashboardResize (the boundary shared
   with the workspace panel) past CHAT_COLLAPSE_SNAP_WIDTH in app.js snaps
   the chat column shut instead of squeezing it below a usable width — same
   "drag to resize, snap closed near the edge" pattern as sidebar/dashboard's
   own collapse-to-rail (.sidebar.collapsed/.dashboard.collapsed). Only
   applies in split view; body.workspace-fullscreen-active#mainChat's own
   width rule (an ID selector) always wins over this while fullscreen. */
.main.collapsed {
  flex: 0 0 44px;
  width: 44px;
  min-width: 44px;
  overflow: hidden;
}
.main.collapsed .chat-float-body {
  display: none;
}
.chat-panel-collapse-btn {
  position: absolute;
  top: 50%;
  right: 4px;
  transform: translateY(-50%);
  z-index: 7;
  height: 44px;
  background: var(--bg-elevated);
  opacity: 0;
  transition: opacity 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.main:hover .chat-panel-collapse-btn,
.chat-panel-collapse-btn:hover,
.chat-panel-collapse-btn:focus-visible,
.main.collapsed .chat-panel-collapse-btn {
  opacity: 1;
}
.chat-panel-collapse-btn svg { transition: transform 0.15s ease; }
.main.collapsed .chat-panel-collapse-btn svg { transform: rotate(180deg); }
body.workspace-fullscreen-active .chat-panel-collapse-btn { display: none; }
@media (max-width: 720px) {
  .chat-panel-collapse-btn { display: none; }
}

/* One .chat pane per chat lives in the stage; only the active chat's pane is
   shown, so switching chats in the sidebar swaps the whole conversation.
   container-type lets message rows react to the chat column's own width
   (e.g. when the dashboard panel eats most of the window) rather than only
   the full viewport width. */
.chat-stage {
  flex: 1;
  min-height: 0;
  position: relative;
  container-type: inline-size;
}
/* Fullscreens the chat column itself (#mainChat) — independent of the
   workspace's own fullscreen toggle (see #workspaceFullscreenBtn), and
   hidden while the workspace is fullscreen since #mainChat is reparented
   into a small floating card at that point (see syncChatFloatState). */
/* Always-visible reminder of which exchange plain crypto lookups (price,
   ticker, order book, trades, charts with no exchange named) currently
   source from — see app.js's savedDefaultExchange. Mirrors the toolbar's
   "Exchange" row in the workspace menu (#defaultExchangeSelect) but sits
   where it's seen without opening that menu first; setPrimaryExchange keeps
   both in sync. Sits in a row with the stock-provider indicator below, left
   corner, opposite the fullscreen/share controls. */
.chat-indicator-row {
  position: absolute;
  top: 10px;
  left: 12px;
  z-index: 6;
  display: flex;
  gap: 6px;
}
.chat-exchange-indicator {
  position: relative;
}
.chat-exchange-indicator-btn {
  display: flex;
  align-items: center;
  gap: 5px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px 8px 3px 9px;
  opacity: 0.85;
  font: inherit;
  font-size: 11.5px;
  color: var(--panel-text-dim);
  cursor: pointer;
}
.chat-exchange-indicator-btn:hover,
.chat-exchange-indicator-btn[aria-expanded="true"] { opacity: 1; border-color: var(--accent); color: var(--text); }
.chat-exchange-indicator-icon { font-size: 11px; line-height: 1; }
.chat-exchange-indicator-chevron { opacity: 0.6; flex-shrink: 0; transition: transform 0.15s ease; }
.chat-exchange-indicator-btn[aria-expanded="true"] .chat-exchange-indicator-chevron { transform: rotate(180deg); }
/* Fully custom listbox — a native <select>'s dropdown popup can't be themed
   beyond its background/text color (no rounded corners, no hover states, no
   shared border/shadow language with the rest of the app), so this is a
   plain floating menu instead, same convention as
   .markets-table-columns-menu/.workspace-grid-popover elsewhere: `hidden`
   attribute toggled from JS, closed on outside click/Escape. Display is
   scoped to :not([hidden]) rather than set on the bare selector — an
   unscoped `display` here would out-rank the browser's own
   `[hidden]{display:none}` rule (author styles always beat the UA
   stylesheet, regardless of specificity) and the menu would never actually
   hide. */
.chat-exchange-indicator-menu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  min-width: 152px;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  padding: 4px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  z-index: 40;
}
.chat-exchange-indicator-menu:not([hidden]) { display: flex; flex-direction: column; gap: 1px; }
.chat-exchange-indicator-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  border-radius: 6px;
  padding: 6px 8px;
  font: inherit;
  font-size: 12.5px;
  color: var(--text);
  cursor: pointer;
}
.chat-exchange-indicator-option:hover { background: var(--panel-header-bg); }
.chat-exchange-indicator-option[aria-selected="true"] { background: var(--accent-dim); color: #fff; font-weight: 600; }
.chat-exchange-indicator-option-check { opacity: 0; flex-shrink: 0; }
.chat-exchange-indicator-option[aria-selected="true"] .chat-exchange-indicator-option-check { opacity: 1; }
.chat-exchange-indicator-option-label { display: inline-flex; align-items: center; gap: 6px; }
/* TradingView's free-tier stock/ETF data runs ~15 minutes delayed, unlike
   investing.com's realtime feed — called out right on the picker option (see
   STOCK_PROVIDER_OPTIONS in app.js) rather than left as a surprise. */
.chat-provider-delay-badge {
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  color: var(--status-amber);
  border: 1px solid currentColor;
  border-radius: 4px;
  padding: 1px 4px;
}
.chat-workspace-fullscreen-btn {
  position: absolute;
  top: 10px;
  right: 12px;
  z-index: 6;
  background: var(--bg-elevated);
  opacity: 0.7;
}
.chat-workspace-fullscreen-btn:hover { opacity: 1; }
body.workspace-fullscreen-active .chat-workspace-fullscreen-btn { display: none; }
/* Same combined Share popover as the workspace header's (#workspaceShareMenu)
   reusing its .workspace-save-menu/.workspace-save-popover styling — just
   floated in the chat column's own corner so sharing doesn't require
   switching to the workspace side first. Left of the fullscreen toggle
   (28px button + 8px gap). */
.chat-share-menu {
  position: absolute;
  top: 10px;
  right: 48px;
  z-index: 6;
}
.chat-share-btn {
  background: var(--bg-elevated);
  opacity: 0.7;
}
.chat-share-btn:hover { opacity: 1; }
.chat-share-popover { right: 0; left: auto; }
/* Scoped to this button's own aria-pressed (chat fullscreen state) rather
   than a body class, so it doesn't get dragged along by the workspace's
   separate fullscreen state via the shared .icon-expand/.icon-compress
   rules below. */
#chatWorkspaceFullscreenBtn[aria-pressed="true"] .icon-expand { display: none; }
#chatWorkspaceFullscreenBtn[aria-pressed="true"] .icon-compress { display: inline; }
.chat {
  display: none;
  flex-direction: column;
  height: 100%;
  overflow-y: auto;
  padding: 20px 0 12px;
}
.chat.active { display: flex; }

.empty-state {
  /* .chat is a column flex container, so this item's auto-margin centering
     (below) makes it shrink-wrap to its content's own preferred width by
     default instead of filling the column — and once a suggestion chip's
     un-ellipsized text is wide enough to be that preferred width, the whole
     block renders wider than the (narrow) chat column and forces a
     horizontal scrollbar. An explicit width pins it to the column's actual
     width; max-width still caps it back down and lets the auto margins
     center it once the column itself is wider than 680px. */
  width: 100%;
  box-sizing: border-box;
  max-width: 680px;
  /* A flat 10vh was tuned for a roomy chat column; once the welcome screen
     also has to fit a narrow first-visit or manually-shrunk column's worth
     of wrapped "Recent"/"Just Asked"/"Explore Markets"/"Build a Workspace"
     chips, that much fixed top space is what pushes the composer's footer
     note below the fold on common laptop viewport heights. clamp() keeps it
     proportional on tall screens but caps how much it costs on short ones. */
  margin: clamp(16px, 5vh, 80px) auto 0;
  text-align: center;
  padding: 0 24px;
}
.empty-title {
  font-size: 26px;
  font-weight: 600;
  margin-bottom: 6px;
}
.empty-sub {
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1.6;
}
.empty-sub code {
  background: var(--bg-elevated);
  padding: 1px 6px;
  border-radius: 5px;
  font-size: 13px;
}

.account-suggestions {
  margin: 16px auto 0;
  max-width: 580px;
}
.account-suggestions-title {
  color: var(--text-dim);
  font-size: 12px;
  margin-bottom: 6px;
}
.account-suggestions-list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px;
}
.account-suggestion {
  appearance: none;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text);
  cursor: pointer;
  font-size: 13px;
  padding: 7px 11px;
  /* A long alert-style prompt ("alert me when sol, doge, avax, bnb move
     5%...") has no single width that reads well as a wrapped 2-line pill,
     and on a narrow chat column every group having a couple of these
     doubles that group's height — enough on its own to push the welcome
     screen past the fold. One line + ellipsis keeps every group's height
     predictable regardless of column width; the full text still reaches
     the composer on click and the hover preview still shows it in full. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.account-suggestion:hover {
  border-color: var(--accent-dim);
  background: color-mix(in srgb, var(--accent) 12%, var(--bg-elevated));
}

/* Hover/focus preview: a real, frozen screenshot of the actual widget (see
   public/preview-shots/ and wireAccountSuggestionPreviews in app.js — never
   a live embed, which loaded slowly and fought its own scrollbars/branding
   in a small tooltip) plus a line explaining what data it shows. Compound
   pills with no single widget to screenshot fall back to a generic
   mini-chart-grid glyph. */
.account-suggestion-wrap { position: relative; display: inline-flex; max-width: 100%; }
.account-suggestion-preview {
  display: none;
  flex-direction: column;
  position: absolute;
  left: 50%;
  bottom: 100%;
  transform: translateX(-50%);
  margin-bottom: 8px;
  width: 240px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  overflow: hidden;
  z-index: 60;
}
.account-suggestion-preview.open { display: flex; }
/* Default direction is upward (bottom:100%), which clips off-screen for a
   card near the top of a scrollable container (e.g. the Add Widget modal's
   very first card, right under its "Market" field) — flip to open downward
   instead when clampAccountPreviewPosition finds no room above. */
.account-suggestion-preview.flip-down { bottom: auto; top: 100%; margin-bottom: 0; margin-top: 8px; }
.account-suggestion-preview-caption {
  appearance: none;
  border: none;
  border-bottom: 1px solid var(--border);
  background: transparent;
  color: var(--text-dim);
  font-size: 11px;
  text-align: left;
  padding: 7px 10px;
  cursor: pointer;
}
.account-suggestion-preview-caption:hover { color: var(--text); background: color-mix(in srgb, var(--accent) 10%, transparent); }
.account-suggestion-preview-caption strong { color: var(--text); font-weight: 600; }
.account-suggestion-preview-mockup {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px;
  box-sizing: border-box;
}
.account-suggestion-preview-img {
  display: block;
  width: 100%;
  height: auto;
  max-height: 340px;
  object-fit: cover;
  object-position: top;
  border-radius: 6px;
  border: 1px solid var(--border);
}
.account-suggestion-preview-desc {
  color: var(--text-dim);
  font-size: 11.5px;
  text-align: center;
  line-height: 1.4;
  padding: 0 4px;
}

.msg-row {
  max-width: calc(760px * var(--chat-width-scale));
  margin: 0 auto;
  padding: 10px 24px;
  display: flex;
}
/* The chat column can end up narrow even on a wide viewport (dashboard
   panel open beside it), so this reacts to the column's own width via
   container queries rather than only the window's. */
@container (max-width: 480px) {
  .msg-row { padding: 10px 12px; }
}
.msg-row.user { justify-content: flex-end; }
.msg-row.assistant { justify-content: flex-start; }

.bubble {
  max-width: 100%;
  border-radius: var(--radius);
  padding: 12px 16px;
  line-height: 1.55;
  white-space: pre-wrap;
  word-break: break-word;
}
.msg-row.user .bubble {
  background: var(--user-bubble);
  border-bottom-right-radius: 4px;
  border-left: 3px solid var(--turn-accent, transparent);
  padding-left: 13px;
}
/* The prompt bubble doubles as the collapse toggle for everything that
   followed it (see chatEl's delegated 'click' listener) — a big, obvious,
   already-there click target beats a dedicated button no one can find. */
.msg-row.user .bubble.turn-collapse-toggle {
  cursor: pointer;
}
.msg-row.user .bubble.turn-collapse-toggle:hover {
  filter: brightness(1.12);
}
/* Dims the prompt bubble slightly while its response is folded away, so a
   collapsed turn still reads as "there's more here" rather than looking
   like the whole exchange was just this one line. */
.msg-row.user.turn-collapsed-source .bubble {
  opacity: 0.65;
}
.msg-row.assistant .bubble {
  background: transparent;
  padding: 4px 9px 4px 0;
  flex: 1 1 auto;
  min-width: 0;
}
/* Widget cards inherit --turn-accent from their .msg-row ancestor, so a
   chat/orderbook/trades/group card rendered inline carries the same accent
   as the command that produced it — see addUserMessage/addAssistantTyping. */
.msg-row.assistant .widget-card {
  border-left: 3px solid var(--turn-accent, var(--panel-border));
}
/* The second way to fold a turn shut (the first is clicking the prompt
   bubble above it — see .turn-collapse-toggle) — a real element rather than
   "click near the response's left edge", which used to also swallow clicks
   on real content living in that same strip (a table's sortable column
   headers, a clickable row, ...). Lives as a flex sibling of .bubble, not
   inside it, so it (a) survives every `bubble.innerHTML = ''` a
   replaceTypingWith* function does when filling in the real response, and
   (b) keeps its own min-height even once the response collapses and
   .bubble's own content shrinks to nothing — never shrinks to an
   unclickable dot. */
.turn-collapse-handle {
  flex: 0 0 14px;
  align-self: stretch;
  min-height: 28px;
  border: none;
  padding: 0;
  margin: 0;
  cursor: pointer;
  background: transparent;
  position: relative;
}
.turn-collapse-handle::before {
  content: '';
  position: absolute;
  left: 5px;
  top: 0;
  bottom: 0;
  width: 3px;
  border-radius: 2px;
  background: var(--turn-accent, var(--panel-border));
  transition: background 0.15s, width 0.15s;
}
.turn-collapse-handle:hover::before,
.turn-collapse-handle:focus-visible::before {
  background: var(--accent);
  width: 4px;
}
/* Collapsed: hide the response's own content AND the handle itself — once
   folded shut, reopening goes through the (already-visible, dimmed) prompt
   bubble above it, not this bar, so there's nothing left to render here but
   a couple of empty padding pixels. */
.msg-row.assistant.turn-collapsed .turn-collapse-handle {
  display: none;
}
.msg-row.assistant.turn-collapsed .bubble > * {
  display: none;
}

.bubble pre {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 12px 14px;
  overflow-x: auto;
  font-size: 12.8px;
  font-family: var(--mono);
  margin: 8px 0 0;
}
.bubble.error pre {
  border-color: var(--danger-bg);
  background: var(--danger-bg);
  color: var(--danger);
  white-space: pre-wrap;
  overflow-x: visible;
  word-break: break-word;
}

.error-suggestion-btn {
  display: inline-block;
  margin-top: 8px;
  padding: 8px 14px;
  border-radius: 8px;
  border: 1px solid var(--accent);
  background: transparent;
  color: var(--accent);
  font-size: 13px;
  font-family: var(--mono);
  cursor: pointer;
}
.error-suggestion-btn:hover {
  background: var(--accent);
  color: var(--bg);
}

/* Large responses stay available without turning the chat transcript into a
   long scrolling wall. Native details also keeps this accessible by keyboard. */
.large-output-details {
  margin-top: 8px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-elevated);
  overflow: hidden;
}
.large-output-details > summary {
  padding: 10px 13px;
  color: var(--accent);
  cursor: pointer;
  font-size: 12.5px;
  font-weight: 600;
  list-style: none;
}
.large-output-details > summary::-webkit-details-marker { display: none; }
.large-output-details > summary::before { content: '▸ '; }
.large-output-details[open] > summary { border-bottom: 1px solid var(--border); }
.large-output-details[open] > summary::before { content: '▾ '; }
.large-output-content { padding: 0 12px 12px; }
.large-output-content .widget-card { margin-top: 12px; }

/* Human-formatted `skills list` output.  The CLI's terminal table has long
   descriptions, so this preserves its information hierarchy while allowing
   each description to wrap naturally at any chat-pane width. */
.bubble .skills-list-card {
  margin-top: 8px;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-elevated);
}
.skills-list-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 11px 14px;
  border-bottom: 1px solid var(--border);
}
.skills-list-title { font-weight: 600; }
.skills-count {
  flex-shrink: 0;
  color: var(--status-blue);
  background: rgba(var(--accent-rgb), 0.13);
  border: 1px solid rgba(var(--accent-rgb), 0.32);
  border-radius: 999px;
  padding: 2px 8px;
  font-size: 11px;
  font-weight: 600;
}
.skill-row {
  display: grid;
  grid-template-columns: minmax(130px, 0.72fr) auto minmax(0, 2.3fr);
  align-items: start;
  gap: 9px 14px;
  padding: 11px 14px;
  border-bottom: 1px solid var(--border);
}
.skill-row:last-child { border-bottom: none; }
.skill-name {
  color: var(--text);
  font-family: var(--mono);
  font-size: 12.5px;
  font-weight: 600;
  overflow-wrap: anywhere;
}
.skill-status {
  margin-top: 1px;
  border-radius: 999px;
  padding: 2px 7px;
  font-size: 10.5px;
  font-weight: 600;
  line-height: 1.35;
  white-space: nowrap;
}
.skill-status.installed { color: var(--status-green); background: rgba(126, 231, 135, 0.12); }
.skill-status.available { color: var(--text-dim); background: rgba(255, 255, 255, 0.07); }
.skill-description {
  min-width: 0;
  color: var(--text-dim);
  font-size: 12.5px;
  line-height: 1.45;
  overflow-wrap: anywhere;
}
.skills-list-footer {
  padding: 10px 14px;
  color: var(--text-dim);
  background: rgba(0, 0, 0, 0.16);
  border-top: 1px solid var(--border);
  font-size: 12px;
}
@media (max-width: 560px) {
  .skill-row { grid-template-columns: minmax(0, 1fr) auto; }
  .skill-description { grid-column: 1 / -1; }
}
.bubble .cmd-echo-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
}
.bubble .cmd-echo {
  color: var(--text-dim);
  font-size: 12px;
  font-family: var(--mono);
}
.cmd-echo-star {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  display: none;
  align-items: center;
  justify-content: center;
}
.bubble:hover .cmd-echo-star { display: flex; }
.cmd-echo-star:hover { background: var(--bg-elevated); color: var(--accent); }
.cmd-echo-star:disabled { cursor: default; }
.bubble .pin-btn {
  background: transparent;
  border: 1px solid transparent;
  /* Blue by default (matches the Workspace-grid icon it now shares — see
     PIN_SVG in app.js) so it reads as "send to Workspace" at a glance,
     instead of blending in like a neutral icon button until hovered. */
  color: var(--accent);
  width: 24px;
  height: 24px;
  border-radius: 7px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.bubble .pin-btn:hover { color: var(--accent); border-color: var(--accent-dim); background: rgba(var(--accent-rgb), 0.16); }
.bubble .pin-btn.pinned { color: var(--accent); border-color: var(--accent-dim); background: rgba(var(--accent-rgb), 0.16); }
.bubble .status-fail {
  color: var(--danger);
  font-weight: 600;
  font-size: 13px;
  margin-bottom: 4px;
}
.bubble .status-ok {
  color: var(--status-green);
  font-weight: 600;
  font-size: 13px;
  margin-bottom: 4px;
}
.bubble .resolve-meta {
  color: var(--accent);
  font-size: 11.5px;
  margin-bottom: 6px;
  opacity: 0.85;
}
.bubble .report-btn {
  display: block;
  margin-top: 8px;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-dim);
  font-size: 12px;
  padding: 5px 10px;
  border-radius: 7px;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.bubble .report-btn:hover:not(:disabled) { color: var(--danger); border-color: var(--danger); background: var(--danger-bg); }
.bubble .report-btn:disabled { cursor: default; opacity: 0.75; }
.bubble .workspace-plan-summary {
  color: var(--text-dim);
  font-size: 12.5px;
  line-height: 1.6;
  margin-top: 2px;
}
/* Structured command output: key/value grids, tables, lists, single values */
.bubble .section-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  margin: 10px 0 6px;
}
.bubble .value-hero {
  font-size: 22px;
  font-weight: 600;
  margin-top: 6px;
}
.bubble .kv-grid {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 7px 18px;
  margin-top: 8px;
  font-size: 13.5px;
}
.bubble .kv-key {
  color: var(--text-dim);
  white-space: nowrap;
  padding-top: 1px;
}
.bubble .kv-val {
  word-break: break-word;
}
.bubble .kv-grid.meta-grid {
  margin-top: 12px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  font-size: 12px;
  opacity: 0.7;
}
.bubble .generic-result {
  margin-top: 8px;
  padding: 4px 14px 13px;
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  background: var(--panel-bg);
}
.bubble .generic-result > .kv-grid:first-child { margin-top: 10px; }
.bubble .generic-result .empty-result {
  padding: 10px 0 6px;
  color: var(--panel-text-dim);
  font-size: 13px;
}
.bubble .object-section {
  margin-top: 13px;
  padding-top: 11px;
  border-top: 1px solid var(--panel-border-soft);
}
.bubble .object-section > .section-label {
  margin: 0 0 8px;
  color: var(--panel-text-dim);
}
.bubble .object-section .kv-grid { margin-top: 0; }
.bubble .text-result {
  margin-top: 8px;
  padding: 10px 12px;
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  background: var(--panel-bg);
  overflow-x: auto;
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.65;
}
.bubble .text-result-line { min-height: 1.65em; white-space: pre-wrap; word-break: break-word; }
.bubble .inline-json {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 1px 6px;
  font-size: 12px;
  font-family: var(--mono);
}

/* Nested data stays available without surfacing developer-oriented object
   labels. Its contents reuse the normal result sections and tables. */
.bubble .nested-value {
  font-size: 12px;
}
.bubble .nested-value > summary {
  cursor: pointer;
  color: var(--accent);
  list-style: none;
}
.bubble .nested-value > summary::-webkit-details-marker { display: none; }
.bubble .nested-value > summary::before {
  content: "▸ ";
  display: inline-block;
}
.bubble .nested-value[open] > summary::before {
  content: "▾ ";
}
.bubble .nested-value-content { margin-top: 8px; }
.bubble .nested-value-content .kv-grid { margin-top: 0; }
.bubble .nested-value-content .object-section:first-child { margin-top: 0; }

/* Workspace tiles already provide their own panel chrome, so the generic
   result uses that surface while retaining the same hierarchy as chat. */
.tile .generic-result { margin: 0; padding: 0; border: 0; background: transparent; }
.tile .generic-result .kv-grid {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 7px 18px;
  margin: 0;
  font-size: 13px;
}
.tile .generic-result .kv-key {
  color: var(--panel-text-dim);
  white-space: nowrap;
  padding-top: 1px;
}
.tile .generic-result .kv-val { word-break: break-word; }
.tile .generic-result .object-section {
  margin-top: 13px;
  padding-top: 11px;
  border-top: 1px solid var(--panel-border-soft);
}
.tile .generic-result .object-section > .section-label {
  margin: 0 0 8px;
  color: var(--panel-text-dim);
}
.tile .generic-result .nested-value > summary {
  cursor: pointer;
  color: var(--accent);
  list-style: none;
}
.tile .generic-result .nested-value > summary::-webkit-details-marker { display: none; }
.tile .generic-result .nested-value > summary::before { content: "▸ "; }
.tile .generic-result .nested-value[open] > summary::before { content: "▾ "; }
.tile .generic-result .nested-value-content { margin-top: 8px; }

/* Pills — status/side/delta badges used throughout widgets, tables, and kv grids */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1.6;
  white-space: nowrap;
  border: 1px solid transparent;
}
.pill-ok { color: var(--status-green); background: rgba(126, 231, 135, 0.12); border-color: rgba(126, 231, 135, 0.3); }
.pill-warn { color: var(--status-amber); background: rgba(227, 179, 65, 0.12); border-color: rgba(227, 179, 65, 0.3); }
.pill-danger { color: var(--danger); background: var(--danger-bg); border-color: rgba(229, 72, 77, 0.35); }
.pill-neutral { color: var(--text-dim); background: var(--bg-elevated); border-color: var(--border); }
.pill-buy { color: var(--status-green); background: rgba(126, 231, 135, 0.12); border-color: rgba(126, 231, 135, 0.3); }
.pill-sell { color: var(--accent); background: rgba(var(--accent-rgb), 0.14); border-color: rgba(var(--accent-rgb), 0.35); }
.pill-up { color: var(--status-green); background: rgba(126, 231, 135, 0.12); border-color: rgba(126, 231, 135, 0.3); }
.pill-down { color: var(--danger); background: var(--danger-bg); border-color: rgba(229, 72, 77, 0.35); }
.pill-flat { color: var(--text-dim); background: var(--bg-elevated); border-color: var(--border); }

/* Widget cards — product/balance/order results rendered as purpose-built
   trading-terminal panels instead of a generic key/value grid. Used both
   inline in chat and (with header-extra controls) pinned into the workspace
   grid, styled to match a dark trading-terminal aesthetic: near-black panel
   body, a tab-like header bar, tight borders, monospace figures. */
.widget-card {
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  background: var(--panel-bg);
  padding: 0;
  margin-top: 8px;
  color: var(--panel-text);
  overflow: hidden;
}
.widget-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 12px;
  background: var(--panel-header-bg);
  border-bottom: 1px solid var(--panel-border);
}
.widget-card-title {
  font-weight: 600;
  font-size: 12px;
  letter-spacing: 0.02em;
  font-family: var(--mono);
  color: var(--panel-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.widget-card-body,
.widget-card > :not(.widget-card-header) {
  padding: 12px 14px;
}
.widget-header-extra {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}
.widget-collapse-btn {
  background: transparent;
  border: 1px solid var(--panel-border);
  color: var(--panel-text-dim);
  width: 20px;
  height: 20px;
  border-radius: 5px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  margin-left: 6px;
}
.widget-collapse-btn:hover { color: var(--accent); border-color: var(--accent-dim); background: rgba(var(--accent-rgb), 0.16); }
.widget-collapse-btn svg { transition: transform 0.15s; }
.widget-card.collapsed .widget-collapse-btn svg { transform: rotate(-90deg); }
/* Collapsing hides everything but the title bar — the header itself (and
   its pin/exchange/settings controls) stays visible and interactive. */
.widget-card.collapsed .widget-card-body,
.widget-card.collapsed > :not(.widget-card-header) {
  display: none;
}
.widget-close-btn {
  background: transparent;
  border: 1px solid var(--panel-border);
  color: var(--panel-text-dim);
  width: 20px;
  height: 20px;
  border-radius: 5px;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.widget-close-btn:hover { color: var(--sell); border-color: var(--sell); background: var(--sell-bg); }
.widget-live-dot {
  background: transparent;
  border: 1px solid var(--panel-border);
  width: 20px;
  height: 20px;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.widget-live-dot::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--panel-text-dim);
}
.widget-live-dot.active { border-color: var(--sell); }
.widget-live-dot.active::before {
  background: var(--sell);
  animation: blink 1.2s infinite ease-in-out;
}

/* Vivid buy/sell/delta colors + tabular figures, scoped to trading panels */
.widget-card .pill-buy, .tile .pill-buy { color: var(--buy); background: var(--buy-bg); border-color: rgba(22, 199, 132, 0.35); }
.widget-card .pill-sell, .tile .pill-sell { color: var(--sell); background: var(--sell-bg); border-color: rgba(234, 57, 67, 0.35); }
.widget-card .pill-up, .tile .pill-up { color: var(--buy); background: var(--buy-bg); border-color: rgba(22, 199, 132, 0.35); }
.widget-card .pill-down, .tile .pill-down { color: var(--sell); background: var(--sell-bg); border-color: rgba(234, 57, 67, 0.35); }
.widget-card .kv-key, .widget-card .widget-meta-row,
.tile .kv-key, .tile .widget-meta-row { color: var(--panel-text-dim); }
.widget-card .widget-hero-price, .widget-card .widget-hero-amount,
.tile .widget-hero-price, .tile .widget-hero-amount { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.widget-card .data-table, .widget-card .widget-row,
.tile .data-table, .tile .widget-row { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* Fee tier widget: turn the API's nested rate object into a compact summary
   with the decision-making numbers (tier, maker/taker, 30-day activity) up
   front. */
.fees-card-body {
  display: grid;
  gap: 12px;
}
.fees-overview {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(178px, 1.2fr);
  gap: 12px;
  align-items: end;
}
.fees-eyebrow,
.fees-section-label {
  display: block;
  color: var(--panel-text-dim);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.fees-tier-name {
  display: block;
  margin-top: 4px;
  color: var(--panel-text);
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.02em;
}
.fees-rate-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 7px;
}
.fees-rate {
  min-width: 0;
  padding: 8px 9px;
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  background: var(--panel-header-bg);
}
.fees-rate span {
  display: block;
  color: var(--panel-text-dim);
  font-size: 10px;
}
.fees-rate strong,
.fees-activity-row strong,
.fees-details strong {
  color: var(--panel-text);
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
.fees-rate strong {
  display: block;
  margin-top: 3px;
  font-size: 14px;
}
.fees-rate-maker strong { color: var(--buy); }
.fees-rate-taker strong { color: var(--status-blue); }
.fees-activity {
  display: grid;
  gap: 7px;
  padding: 10px;
  border: 1px solid var(--panel-border-soft);
  border-radius: 7px;
  background: rgba(var(--accent-rgb), 0.045);
}
.fees-section-label { margin-bottom: 1px; }
.fees-activity-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  color: var(--panel-text-dim);
  font-size: 12px;
}
.fees-activity-row strong { font-size: 13px; }
.fees-details {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  padding-top: 10px;
  border-top: 1px solid var(--panel-border-soft);
  color: var(--panel-text-dim);
  font-size: 11px;
}
.fees-details span { white-space: nowrap; }
@media (max-width: 440px) {
  .fees-overview { grid-template-columns: 1fr; }
}

/* Product widget */
.widget-price-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}
.widget-hero-price {
  font-size: 24px;
  font-weight: 700;
}
.widget-meta-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 10px;
  font-size: 12.5px;
  color: var(--text-dim);
}
.widget-meta-item strong { color: var(--text); font-weight: 600; }
.widget-details {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  font-size: 12px;
}
.widget-details > summary {
  cursor: pointer;
  color: var(--accent);
  list-style: none;
}
.widget-details > summary::-webkit-details-marker { display: none; }
.widget-details > summary::before { content: "▸ "; }
.widget-details[open] > summary::before { content: "▾ "; }
.widget-details .kv-grid { margin-top: 8px; }

/* Ticker widget: live quote plus a compact execution tape. This is separate
   from the product-metadata card, so tickers do not hide market data behind
   the product card's More details disclosure. */
.ticker-price-row { margin-bottom: 2px; }
.ticker-hero-price { font-size: 30px; letter-spacing: -0.01em; }
/* auto-fit (not a fixed column count) means a trailing lone cell — e.g. just
   "Spread" after an even Best bid/Best ask pair — fills the row instead of
   sitting half-empty next to blank space. The 1px gap over a border-colored
   background draws hairline dividers between cells without per-cell borders
   that would otherwise double up where two cells meet. */
.ticker-metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
  gap: 1px;
  margin-top: 12px;
  border: 1px solid var(--panel-border-soft);
  border-radius: 8px;
  background: var(--panel-border-soft);
  overflow: hidden;
}
.ticker-metric {
  min-width: 0;
  padding: 8px 10px;
  background: var(--panel-header-bg);
}
.ticker-metric-label {
  display: block;
  margin-bottom: 3px;
  color: var(--panel-text-dim);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.ticker-metric strong {
  display: block;
  overflow: hidden;
  color: var(--panel-text);
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ticker-metric strong.buy { color: var(--buy); }
.ticker-metric strong.sell { color: var(--sell); }
.ticker-trades-heading {
  margin-top: 12px;
  color: var(--text-dim);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.ticker-trades { margin-top: 5px; }
.ticker-trade-row {
  display: grid;
  grid-template-columns: 42px minmax(0, 1fr) auto;
  gap: 8px;
  padding: 4px 0;
  border-bottom: 1px solid var(--border);
  color: var(--text);
  font-family: var(--mono);
  font-size: 11px;
}
.ticker-trade-row:last-child { border-bottom: 0; }

/* Recent-trades can stay in the original, roomy layout or switch to the
   compact exchange-style tape. The choice is local to each pinned tile. */
.trades-widget {
  display: flex;
  flex-direction: column;
  min-height: 0;
  height: 100%;
  background: var(--panel-bg);
}
.widget-trades {
  border-color: var(--panel-border);
  border-radius: 5px;
  background: var(--panel-bg);
  box-shadow: 0 8px 22px rgba(0, 0, 0, .4);
}
.widget-trades .tile-header {
  min-height: 30px;
  padding: 4px 7px 4px 10px;
  background: var(--panel-header-bg);
  border-color: var(--panel-border);
}
.widget-trades .tile-title {
  color: var(--panel-text);
  font-size: 11px;
  letter-spacing: .01em;
}
.widget-trades .tile-body { overflow: hidden; }
.widget-trades .tile-controls { gap: 5px; align-items: center; }
.widget-trades .widget-live-dot,
.widget-trades .widget-close-btn {
  width: 18px;
  height: 18px;
  border-color: var(--panel-border);
}
.trades-widget .trades-header-row,
.trades-widget .trade-row {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, .85fr) minmax(54px, .72fr);
  column-gap: 7px;
  align-items: center;
  padding-left: 9px;
  padding-right: 9px;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
.trades-widget .trades-header-row {
  padding-top: 4px;
  padding-bottom: 4px;
  color: var(--panel-text-dim);
  border-color: var(--panel-border-soft);
  font-size: 9px;
}
.trades-widget .trade-row {
  min-height: 18px;
  padding-top: 1px;
  padding-bottom: 1px;
  font-size: 10.5px;
}
.trades-widget .trades-header-row span:nth-child(n + 2),
.trades-widget .trade-row span:nth-child(n + 2) { text-align: right; }
.trades-widget .trades-rows { min-height: 0; overflow: auto; }
.trades-widget .trade-price { color: var(--sell); font-weight: 650; }
.trades-widget .trade-up .trade-price { color: var(--buy); }
.trades-widget .trade-down .trade-price { color: var(--sell); }
.trades-widget .trade-amount { color: var(--panel-text); font-weight: 650; }
.trades-widget .trade-time { color: var(--panel-text-dim); }
.trades-widget .trade-row-enter { animation: trade-tape-enter .25s ease-out; }
@keyframes trade-tape-enter { from { background: rgba(63, 142, 216, .18); } to { background: transparent; } }

.ticker-trade-side { color: var(--text-dim); font-weight: 600; }
.ticker-trade-side.buy { color: var(--buy); }
.ticker-trade-side.sell { color: var(--sell); }

/* Balance widget */
.widget-balance-head {
  display: flex;
  align-items: center;
  gap: 12px;
}
.balance-badge {
  position: relative;
  overflow: hidden;
  flex-shrink: 0;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--accent), var(--accent-dim));
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.01em;
}
.balance-badge.small { width: 28px; height: 28px; font-size: 9.5px; }
/* Real coin logo, layered over the text-initials fallback underneath — see
   attachAssetIcon() in app.js. Only ends up visible if the icon loads. */
.balance-badge img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.widget-hero-amount { font-size: 19px; font-weight: 700; }
.widget-sub-amount { font-size: 12px; color: var(--text-dim); margin-top: 2px; }

/* Portfolio breakdown widget — total + Cash/Crypto sections, echoing the
   layout of Coinbase's own portfolio page. */
.widget-body-portfolio .portfolio-total { font-size: 28px; margin-top: 2px; }
.portfolio-section { margin-top: 18px; }
.portfolio-section:first-of-type { margin-top: 14px; }
.portfolio-section-head {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-dim);
  margin-bottom: 6px;
}
.widget-row-portfolio {
  display: grid;
  grid-template-columns: 28px 1fr 74px 74px 64px;
  align-items: center;
  gap: 10px;
}
.widget-row-portfolio .widget-row-portfolio-col {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.portfolio-columns-head {
  border-bottom: 1px solid var(--panel-border-soft);
  padding: 4px 10px 6px;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--panel-text-dim);
  background: transparent;
}
.portfolio-columns-head .widget-row-portfolio-col,
.portfolio-columns-head .widget-row-main { font-weight: 600; }
.portfolio-allocation { color: var(--text-dim); }
.portfolio-show-more {
  width: 100%;
  margin-top: 4px;
  padding: 6px;
  background: transparent;
  border: none;
  color: var(--accent);
  font-size: 11.5px;
  cursor: pointer;
  text-align: center;
}
.portfolio-show-more:hover { text-decoration: underline; }

/* Order widget */
.widget-order-head {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.widget-order-product { font-weight: 600; font-size: 14px; }
.widget-body-order .kv-grid { margin-top: 12px; }

/* Order book widget — asks stacked above bids around a mid-price divider,
   each row's depth bar sized by cumulative size so size walls read at a
   glance instead of as a column of numbers. */
.widget-body-orderbook {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 12.5px;
  /* Stretch to the tile body's real height (when it has one) so the live
     updater's ResizeObserver measures actual available space, not just this
     element's own content-driven size — a definite parent height makes this
     resolve; an auto-height one (e.g. inline chat) leaves it a no-op. That
     no-op case still needs *some* cap though: with no bound at all, the
     ladder renders every one of ORDERBOOK_LEVELS rows per side at full
     natural height (originally invisible since no data ever reached this
     far — see market-feed.js's WS relay fix — so this was never actually
     exercised), which can grow tall enough to push chat content below it
     (e.g. follow-up-suggestion buttons) out of easy reach. 360px matches
     this app's other compact-widget-in-chat caps (e.g. .ac-panel-browse).
     Tiles override both back below since their own fixed geometry already
     caps the ladder via fitLevels(). The cap itself comes from
     --chat-ob-max-h (set in app.js from the chat panel's own real height —
     see the #chatStage ResizeObserver), falling back to 360px until that
     first measurement lands. */
  height: 100%;
  max-height: var(--chat-ob-max-h, 360px);
  overflow-y: auto;
}
/* "More rows" (see .ob-expand-toggle) — a fixed, generous cap rather than
   removing max-height outright: an inline chat card sitting in a normal
   viewport shouldn't be able to grow to the full 100-row ORDERBOOK_LEVELS_MAX
   and push everything else out of view. min() keeps it from overflowing a
   short viewport too. Same specificity trick as the tile override just
   above it in the cascade: two classes beats the base rule's one. */
.widget-body-orderbook.ob-expanded {
  max-height: min(900px, 80vh);
}
.ob-controls {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  min-height: 20px;
  margin-bottom: 3px;
}
.ob-expand-toggle {
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 2px 7px;
  background: var(--bg);
  color: var(--text-dim);
  font: inherit;
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: .03em;
  cursor: pointer;
}
.ob-expand-toggle:hover,
.ob-expand-toggle:focus-visible { border-color: var(--accent); color: var(--text); outline: none; }
.ob-expand-toggle[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
.ob-grouping-label {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--text-dim);
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: .03em;
}
.ob-grouping-select {
  max-width: 88px;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 2px 4px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 10px;
  cursor: pointer;
}
.ob-grouping-select:hover,
.ob-grouping-select:focus { border-color: var(--accent); outline: none; }
.ob-header-row {
  display: flex;
  justify-content: space-between;
  padding: 0 2px 6px;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--panel-text-dim, var(--text-dim));
}
.ob-side {
  display: flex;
  flex-direction: column;
}
.ob-row {
  position: relative;
  display: flex;
  justify-content: space-between;
  padding: 2.5px 4px;
  border-radius: 3px;
}
.ob-depth-bar {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  border-radius: 3px;
  z-index: 0;
  transition: filter .15s ease;
}
.ob-ask .ob-depth-bar { background: var(--sell-bg); }
.ob-bid .ob-depth-bar { background: var(--buy-bg); }
/* A wall row's own bar segment gets a saturated color instead of the
   ordinary depth-shading tint so a single outsized level pops out of the
   gradual cumulative gradient rather than just being "a bit wider." */
.ob-ask .ob-row.ob-wall .ob-depth-bar { background: color-mix(in srgb, var(--sell-bg) 55%, var(--sell)); }
.ob-bid .ob-row.ob-wall .ob-depth-bar { background: color-mix(in srgb, var(--buy-bg) 55%, var(--buy)); }
.ob-row.ob-wall .ob-size { color: var(--text); font-weight: 700; }
/* Coinbase-style hover sweep: every level between the spread and whatever
   row the pointer is on brightens together (see setDepthHover in app.js),
   reading as "this much of the book" rather than just one row's tooltip.
   `filter` rather than swapping `background` so it composes with the wall
   tint above instead of one silently overriding the other via specificity. */
.ob-row.ob-depth-hover .ob-depth-bar { filter: brightness(1.4); }
.ob-row.ob-depth-hover .ob-price,
.ob-row.ob-depth-hover .ob-size { color: var(--text); }
.ob-price, .ob-size {
  position: relative;
  z-index: 1;
  transition: color .15s ease;
}
.ob-ask .ob-price { color: var(--sell); }
.ob-bid .ob-price { color: var(--buy); }
.ob-size { color: var(--text-dim); }
.ob-mid-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 6px 4px;
  margin: 2px 0;
  border-top: 1px solid var(--panel-border-soft, var(--border));
  border-bottom: 1px solid var(--panel-border-soft, var(--border));
}
.ob-mid-price { font-size: 15px; font-weight: 700; }
.ob-spread { font-size: 11px; color: var(--text-dim); }
/* Fixed to the pointer rather than nested in the ladder so this detail card
   remains readable even when the book is one narrow panel in a group. */
.ob-level-tooltip {
  position: fixed;
  z-index: 2100;
  width: 208px;
  padding: 7px 9px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: color-mix(in srgb, var(--panel-bg) 96%, #000);
  box-shadow: 0 8px 20px rgba(0, 0, 0, .42);
  color: var(--text);
  font: 10.5px/1.45 var(--mono);
  white-space: pre-line;
  pointer-events: none;
}

/* Compact rows for list widgets (balance/order/product lists) — styled like
   a terminal order/asset ledger: tight, monospace, zebra-free with a subtle
   hover highlight instead of individual card chrome per row. */
.widget-row-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
  border: 1px solid var(--panel-border-soft);
  border-radius: 6px;
  overflow: hidden;
}
.widget-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 10px;
  border: none;
  border-bottom: 1px solid var(--panel-border-soft);
  background: var(--panel-bg);
  font-size: 12.5px;
  transition: background 0.1s;
}
.widget-row:last-child { border-bottom: none; }
.widget-row:hover { background: var(--panel-header-bg); }
.widget-row-clickable { cursor: pointer; }
.widget-row-clickable:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }
.widget-row-title { font-weight: 600; flex: 1; }
.widget-row-price { color: var(--panel-text-dim); }
.widget-row-balance .widget-row-title { flex: unset; }
.widget-row-main {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}
.widget-row-sub {
  font-size: 11px;
  color: var(--panel-text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.widget-row-hold {
  font-size: 11px;
  color: var(--panel-text-dim);
  flex-shrink: 0;
}
.widget-row-volume {
  font-size: 11px;
  color: var(--panel-text-dim);
  flex-shrink: 0;
}

/* Best-bid-ask row: product id, then bid/ask price+size side by side so the
   top of book reads at a glance without expanding anything. */
.widget-row-bba { gap: 14px; }
.bba-product { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bba-side {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  min-width: 82px;
  line-height: 1.35;
}
.bba-price { font-weight: 600; }
.bba-bid .bba-price { color: var(--buy); }
.bba-ask .bba-price { color: var(--sell); }
.bba-size { font-size: 10.5px; color: var(--panel-text-dim); }
.bba-spread {
  font-size: 11px;
  color: var(--panel-text-dim);
  min-width: 52px;
  text-align: right;
  flex-shrink: 0;
}
.bba-time {
  font-size: 11px;
  color: var(--panel-text-dim);
  flex-shrink: 0;
  min-width: 64px;
  text-align: right;
}

.widget-last-updated {
  font-size: 10.5px;
  color: var(--panel-text-dim);
  white-space: nowrap;
  flex-shrink: 0;
}

/* Pinned-tile counterpart of the chat live-toggle's status line (.bubble
   .live-status) — same wording ("live · updated ...", "snapshot from ...")
   and the same color states, so a result reads identically whether it's
   still in chat or pinned to the Workspace. */
.widget-live-status {
  display: block;
  padding: 5px 14px 8px;
  font-size: 10.5px;
  color: var(--panel-text-dim);
  border-top: 1px solid var(--panel-border);
}
.widget-live-status.ok { color: var(--status-green); }
.widget-live-status.warn { color: var(--status-amber); }
.widget-live-status.err { color: var(--danger); }

/* Dashboard/workspace-pinned variant: slightly tighter padding so more fit on screen */
.dashboard-card.widget-card { margin-top: 0; }
.dashboard-card .widget-row-list { max-height: 260px; overflow-y: auto; }
/* A pinned tile already scrolls its whole body (.tile-body is flex:1;
   overflow:auto — see above), so capping the row list itself at a fixed
   260px just clips rows early and leaves the rest of a taller (or resized)
   tile empty instead of showing them. Let the list grow to its natural
   height here and leave scrolling, if any is actually needed, to the tile
   body around it. */
.tile .widget-row-list { max-height: none; overflow-y: visible; }

/* Table filter bar */
.bubble .table-filter-bar, .tile .table-filter-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}
.bubble input.table-filter, .tile input.table-filter {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 8px;
  padding: 6px 10px;
  font-size: 12.5px;
  outline: none;
  width: 220px;
  max-width: 100%;
}
.bubble input.table-filter:focus, .tile input.table-filter:focus { border-color: var(--accent-dim); }
.bubble .filter-count, .tile .filter-count {
  font-size: 11.5px;
  color: var(--text-dim);
}
.bubble .table-filter-wrap .data-table-wrap, .tile .table-filter-wrap .data-table-wrap { margin-top: 0; }

/* Top Movers/Gainers/Losers panel: select-mode row checkbox + bulk-open bar */
.widget-row-select-checkbox {
  flex-shrink: 0;
  cursor: pointer;
  accent-color: var(--accent);
}
.product-list-select-toggle.active { color: var(--accent); background: var(--bg-elevated); }
.product-list-action-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px 0;
}
.product-list-action-bar button {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 999px;
  padding: 5px 12px;
  font-size: 12px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.product-list-action-bar button:hover:not(:disabled) { background: var(--border); }
.product-list-action-bar button:disabled { color: var(--text-dim); cursor: not-allowed; }

/* Live-update toggle */
.bubble .live-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}
.bubble .live-toggle {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 999px;
  padding: 5px 12px;
  font-size: 12px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.bubble .live-toggle:hover { background: var(--border); }
.bubble .live-toggle.active {
  background: var(--danger-bg);
  border-color: var(--danger);
  color: var(--danger);
}
.bubble .live-toggle.active::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #ff6b6b;
  margin-right: 6px;
  animation: blink 1.2s infinite ease-in-out;
}
.bubble .live-status {
  font-size: 11.5px;
  color: var(--text-dim);
}
.bubble .live-status.ok { color: var(--status-green); }
.bubble .live-status.warn { color: var(--status-amber); }
.bubble .live-status.err { color: var(--danger); }

/* Inline "add a chart?" offer shown the first time a product's live feed starts */
.bubble .chart-prompt {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 7px;
  margin-bottom: 8px;
  padding: 7px 10px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 10px;
  font-size: 12px;
  color: var(--text-dim);
}
/* When several prompts stack back-to-back (e.g. a follow-up chain of
   question -> destination -> live/snapshot), fuse them into one connected
   panel with a shared divider instead of three separate floating boxes —
   reads as one system asking sequential questions, not three stacked alerts. */
.bubble .chart-prompt:has(+ .chart-prompt) {
  margin-bottom: 0;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.bubble .chart-prompt + .chart-prompt {
  margin-top: -1px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}
.bubble .chart-prompt-btn {
  border-radius: 999px;
  padding: 4px 12px;
  font-size: 12px;
  cursor: pointer;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  transition: background 0.15s, border-color 0.15s;
}
.bubble .chart-prompt-btn.yes {
  background: var(--accent-dim);
  border-color: var(--accent);
  color: #fff;
}
.bubble .chart-prompt-btn:hover { filter: brightness(1.12); }
.bubble .chart-prompt-btn.pin-all-btn { display: inline-flex; align-items: center; gap: 6px; }
/* .chart-prompt-btn is normally a <button>; the classic-migration welcome
   notice's "Back to classic" action is a plain link instead (it navigates
   away rather than acting in-place) — strip the underline so it still reads
   as a pill button, not inline text. */
a.chart-prompt-btn { display: inline-block; text-decoration: none; }
.bubble .chart-prompt-count-input {
  width: 64px;
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}
.bubble .market-picker {
  display: grid;
  gap: 8px;
  max-width: 440px;
  margin-top: 10px;
}
.bubble .market-picker-search {
  width: 100%;
  box-sizing: border-box;
  padding: 7px 9px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 12.5px;
}
.bubble .market-picker-selection,
.bubble .market-picker-empty {
  color: var(--text-dim);
  font-size: 11.5px;
}
.bubble .market-picker-results {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  max-height: 176px;
  overflow-y: auto;
}
.bubble .market-picker-result {
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
  color: var(--text);
  cursor: pointer;
  font: 11.5px var(--mono);
}
.bubble .market-picker-result.selected {
  border-color: var(--accent);
  background: var(--accent-dim);
  color: #fff;
}
.bubble .chart-prompt-btn:disabled {
  cursor: not-allowed;
  opacity: .55;
}

/* Per-message "report a problem" flag (attachFeedbackTrigger in app.js) and
   the panel it opens (feedback.js). Hover-revealed like .cmd-echo-star, just
   anchored to the bubble's own corner since not every assistant bubble has a
   .cmd-echo-row to sit inside. */
.msg-row.assistant .bubble { position: relative; }
.feedback-trigger-btn {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 22px;
  height: 22px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  display: none;
  align-items: center;
  justify-content: center;
}
.bubble:hover .feedback-trigger-btn { display: flex; }
.feedback-trigger-btn:hover { background: var(--bg-elevated); color: var(--accent); }
.feedback-trigger-btn:disabled { cursor: default; opacity: .5; }

/* Command responses put their pin in .cmd-echo-row. The report flag joins
    that flex row so both actions remain independently clickable instead of
    occupying the same top-right corner. */
.bubble .cmd-echo-row:has(.feedback-trigger-inline) { justify-content: flex-start; }
.bubble .cmd-echo-row:has(.feedback-trigger-inline) .cmd-echo { margin-right: auto; }
.cmd-echo-row .feedback-trigger-inline {
  position: static;
  flex-shrink: 0;
}

.bubble .feedback-panel { flex-direction: column; align-items: stretch; }
.feedback-chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
.bubble .feedback-chip.selected {
  background: var(--accent-dim);
  border-color: var(--accent);
  color: #fff;
}
.feedback-note {
  width: 100%;
  box-sizing: border-box;
  resize: vertical;
  padding: 7px 9px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 12.5px;
}
.feedback-action-row { display: flex; gap: 8px; }

/* Live data + chart side by side, widened via .msg-row.has-chart. A direct
   single-chart answer uses the same wide row so its widget does not inherit
   the narrow text-message measure. */
.msg-row.has-chart { max-width: calc(1180px * var(--chat-width-scale)); width: 100%; }

/* A wide, many-column result (e.g. order history) reads like a spreadsheet
   and needs the same escape from the narrow text-message measure. */
.msg-row.has-data-table { max-width: calc(1180px * var(--chat-width-scale)); width: 100%; }

/* Multi-widget chat replies (matchWorkspacePlan's "N widgets:" summary,
   cluster follow-ups, "add more" buttons, ...) build their chart/order
   book/trades cards through several different code paths in app.js, and
   not all of them remembered to toggle .has-chart on their row the way the
   single inline-chart reply does. Catching any of these widget kinds
   directly via :has() widens the row regardless of which builder produced
   it, so a chart/order book/trades card is never stuck shrink-wrapped to
   the narrow text-message measure (and stays correctly sized as the chat
   column is resized) even if a future call site forgets the class too. */
.msg-row:has(.widget-group),
.msg-row:has(.widget-orderbook),
.msg-row:has(.widget-trades) { max-width: calc(1180px * var(--chat-width-scale)); width: 100%; }

/* Same escape for the "+" widget picker's chat-inline cards (charts,
   TradingView public widgets, CryptoPanic news) — see pinChartWidgetInChat,
   pinTradingViewWidget's chat branch, and pinCryptoPanicWidget's chat branch. */
.msg-row:has(.widget-wide-card) { max-width: calc(1180px * var(--chat-width-scale)); width: 100%; }
.bubble .live-with-chart {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 16px;
}
.bubble .live-data-col {
  flex: 1 1 260px;
  min-width: 220px;
}
.bubble .live-with-chart .pinned-chart-prompt {
  flex: 1 0 100%;
  margin: 0;
}

/* Lightweight Charts candlestick+volume widget and its indicator box.
   Base rules are unscoped so the same chart instance also works pinned
   inside a Workspace tile (see .tile overrides below), not just in a
   chat bubble. */
.lw-chart-root {
  position: relative;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  background: var(--bg-elevated);
}
.lw-chart {
  width: 100%;
  position: relative;
}
.lw-chart.lw-loading::after {
  content: "Loading chart…";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-dim);
  font-size: 13px;
  background: var(--bg-elevated);
}
.bubble .lw-chart-root {
  flex: 2 1 460px;
  min-width: 340px;
}
.bubble .lw-chart {
  height: 420px;
}
.lw-tf-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 6px 8px;
  border-top: 1px solid var(--border);
  flex-shrink: 0;
}
.lw-tf-btn {
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-dim);
  border-radius: 6px;
  padding: 3px 8px;
  font-size: 11px;
  font-family: var(--mono);
  cursor: pointer;
}
.lw-tf-btn:hover { background: var(--bg-elevated); color: var(--text); }
.lw-tf-btn.active { background: var(--accent-dim); color: #fff; }
.chart-pin-btn {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 3;
  background: rgba(42, 42, 42, 0.85) !important;
}

/* TradingView Advanced Chart widget — Coinbase's own site runs on this same
   charting library, so the dark theme + candle-color overrides passed to the
   widget (see tv-chart.js) plus this shell are what get it looking native. */
.tv-chart-root {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  background: var(--tv-bg, #0a0a0a);
}
.tv-chart-container {
  width: 100%;
  position: relative;
}
.tv-chart-container.tv-loading::after {
  content: "Loading chart…";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-dim);
  font-size: 13px;
  background: var(--tv-bg, #0a0a0a);
}
.tv-chart-timeout {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 20px;
  box-sizing: border-box;
  background: var(--tv-bg, #0a0a0a);
  text-align: center;
}
.tv-chart-timeout-msg {
  color: var(--text-dim);
  font-size: 13px;
  max-width: 360px;
}
.tv-chart-timeout-actions {
  display: flex;
  gap: 8px;
}
.bubble .tv-chart-root {
  flex: 2 1 460px;
  min-width: 340px;
}
.bubble .tv-chart-container {
  height: 420px;
}
/* A standalone chart should read like the focal result, not a compact
   preview. Pinned tiles keep their independent sizing rules below. */
.bubble.has-chart .tv-chart-root {
  width: min(100%, 960px);
  min-width: 0;
}
.bubble.has-chart .tv-chart-container {
  height: clamp(460px, 60vh, 640px);
}
.tv-chart-bar {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  padding: 6px 8px;
  border-top: 1px solid var(--border);
  flex-shrink: 0;
}
.tv-chart-bar .chart-pin-btn {
  position: static;
  background: transparent !important;
}
.tile .tv-chart-root {
  height: 100%;
  border: none;
  border-radius: 0;
}
.tile .tv-chart-container {
  flex: 1 1 auto;
  min-height: 0;
}
.tile .tv-chart-bar {
  display: none; /* pinned tiles never carry their own re-pin button */
}
.investing-chart-root {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  background: var(--panel-bg);
  height: 420px;
}
/* buildInvestingStockChart's (investing-widget.js) wrapper around whatever
   resolved for a ticker that needed a live lookup — a real chart's own root
   (.investing-chart-root or .tv-chart-root) nests inside it once resolved.
   `display: contents` drops this wrapper's own box out of layout entirely,
   so the nested widget sizes exactly as if it were a direct child of
   .tile/.bubble, same as a curated ticker's chart — no need to duplicate
   every tile/bubble-scoped sizing rule above for a wrapper that shouldn't
   visually exist. */
.investing-chart-stock-resolved {
  display: contents;
}
.investing-chart-root.investing-chart-loading {
  align-items: center;
  justify-content: center;
  color: var(--text-dim);
  font-size: 13px;
}
.investing-chart-frame {
  border: none;
  flex: 1 1 auto;
  min-height: 0;
}
.investing-interval-note {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  background: rgba(var(--overlay-rgb), .92);
  color: var(--text);
  font-size: 12px;
  line-height: 1.4;
  border-bottom: 1px solid var(--border);
}
.investing-interval-note-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-left: auto;
  flex-shrink: 0;
}
.investing-interval-note-dismiss {
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text);
  font-size: 12px;
  padding: 3px 9px;
  border-radius: 6px;
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
}
.investing-interval-note-dismiss:hover {
  filter: brightness(1.12);
}
.investing-interval-note-dismiss.active {
  background: var(--accent);
  color: var(--accent-text);
  border-color: var(--accent);
}
.bubble .investing-chart-root {
  flex: 2 1 460px;
  min-width: 340px;
}
.bubble.has-chart .investing-chart-root {
  width: min(100%, 960px);
  min-width: 0;
  height: clamp(460px, 60vh, 640px);
}
.tile .investing-chart-root {
  height: 100%;
  border: none;
  border-radius: 0;
}
.tv-market-widget,
.tv-market-widget .tradingview-widget-container,
.tv-market-widget .tradingview-widget-container__widget {
  width: 100%;
  height: 100%;
  min-height: 0;
}
.tv-market-widget {
  position: relative;
  overflow: hidden;
  background: var(--panel-bg);
}
.tv-market-widget .tradingview-widget-container { position: relative; }
.tv-market-widget .tradingview-widget-container iframe { width: 100% !important; height: 100% !important; }
.tv-market-widget-credit {
  position: absolute;
  right: 7px;
  bottom: 5px;
  z-index: 2;
  color: var(--text-dim);
  font-size: 10px;
  text-decoration: none;
  background: rgba(var(--overlay-rgb), .72);
  padding: 1px 3px;
  border-radius: 3px;
}
.tv-market-widget-credit:hover { color: var(--text); }
.tv-market-widget-error {
  display: grid;
  place-items: center;
  padding: 20px;
  box-sizing: border-box;
  color: var(--text-dim);
  font-size: 13px;
  text-align: center;
}
.chart-pending {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-dim);
  font-size: 13px;
}
.chart-error {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 20px;
  box-sizing: border-box;
  text-align: center;
}
.chart-error-msg {
  color: var(--text-dim);
  font-size: 13px;
}

/* Standalone chart panel (pinChartWidget) fills its tile-body. Inside a
   group tile the same element also carries .group-panel-chart below, which
   sizes it via its CSS grid area instead — this height:100% only matters
   for the standalone case, where .tile-body isn't itself a grid/flex
   container. */
.chart-panel {
  height: 100%;
  min-height: 0;
  /* Anchors .chart-chrome-hint below to the chart's own box, in both a
     standalone tile and a group tile's CSS-grid cell (pinGroupWidget places
     this exact same element straight into its grid, unwrapped). */
  position: relative;
}
/* Suggests hiding TradingView's own toolbars/legend once a chart tile gets
   too small for them to coexist comfortably with the candles — see
   createChartPanel's ResizeObserver in app.js. Sits low and centered so it
   clears the widget's own bottom date-range bar rather than covering it. */
.chart-chrome-hint {
  position: absolute;
  left: 50%;
  bottom: 34px;
  transform: translateX(-50%);
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 6px;
  max-width: calc(100% - 24px);
  padding: 5px 6px 5px 10px;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 11px;
  color: var(--text-dim);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
}
.chart-chrome-hint[hidden] { display: none; }
.chart-chrome-hint-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.chart-chrome-hint-apply {
  flex-shrink: 0;
  border-radius: 999px;
  padding: 3px 10px;
  font-size: 11px;
  border: 1px solid var(--accent);
  background: var(--accent-dim);
  color: #fff;
  cursor: pointer;
  white-space: nowrap;
}
.chart-chrome-hint-apply:hover { filter: brightness(1.12); }
.chart-chrome-hint-dismiss {
  flex-shrink: 0;
  border: 0;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 13px;
  line-height: 1;
  padding: 2px 3px;
}
.chart-chrome-hint-dismiss:hover { color: var(--text); }

/* Combined chart + order book + trades tile (pinGroupWidget): a CSS grid so
   any subset/order of the three panels can be arranged as "chart focus"
   (chart full-width on top, the rest split evenly beneath — the default),
   even rows, or even columns (see the drag-to-combine layout picker). The
   actual grid-template/area assignment is computed in JS (applyGroupLayout)
   since it depends on which panels are present, not just a fixed class
   name — this stylesheet only supplies the shared look. */
.tile.widget-group { min-width: 420px; min-height: 480px; }
.widget-group-body {
  display: grid;
  height: 100%;
  min-height: 0;
  min-width: 0;
  gap: 1px;
  background: var(--panel-border);
  overflow: hidden;
}
.group-panel {
  min-height: 0;
  min-width: 0;
  overflow: auto;
  background: var(--panel-bg);
}
/* Grid tracks use minmax(0, …) (app.js). A fixed chart floor here would
   expand the track again and recreate the group-overlap bug in a short cell. */
.group-panel-chart { min-height: 0; }
.group-panel-orderbook,
.group-panel-trades {
  min-width: 160px;
}
/* A group panel is already the exact grid cell to fill. Let the ladder own
   that entire cell instead of inheriting the generic card's content height. */
.group-panel-orderbook.widget-body-orderbook {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  height: 100%;
  max-height: none;
  min-height: 0;
  padding: 5px 6px;
  overflow: hidden;
}
.group-panel-orderbook .ob-controls,
.group-panel-orderbook .ob-header-row,
.group-panel-orderbook .ob-mid-row { flex: none; }
.group-panel-orderbook .ob-side {
  flex: 1 1 0;
  min-height: 0;
  overflow: hidden;
}
/* Keep ladder rows exchange-compact. A handful of grouped price buckets
   should remain a small stack nearest the spread, not become huge panels
   that disguise the available depth. */
.group-panel-orderbook .ob-ask { justify-content: flex-end; }
.group-panel-orderbook .ob-bid { justify-content: flex-start; }
.group-panel-orderbook .ob-row {
  box-sizing: border-box;
  flex: 0 0 14px;
  min-height: 0;
}

/* A grouped book shares its height with a chart and/or trades. Keep its
   controls legible, but spend far less of that scarce space on chrome than a
   full standalone order book does. */
.group-panel-orderbook .ob-controls {
  min-height: 16px;
  margin-bottom: 1px;
}
.group-panel-orderbook .ob-grouping-label {
  gap: 3px;
  font-size: 8px;
}
.group-panel-orderbook .ob-grouping-select {
  max-width: 58px;
  min-height: 16px;
  padding: 0 3px;
  font-size: 9px;
}
.group-panel-orderbook .ob-header-row {
  padding: 0 1px 3px;
  font-size: 8.5px;
  line-height: 1.1;
}
.group-panel-orderbook .ob-mid-row {
  min-height: 17px;
  margin: 1px 0;
  padding: 2px 3px;
  line-height: 1;
}
.group-panel-orderbook .ob-mid-price { font-size: 11px; }
.group-panel-orderbook .ob-spread { font-size: 8.5px; }
.group-panel-orderbook .ob-row {
  padding: 1px 3px;
  font-size: 10px;
  line-height: 1.1;
}

/* Give standalone dashboard books a dense, Coinbase-like cadence.
   fitLevels() creates as many real rows as their height allows, but even a
   liquid book runs out of real buckets at a coarse enough GROUP LEVELS
   setting (e.g. $500 on BTC) — no amount of retained depth changes that,
   since real liquidity genuinely thins out away from the spread. A hard
   0-grow here left that case as dead black space above/below the real rows.
   Letting rows grow (bounded, so a genuinely tiny book — 2-3 real quotes —
   doesn't balloon into a couple of absurd giant bars) fills that leftover
   room with the rows that do exist instead of leaving it empty. */
.tile.widget-orderbook .ob-ask { justify-content: flex-end; }
.tile.widget-orderbook .ob-bid { justify-content: flex-start; }
.tile.widget-orderbook .ob-row {
  box-sizing: border-box;
  flex: 1 1 18px;
  max-height: 48px;
}

.tile .lw-chart-root {
  height: 100%;
  display: flex;
  flex-direction: column;
  border: none;
  border-radius: 0;
}
.tile .lw-chart {
  flex: 1 1 auto;
  min-height: 0;
}
.tile .lw-indicator-bar {
  flex-shrink: 0;
}
.bubble .lw-indicator-bar, .tile .lw-indicator-bar {
  border-top: 1px solid var(--border);
  padding: 8px 10px;
}
.bubble .lw-indicator-chips, .tile .lw-indicator-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 6px;
}
.bubble .lw-chip, .tile .lw-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px 6px 3px 10px;
  font-size: 11.5px;
  color: var(--text);
}
.bubble .lw-chip-x, .tile .lw-chip-x {
  border: none;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 13px;
  line-height: 1;
  padding: 2px 4px;
  border-radius: 50%;
}
.bubble .lw-chip-x:hover, .tile .lw-chip-x:hover { color: var(--danger); background: var(--danger-bg); }
.bubble .lw-indicator-form, .tile .lw-indicator-form { display: flex; }
.bubble .lw-indicator-input, .tile .lw-indicator-input {
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 6px 10px;
  font-size: 12.5px;
  color: var(--text);
}
.bubble .lw-indicator-input:focus, .tile .lw-indicator-input:focus { border-color: var(--accent-dim); outline: none; }
.bubble .lw-indicator-hint, .tile .lw-indicator-hint {
  margin-top: 6px;
  font-size: 11.5px;
  color: var(--text-dim);
  min-height: 14px;
}
.bubble .lw-indicator-hint.err, .tile .lw-indicator-hint.err { color: var(--danger); }

.bubble .list-plain {
  margin: 8px 0 0;
  padding-left: 20px;
}
.bubble .list-plain li {
  margin-bottom: 5px;
  font-size: 13.5px;
}
.bubble .data-table-wrap, .tile .data-table-wrap {
  overflow-x: auto;
  margin-top: 8px;
  border: 1px solid var(--border);
  border-radius: 10px;
}
.bubble table.data-table, .tile table.data-table {
  border-collapse: collapse;
  width: 100%;
  font-size: 12.8px;
}
.bubble table.data-table th,
.bubble table.data-table td,
.tile table.data-table th,
.tile table.data-table td {
  padding: 8px 14px;
  text-align: left;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.bubble table.data-table th, .tile table.data-table th {
  color: var(--text-dim);
  font-weight: 600;
  background: var(--bg-elevated);
}
.bubble table.data-table th.sortable, .tile table.data-table th.sortable {
  cursor: pointer;
  user-select: none;
}
.bubble table.data-table th.sortable:hover, .tile table.data-table th.sortable:hover { color: var(--text); }
.bubble table.data-table th.sortable::after, .tile table.data-table th.sortable::after {
  content: '';
  display: inline-block;
  width: 10px;
}
.bubble table.data-table th.sort-asc::after, .tile table.data-table th.sort-asc::after { content: '▲'; margin-left: 4px; font-size: 9px; }
.bubble table.data-table th.sort-desc::after, .tile table.data-table th.sort-desc::after { content: '▼'; margin-left: 4px; font-size: 9px; }
.bubble table.data-table tr:last-child td, .tile table.data-table tr:last-child td {
  border-bottom: none;
}
.bubble table.data-table tr.row-clickable, .tile table.data-table tr.row-clickable {
  cursor: pointer;
}
.bubble table.data-table tr.row-clickable:hover,
.bubble table.data-table tr.row-clickable:focus-visible,
.tile table.data-table tr.row-clickable:hover,
.tile table.data-table tr.row-clickable:focus-visible {
  background: var(--bg-elevated);
  outline: none;
}
.bubble table.data-table td .inline-json, .tile table.data-table td .inline-json {
  color: var(--text-dim);
}

.typing {
  display: inline-flex;
  gap: 4px;
  padding: 8px 0;
}
.typing span {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--text-dim);
  animation: blink 1.2s infinite ease-in-out;
}
.typing span:nth-child(2) { animation-delay: 0.2s; }
.typing span:nth-child(3) { animation-delay: 0.4s; }
@keyframes blink { 0%, 80%, 100% { opacity: 0.2; } 40% { opacity: 1; } }

/* Composer */
.composer-wrap {
  position: relative;
  max-width: calc(760px * var(--chat-width-scale));
  width: calc(100% - 48px);
  margin: 0 auto 4px;
}

.composer {
  background: var(--bg-composer);
  border: 1px solid var(--border);
  border-radius: 22px;
  display: flex;
  align-items: flex-end;
  padding: 10px 10px 10px 18px;
  gap: 8px;
}

/* Live command hint above the composer */
.cmd-hint {
  display: none;
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--text-dim);
  padding: 0 18px 6px;
}
.cmd-hint .hint-cmd { color: var(--text); font-weight: 600; }
.cmd-hint .hint-req { color: var(--accent); }
.cmd-hint .hint-opt { color: var(--text-dim); }
.cmd-hint .hint-used { color: var(--text-dim); opacity: 0.45; text-decoration: line-through; }

/* Autocomplete suggestion panel */
.ac-panel {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  margin-bottom: 8px;
  max-height: 260px;
  overflow-y: auto;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  padding: 6px;
}
.ac-panel-browse { max-height: 360px; }
.ac-browse-heading {
  padding: 7px 10px 8px;
  color: var(--text-dim);
  font-size: 11.5px;
  font-weight: 600;
}
.ac-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 10px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 13px;
}
.ac-item:hover,
.ac-item.active {
  background: var(--bg-sidebar);
}
.ac-label {
  font-family: var(--mono);
  font-size: 12.5px;
  white-space: nowrap;
  color: var(--text);
  flex-shrink: 0;
}
.ac-label.kind-resource { color: var(--accent); }
.ac-label.kind-action { color: var(--status-green); }
.ac-label.kind-field-req { color: var(--accent); }
.ac-label.kind-field { color: var(--text); }
.ac-label.kind-query { color: var(--status-teal); }
.ac-label.kind-flag { color: var(--status-purple); }
.ac-label.kind-value { color: var(--text-dim); }
.ac-label mark {
  background: transparent;
  color: var(--text);
  font-weight: 700;
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-underline-offset: 2px;
}
.ac-desc {
  flex: 1;
  min-width: 0;
  font-size: 11.5px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: right;
}

/* Provider toggle + exchange picker atop a search results list — shared by
   #chartSearchPanel (the composer's "@" picker) and the tile header's ⇄
   "change symbol" popover (.symbol-search-panel, see symbol-search-panel.js).
   Scoped to .ac-search-host rather than .ac-panel/.ac-item, which are also
   used by the unrelated "/" command panel. */
/* JS toggles #chartSearchPanel's display between 'none' and 'flex' directly
   (an inline style, so it always wins over the .ac-panel default below) —
   flex-direction here only applies layout for when it's shown. */
#chartSearchPanel {
  flex-direction: column;
  padding: 0;
  /* Not hidden: the exchange suggestions dropdown below is positioned
     absolutely and needs to be able to render outside the exchange bar's own
     box without getting clipped. .ac-list handles its own scroll/clip. */
  overflow: visible;
}
.ac-search-host .ac-provider-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.ac-search-host .ac-provider-label {
  font-size: 11px;
  color: var(--text-dim);
  flex-shrink: 0;
}
.ac-search-host .ac-provider-toggle {
  display: flex;
  gap: 4px;
  background: var(--bg-sidebar);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 2px;
}
.ac-search-host .ac-provider-btn {
  border: none;
  background: transparent;
  color: var(--text-dim);
  font-size: 11.5px;
  padding: 3px 8px;
  border-radius: 4px;
  cursor: pointer;
}
.ac-search-host .ac-provider-btn:hover {
  color: var(--text);
}
.ac-search-host .ac-provider-btn.active {
  background: var(--accent);
  color: #fff;
}
.ac-search-host .ac-exchange-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.ac-search-host .ac-exchange-label {
  font-size: 11px;
  color: var(--text-dim);
  flex-shrink: 0;
}
.ac-search-host .ac-exchange-input-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
}
.ac-search-host .ac-exchange-input {
  width: 100%;
  background: var(--bg-sidebar);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px 6px;
  font-size: 12px;
}
.ac-search-host .ac-exchange-suggestions {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 4px;
  max-height: 180px;
  overflow-y: auto;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  z-index: 2;
}
.ac-search-host .ac-exchange-suggestion {
  padding: 6px 10px;
  font-size: 12px;
  cursor: pointer;
}
.ac-search-host .ac-exchange-suggestion:hover {
  background: var(--bg-sidebar);
}
.ac-search-host .ac-exchange-suggestion-all {
  color: var(--text-dim);
  border-bottom: 1px solid var(--border);
}
.ac-search-host .ac-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 6px;
}

/* .symbol-search-panel: the tile header's ⇄ popover's own .ac-search-host —
   unlike #chartSearchPanel (which floats as its own box above the composer),
   this sits in normal flow right below the popover's plain text input, so it
   carries its own elevated box (border/shadow/bg) rather than relying on
   .ac-panel's absolute positioning. */
.symbol-search-panel {
  display: flex;
  flex-direction: column;
  width: 320px;
  max-height: 320px;
  margin-top: 6px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  overflow: hidden;
}

/* Clickable, plain-English version of the terminal-oriented `coinbase --help`. */
.command-guide-card {
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--bg-elevated);
}
.command-guide-intro { padding: 16px 16px 12px; }
.command-guide-intro h2 { margin: 0; color: var(--text); font-size: 16px; }
.command-guide-intro p,
.command-guide-section > p {
  margin: 5px 0 0;
  color: var(--text-dim);
  font-size: 12.5px;
  line-height: 1.45;
}
.command-guide-section { padding: 12px 16px; border-top: 1px solid var(--border); }
.command-guide-section h3 { margin: 0; color: var(--text); font-size: 12px; }
.command-guide-list { display: grid; gap: 7px; margin-top: 10px; }
.command-guide-button {
  display: grid;
  width: 100%;
  gap: 2px;
  padding: 10px 11px;
  text-align: left;
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  font: inherit;
  cursor: pointer;
}
.command-guide-button:hover,
.command-guide-button:focus-visible { border-color: var(--accent); background: var(--bg-sidebar); outline: none; }
.command-guide-label { color: var(--accent); font-size: 13px; font-weight: 600; }
.command-guide-detail { color: var(--text-dim); font-size: 12px; line-height: 1.4; }

.composer-input-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
  display: flex;
  /* Queried below so the 140px hint gutter collapses off *this* box's own
     shrinking width, not just the whole chat column's — the column can stay
     well above 440px (see the @container chat-column rule) while the icon
     buttons in .composer have already squeezed this wrap down to a sliver,
     which otherwise left almost no room for text and forced ugly 2-3 char
     line wraps with a big dead gutter next to them. */
  container-type: inline-size;
  container-name: composer-input;
}

.composer-ghost {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  /* Reserves the same gutter as .composer-input's scrollbar-gutter: stable
     below, even though this layer never scrolls — otherwise the two layers
     wrap text at different widths once the real textarea grows a scrollbar,
     and the ghost suffix drifts out of alignment with the real caret. */
  scrollbar-gutter: stable;
  white-space: pre-wrap;
  word-wrap: break-word;
  pointer-events: none;
  color: transparent;
  font-family: var(--font);
  font-size: calc(14.5px * var(--ui-font-scale));
  line-height: 1.5;
  padding: 6px 140px 6px 0;
}
.composer-ghost .ghost-suffix { color: var(--text-dim); opacity: 0.65; }

.composer-input {
  position: relative;
  z-index: 1;
  flex: 1;
  min-width: 0;
  background: transparent;
  border: none;
  outline: none;
  resize: none;
  color: var(--text);
  font-family: var(--font);
  font-size: calc(14.5px * var(--ui-font-scale));
  line-height: 1.5;
  min-height: calc(1.5em + 12px);
  max-height: calc(3em + 12px);
  overflow-y: auto;
  scrollbar-gutter: stable;
  transition: height 0.1s ease-out;
  padding: 6px 140px 6px 0;
}
.composer-input::placeholder {
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The "history / Ctrl K" hint sits absolutely over the composer's bottom-right
   corner; without this reserved gutter its pill overlaps the placeholder (or
   typed text) whenever the composer is narrower than both combined. */

.composer-hint {
  position: absolute;
  z-index: 2;
  right: 0;
  bottom: 6px;
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  color: var(--text-dim);
  white-space: nowrap;
  opacity: 0.7;
  cursor: pointer;
  transition: opacity 0.15s;
}
.composer-hint:hover { opacity: 1; }
.composer-hint.hidden { opacity: 0; pointer-events: none; }
.composer-hint span { font-size: 12px; }
/* Fires purely off the input wrap's own width, so it kicks in as soon as
   the reserved hint gutter would actually start crowding the text — well
   before the whole chat column drops to the @container chat-column
   breakpoint above. */
@container composer-input (max-width: 260px) {
  .composer-hint { display: none; }
  .composer-input, .composer-ghost { padding-right: 6px; }
}
.composer-hint kbd {
  background: var(--bg-composer);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 4px;
  font-family: var(--mono);
  font-size: 10px;
  color: var(--text);
}

/* Ctrl+R history search overlay */
.history-panel {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  margin-bottom: 8px;
  max-height: 360px;
  overflow: hidden;
  flex-direction: column;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}
.history-panel-search {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  color: var(--text-dim);
  flex-shrink: 0;
}
.history-panel-search input {
  flex: 1;
  background: transparent;
  border: none;
  outline: none;
  color: var(--text);
  font-family: var(--font);
  font-size: 13.5px;
}
.history-panel-list {
  overflow-y: auto;
  padding: 6px;
}
.history-item {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 7px 10px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 13px;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.history-item:hover,
.history-item.active { background: var(--bg-sidebar); }
.history-item mark {
  background: transparent;
  color: var(--text);
  font-weight: 700;
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-underline-offset: 2px;
}
.history-panel-empty {
  padding: 16px 12px;
  color: var(--text-dim);
  font-size: 12.5px;
  text-align: center;
}

.send-btn {
  background: var(--accent);
  border: none;
  color: var(--accent-text);
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
  transition: opacity 0.15s;
}
.send-btn:disabled { opacity: 0.4; cursor: default; }
.send-btn:not(:disabled):hover { opacity: 0.85; }

.chart-search-btn {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-dim);
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
  align-self: flex-end;
  transition: color 0.15s, border-color 0.15s;
}
.chart-search-btn:hover,
.chart-search-btn:focus-visible { color: var(--text); border-color: var(--accent); outline: none; }

/* Always-visible "where do new results go" switch beside the send button —
   mirrors the buried Results select in the workspace ⋯ menu (see
   workspaceResultMode/syncResultDestControls in app.js) so the choice never
   requires hunting for that menu. */
.result-dest-toggle {
  display: flex;
  align-items: center;
  gap: 5px;
  height: 32px;
  padding: 0 10px;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 16px;
  color: var(--text-dim);
  font-size: 12px;
  cursor: pointer;
  flex-shrink: 0;
  align-self: flex-end;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.result-dest-toggle:hover,
.result-dest-toggle:focus-visible { color: var(--text); border-color: var(--accent); outline: none; }
.result-dest-toggle[data-dest="workspace"] {
  color: var(--accent-text);
  background: var(--accent);
  border-color: var(--accent);
}
.result-dest-toggle .result-dest-icon-workspace { display: none; }
.result-dest-toggle[data-dest="workspace"] .result-dest-icon-chat { display: none; }
.result-dest-toggle[data-dest="workspace"] .result-dest-icon-workspace { display: inline-flex; }

/* .main sets container-type/container-name: chat-column (see the base .main
   rule) so this reacts to the chat column's own width, not just the
   viewport's — the floating fullscreen-chat panel routinely gets this
   narrow, same as the @media(max-width:720px) phone layout already
   accounts for below, but that one only fires on a narrow *viewport*. */
@container chat-column (max-width: 440px) {
  .composer-hint { display: none; }
  .composer-input, .composer-ghost { padding-right: 6px; }
  .result-dest-toggle { padding: 0 8px; gap: 0; }
  .result-dest-toggle-label { display: none; }
}

.disclaimer {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  text-align: center;
  font-size: 11.5px;
  color: var(--text-dim);
  padding: 4px 24px 14px;
}
.disclaimer-help-wrap { position: relative; display: inline-flex; }
.disclaimer-help {
  appearance: none;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-dim);
  font-size: 10px;
  line-height: 1;
  padding: 0;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.disclaimer-help:hover,
.disclaimer-help:focus-visible,
.disclaimer-help.open { color: var(--text); border-color: var(--accent); outline: none; }
.disclaimer-help-panel {
  display: none;
  position: absolute;
  left: 50%;
  bottom: 100%;
  transform: translateX(-50%);
  margin-bottom: 8px;
  width: max-content;
  max-width: 300px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  padding: 8px 10px;
  font-size: 11.5px;
  color: var(--text-dim);
  text-align: left;
}
.disclaimer-help-wrap:hover .disclaimer-help-panel,
.disclaimer-help-panel.open { display: block; }
.disclaimer-help-panel div { padding: 2px 0; }
.disclaimer-help-panel kbd {
  background: var(--bg-composer);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 4px;
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--text);
}

/* Confirm modal */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.55);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 50;
}
.modal-backdrop.show { display: flex; }
/* Both are generic utility dialogs (confirm/prompt) that any other modal can
   open on top of itself — e.g. renaming a theme or naming a new custom theme
   from inside the Appearance modal. Without a higher z-index than the base
   .modal-backdrop, DOM order alone decides the stacking tie and whichever
   modal was opened first (they're both fixed-position, same z-index) can
   paint over these, leaving an unusable dialog hidden behind it. */
#confirmBackdrop, #textPromptBackdrop, .alert-help-backdrop { z-index: 70; }
/* A help modal with several steps can run taller than a modal-content of
   static height — without a cap it just overflows the viewport with no way
   to reach the Close button on a shorter screen. */
.alert-help-backdrop .modal { max-height: 85vh; overflow-y: auto; }

.modal {
  width: 420px;
  max-width: calc(100vw - 32px);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 20px;
}
.modal-title { font-weight: 600; font-size: 15px; margin-bottom: 10px; }
.modal-body { font-size: 13.5px; color: var(--text-dim); line-height: 1.5; }
.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 18px;
}

/* window.prompt() replacement (see showTextPrompt in app.js) — same input
   look as .alias-add-row's, just full-width in its own single-field form. */
.text-prompt-modal { width: 380px; }
.text-prompt-modal #textPromptMessage { margin: 0 0 12px; }
.text-prompt-modal #textPromptForm { margin: 0; }
.text-prompt-input {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 13px;
  font: inherit;
}
.text-prompt-input:focus { border-color: var(--accent); outline: none; }
.text-prompt-input[readonly] { cursor: text; color: var(--text-dim); }

/* Confirm modal: order-review layout ---------------------------------- */
.confirm-order-modal { width: 380px; }
.confirm-order-header {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 14px;
}
.confirm-order-badge {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 4px 10px;
  border-radius: 6px;
  text-transform: uppercase;
  white-space: nowrap;
}
.confirm-badge-buy { color: var(--buy); background: var(--buy-bg); border: 1px solid rgba(22, 199, 132, 0.35); }
.confirm-badge-sell { color: var(--sell); background: var(--sell-bg); border: 1px solid rgba(234, 57, 67, 0.35); }
.confirm-badge-convert { color: var(--accent); background: rgba(var(--accent-rgb), 0.14); border: 1px solid rgba(var(--accent-rgb), 0.35); }
.confirm-badge-neutral { color: var(--text-dim); background: var(--bg-sidebar); border: 1px solid var(--border); }
.confirm-order-pair { font-size: 16px; font-weight: 600; color: var(--text); }
.confirm-order-rows {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  margin-bottom: 4px;
}
.confirm-order-row { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; font-size: 13px; }
.confirm-order-row-label { color: var(--text-dim); }
.confirm-order-row-value { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; text-align: right; }
.confirm-order-raw { margin-top: 12px; font-size: 12px; color: var(--text-dim); }
.confirm-order-raw summary { cursor: pointer; user-select: none; }
.confirm-order-raw summary:hover { color: var(--text); }
.confirm-order-raw pre {
  background: var(--bg-sidebar);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 10px;
  color: var(--text);
  margin-top: 8px;
  font-size: 12px;
  overflow-x: auto;
}
.btn {
  border: none;
  border-radius: 8px;
  padding: 8px 14px;
  font-size: 13px;
  cursor: pointer;
}
.btn-ghost {
  background: transparent;
  color: var(--text-dim);
  border: 1px solid var(--border);
}
.btn-ghost:hover { color: var(--text); }
.btn-danger {
  background: var(--danger);
  color: #fff;
}
.btn-danger:hover { opacity: 0.9; }
.btn-primary { background: var(--accent); color: #fff; }
.btn-primary:hover { filter: brightness(1.08); }
.btn-buy { background: var(--buy); color: #06231a; }
.btn-buy:hover { opacity: 0.9; }
.btn-sell { background: var(--sell); color: #fff; }
.btn-sell:hover { opacity: 0.9; }
/* Title stays put and Save/Cancel (.modal-actions, appended below) stays
   pinned at the bottom — only chart-preferences-scroll's own settings
   sections scroll, so the buttons that actually save the form are never
   scrolled out of view no matter how far down someone scrolls. */
.chart-preferences-modal {
  width: 420px;
  max-height: calc(100vh - 30px);
  /* 100vh on mobile browsers includes area the address bar can cover,
     overstating the actually-visible height and letting .modal-actions'
     Save button render below the real fold; 100dvh tracks the browser
     chrome's current state instead. Kept as a second declaration (not a
     replacement) so browsers without dvh support silently keep the vh
     value above rather than getting an invalid, ignored max-height. */
  max-height: calc(100dvh - 30px);
  display: flex;
  flex-direction: column;
  padding: 16px;
  background: var(--tv-modal-bg);
  border-color: var(--tv-modal-border);
  border-radius: 4px;
}
.chart-preferences-modal .modal-title { font-size: 20px; margin-bottom: 18px; flex-shrink: 0; color: var(--tv-modal-text); }
.chart-preferences-modal .modal-body { display: none; }
.chart-preferences-scroll { overflow-y: auto; min-height: 0; padding-bottom: 4px; }
.chart-preset-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 16px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--tv-modal-divider);
}
.workspace-view-select {
  appearance: none;
  -webkit-appearance: none;
  background: var(--tv-modal-input-bg) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%23999999' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right 10px center;
  color: var(--tv-modal-text);
  border: 1px solid var(--tv-modal-input-border);
  border-radius: 6px;
  padding: 7px 26px 7px 10px;
  font: inherit;
  font-size: 12px;
  min-height: 30px;
  cursor: pointer;
  text-overflow: ellipsis;
  transition: border-color .12s ease;
}
.workspace-view-select:hover { border-color: var(--tv-modal-input-border-hover); }
.workspace-view-select:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(0, 82, 255, .25); }
.chart-preset-bar .workspace-view-select { flex: 1 1 120px; min-width: 0; max-width: none; }
.workspace-view-btn {
  background: var(--tv-modal-btn-bg);
  color: var(--tv-modal-text);
  border: 1px solid var(--tv-modal-input-border);
  border-radius: 6px;
  padding: 7px 11px;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  white-space: nowrap;
  transition: border-color .12s ease, background .12s ease;
}
.workspace-view-btn:hover:not(:disabled) { border-color: var(--tv-modal-input-border-hover); background: var(--tv-modal-hover); }
.workspace-view-btn:disabled { opacity: .4; cursor: default; }
.workspace-view-delete:hover:not(:disabled) { color: var(--danger); background: var(--danger-bg); border-color: rgba(234, 57, 67, .4); }
.chart-preferences-form { margin: 0; color: var(--tv-modal-text); font-size: 12px; }
.settings-section { border-top: 1px solid var(--tv-modal-divider); }
.settings-section:last-child { border-bottom: 1px solid var(--tv-modal-divider); }
.settings-section summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 38px;
  cursor: pointer;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--tv-modal-text-dim);
  list-style: none;
}
.settings-section summary:hover { color: var(--tv-modal-text); }
.settings-section summary::-webkit-details-marker { display: none; }
.settings-section summary::after { content: '⌄'; font-size: 15px; color: var(--tv-modal-text-dim); }
.settings-section[open] summary::after { content: '⌃'; }
.settings-section-body { display: grid; gap: 10px; padding: 4px 0 14px; }
.settings-field {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 187px;
  align-items: center;
  gap: 12px;
  color: var(--tv-modal-text-dim);
  font-size: 12.5px;
}
.settings-field select, .settings-field input:not([type="checkbox"]) {
  width: 100%;
  min-width: 0;
  box-sizing: border-box;
  background: var(--tv-modal-input-bg);
  color: var(--tv-modal-text);
  border: 1px solid var(--tv-modal-input-border);
  border-radius: 6px;
  padding: 7px 10px;
  min-height: 30px;
  font: inherit;
  font-size: 12.5px;
  transition: border-color .12s ease;
}
.settings-field select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%23999999' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  padding-right: 26px;
  cursor: pointer;
  text-overflow: ellipsis;
}
.settings-field select:hover, .settings-field input:not([type="checkbox"]):not(:disabled):hover { border-color: var(--tv-modal-input-border-hover); }
.settings-field select:focus-visible, .settings-field input:not([type="checkbox"]):focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(0, 82, 255, .25);
}
.settings-field input:disabled { background: var(--tv-modal-hover); color: var(--tv-modal-text-dim); border-color: var(--tv-modal-input-border); }
.settings-field input[type="color"] { padding: 3px; cursor: pointer; }
.opacity-control { display: flex; align-items: center; gap: 8px; }
.opacity-control input[type="range"] { flex: 1 1 auto; min-width: 0; }
.opacity-value { flex: 0 0 auto; min-width: 34px; text-align: right; color: var(--tv-modal-text-dim); font-size: 11.5px; }
.settings-size-field > div { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.settings-switch-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 23px; color: var(--tv-modal-text-dim); font-size: 12.5px; }
.settings-investing-note {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 10px 0 0;
  padding: 9px 11px;
  background: rgba(227, 179, 65, .12);
  border: 1px solid rgba(227, 179, 65, .35);
  border-radius: 6px;
  color: var(--status-amber);
  font-size: 11.5px;
  line-height: 1.4;
}
.settings-investing-note::before { content: 'ⓘ'; flex: 0 0 auto; color: var(--status-amber); }
/* .settings-field/.settings-switch-row's own `display` ties in specificity with
   the UA stylesheet's `[hidden] { display: none }`, and the author rule wins —
   so a plain `hidden` attribute silently does nothing on those rows without this. */
.investing-hide[hidden] { display: none; }
.settings-investing-note[hidden] { display: none; }
[data-news-field][hidden] { display: none; }
.settings-switch-row input { position: absolute; opacity: 0; pointer-events: none; }
.settings-switch {
  width: 25px;
  height: 14px;
  border-radius: 999px;
  background: #4a4a4a;
  position: relative;
  flex: 0 0 auto;
}
.settings-switch::after { content: ''; position: absolute; width: 10px; height: 10px; left: 2px; top: 2px; border-radius: 50%; background: #b9b9b9; transition: transform .15s; }
.settings-switch-row input:checked + .settings-switch { background: #f3f3f3; }
.settings-switch-row input:checked + .settings-switch::after { transform: translateX(11px); background: #222; }
.settings-switch-row input:disabled + .settings-switch { opacity: .9; }
.settings-options-grid { gap: 8px; }
.settings-field small { grid-column: 1 / -1; color: var(--tv-modal-text-dim); font-size: 11px; line-height: 1.4; }
/* Same full-width span as .settings-field small above, for the news widget's
   Symbol field's search dropdown (attachInlineSymbolSearch, app.js) — left to
   .settings-field's default grid placement it would land in the label-text
   column, squeezed to a sliver instead of sitting under the input. */
.settings-field .symbol-search-panel { grid-column: 1 / -1; width: 100%; max-height: 240px; }
.rapid-movers-master { margin: 2px 0 2px; }
.rapid-movers-status { margin: 0 0 12px; color: var(--tv-modal-text-dim); font-size: 12px; min-height: 15px; }
/* These two fields hold a whole multi-item row, not one input — letting them
   sit in .settings-field's normal fixed 187px control column (built for a
   single input/select) squeezed 13 exchange chips or 3 direction options
   down to 1-2 items per line, ballooning the panel's height. Give just these
   two fields their own full-width, stacked-label layout instead. */
.settings-field:has(.rapid-movers-exchange-list),
.settings-field:has(.settings-segmented) {
  grid-template-columns: 1fr;
  gap: 4px;
}
.rapid-movers-exchange-header { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.rapid-movers-exchange-header-actions { display: flex; gap: 10px; flex: 0 0 auto; }
.rapid-movers-exchange-header-actions .link-btn {
  background: none;
  border: none;
  padding: 0;
  color: var(--tv-modal-text-dim);
  font-size: 11px;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.rapid-movers-exchange-header-actions .link-btn:hover { color: var(--tv-modal-text); }
.rapid-movers-exchange-list { display: flex; flex-wrap: wrap; gap: 7px; justify-content: flex-start; }
/* Icon-only exchange picker: each exchange is a real-logo tile instead of a
   text pill, so 13 exchanges read as one compact row of marks instead of
   3-4 lines of wrapped labels. Name lives in the tooltip/aria-label. */
.rapid-movers-exchange-tile {
  position: relative;
  width: 34px;
  height: 34px;
  border-radius: 9px;
  border: 1px solid var(--tv-modal-input-border);
  background: var(--tv-modal-input-bg);
  overflow: hidden;
  cursor: pointer;
  flex: 0 0 auto;
  transition: border-color .12s ease, opacity .12s ease, box-shadow .12s ease;
}
.rapid-movers-exchange-tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: grayscale(55%);
  opacity: .55;
  transition: filter .12s ease, opacity .12s ease;
}
.rapid-movers-exchange-tile:hover { border-color: var(--tv-modal-input-border-hover); }
.rapid-movers-exchange-tile:hover img { opacity: .85; filter: grayscale(15%); }
.rapid-movers-exchange-tile:has(input:checked) {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(0, 82, 255, .2);
}
.rapid-movers-exchange-tile:has(input:checked) img { opacity: 1; filter: grayscale(0); }
.rapid-movers-exchange-tile:has(input:checked)::after {
  content: '✓';
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 14px;
  height: 14px;
  border-radius: 50% 0 0 0;
  background: var(--accent);
  color: #fff;
  font-size: 9px;
  line-height: 14px;
  text-align: center;
  padding: 0 0 1px 2px;
  box-sizing: border-box;
}
.rapid-movers-exchange-tile input { position: absolute; opacity: 0; pointer-events: none; }
.rapid-movers-grid-default { display: flex; align-items: center; gap: 6px; }
.rapid-movers-grid-default input { width: 100%; min-width: 0; }
.rapid-movers-grid-default span { color: var(--tv-modal-text-dim); flex: 0 0 auto; }
.rapid-movers-direction-icon { font-size: 10px; line-height: 1; }
.rapid-movers-direction-icon-pump { color: var(--status-green); }
.rapid-movers-direction-icon-dump { color: var(--danger); }
.rapid-movers-pin-btn { border: 1px solid transparent; border-radius: 4px; }
.rapid-movers-pin-btn:hover { border-color: var(--border); }
.rapid-movers-pin-btn.active { background: var(--accent); border-radius: 4px; box-shadow: 0 0 0 1px var(--accent); }

/* Compact segmented-button toggle for a small radio group (Direction, Update
   mode) — same look as a TradingView interval picker. Keeps the underlying
   radio inputs (rapid-movers.js queries them via `input:checked`) but hides
   the dot and turns the <label> itself into a button segment. */
.settings-segmented {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 3px;
  padding: 3px;
  background: var(--tv-modal-input-bg);
  border: 1px solid var(--tv-modal-input-border);
  border-radius: 8px;
}
.settings-segmented label {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 5px 10px;
  border-radius: 5px;
  color: var(--tv-modal-text-dim);
  font-size: 12px;
  white-space: nowrap;
  cursor: pointer;
  transition: background .12s ease, color .12s ease;
}
.settings-segmented label:has(input:checked) { background: var(--tv-modal-hover); color: var(--tv-modal-text); }
.settings-segmented input { position: absolute; opacity: 0; pointer-events: none; }

/* Pairs two related fields on one row instead of each taking a full-width
   row — mirrors .settings-size-field's existing 1fr/1fr split. */
.settings-field-pair { grid-template-columns: 1fr 1fr; align-items: start; }
.settings-field-pair > label, .settings-field-pair > div { display: grid; gap: 4px; font-size: inherit; }

.rapid-movers-modal { width: 480px; }
.rapid-movers-modal .settings-section-body { gap: 8px; }
.rapid-movers-modal .settings-section summary { min-height: 34px; }

/* Rapid Movers workspace widget — the persistent, always-visible tile that
   shows scan status and a live list of what's currently on screen, with a
   gear button back into the settings modal above. Without this tile,
   closing the modal left no on-screen sign the scan was still running or
   way back into its settings. */
.rapid-movers-widget-live-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--panel-text-dim);
  flex: 0 0 auto;
  align-self: center;
}
.rapid-movers-widget-live-dot.active { background: var(--status-green); animation: blink 1.2s infinite ease-in-out; }
.rapid-movers-widget-body { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; height: 100%; box-sizing: border-box; overflow: hidden; }
/* The search bar sits outside this — see buildRapidMoversSearchBar — so it
   stays put and keeps focus while renderRapidMoversWidgetBody wipes and
   rebuilds this content area on every tick. */
.rapid-movers-widget-content { display: flex; flex-direction: column; gap: 8px; flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.rapid-movers-widget-search { flex: 0 0 auto; }
.rapid-movers-widget-search input {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  font-size: 12px;
  padding: 5px 9px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
}
.rapid-movers-widget-search input:focus { outline: none; border-color: var(--accent); }
.rapid-movers-widget-status { color: var(--panel-text-dim); font-size: 12px; }
.rapid-movers-widget-quote-notice { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }
.rapid-movers-widget-quote-notice-row { color: var(--panel-text-dim); font-size: 11.5px; line-height: 1.5; }
.rapid-movers-widget-quote-switch-btn {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  font-size: 11.5px;
  text-decoration: underline;
  cursor: pointer;
}
.rapid-movers-widget-list { display: flex; flex-direction: column; gap: 4px; }
.rapid-movers-widget-empty { color: var(--panel-text-dim); font-size: 12.5px; padding: 8px 0; }
.rapid-movers-widget-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px 8px;
  border-radius: 6px;
  border: 1px solid var(--panel-border);
  /* Opaque so the sidebar placement's dot-grid fill (.rapid-movers-sidebar-
     panel .tile-body, below) only ever shows through genuinely empty space,
     never behind an actual row. */
  background: var(--panel-bg);
}
/* Only a candidate with an open chart tile can be scrolled to -- the rest of
   the list (see renderRapidMoversWidgetBody) is informational only, so only
   this subset gets the clickable affordance. */
.rapid-movers-widget-row-live { cursor: pointer; }
.rapid-movers-widget-row-live:hover { background: var(--bg-elevated); }
.rapid-movers-widget-row-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.rapid-movers-widget-row-left { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.rapid-movers-widget-row-symbol { font-size: 12.5px; font-weight: 600; }
.rapid-movers-widget-row-exchange { font-size: 11px; color: var(--panel-text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rapid-movers-widget-row-time { font-size: 10.5px; color: var(--panel-text-dim); }

/* Trending widget — mirrors the .rapid-movers-widget-* rules above for a
   consistent list/row/status visual language, plus a small tab strip. */
.trending-widget-body { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; height: 100%; box-sizing: border-box; overflow: hidden; }
.trending-widget-tabs { display: flex; gap: 4px; flex: 0 0 auto; }
.trending-widget-tab {
  flex: 1 1 auto;
  padding: 5px 9px;
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  background: var(--panel-bg);
  color: var(--panel-text-dim);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.trending-widget-tab.active { color: var(--text); border-color: var(--accent); background: var(--bg-elevated); }
.trending-widget-status { flex: 0 0 auto; font-size: 10.5px; color: var(--panel-text-dim); }
.trending-widget-list { display: flex; flex-direction: column; gap: 4px; flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.trending-widget-empty { color: var(--panel-text-dim); font-size: 12.5px; padding: 8px 0; }
.trending-widget-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border-radius: 6px;
  border: 1px solid var(--panel-border);
  background: var(--panel-bg);
  cursor: pointer;
}
.trending-widget-row:hover { background: var(--bg-elevated); }
.trending-widget-row-rank { flex: 0 0 auto; width: 16px; text-align: right; font-size: 11px; color: var(--panel-text-dim); }
.trending-widget-row-label { flex: 1 1 auto; min-width: 0; font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.trending-widget-row-count { flex: 0 0 auto; font-size: 11px; color: var(--panel-text-dim); }
.trending-widget-row-seen { flex: 0 0 auto; width: 44px; text-align: right; font-size: 10px; color: var(--panel-text-dim); }
/* Empty until attachTrendingGains resolves (see trending.js) — no placeholder
   text, so a still-loading badge just reads as blank space rather than a
   flash of "…" for every row. */
.trending-widget-row-gain { flex: 0 0 auto; width: 52px; text-align: right; font-size: 11px; color: var(--panel-text-dim); }
.trending-widget-row-gain.up { color: var(--status-green); }
.trending-widget-row-gain.down { color: var(--danger); }
.trending-widget-groupby-row { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
.trending-widget-groupby-label { font-size: 11px; color: var(--panel-text-dim); }
.trending-widget-groupby {
  padding: 2px 8px;
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  background: var(--panel-bg);
  color: var(--panel-text-dim);
  font: inherit;
  font-size: 11px;
  cursor: pointer;
}
.trending-widget-groupby.active { color: var(--text); border-color: var(--accent); background: var(--bg-elevated); }
.trending-widget-filter-row { display: flex; flex-direction: column; gap: 6px; flex: 0 0 auto; }
.trending-widget-search {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  font-size: 12px;
  padding: 5px 9px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
}
.trending-widget-search:focus { outline: none; border-color: var(--accent); }
.trending-widget-exchange-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.trending-widget-exchange-chip {
  padding: 2px 8px;
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  background: var(--panel-bg);
  color: var(--panel-text-dim);
  font: inherit;
  font-size: 10.5px;
  cursor: pointer;
}
.trending-widget-exchange-chip.active { color: var(--text); border-color: var(--accent); background: var(--bg-elevated); }

/* Shared accent across the summary tile and every chart tile the engine
   currently owns (see rapid-movers.js's tagTileAsRapidMovers) — the group
   stays contiguous in the grid via rapidMoversGroupAnchor, and this ties it
   together visually too, so it still reads as one pool at a glance even
   with other, unrelated widgets pinned elsewhere in the same workspace.
   A solid border alone reads as "just this tile's color" at a glance; the
   tinted header band plus a repeated badge icon is what actually reads as
   "these tiles belong together" across a multi-tile grid. */
.tile.rapid-movers-group-member {
  border-color: var(--accent);
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35), inset 0 0 0 2px rgba(var(--accent-rgb), 0.5);
}
.tile.rapid-movers-group-member > .tile-header {
  background: color-mix(in srgb, var(--accent) 20%, var(--panel-header-bg));
  border-bottom-color: rgba(var(--accent-rgb), 0.5);
}
.tile.rapid-movers-group-member > .tile-header .tile-title::before {
  content: '⚡';
  margin-right: 5px;
}

/* Trending Stocks (trending-stocks.js) — the equity counterpart to Rapid
   Movers above. Structurally identical widget/tile styling, reusing the
   same tokens, just under its own class prefix and title::before glyph so
   the two scanners' chart pools stay visually distinguishable from each
   other in a workspace that has both running at once. */
.trending-stocks-modal { width: 480px; }
.trending-stocks-modal .settings-section-body { gap: 8px; }
.trending-stocks-modal .settings-section summary { min-height: 34px; }

.trending-stocks-widget-live-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--panel-text-dim);
  flex: 0 0 auto;
  align-self: center;
}
.trending-stocks-widget-live-dot.active { background: var(--status-green); animation: blink 1.2s infinite ease-in-out; }
.trending-stocks-widget-body { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; height: 100%; box-sizing: border-box; overflow: hidden; }
.trending-stocks-widget-content { display: flex; flex-direction: column; gap: 8px; flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* The category pill row (Trending/Most Active/Watchers/Most Bullish/Most
   Bearish/Top Gainers/Top Losers) — scrolls horizontally rather than
   trending-widget.js's equal-width .trending-widget-tab(s) (reused above
   for the 3-way All/Equities/Crypto row instead), since squeezing 7 labels
   that wide into equal columns would make them unreadable on a
   workspace-tile-sized panel. */
.trending-stocks-widget-category-tabs { display: flex; gap: 4px; flex: 0 0 auto; overflow-x: auto; padding-bottom: 2px; }
.trending-stocks-widget-category-tab {
  flex: 0 0 auto;
  padding: 5px 9px;
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  background: var(--panel-bg);
  color: var(--panel-text-dim);
  font: inherit;
  font-size: 11.5px;
  white-space: nowrap;
  cursor: pointer;
}
.trending-stocks-widget-category-tab.active { color: var(--text); border-color: var(--accent); background: var(--bg-elevated); }
.trending-stocks-widget-search { flex: 0 0 auto; }
.trending-stocks-widget-search input {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  font-size: 12px;
  padding: 5px 9px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
}
.trending-stocks-widget-search input:focus { outline: none; border-color: var(--accent); }
.trending-stocks-widget-status { color: var(--panel-text-dim); font-size: 12px; }
.trending-stocks-widget-list { display: flex; flex-direction: column; gap: 4px; }
.trending-stocks-widget-empty { color: var(--panel-text-dim); font-size: 12.5px; padding: 8px 0; }
.trending-stocks-widget-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px 8px;
  border-radius: 6px;
  border: 1px solid var(--panel-border);
  background: var(--panel-bg);
}
.trending-stocks-widget-row-live { cursor: pointer; }
.trending-stocks-widget-row-live:hover { background: var(--bg-elevated); }
.trending-stocks-widget-row-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.trending-stocks-widget-row-left { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.trending-stocks-widget-row-symbol { font-size: 12.5px; font-weight: 600; }
.trending-stocks-widget-row-title { font-size: 11px; color: var(--panel-text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.trending-stocks-widget-row-quote { display: flex; align-items: center; gap: 6px; margin-top: 4px; }
.trending-stocks-widget-row-price { font-size: 11.5px; font-weight: 600; }
.trending-stocks-widget-row-volume { font-size: 10.5px; color: var(--panel-text-dim); margin-left: auto; }

.tile.trending-stocks-group-member {
  border-color: var(--accent);
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35), inset 0 0 0 2px rgba(var(--accent-rgb), 0.5);
}
.tile.trending-stocks-group-member > .tile-header {
  background: color-mix(in srgb, var(--accent) 20%, var(--panel-header-bg));
  border-bottom-color: rgba(var(--accent-rgb), 0.5);
}
.tile.trending-stocks-group-member > .tile-header .tile-title::before {
  content: '📈';
  margin-right: 5px;
}

/* StockTwits symbol/user stream tiles (stocktwits-widgets.js) — plain
   always-on content tiles, not part of a scanner "pool" the way Rapid
   Movers/Trending Stocks tiles are, so no group-accent border here, just the
   post-list layout itself. */
.stocktwits-widget-body { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; height: 100%; box-sizing: border-box; overflow: hidden; }
.stocktwits-widget-status { color: var(--panel-text-dim); font-size: 12px; flex: 0 0 auto; }
.stocktwits-widget-list { display: flex; flex-direction: column; gap: 8px; flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.stocktwits-widget-empty { color: var(--panel-text-dim); font-size: 12.5px; padding: 8px 0; }
.stocktwits-widget-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 8px;
  border-radius: 6px;
  border: 1px solid var(--panel-border);
  background: var(--panel-bg);
}
.stocktwits-widget-avatar { width: 28px; height: 28px; border-radius: 50%; flex: 0 0 auto; object-fit: cover; background: var(--bg-elevated); }
.stocktwits-widget-row-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1 1 auto; }
.stocktwits-widget-row-top { display: flex; align-items: center; gap: 6px; }
.stocktwits-widget-username { font-size: 12.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A poster's name/avatar is a <button> when this app knows which chat to
   pin a User stream tile into (see renderStocktwitsMessages) — reset the
   native button chrome so it still reads as plain bold text, just with a
   pointer cursor and a hover cue that it's clickable. */
button.stocktwits-widget-username {
  background: none;
  border: 0;
  padding: 0;
  color: inherit;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
button.stocktwits-widget-username:hover { color: var(--accent); text-decoration: underline; }
.stocktwits-widget-time { font-size: 10.5px; color: var(--panel-text-dim); margin-left: auto; flex: 0 0 auto; }
.stocktwits-widget-body-text { font-size: 12.5px; line-height: 1.4; white-space: pre-wrap; word-break: break-word; }
/* Cashtags inside a post's body (see renderStocktwitsBodyText) — colored and
   clickable, like a plain inline link, so a mentioned ticker stands out from
   the surrounding sentence and can be opened as a chart without retyping it. */
.stocktwits-widget-cashtag {
  font: inherit;
  background: none;
  border: 0;
  padding: 0 1px;
  color: var(--accent);
  font-weight: 600;
  cursor: pointer;
}
.stocktwits-widget-cashtag:hover { text-decoration: underline; }
/* This tile's own ticker (a symbol-stream tile only) gets a tinted
   background on top of the usual cashtag color, so the symbol you're
   actually watching still jumps out even in a post that name-drops several
   other tickers alongside it. */
.stocktwits-widget-cashtag-subject { background: rgba(0, 82, 255, 0.14); border-radius: 4px; }

/* The StockTwits "+" picker fields' own search-as-you-type dropdown (see
   attachStocktwitsSearch) — positioned like symbol-search-panel.js's own
   .ac-search-host dropdown, but a lighter local component since this one
   only ever needs a flat result list, no provider/exchange sub-bars. */
.stocktwits-search-panel {
  margin-top: 4px;
  max-height: 220px;
  overflow-y: auto;
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  background: var(--panel-bg);
}
.stocktwits-search-empty { padding: 8px 10px; font-size: 12px; color: var(--panel-text-dim); }
.stocktwits-search-row {
  all: unset; /* it's a <button> — strip native chrome (background/border/font/text-align) before re-applying layout below */
  box-sizing: border-box;
  width: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  cursor: pointer;
  font-size: 12.5px;
  color: var(--text);
}
.stocktwits-search-row.active, .stocktwits-search-row:hover { background: var(--bg-elevated); }
.stocktwits-search-row-symbol { font-weight: 600; flex: 0 0 auto; }
.stocktwits-search-row-title { color: var(--panel-text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 auto; }
.stocktwits-search-row-avatar { width: 20px; height: 20px; border-radius: 50%; object-fit: cover; flex: 0 0 auto; background: var(--bg-elevated); }

/* Per-tile "change ticker/username" control (attachStocktwitsChangeControl)
   — the ⇄ button lives in the tile header like any other tile-settings-btn.
   position:fixed (top/left set from JS, see attachStocktwitsChangeControl's
   open()) rather than absolute-relative-to-this-wrapper: .tile has its own
   overflow:hidden (see the base .tile rule), which clips any descendant
   still part of its containing-block chain — position:absolute included —
   once that descendant extends past the tile's own bottom edge. A
   position:fixed element's containing block is the viewport instead
   (nothing between here and the viewport uses transform/filter/
   will-change/contain, which is the only thing that would change that),
   so it genuinely escapes the clip rather than just document flow. */
.stocktwits-change-control { position: relative; display: inline-flex; }
.stocktwits-change-popover {
  position: fixed;
  z-index: 200;
  width: 240px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 8px;
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  background: var(--panel-bg);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
/* This is the actual root cause behind "the popover won't close" and the
   whole "dropdown shows nothing" saga: the `display: flex` above has
   higher specificity (a class selector) than the browser's own built-in
   `[hidden] { display: none }` rule, so toggling the `hidden` attribute
   via popover.hidden = true/false in JS was silently not doing anything —
   the element stayed rendered and interactive at all times, just
   (confusingly) still looking closed in most cases since it happened to
   have no content yet. Confirmed live: a Playwright reproduction script
   found the "hidden" popover still intercepting pointer events. This rule
   restores `[hidden]` actually meaning hidden. */
.stocktwits-change-popover[hidden] { display: none; }
.stocktwits-change-popover input {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  font-size: 12px;
  padding: 6px 9px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
}
.stocktwits-change-popover input:focus { outline: none; border-color: var(--accent); }
.stocktwits-change-popover .stocktwits-search-panel { width: 100%; margin-top: 0; }
.stocktwits-change-popover .add-widget-picker-chips { padding: 0; }
.stocktwits-change-apply { width: 100%; padding: 6px; font-size: 12px; }

/* The docked-sidebar placement (see rapid-movers.js's
   ensureRapidMoversSidebarPanel) — a real flex sibling of .dashboard-widgets
   inside that chat's own .dashboard-pane, not an overlay: it reserves actual
   width, runs the pane's full height, and the grid canvas shrinks to share
   the row with it, same as classic multicoincharts.com's dockable panel.
   `.has-rapid-movers-dock` is added/removed on the pane alongside the panel
   itself (see the JS) so every OTHER chat's pane — the overwhelming
   majority, without this dock — keeps its plain `display: block` layout
   untouched; this flex treatment only ever applies to the one pane that
   actually has the dock open. Reuses .tile/.tile-header/.tile-body for
   matching visuals only; none of their JS drag/resize/combine behavior
   applies since this element is never built through workspace.js's addTile
   — it gets its own resize handle instead (.rapid-movers-sidebar-resize). */
.dashboard-pane.has-rapid-movers-dock.active { display: flex; }
.dashboard-pane.has-rapid-movers-dock .dashboard-widgets { flex: 1 1 auto; width: auto; min-width: 0; }
.rapid-movers-sidebar-panel {
  position: relative;
  flex: 0 0 auto;
  width: 300px; /* JS sets an inline width once the user drags the resize handle */
  height: 100%;
  order: 1; /* after .dashboard-widgets, i.e. visually on the right */
}
.rapid-movers-sidebar-panel.dock-left { order: -1; } /* before .dashboard-widgets, i.e. on the left */
/* Full height means the list (as few as one row when movers are scarce)
   rarely fills the panel — same dot-grid texture as the workspace canvas
   itself (.dashboard-widgets) instead of a flat, empty-looking void below
   it, so the leftover space still reads as "part of the surface" rather
   than a layout bug. */
.rapid-movers-sidebar-panel .tile-body {
  background-image: radial-gradient(circle, var(--panel-border-soft) 1px, transparent 1px);
  background-size: 16px 16px;
}
.rapid-movers-sidebar-resize {
  position: absolute;
  top: 0;
  width: 7px;
  height: 100%;
  cursor: ew-resize;
  z-index: 5;
  transform: translateX(-50%);
}
/* Sits on whichever edge actually borders the canvas — the panel's left
   edge when it's on the right side of the row, its right edge when it's on
   the left side (mirrors dashboardResize's own left-edge drag handle for
   the whole workspace panel, same interaction just scoped to this split). */
.rapid-movers-sidebar-panel:not(.dock-left) .rapid-movers-sidebar-resize { left: 0; }
.rapid-movers-sidebar-panel.dock-left .rapid-movers-sidebar-resize { left: 100%; }
.rapid-movers-sidebar-resize:hover, .rapid-movers-sidebar-resize.active { background: rgba(var(--accent-rgb), 0.35); }

/* Generic sidebar dock (sidebar-dock.js) — same placement mechanics as
   Rapid Movers' own dock above (a real flex sibling of .dashboard-widgets,
   not an overlay), generalized for any widget that wants "pin to the
   side": Binance Scanner, gainers/losers/movers rankings. Kept as separate
   classes from the rapid-movers-* ones above rather than reusing them —
   multiple docks (e.g. Rapid Movers AND a Binance Scanner dock) can be open
   in the same pane at once, each just another flex child. */
.dashboard-pane.has-sidebar-dock.active { display: flex; }
.dashboard-pane.has-sidebar-dock .dashboard-widgets { flex: 1 1 auto; width: auto; min-width: 0; }
.sidebar-dock-panel { position: relative; flex: 0 0 auto; width: 300px; height: 100%; order: 1; }
.sidebar-dock-panel.dock-left { order: -1; }
.sidebar-dock-body { overflow: auto; }
.sidebar-dock-resize {
  position: absolute; top: 0; width: 7px; height: 100%; cursor: ew-resize; z-index: 5; transform: translateX(-50%);
}
.sidebar-dock-panel:not(.dock-left) .sidebar-dock-resize { left: 0; }
.sidebar-dock-panel.dock-left .sidebar-dock-resize { left: 100%; }
.sidebar-dock-resize:hover, .sidebar-dock-resize.active { background: rgba(var(--accent-rgb), 0.35); }
/* At the default 300px dock width, the title ("Top gainers"/"Top losers"/
   "Top movers"/"Most volume"/...) was losing to the live dot + "Xs ago" +
   exchange picker + flip/close buttons sharing its row — .tile-title's
   shared overflow:ellipsis rule let it get crushed down to "Top…", making
   gainers/losers/movers indistinguishable at a glance. Give the title its
   own full-width row (bumped up in size/weight to actually read as a
   heading) and let the controls wrap to a second row underneath instead. */
.sidebar-dock-panel .tile-header { flex-wrap: wrap; row-gap: 3px; }
.sidebar-dock-panel .tile-title {
  flex: 1 1 100%;
  overflow: visible;
  white-space: normal;
  font-size: 13px;
  font-weight: 700;
}
.sidebar-dock-panel .tile-controls { margin-left: auto; }

/* The chat-card placement (see ensureRapidMoversChatCard) needs its own
   status/list sizing since a chat card has no fixed height to flex
   against — .rapid-movers-widget-body's shared rules (status line, list,
   rows) already apply either way, this just caps how tall the list itself
   grows inside a chat message before it scrolls. */
.widget-rapid-movers.widget-card .rapid-movers-widget-list {
  max-height: 260px;
  overflow-y: auto;
}
.indicator-library-btn {
  display: inline-flex;
  width: max-content;
  align-items: center;
  gap: 7px;
  background: var(--tv-modal-btn-bg);
  color: var(--tv-modal-text);
  border: 1px solid var(--tv-modal-input-border);
  border-radius: 6px;
  padding: 8px 10px;
  font: inherit;
  cursor: pointer;
}
.indicator-library-btn:hover { background: var(--tv-modal-hover); }
.indicator-library-btn span { color: var(--tv-modal-text-dim); font-size: 17px; line-height: 10px; }
.indicator-library-summary { color: var(--tv-modal-text-dim); min-height: 16px; overflow-wrap: anywhere; }
.settings-advanced-study { grid-template-columns: minmax(0, 1fr) 187px; }
.indicator-hidden-fields { display: none; }
.advanced-study-combo { position: relative; width: 100%; }
.advanced-study-suggestions {
  display: none;
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 20;
  background: var(--tv-modal-bg);
  border: 1px solid var(--tv-modal-input-border);
  border-radius: 6px;
  max-height: 220px;
  overflow-y: auto;
  padding: 4px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .45);
}
.advanced-study-suggestions.show { display: block; }
.advanced-study-suggestion-row {
  display: flex;
  flex-direction: column;
  width: 100%;
  border: 0;
  background: transparent;
  color: var(--tv-modal-text);
  padding: 6px 8px;
  border-radius: 4px;
  text-align: left;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.advanced-study-suggestion-row:hover, .advanced-study-suggestion-row.active { background: var(--tv-modal-hover); }
.advanced-study-suggestion-row span { color: var(--tv-modal-text-dim); font-size: 10.5px; }
.advanced-study-suggestions-empty { padding: 8px; color: var(--tv-modal-text-dim); font-size: 12px; }
.chart-preferences-modal .modal-actions { margin-top: 0; padding-top: 14px; border-top: 1px solid var(--tv-modal-divider); flex-shrink: 0; flex-wrap: wrap; }
.chart-preferences-modal .btn { padding: 7px 11px; border-radius: 5px; font-size: 12px; }
/* The ghost buttons (Cancel/Set as global default/Use global defaults) are
   long enough at this modal's 420px width that the primary Save button has
   nowhere left on the row and wraps onto its own line — right-aligned there
   it read as a stray floating button. Forcing its flex-basis to 100% makes
   it claim that second line outright and stretch edge-to-edge instead,
   which lines up with the ghost-button row above it and doubles as a
   thumb-friendly full-width primary CTA on narrow/mobile widths. */
.chart-preferences-modal .modal-actions .btn-primary { flex: 1 1 100%; text-align: center; }
.tradingview-widgets-modal {
  width: min(680px, calc(100vw - 32px));
  max-height: calc(100vh - 32px);
  overflow-y: auto;
}
.tradingview-symbol-field {
  display: grid;
  gap: 6px;
  color: var(--text-dim);
  font-size: 12px;
  margin-bottom: 14px;
}
.tradingview-symbol-field input {
  width: 100%;
  box-sizing: border-box;
  padding: 8px 10px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 7px;
  color: var(--text);
  font: 12px var(--mono);
}
/* Fixed 320px (.symbol-search-panel's own default, sized for the narrower
   tile "change symbol" popover) would either overflow or look stranded next
   to this field's full-width input — same override .tile-symbol-popover
   already applies for its own full-width context. */
.tradingview-symbol-field .symbol-search-panel { width: 100%; max-height: 240px; }
.tradingview-widget-picker {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 9px;
}
.tradingview-widget-picker button {
  display: grid;
  gap: 4px;
  text-align: left;
  padding: 12px;
  background: var(--bg-elevated);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
}
.tradingview-widget-picker button:hover:not(.tv-widget-settings-btn) { border-color: var(--accent); background: var(--border); }
.tradingview-widget-picker strong { font-size: 13px; }
.tradingview-widget-picker span { color: var(--text-dim); font-size: 11.5px; line-height: 1.35; }
/* Without this, these buttons fall back to Chrome's native focus ring, which
   sits right on top of a wrapped card's corner gear (see .tv-widget-card
   below) — same "own ring, not the browser's" swap .settings-field inputs
   already use elsewhere in this file. Excludes .tv-widget-settings-btn: that
   ring is sized for a whole card, and looks like a stray box glued to the
   corner when it lands on the much smaller gear icon instead — it gets its
   own, appropriately-sized focus style right below. */
.tradingview-widget-picker button:focus-visible:not(.tv-widget-settings-btn), .external-widget-button:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(0, 82, 255, .25);
}
/* A widget with saved global defaults (chart/news/CryptoPanic) gets its own
   gear button here, so those defaults are reachable without first pinning a
   tile. The card wrapper exists only so the gear can sit in a fixed corner
   independent of how tall the label/description text grows; overflow:hidden
   keeps any focus ring on the button inside it from bleeding past the
   card's own rounded corner into the gear's spot. */
.tv-widget-card { position: relative; overflow: hidden; border-radius: 8px; }
.tv-widget-card > button[data-tv-widget], .tv-widget-card > button[data-external-widget] { width: 100%; height: 100%; }
.tv-widget-has-settings { padding-right: 30px; }
/* Scoped as ".tv-widget-card > .tv-widget-settings-btn" (two classes) rather
   than plain ".tv-widget-settings-btn" (one class) — ".tradingview-widget-picker
   button" (a class + an element type) outranks a single class on specificity,
   so without this the gear silently inherited that rule's 12px padding and
   1px border instead of the 22px/no-border box declared here, which is what
   made it look like an oversized, off-center, randomly-bordered square. */
.tv-widget-card > .tv-widget-settings-btn {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 5px;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}
.tv-widget-card > .tv-widget-settings-btn:hover { background: var(--border); color: var(--text); }
.tv-widget-card > .tv-widget-settings-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(0, 82, 255, .35); }
.widget-param-badge {
  justify-self: start;
  margin-top: 2px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--border);
  border: 1px solid var(--border);
  color: var(--text-dim);
  font-size: 10px;
  font-style: normal;
  letter-spacing: 0.02em;
}
.widget-provider-group { margin-bottom: 18px; }
.widget-provider-group h3 { margin: 0 0 10px; font-size: 13px; display: flex; align-items: center; gap: 6px; }
.widget-provider-group + .widget-provider-group,
.widget-provider-group + .external-widget-section { margin-top: 4px; }
.external-widget-section + .widget-provider-group {
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}
.tradingview-widgets-modal .modal-actions { margin-top: 16px; }
.widget-provider-logo { width: 16px; height: 16px; object-fit: contain; border-radius: 3px; flex: none; }
/* Anchors a hover/focus preview popover (see wireWidgetCardPreviews in app.js)
   to every widget card. The preview panel has to live outside .tv-widget-card
   (below) rather than inside it — that card sets overflow:hidden to clip the
   settings gear's focus ring, which would also silently clip this popover,
   positioned above the card via bottom:100%. width:100% + flex:1 on the child
   keeps the card/button filling the space exactly as it did before this
   wrapper existed, in both its CSS-grid (.tradingview-widget-picker) and
   plain-block (.external-widget-section) contexts. */
.widget-card-wrap { position: relative; display: flex; width: 100%; }
.widget-card-wrap > button, .widget-card-wrap > .tv-widget-card { flex: 1; min-width: 0; }

/* Drag-to-combine layout picker (see openCombineLayoutPicker in app.js) —
   same small bordered-cells preview language as the workspace Grid picker's
   .grid-preset-icon above, so choosing a combined tile's panel arrangement
   reads as the same kind of choice. */
.combine-layout-modal .modal-title { margin-bottom: 16px; }
.combine-layout-picker {
  display: flex;
  gap: 10px;
}
.combine-layout-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 14px 16px;
  background: var(--bg-elevated);
  color: var(--text-dim);
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
  font-size: 12px;
}
.combine-layout-btn:hover { border-color: var(--buy); color: var(--text); }
.combine-layout-icon {
  display: grid;
  gap: 2px;
  width: 44px;
  height: 32px;
}
.combine-layout-icon i {
  display: block;
  font-style: normal;
  border-radius: 2px;
  background: var(--text-dim);
}
.combine-layout-btn:hover .combine-layout-icon i { background: var(--buy); }

/* The custom-group picker is intentionally a small decision before the
   existing layout picker: panel membership and arrangement are separate
   choices, so the common "chart + book" case doesn't require drag/drop. */
.group-members-modal { max-width: 410px; }
.group-members-options {
  display: grid;
  gap: 8px;
  margin: 14px 0 8px;
}
.group-members-options label {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 7px;
  color: var(--text);
  cursor: pointer;
}
.group-members-options label:hover { border-color: var(--buy); }
.group-members-options input { accent-color: var(--buy); }
.group-members-hint { min-height: 18px; margin: 0; color: var(--text-dim); font-size: 12px; }

/* Live inline widgets start live by default. Pause freezes the visible data
   and records the time in the same compact header area. */
.inline-live-action {
  border: 1px solid var(--border);
  border-radius: 5px;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 10px;
  line-height: 1;
  padding: 4px 6px;
  cursor: pointer;
}
.inline-live-action:hover { color: var(--text); border-color: var(--accent); }
.inline-live-action:disabled { cursor: default; opacity: .6; }
.inline-live-paused-at {
  color: var(--status-amber);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Grid-fit conflict prompt (see resolveGridFit in workspace.js and
   openGridFitPrompt in app.js) — asks once per workspace session whether an
   over-wide picked grid should shrink to fit or stay exact and scroll. */
.grid-fit-modal .modal-title { margin-bottom: 10px; }
.grid-fit-remember {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-top: 12px;
  color: var(--text-dim);
  font-size: 12.5px;
  cursor: pointer;
}
.grid-fit-remember input { margin: 0; }

/* Appearance/theme picker (openThemeModal in app.js) — a curated gallery of
   full palettes (see THEME_PRESETS) plus a "Customize colors" section for
   the same per-region overrides the free-form "make the sidebar green" chat
   command already writes, so both paths land on the same swatch. */
.theme-modal { width: 460px; }
.theme-modal-intro { margin: 0 0 14px; }
.theme-gallery {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 9px;
  margin-bottom: 16px;
}
.theme-gallery-item { position: relative; }
.theme-gallery-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 10px;
  background: var(--bg-elevated);
  color: var(--text-dim);
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
  font-size: 11.5px;
}
.theme-gallery-btn:hover { border-color: var(--accent); color: var(--text); }
.theme-gallery-btn.active { border-color: var(--accent); color: var(--text); box-shadow: 0 0 0 1px var(--accent); }

/* Rename/share/delete overlay for a saved custom theme's gallery tile —
   siblings of .theme-gallery-btn (not nested, since a <button> can't
   contain another <button>), positioned over its top-right corner and
   revealed on hover, same reveal-on-hover convention as
   .saved-dashboard-item's per-row controls. */
.theme-gallery-item-actions {
  position: absolute;
  top: 4px;
  right: 4px;
  display: none;
  gap: 2px;
}
.theme-gallery-item:hover .theme-gallery-item-actions { display: flex; }
.theme-gallery-item-action {
  width: 18px;
  height: 18px;
  border-radius: 5px;
  border: none;
  background: var(--bg);
  color: var(--text-dim);
  font-size: 10px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.theme-gallery-item-action:hover { color: var(--text); background: var(--bg-elevated); }
.theme-gallery-item-action-delete:hover { color: var(--danger); background: var(--danger-bg); }
/* The .chart-prompt/.chart-prompt-btn classes elsewhere in the app are only
   styled under .bubble (chat messages) — buildChartThemeSyncPrompt reuses
   them for the same yes/no affordance, but when it's dropped straight into
   the Appearance modal (not a chat bubble — see maybeSyncChartsToAppTheme)
   it needs its own themed look. Scoped under the #themeSyncChartsPrompt id
   (not the bare .theme-sync-charts-prompt class) so none of this leaks into
   the chat-bubble instance, which must keep the .bubble .chart-prompt look. */
#themeSyncChartsPrompt:empty { display: none; }
#themeSyncChartsPrompt .theme-sync-charts-prompt {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin: -6px 0 14px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-sidebar);
  color: var(--text);
  font-size: 12.5px;
}
#themeSyncChartsPrompt .chart-prompt-btn {
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--text);
  border-radius: 6px;
  padding: 5px 10px;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  white-space: nowrap;
}
#themeSyncChartsPrompt .chart-prompt-btn.yes { background: var(--accent); color: #fff; border-color: var(--accent); }
#themeSyncChartsPrompt .chart-prompt-btn:hover { filter: brightness(1.1); }
.theme-swatch {
  position: relative;
  display: block;
  width: 100%;
  height: 34px;
  border-radius: 6px;
  border: 1px solid var(--border);
}
.theme-swatch i {
  position: absolute;
  right: 5px;
  bottom: 5px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  font-style: normal;
}
.theme-custom-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}
.theme-custom-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-size: 12px;
  color: var(--text-dim);
}
.theme-custom-row input[type="color"] {
  width: 32px;
  height: 24px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: none;
  cursor: pointer;
}
/* Prompt banner that appears above the gallery only once the current look
   (colors and/or font) no longer matches any built-in preset or saved
   custom theme — see isCurrentLookUnsaved/renderThemeCustomSavePrompt in
   app.js. Accent-tinted so it reads as "something needs your attention"
   rather than blending in like the neutral chart-theme-sync prompt below. */
#themeCustomSavePrompt:empty { display: none; }
#themeCustomSavePrompt .theme-custom-save-prompt {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin: -6px 0 14px;
  padding: 10px 12px;
  border: 1px solid var(--accent);
  border-radius: 8px;
  background: rgba(var(--accent-rgb), 0.1);
  color: var(--text);
  font-size: 12.5px;
}
#themeCustomSavePrompt .theme-custom-save-prompt > span:first-child { flex: 1; min-width: 160px; }
.theme-custom-save-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
  margin-left: auto;
  padding: 5px 10px;
  border: 1px solid var(--accent);
  border-radius: 6px;
  background: var(--accent);
  color: #fff;
  font: inherit;
  font-size: 12px;
  white-space: nowrap;
  cursor: pointer;
}
.theme-custom-save-btn:hover { filter: brightness(1.1); }
.theme-custom-save-btn svg { flex-shrink: 0; }
.theme-restore-defaults-btn { margin-right: auto; background: transparent; color: var(--text-dim); border: 1px solid var(--border); }
.theme-restore-defaults-btn:hover { color: var(--text); border-color: var(--accent); }
.theme-font-size-stepper {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
}
.theme-font-size-btn {
  padding: 6px 10px;
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
  background: var(--bg-elevated);
  color: var(--text);
  border: 1px solid var(--border);
}
.theme-font-size-btn:hover { border-color: var(--accent); }
.theme-font-size-label {
  flex: 1;
  text-align: center;
  font-size: 12px;
  color: var(--text-dim);
}
.alias-modal { width: 460px; }
.alias-add-row {
  display: grid;
  grid-template-columns: 1fr auto 1fr auto;
  align-items: center;
  gap: 8px;
  margin: 14px 0;
}
.alias-add-row input {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 7px 9px;
  font-size: 12.5px;
}
.alias-arrow { color: var(--text-dim); font-size: 13px; }
.alias-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 260px;
  overflow-y: auto;
}
.alias-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 12.5px;
}
.alias-row .alias-shortcut { font-weight: 600; white-space: nowrap; }
.alias-row .alias-expansion { flex: 1; min-width: 0; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.alias-row-remove {
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--text-dim);
  border-radius: 5px;
  width: 22px;
  height: 22px;
  line-height: 1;
  cursor: pointer;
}
.alias-row-remove:hover { border-color: var(--accent); color: var(--text); }
.alias-list-empty { color: var(--text-dim); font-size: 12.5px; padding: 6px 2px; }
.external-widget-section {
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}
.external-widget-section h3 { margin: 0; font-size: 13px; display: flex; align-items: center; gap: 6px; }
.external-widget-section .tradingview-symbol-field { margin-bottom: 10px; }
.external-widget-button {
  display: grid;
  width: 100%;
  gap: 4px;
  text-align: left;
  padding: 12px;
  background: var(--bg-elevated);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
}
.external-widget-button:hover { border-color: var(--accent); background: var(--border); }
.external-widget-button strong { font-size: 13px; }
.external-widget-button span { color: var(--text-dim); font-size: 11.5px; line-height: 1.35; }
/* Twitch/YouTube entries are logos-only for now (see comment in index.html)
   — disabled and visually dimmed rather than a live-looking button that
   does nothing when clicked. */
.external-widget-button:disabled { cursor: default; opacity: 0.55; }
.external-widget-button:disabled:hover { border-color: var(--border); background: var(--bg-elevated); }
.tile.widget-cryptopanic .tile-body { overflow: hidden; background: #171a1f; }
.cryptopanic-widget-frame { display: block; width: 100%; height: 100%; border: 0; background: #171a1f; }
.widget-input-error { margin: -6px 0 8px; font-size: 11.5px; color: var(--danger); }
.tile.widget-twitch .tile-body { display: flex; overflow: hidden; background: #0e0e10; }
.twitch-widget-player-frame { flex: 2 1 0; width: 100%; height: 100%; border: 0; }
.twitch-widget-chat-frame { flex: 1 1 0; min-width: 220px; height: 100%; border: 0; border-left: 1px solid var(--border); }
.tile.widget-youtube .tile-body { overflow: hidden; background: #000; }
.youtube-widget-frame { display: block; width: 100%; height: 100%; border: 0; }

.rich-text-body {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 0;
}
.rich-text-toolbar {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px;
  padding: 6px 8px;
  border-bottom: 1px solid var(--panel-border);
  background: var(--panel-header-bg);
  flex-shrink: 0;
}
.rich-text-toolbar-btn {
  border: 0;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  min-width: 26px;
  padding: 3px 6px;
  border-radius: 5px;
  font-size: 12.5px;
  line-height: 1.3;
}
.rich-text-toolbar-btn:hover { color: var(--text); background: var(--bg-elevated); }
.rich-text-fontsize-select {
  margin-left: auto;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg-elevated);
  color: var(--text-dim);
  font-size: 11px;
  padding: 3px 4px;
}
.rich-text-editor {
  flex: 1;
  overflow: auto;
  padding: 10px 12px;
  color: var(--panel-text);
  line-height: 1.5;
  outline: none;
}
.rich-text-editor:empty::before {
  content: attr(data-placeholder);
  color: var(--text-dim);
  pointer-events: none;
}
.rich-text-editor img {
  max-width: 100%;
  border-radius: 6px;
  margin: 4px 0;
  display: block;
}
.rich-text-editor a { color: var(--accent); }
.rich-text-editor h2 { margin: 0.4em 0; font-size: 1.15em; }
.rich-text-editor ul, .rich-text-editor ol { padding-left: 1.4em; margin: 0.4em 0; }
.rich-text-limit-warning {
  flex-shrink: 0;
  padding: 6px 12px;
  border-top: 1px solid var(--danger-bg);
  background: var(--danger-bg);
  color: var(--danger);
  font-size: 11.5px;
}
.rich-text-capture-picker {
  position: fixed;
  z-index: 60;
  min-width: 200px;
  max-width: 280px;
  max-height: 260px;
  overflow: auto;
  padding: 6px;
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  background: var(--panel-bg);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
}
.rich-text-capture-row {
  display: block;
  width: 100%;
  text-align: left;
  border: 0;
  background: transparent;
  color: var(--text);
  cursor: pointer;
  padding: 6px 8px;
  border-radius: 6px;
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rich-text-capture-row:hover { background: var(--bg-elevated); }
.rich-text-capture-empty {
  padding: 8px;
  color: var(--text-dim);
  font-size: 11.5px;
  line-height: 1.4;
  max-width: 260px;
}
/* A-ADS only ever serves a fixed 300x250 creative, so the tile body centers
   it rather than stretching it to fill a larger/resized tile. */
.tile.widget-ad-slot .tile-body { overflow: hidden; display: flex; align-items: center; justify-content: center; background: var(--panel-bg); }
/* width/height come from the active variant's A-ADS spec, set inline by
   buildAadsFrame (app.js) since sizes differ per variant under test. */
.ad-slot-frame { display: block; border: 0; padding: 0; overflow: hidden; background: transparent; }
/* Adaptive/responsive A-ADS format fills its container's width instead of
   a fixed px width — see buildAadsFrame's `spec.adaptive` branch. Capped at
   a sane ad-sized max rather than 100% of the tile's own height: the tile
   itself still stretches to fill its grid cell exactly like any other
   widget (there's no maxH concept in the tile system, nor should there
   be — that's correct for infinitely-flexible chart content), but unlike a
   chart, real ad creative isn't built to adapt to an arbitrary tall/narrow
   aspect ratio. Without this, a tile that ends up very tall (e.g. the only
   other tile alongside "Add widget" in a 2x1 grid) stretches the iframe to
   match, and whichever advertiser's creative actually loaded would render
   at that height without necessarily reflowing to fill it cleanly — it
   just runs out of its own content partway down and looks cut off. Capping
   the iframe and centering it (see .tile-body above) leaves clean empty
   space around it instead. */
.ad-slot-frame-adaptive { width: 100%; max-width: 468px; max-height: 600px; }
@media (max-width: 520px) {
  .tradingview-widget-picker { grid-template-columns: 1fr; }
}
.indicator-library-backdrop { z-index: 70; background: rgba(0, 0, 0, .62); }
.indicator-library-modal {
  width: 398px;
  max-width: calc(100vw - 32px);
  height: min(610px, calc(100vh - 32px));
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--tv-modal-bg);
  color: var(--tv-modal-text);
  border: 1px solid var(--tv-modal-border);
  border-radius: 4px;
  box-shadow: 0 18px 60px rgba(0,0,0,.55);
}
.indicator-library-header { display: flex; align-items: center; justify-content: space-between; padding: 20px 20px 18px; border-bottom: 1px solid var(--tv-modal-divider); }
.indicator-library-header h2 { margin: 0; font-size: 20px; letter-spacing: -.02em; }
.indicator-library-header button { border: 0; background: transparent; color: var(--tv-modal-text-dim); font-size: 32px; line-height: 20px; padding: 0; cursor: pointer; }
.indicator-library-search { display: flex; align-items: center; gap: 9px; padding: 13px 18px 12px; border-bottom: 1px solid var(--tv-modal-divider); color: var(--tv-modal-text-dim); }
.indicator-library-search span { font-size: 27px; transform: rotate(-20deg); line-height: 20px; }
.indicator-library-search input { width: 100%; border: 0; outline: 0; background: transparent; color: var(--tv-modal-text); font: inherit; font-size: 14px; }
.indicator-library-search input::placeholder { color: var(--tv-modal-text-dim); }
.indicator-library-label { padding: 10px 30px 8px; color: var(--tv-modal-text-dim); font-size: 10px; letter-spacing: .05em; }
.indicator-library-list { overflow-y: auto; padding: 0 18px 16px; }
.indicator-library-row { display: flex; width: 100%; border: 0; background: transparent; color: var(--tv-modal-text); padding: 8px 12px; border-radius: 4px; text-align: left; font: inherit; font-size: 14px; cursor: pointer; }
.indicator-library-row:hover { background: var(--tv-modal-hover); }
.indicator-library-row.selected { background: var(--tv-modal-hover); color: var(--tv-modal-text); }
.indicator-library-row.selected::after { content: '✓'; margin-left: auto; color: var(--status-blue); }
.indicator-library-empty { margin: 12px; color: var(--tv-modal-text-dim); font-size: 13px; }

.mobile-tabbar {
  display: none;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: 56px;
  z-index: 41;
  background: var(--bg-elevated);
  border-top: 1px solid var(--border);
}
.mobile-tabbar-btn {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  background: transparent;
  border: 0;
  color: var(--text-dim);
  font-size: 11px;
  cursor: pointer;
}
.mobile-tabbar-btn.active { color: var(--accent); }
.mobile-tabbar-btn:hover { color: var(--text); }
.mobile-tabbar-btn.active:hover { color: var(--accent); }
.sidebar-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 39;
}

/* Below 720px: sidebar becomes an off-canvas drawer, and .main/.dashboard
   each go full-width with only one visible at a time (body[data-mobile-view],
   toggled by #mobileTabbar in app.js) instead of the three-pane desktop
   layout that has no room to coexist at phone width. */
@media (max-width: 720px) {
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    max-width: 85vw;
    z-index: 40;
    box-shadow: 0 0 28px rgba(0, 0, 0, 0.5);
    transform: translateX(-100%);
    transition: transform 0.2s ease;
  }
  .sidebar.open { transform: translateX(0); }
  .sidebar-backdrop.open { display: block; }

  .main, .dashboard {
    width: 100% !important;
    max-width: none;
    min-width: 0;
    flex: 1 1 100%;
    padding-bottom: 56px; /* clears the fixed mobile-tabbar */
  }
  .dashboard { border-left: none; }
  body[data-mobile-view="chat"] .dashboard { display: none; }
  body[data-mobile-view="workspace"] .main { display: none; }
  .dashboard-resize { display: none; }
  /* The off-canvas drawer above already owns show/hide at this width via
     #sidebarToggleBtn — the desktop drag-resize/collapse controls would
     only fight it. */
  .sidebar-resize, .sidebar-collapse-btn { display: none; }
  .sidebar.collapsed { width: var(--sidebar-w, 260px) !important; min-width: 0; padding: 16px 12px; }
  .sidebar.collapsed .brand-name,
  .sidebar.collapsed .new-chat-btn,
  .sidebar.collapsed .language-picker,
  .sidebar.collapsed .theme-picker-btn,
  .sidebar.collapsed .sidebar-section {
    display: revert;
  }
  .mobile-tabbar { display: flex; }

  /* Ctrl+K/history shortcut hint has no phone-keyboard equivalent, and at
     this width the placeholder text wraps under its absolute bottom-right
     position, garbling both. */
  .composer-hint { display: none; }
  /* Free the gutter the hint above used to reserve — otherwise it keeps
     fighting the composer-input min-width:0 shrink fix right where phone
     widths need it most. */
  .composer-input, .composer-ghost { padding-right: 6px; }

  /* The workspace header's "More" popover already spans the full width on
     desktop too (see base .dashboard-header-secondary rules); on a phone
     screen just stretch it edge-to-edge instead of a fixed 260px. */
  .dashboard-header-secondary { left: 12px; right: 12px; width: auto; }
  .dashboard-header-secondary select,
  .dashboard-header-secondary .workspace-view-select { max-width: none; flex: 1; }
}

/* These row/item action buttons are hover-revealed everywhere above
   (display:none by default, shown via :hover on the row) — on a touchscreen
   there's no hover state to reveal them with, so they're currently
   unreachable by tap at ANY width, not just narrow ones. (hover: none)
   targets touch-only devices (and Playwright's mobile device emulation);
   (max-width: 720px) is a fallback for narrow-but-mouse-driven testing.
   Single-class selectors match the unconditional display:none rules above,
   so being later in the file wins the cascade without !important. */
@media (hover: none), (max-width: 720px) {
  .cmd-echo-star,
  .feedback-trigger-btn,
  .quick-action-remove, .quick-action-edit,
  .saved-item-row-action,
  .saved-dashboard-item-folder, .saved-dashboard-item-link,
  .saved-dashboard-item-share, .saved-dashboard-item-close,
  .chat-list-item-pin, .chat-list-item-clear, .chat-list-item-close,
  .theme-gallery-item-actions {
    display: flex;
  }
}

/* Touch-target sizing, ~20-24px -> 32px, for the controls above plus a few
   already-visible ones. Verified against sibling layout before applying:
   .quick-action-row/.saved-item-row/.chat-list-item all use flex `gap`
   (2-8px), which sizes the gap independent of child box size, so this only
   shrinks the row's ellipsized label — never the spacing between buttons.
   The sidebar drawer itself is max-width:85vw (~319px at a 375px phone),
   comfortable room for 1-3 32px buttons plus a shrinking label.
   Selectors are written to match (or exceed) the specificity of the base
   rule they override — e.g. `.bubble .pin-btn`, not bare `.pin-btn` — so
   being later in the file is what wins, not an accidental specificity bump. */
@media (hover: none), (max-width: 720px) {
  .bubble .pin-btn,
  .cmd-echo-star,
  .feedback-trigger-btn,
  .chat-list-item-pin, .chat-list-item-clear, .chat-list-item-close,
  .saved-item-row-action,
  .saved-dashboard-item-folder, .saved-dashboard-item-link,
  .saved-dashboard-item-share, .saved-dashboard-item-close,
  .quick-action-remove, .quick-action-edit {
    width: 32px;
    height: 32px;
  }
  .tv-widget-card > .tv-widget-settings-btn {
    width: 32px;
    height: 32px;
    top: 6px;
    right: 6px;
  }
  /* Was glyph-only (font-size:32px text, no box) — give it a real hit area. */
  .indicator-library-header button {
    width: 40px;
    height: 40px;
  }
  /* Grid paging arrows are the primary way to reach a multi-column grid's
     other tiles on mobile (workspace.js forces 'page' fit mode below this
     width — see layoutGrid's explicitFitMode) — load-bearing navigation,
     not a minor action, so sized up to match the indicator-library close
     button rather than the 32px baseline above. Corner-anchored via
     left/right/top/bottom offsets (not centered on a shared boundary), so
     enlarging needs no position math the way .dashboard-resize did. */
  .grid-page-arrow {
    width: 40px;
    height: 40px;
  }
}

/* .dashboard-resize (a 6px-wide strip, styles.css ~751-760) is already
   hidden below 720px (see .main/.dashboard rules above) but left at its
   desktop 6px width between 720-1280px — exactly where a touch tablet in
   portrait would still show it. Widens only the invisible hit-box (visual
   feedback comes from :hover/.active background, not from width), so this
   is a zero-layout-risk change scoped to touch-capable pointers only.
   Keeps the original left:-3px edge (the part that already overlaps .main)
   unchanged and extends the added width entirely rightward, into
   .dashboard's own box — widening further left instead measurably risked
   the strip losing paint priority to .main's (unpositioned, but DOM-later-
   painted-per-region) content at that overlap depth, confirmed by
   elementFromPoint landing on .empty-state instead of the handle. */
@media (pointer: coarse) and (min-width: 721px) {
  .dashboard-resize {
    width: 20px;
    left: -3px;
  }
}

/* The "More" workspace popover (#dashboardHeaderSecondary) becomes a real
   bottom sheet below 720px: a grab handle + explicit close button instead
   of relying only on outside-click/Escape (app.js's existing open/close
   logic, ~line 7435), which the research behind this pass flags as
   ambiguous with OS gestures on a phone. Desktop keeps the plain popover —
   handle/close/backdrop stay hidden outside the mobile media block. Note:
   the toggle mechanism is display:none/flex (same as the base popover
   rule), which can't animate a slide-in transition (display isn't
   transitionable and the element isn't in the render tree beforehand) —
   this pops open instantly, same as the popover did, not a regression. */
.dashboard-sheet-handle, .dashboard-sheet-close, .dashboard-sheet-backdrop { display: none; }
@media (max-width: 720px) {
  .dashboard-sheet-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, .5);
    z-index: 41;
  }
  .dashboard-sheet-backdrop.open { display: block; }

  .dashboard-header-secondary {
    position: fixed;
    top: auto;
    left: 0;
    right: 0;
    bottom: 0;
    width: auto;
    max-width: none;
    max-height: 70vh;
    overflow-y: auto;
    border-radius: 14px 14px 0 0;
    margin-top: 0;
    padding-top: 4px;
    padding-bottom: max(10px, env(safe-area-inset-bottom));
    z-index: 42;
  }
  .dashboard-sheet-handle {
    display: block;
    width: 36px;
    height: 4px;
    border-radius: 2px;
    background: var(--border);
    margin: 4px auto 8px;
    flex-shrink: 0;
  }
  .dashboard-sheet-close {
    display: flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: 6px;
    right: 8px;
    width: 32px;
    height: 32px;
    border: none;
    background: transparent;
    color: var(--text-dim);
    font-size: 16px;
    border-radius: 7px;
    cursor: pointer;
  }
  .dashboard-sheet-close:hover { background: var(--bg-elevated); color: var(--text); }
}

/* Markets catalog table (Tabulator) — dashboard tile + inline chat copy.
   Scoped under .markets-table-widget so it can't leak into unrelated
   .tile/.bubble content. Values are built entirely from this app's existing
   custom properties (already redefined per-theme under
   :root[data-theme="light"]) rather than new colors, so dark/light both
   track automatically. */
.markets-table-widget { display: flex; flex-direction: column; gap: 6px; }
.markets-table-toolbar { display: flex; align-items: center; gap: 10px; padding: 2px 2px 4px; position: relative; }
.markets-table-progress { font-size: 11.5px; color: var(--panel-text-dim); }
.markets-table-toolbar button {
  background: var(--bg-elevated); border: 1px solid var(--border); color: var(--text);
  border-radius: 8px; padding: 4px 10px; font-size: 12.5px; cursor: pointer;
}
.markets-table-toolbar button:hover { border-color: var(--accent-dim); }
.markets-table-columns-wrap { position: relative; }
.markets-table-columns-menu {
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 5; min-width: 180px;
  max-height: 320px; overflow-y: auto; background: var(--bg-elevated); border: 1px solid var(--border);
  border-radius: 8px; padding: 6px; flex-direction: column; gap: 2px;
}
.markets-table-columns-menu:not([hidden]) { display: flex; }
.markets-table-columns-row { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--text); padding: 3px 4px; cursor: pointer; border-radius: 4px; }
.markets-table-columns-row:hover { background: var(--panel-header-bg); }

.markets-table-body .tabulator {
  background: var(--panel-bg); color: var(--panel-text); border: 1px solid var(--panel-border);
  font-family: var(--mono); font-variant-numeric: tabular-nums;
}
.markets-table-body .tabulator-header { background: var(--panel-header-bg); border-color: var(--panel-border); color: var(--panel-text-faint); }
.markets-table-body .tabulator-col-title { color: var(--panel-text-faint); }
.markets-table-body .tabulator-col-resize-handle { background: transparent; }
/* Tabulator paints each header cell's own background (.tabulator-col), not
   just the outer .tabulator-header bar — that 3-class vendor selector beats
   a plain .tabulator-header override, so it needs matching specificity here
   too or the header stays vendor's default light gray regardless of theme. */
.markets-table-body .tabulator-header .tabulator-col { background: var(--panel-header-bg); }
.markets-table-body .tabulator-row { background: var(--panel-bg); color: rgba(var(--panel-text-rgb), 0.88); cursor: pointer; }
.markets-table-body .tabulator-row.tabulator-row-even { background: var(--panel-header-bg); }
.markets-table-body .tabulator-row:hover { background: var(--bg-elevated); }
.markets-table-body .tabulator-cell { border-color: var(--panel-border-soft); }
.markets-table-body .tabulator-header .tabulator-col,
.markets-table-body .tabulator-headers { border-color: var(--panel-border-soft); }
/* Same specificity issue as the header: vendor's .tabulator-tableholder
   .tabulator-table sets a white canvas background behind the rows, which
   shows through the empty-state placeholder and any unfilled space. */
.markets-table-body .tabulator-tableholder .tabulator-table { background: var(--panel-bg); color: rgba(var(--panel-text-rgb), 0.88); }
.markets-table-body .tabulator-header-filter input,
.markets-table-body .tabulator-header-filter select {
  background: var(--bg-elevated); border: 1px solid var(--border); color: var(--text); border-radius: 4px;
}
.markets-table-body .tabulator-footer {
  background: var(--panel-header-bg); color: var(--panel-text-dim); border-color: var(--panel-border);
}
/* Vendor's placeholder (the "No results" row) paints its own light background
   independent of .tabulator-tableholder above — same specificity story as
   the header/table fixes, needs its own override or it stays a stark light-
   gray bar cutting across the dark panel. */
.markets-table-body .tabulator-placeholder { color: var(--panel-text-dim); background: var(--panel-bg); }

/* Binance Scanner widget (binance-scanner.js) — reuses .markets-table-*
   classes above for the shared toolbar/table look; these add only what's
   specific to it: the min-pings filter input, the Symbol column's emphasis,
   and the highly-pinged row accent (the .pill-danger badge on the Pings cell
   itself carries the main "hot" signal — this is a quieter supporting cue,
   not a duplicate of it). */
.binance-scanner-min-pings-label {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12.5px; color: var(--panel-text-dim);
}
.binance-scanner-min-pings {
  background: var(--bg-elevated); border: 1px solid var(--border); color: var(--text);
  border-radius: 8px; padding: 4px 8px; font-size: 12.5px; width: 56px;
}
/* Docked to the sidebar, this toolbar can end up much narrower than a full
   tile — without wrapping, the later buttons overflowed the panel width and
   sat unclickable past its edge instead of just dropping to a second row. */
.binance-scanner-toolbar { flex-wrap: wrap; row-gap: 6px; }
/* Same [aria-pressed] convention as the tile interval quick-buttons and
   saved-item filter chips elsewhere in this app. */
.binance-scanner-toolbar button[aria-pressed="true"] { background: var(--accent-dim); color: #fff; border-color: var(--accent-dim); }
.binance-scanner-status { margin-left: auto; }
.binance-scanner-body .tabulator-cell.binance-scanner-symbol-cell { font-weight: 600; letter-spacing: 0.01em; }
.binance-scanner-body .tabulator-row.binance-scanner-row-highlighted { background: var(--danger-bg); }
.binance-scanner-body .tabulator-row.binance-scanner-row-highlighted:hover { background: var(--danger-bg); filter: brightness(1.1); }

/* "Suggest a widget" + "Share to Recently Shared"/gallery browsing.
   The .chart-prompt/.chart-prompt-btn/.feedback-chip/.feedback-note/
   .feedback-action-row classes reused below are elsewhere only styled under
   .bubble (chat messages) — same reasoning as #themeSyncChartsPrompt above,
   scoped here under #suggestWidgetPanelHost so this modal instance gets its
   own themed look without touching the chat-bubble one. */
.suggestion-modal { width: 460px; }
#suggestWidgetPanelHost .suggestion-panel {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
#suggestWidgetPanelHost .suggestion-title-input,
#suggestWidgetPanelHost .suggestion-author-input {
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--text);
  border-radius: 6px;
  padding: 7px 9px;
  font: inherit;
  font-size: 13px;
}
#suggestWidgetPanelHost .feedback-chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
#suggestWidgetPanelHost .chart-prompt-btn {
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--text);
  border-radius: 6px;
  padding: 6px 11px;
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
  white-space: nowrap;
}
#suggestWidgetPanelHost .feedback-chip.selected { background: var(--accent); color: #fff; border-color: var(--accent); }
#suggestWidgetPanelHost .chart-prompt-btn.yes { background: var(--accent); color: #fff; border-color: var(--accent); }
#suggestWidgetPanelHost .chart-prompt-btn:hover { filter: brightness(1.1); }
#suggestWidgetPanelHost .chart-prompt-btn:disabled { opacity: 0.6; cursor: default; }
#suggestWidgetPanelHost .feedback-note {
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--text);
  border-radius: 6px;
  padding: 7px 9px;
  font: inherit;
  font-size: 13px;
  resize: vertical;
}
#suggestWidgetPanelHost .feedback-action-row { display: flex; gap: 8px; }

/* Anti-spam honeypot for the Suggest-a-widget panel and gallery comment form
   (see looksLikeSpam in server.js) — moved off-canvas rather than
   display:none/visibility:hidden, since some scrapers skip fields hidden
   that way but still fill an off-screen one; aria-hidden plus a negative
   tabindex keeps it out of the way for keyboard/screen-reader users, who
   should never encounter it either way. */
.honeypot-field {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

/* Error line under the combined-share popover's "Share to Recently Shared"
   button (see initCombinedSharePopover in app.js) — sibling to the popover's
   existing save-option-rows/confirm button. */
.share-community-fields {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 4px;
  padding-top: 8px;
  border-top: 1px solid var(--panel-border);
}
.share-community-error {
  margin: 0;
  font-size: 11.5px;
  color: var(--danger);
}
.workspace-save-confirm-secondary {
  background: var(--bg-elevated);
  color: var(--text);
  border: 1px solid var(--border);
}

/* Price alerts widget (see alertsBtn/pinAlertsWidget in app.js /
   public/alerts.js). buildAlertsPanel's own .alerts-panel root is mounted
   straight into a workspace tile body, so everything here is scoped by that
   class rather than a modal host id — the same DOM works wherever it's
   dropped in. */
.alerts-tile-body { padding: 10px; }
.alerts-panel { display: flex; flex-direction: column; gap: 10px; }
.alerts-panel .alert-form { display: flex; flex-direction: column; gap: 8px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
.alerts-panel .alert-form-row,
.alert-help-backdrop .alert-form-row { display: flex; gap: 8px; align-items: center; }
/* The class rule above beats the UA [hidden] rule at equal specificity
   (normal author > normal user-agent in the cascade), so a hidden
   .alert-form-row (the pct_change window row, Telegram's token/chat-id
   pair) would otherwise stay visible — this restores hidden's effect. */
.alerts-panel [hidden] { display: none !important; }
.alerts-panel .alert-form-row select,
.alerts-panel .alert-form-row input,
.alerts-panel .alert-channel-input,
.alert-help-backdrop .alert-form-row select,
.alert-help-backdrop .alert-form-row input,
.alert-help-backdrop .alert-channel-input {
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--text);
  border-radius: 6px;
  padding: 7px 9px;
  font: inherit;
  font-size: 13px;
  min-width: 0;
}
.alerts-panel .alert-product-search-wrap { position: relative; flex: 1; min-width: 0; }
.alerts-panel .alert-product-input,
.alerts-panel .alert-channel-input,
.alert-help-backdrop .alert-channel-input { flex: 1; width: 100%; box-sizing: border-box; }
.alerts-panel .alert-value-input,
.alerts-panel .alert-window-input,
.alerts-panel .alert-cooldown-input { width: 90px; }
.alerts-panel .alert-value-presets-row { gap: 6px; margin-top: -2px; }
.alerts-panel .alert-value-preset-btn {
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-dim);
  border-radius: 6px;
  padding: 4px 9px;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.alerts-panel .alert-value-preset-btn:hover,
.alerts-panel .alert-value-preset-btn:focus-visible { border-color: var(--accent); color: var(--text); outline: none; }
/* "This symbol" / "Any symbol" scope toggle (see updateScopeUI in
   public/alerts.js) — reuses the percent-preset button look, distinguished
   by aria-pressed instead of a separate class, so the one selected reads as
   a filled/active choice rather than just another outlined button. */
.alerts-panel .alert-scope-toggle-row .alert-value-preset-btn[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
/* Suggestions drop *below* the input (unlike the composer's "@" search,
   which opens upward from the bottom of the screen) — this panel usually
   sits well above the fold, in a modal or a mid-grid tile. */
.alerts-panel .alert-product-suggestions {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  bottom: auto;
  margin-top: 4px;
  margin-bottom: 0;
  max-height: 220px;
  z-index: 5;
}
.alerts-panel .alert-current-price { margin: -4px 0 0; font-size: 12px; color: var(--text-dim); }
.alerts-panel .alert-bid-ask { margin: -6px 0 0; font-size: 11px; color: var(--text-dim); opacity: 0.8; font-variant-numeric: tabular-nums; }
.alerts-panel .alert-form-inline-label { font-size: 12.5px; color: var(--text-dim); white-space: nowrap; }
.alerts-panel .alert-section-label { margin: 4px 0 0; font-size: 12px; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.03em; }
.alerts-panel .alert-checkbox-row { display: flex; align-items: center; gap: 7px; font-size: 13px; color: var(--text); cursor: pointer; }
.alerts-panel .alert-checkbox-row input[type="checkbox"] { cursor: pointer; }
.alerts-panel .alert-checkbox-row-disabled { opacity: 0.6; cursor: default; }
.alerts-panel .alert-checkbox-row-disabled input[type="checkbox"] { cursor: default; }
.alerts-panel .alert-browser-status { font-size: 11.5px; color: var(--text-dim); margin-left: 22px; }
.alerts-panel .alert-form-error { margin: 0; font-size: 12px; color: var(--danger); }
.alerts-panel .chart-prompt-btn,
.alert-help-backdrop .chart-prompt-btn {
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--text);
  border-radius: 6px;
  padding: 6px 11px;
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
  white-space: nowrap;
}
.alerts-panel .chart-prompt-btn.yes,
.alert-help-backdrop .chart-prompt-btn.yes { background: var(--accent); color: #fff; border-color: var(--accent); }
.alerts-panel .chart-prompt-btn:hover,
.alert-help-backdrop .chart-prompt-btn:hover { filter: brightness(1.1); }
.alerts-panel .chart-prompt-btn:disabled,
.alert-help-backdrop .chart-prompt-btn:disabled { opacity: 0.6; cursor: default; }
.alerts-panel .alert-create-btn { align-self: flex-start; }
.alerts-panel .alert-list { display: flex; flex-direction: column; gap: 8px; }
.alerts-panel .alert-list-empty { margin: 0; font-size: 12.5px; color: var(--text-dim); }
.alerts-panel .alert-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  border-radius: 8px;
  padding: 9px 11px;
}
.alerts-panel .alert-card-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.alerts-panel .alert-card-title-wrap { position: relative; min-width: 0; }
.alerts-panel .alert-card-title {
  font-weight: 600;
  font-size: 13px;
  color: var(--text);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  text-align: left;
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}
.alerts-panel .alert-card-title:hover,
.alerts-panel .alert-card-title[aria-expanded="true"] { color: var(--accent); }
/* scope:'any' alert title (see buildSymbolTitle) — no single market to open
   a chart/order-book/trades menu for, so it's a plain label, not a button:
   same weight/size as .alert-card-title, but no underline/pointer/hover. */
.alerts-panel .alert-card-title-static { font-weight: 600; font-size: 13px; color: var(--text); }
.alerts-panel .alert-symbol-popover {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 6;
  margin-top: 4px;
  display: flex;
  gap: 4px;
  padding: 4px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}
.alerts-panel .alert-card-last-triggered { margin: 0; font-size: 11px; color: var(--text-dim); opacity: 0.75; line-height: 1.4; }
.alerts-panel .alert-card-actions { display: flex; gap: 6px; margin-top: 4px; }
.alerts-panel .alert-delete-btn:hover { color: var(--danger); border-color: var(--danger); }
.alerts-panel .alert-status-badge { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; padding: 2px 7px; border-radius: 999px; white-space: nowrap; }
.alerts-panel .alert-status-active { background: rgba(126, 231, 135, 0.12); color: var(--status-green); }
.alerts-panel .alert-status-triggered { background: rgba(0, 82, 255, 0.16); color: var(--accent); }
.alerts-panel .alert-status-disabled { background: var(--border); color: var(--text-dim); }

/* Condition line (direction glyph + "rises above $X"/"falls below $X") and
   the de-emphasized meta line (source/repeat note + channel chips) below it
   — see buildAlertDetail in public/alerts.js, shared by cards and rows. */
.alerts-panel .alert-condition-row { display: flex; align-items: baseline; gap: 5px; }
.alerts-panel .alert-direction-icon { font-size: 10px; line-height: 1; }
.alerts-panel .alert-direction-icon-up { color: var(--status-green); }
.alerts-panel .alert-direction-icon-down { color: var(--danger); }
.alerts-panel .alert-condition-text { margin: 0; font-size: 13px; font-weight: 600; color: var(--text); }
.alerts-panel .alert-meta-line { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
.alerts-panel .alert-meta-note { font-size: 11px; color: var(--text-dim); opacity: 0.75; line-height: 1.4; white-space: nowrap; }
.alerts-panel .alert-channel-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.alerts-panel .alert-channel-chip {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 10.5px;
  padding: 1px 6px;
  border-radius: 4px;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text-dim);
}
.alerts-panel .alert-channel-chip-icon { display: flex; align-items: center; flex-shrink: 0; }
.alerts-panel .alert-channel-chip-icon svg { display: block; width: 10px; height: 10px; }

/* Icon-only Resume/Pause/Delete (see buildAlertActions) — same
   .chart-prompt-btn base as every other button in this panel, just resized
   to a square icon footprint. Scoped under .alerts-panel .alert-card-actions
   so it can't affect the unrelated .alert-symbol-popover's Chart/Order
   book/Trades buttons or any other .chart-prompt-btn caller in the app. */
.alerts-panel .alert-card-actions .alert-action-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
}

/* Alerts for the same productId/exchange cluster into one collapsible
   .alert-group (see buildAlertGroup in public/alerts.js) instead of
   repeating the symbol title on every card — .alert-row is the condensed
   per-alert card inside a group's body, styled like .alert-card minus the
   title row it no longer needs. */
.alerts-panel .alert-group-header { display: flex; align-items: center; gap: 8px; }
.alerts-panel .alert-group-chevron {
  border: none;
  background: none;
  color: var(--text-dim);
  padding: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
  transition: transform 0.12s ease;
}
.alerts-panel .alert-group-chevron.expanded { transform: rotate(90deg); }
.alerts-panel .alert-group-chevron:hover { color: var(--text); }
.alerts-panel .alert-group-header .alert-card-title-wrap { flex: 1; min-width: 0; }
.alerts-panel .alert-group-badges { display: flex; gap: 4px; flex-wrap: wrap; }
.alerts-panel .alert-group-body { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; padding-left: 20px; }
.alerts-panel .alert-row {
  display: flex;
  gap: 8px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  border-radius: 8px;
  padding: 8px 10px;
}
.alerts-panel .alert-row-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.alerts-panel .alert-row-select { display: flex; align-items: flex-start; padding-top: 2px; cursor: pointer; flex-shrink: 0; }
.alerts-panel .alert-row-select input[type="checkbox"] { cursor: pointer; }

/* "Select" toggle (in the .alert-section-header next to "Your alerts") +
   the bulk toolbar it reveals — same active-state look as
   .alert-new-toggle-btn, and the toolbar wraps onto a second line rather
   than overflowing the panel's fixed 480px width once "{n} selected" plus
   four action buttons don't all fit on one row. */
.alerts-panel .alert-select-mode-btn[aria-expanded="true"] { background: var(--bg-elevated); border-color: var(--accent); color: var(--accent); }
.alerts-panel .alert-bulk-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: -2px; }
.alerts-panel .alert-bulk-selected-count { font-size: 12px; color: var(--text-dim); margin-right: 2px; }
.alerts-panel .alert-bulk-toolbar .chart-prompt-btn:disabled { opacity: 0.5; }

/* "+ New alert" toggle + the form it reveals — the list is the default
   view (see buildAlertsPanel's setFormOpen), so the create form is tucked
   behind this button instead of always taking up the top of the panel. */
.alerts-panel .alert-new-toggle-btn { align-self: flex-start; }
.alerts-panel .alert-new-toggle-btn[aria-expanded="true"] { background: var(--bg-elevated); border-color: var(--accent); color: var(--accent); }
.alerts-panel .alert-form-wrap .alert-form { border-bottom: none; padding-bottom: 0; padding-top: 4px; }

/* "Notify me via" heading + its settings gear (see openChannelSettings in
   public/alerts.js) — Discord/Telegram credentials live in the popover this
   opens, not inline in the form. */
.alerts-panel .alert-section-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; position: relative; margin-top: 4px; }
.alerts-panel .alert-section-header .alert-section-label { margin: 0; }
.alerts-panel .alert-channel-settings-btn {
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--text-dim);
  border-radius: 6px;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
}
.alerts-panel .alert-channel-settings-btn:hover,
.alerts-panel .alert-channel-settings-btn[aria-expanded="true"] { color: var(--text); border-color: var(--accent); }
.alerts-panel .alert-channel-settings-popover {
  position: absolute;
  top: 100%;
  right: 0;
  z-index: 6;
  margin-top: 4px;
  width: 270px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}
.alerts-panel .alert-channel-settings-field-row { display: flex; align-items: center; gap: 6px; }
.alerts-panel .alert-channel-settings-field-label { font-size: 11.5px; color: var(--text-dim); }
.alerts-panel .alert-channel-help-btn {
  border: none;
  background: none;
  color: var(--text-dim);
  padding: 0;
  width: 14px;
  height: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
}
.alerts-panel .alert-channel-help-btn:hover { color: var(--accent); }
.alerts-panel .alert-channel-config-hint,
.alert-help-backdrop .alert-channel-config-hint { font-size: 11px; color: var(--text-dim); opacity: 0.75; margin-left: 2px; }
.alerts-panel .alert-channel-picker { display: flex; flex-direction: column; gap: 6px; }
.alerts-panel .alert-channel-picker select { flex: 1; min-width: 0; }
/* One labelled group per field (see buildChannelPicker), stacked vertically
   — Telegram's bot token and chat id used to sit in one bare row with
   nothing but placeholder text telling them apart, which vanished once
   each field actually held a value (dots vs a bare number). */
.alerts-panel .alert-channel-new-fields { display: flex; flex-direction: column; gap: 8px; }
.alerts-panel .alert-channel-field-group,
.alert-help-backdrop .alert-channel-field-group { display: flex; flex-direction: column; gap: 3px; }
.alerts-panel .alert-channel-field-label,
.alert-help-backdrop .alert-channel-field-label { font-size: 11px; color: var(--text-dim); }
/* The settings popover is a fixed 270px — narrower than a field's
   input+Show-button row comfortably fits without wrapping. */
.alerts-panel .alert-channel-settings-popover .alert-form-row { flex-wrap: wrap; }
.alerts-panel .alert-channel-settings-popover .alert-channel-input { min-width: 110px; }

/* Numbered steps inside the "how do I get this?" help modal (see
   openAlertsHelpModal in public/alerts.js) — reuses .modal-body's color/
   size but needs its own left padding since it's an <ol>, not a <p>. */
.alert-help-steps { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 6px; }
/* Real (redacted) screenshots of the flow a step describes — capped narrow
   so they read as a supporting thumbnail, not the focus of the modal. */
.alert-help-step-image {
  display: block;
  max-width: 200px;
  width: 100%;
  border-radius: 8px;
  border: 1px solid var(--border);
  margin-top: 6px;
}

/* Standalone Price alerts dialog (#alertsBackdrop in index.html, wired in
   app.js's openAlertsModal) — the .alerts-panel content built by
   buildAlertsPanel mounts straight into #alertsModalBody, same DOM either
   way (see the .alerts-panel comment above), just a wider/taller .modal
   than the 420px default so the list + form don't feel cramped. */
.alerts-modal { width: 480px; max-height: 85vh; overflow-y: auto; }

/* Bell popover (#workspaceAlertsPopover in index.html, wired in app.js) —
   a quick glance at active/triggered alerts only, reusing the same
   .alerts-panel/.alert-card styling above via the shared class rather than
   duplicating it. The bell sits well to the right in the workspace header
   (after Save/Widget settings/Share), so the popover's default left:0 (see
   .workspace-save-popover) runs it past the panel's right edge — same fix
   as .sidebar-save-popover: hang it from the button's right edge instead. */
/* Only the .alert-list scrolls (capped below); the footer's + New alert/
   Manage all buttons sit outside that scroll area so they're always
   visible on open instead of requiring a scroll past the alert cards. */
.alert-recent-popover { left: auto; right: 0; width: 300px; max-height: 360px; overflow: hidden; }
.alert-recent-popover-body .alert-list { max-height: 220px; overflow-y: auto; }
.alert-recent-popover-footer { display: flex; gap: 6px; margin-top: 2px; flex: 0 0 auto; }
.alert-recent-popover-footer .chart-prompt-btn { flex: 1; text-align: center; }

/* "Recently shared" browsing modal (see openCommunityGalleryBtn in app.js). */
.community-gallery-modal { width: 640px; }
.community-gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
  max-height: 60vh;
  overflow-y: auto;
  padding: 2px;
}
.gallery-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  border-radius: 10px;
  padding: 12px;
}
.gallery-card-title { font-weight: 600; font-size: 13.5px; color: var(--text); }
.gallery-card-actions { display: flex; align-items: center; gap: 8px; margin-top: auto; padding-top: 6px; }
.gallery-card-views { font-size: 12px; color: var(--text-dim); }
.gallery-card-like {
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text);
  border-radius: 6px;
  padding: 5px 9px;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.gallery-card-like:hover:not(:disabled) { filter: brightness(1.1); }
.gallery-card-like:disabled { color: var(--accent); border-color: var(--accent); cursor: default; }
.gallery-card-open {
  margin-left: auto;
  border: none;
  border-radius: 6px;
  padding: 5px 12px;
  background: var(--accent-dim);
  color: #fff;
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.gallery-card-open:hover { filter: brightness(1.08); }
.gallery-card-comments-toggle {
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-dim);
  border-radius: 6px;
  padding: 5px 9px;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.gallery-card-comments-toggle:hover { color: var(--text); filter: brightness(1.1); }
/* :not([hidden]) rather than a bare display:flex — same convention as
   .workspace-save-popover above — so the [hidden] attribute this thread is
   toggled with actually hides it instead of being silently overridden by a
   same-specificity author rule. */
.gallery-comment-thread:not([hidden]) {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.gallery-comment-list { display: flex; flex-direction: column; gap: 8px; font-size: 12px; color: var(--text-dim); }
.gallery-comment { display: flex; flex-direction: column; gap: 2px; }
.gallery-comment-author { font-size: 11px; font-weight: 600; color: var(--text); }
.gallery-comment-text { margin: 0; line-height: 1.4; }
.gallery-comment-form { display: flex; flex-direction: column; gap: 6px; }
.gallery-comment-input,
.gallery-comment-author-input {
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--text);
  border-radius: 6px;
  padding: 6px 8px;
  font: inherit;
  font-size: 12px;
  resize: vertical;
}
.gallery-comment-post {
  align-self: flex-end;
  border: none;
  border-radius: 6px;
  padding: 5px 12px;
  background: var(--accent-dim);
  color: #fff;
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.gallery-comment-post:hover { filter: brightness(1.08); }
.gallery-comment-post:disabled { opacity: 0.6; cursor: default; }
