/* Manrope 700 -- self-hosted, used sparingly for brand/headings/table-header
   weight only. Body copy stays on the system stack (fast, no download). */
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/static/fonts/manrope-700.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+20AC, U+2122;
}
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/static/fonts/manrope-700-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+1E00-1E9F, U+2C60-2C7F;
}

:root {
  /* Light, monochrome-first admin-panel palette: black/white/gray with a
     single semantic green reserved for status, not a bright brand accent. */
  --bg: #f7f7f8;
  --surface: #ffffff;
  --border: #e6e6e9;
  --text: #18181b;
  --text-muted: #71717a;
  --text-faint: #a1a1aa;
  --ink: #18181b;
  --ink-contrast: #ffffff;
  --active-bg: #f1f1f3;
  --success: #16a34a;
  --danger: #dc2626;
  --warning: #d97706;
  --chart-rsi: #7c3aed;

  --font-display: "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;

  --radius: 12px;
  --topbar-height: 64px;
  --sidebar-width: 248px;
}

/* Light is the only enabled look for now -- deliberately NOT following
   prefers-color-scheme, since the app is being built against a specific
   light reference and auto-switching to dark made every review compare
   against a palette nobody had actually approved. The dark token set
   below is kept ready (unused) for whenever a real, reviewed dark mode
   and a manual toggle are wanted -- see [[project-app-web-scaffold]]. */
:root[data-theme="dark"] {
  --bg: #0f0f11;
  --surface: #18181b;
  --border: #2a2a2e;
  --text: #f1f1f3;
  --text-muted: #a1a1aa;
  --text-faint: #71717a;
  --ink: #f1f1f3;
  --ink-contrast: #18181b;
  --active-bg: #232327;
  --success: #4ade80;
  --danger: #f87171;
  --warning: #fbbf24;
  --chart-rsi: #a78bfa;
}

:root[data-theme="light"] {
  --bg: #f7f7f8;
  --surface: #ffffff;
  --border: #e6e6e9;
  --text: #18181b;
  --text-muted: #71717a;
  --text-faint: #a1a1aa;
  --ink: #18181b;
  --ink-contrast: #ffffff;
  --active-bg: #f1f1f3;
  --success: #16a34a;
  --danger: #dc2626;
  --warning: #d97706;
  --chart-rsi: #7c3aed;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
}

/* Site-wide slim scrollbar (2026-07-28) -- the browser's own outer page
   scrollbar (not any panel's own -- this is the whole document, distinct
   from .table-wrap/.ai-chat-scroll which only cover their own scroll
   containers) was the last remaining spot still using the platform
   default chunky bar with up/down arrow buttons. Same treatment,
   applied globally so nothing on the page stands out as unstyled. */
html {
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
html::-webkit-scrollbar {
  width: 8px;
}
html::-webkit-scrollbar-track {
  background: transparent;
}
html::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 100px;
}
html::-webkit-scrollbar-thumb:hover {
  background: var(--text-faint);
}

a {
  color: inherit;
}

:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 2px;
}

/* ---- Shell: topbar + sidebar ---- */

.topbar {
  height: var(--topbar-height);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--space-6);
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  position: sticky;
  top: 0;
  z-index: 10;
}

.page-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.15rem;
  letter-spacing: -0.01em;
}

.topbar-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.icon-btn {
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  border-radius: 999px;
  color: var(--text-muted);
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease;
}

.icon-btn:hover {
  background: var(--active-bg);
  color: var(--text);
}

.icon-btn svg {
  width: 18px;
  height: 18px;
}

.avatar {
  width: 32px;
  height: 32px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--ink-contrast);
  display: flex;
  align-items: center;
  justify-content: center;
}

.session-countdown {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--active-bg);
}

.session-countdown-warn {
  color: #dc2626;
  background: color-mix(in srgb, #dc2626 12%, transparent);
}

.avatar svg {
  width: 18px;
  height: 18px;
}

.layout {
  display: flex;
  min-height: 100vh;
}

/* Sidebar runs the full page height, topbar+content sit in their own
   column to its right (2026-09-06, was topbar-full-width-above-everything
   including the sidebar) -- the topbar's width is just whatever .main-col
   (a flex sibling of .sidebar, not a descendant of it) has left, so it
   naturally stops where the sidebar starts instead of spanning behind it. */
.main-col {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.sidebar {
  width: var(--sidebar-width);
  flex-shrink: 0;
  border-right: 1px solid var(--border);
  background: var(--surface);
  padding: var(--space-4) var(--space-3) var(--space-4);
  display: flex;
  flex-direction: column;
  /* Pinned for the full page height (2026-09-06, was top: topbar-height /
     height: 100vh-topbar-height back when the topbar sat above this too)
     -- sticky + its own scroll keeps nav reachable at any scroll position;
     overflow only kicks in if the nav list itself ever outgrows the
     viewport. */
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}

.brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  /* Was var(--space-5) (24px) below -- three nav groups' worth of items
     (2026-09-06) no longer fit in the sidebar's own height without
     scrolling, and this bottom gap was pure breathing room, not content. */
  padding: var(--space-2) var(--space-2) var(--space-3);
}

.brand-mark {
  width: 36px;
  height: 36px;
  flex-shrink: 0;
  border-radius: 9px;
  /* Fixed brand color, not a theme token -- a logo mark shouldn't invert
     with light/dark the way UI chrome does. */
  background: #18181b;
  color: #ffffff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1rem;
}

.brand-text {
  display: flex;
  flex-direction: column;
  line-height: 1.25;
}

.brand-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.95rem;
  letter-spacing: -0.01em;
}

.brand-sub {
  font-size: 0.75rem;
  color: var(--text-muted);
}

.nav-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.nav-section-label {
  /* Was var(--space-4)/var(--space-1) (16px/4px) -- three group headers
     (2026-09-06, up from one) each cost this much twice, on top of every
     item's own tightened padding below; this is the other place that
     space was actually coming from. */
  padding: var(--space-2) var(--space-3) 2px;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-faint);
}

.nav-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  /* Was var(--space-2) (8px) vertical -- with 17 nav items across 3
     groups (2026-09-06) that no longer fits this sidebar's own height
     without scrolling; still comfortably tappable at 6px. */
  padding: 6px var(--space-3);
  border-radius: 8px;
  color: var(--text-muted);
  text-decoration: none;
  font-size: 0.88rem;
  font-weight: 500;
  transition: background-color 120ms ease, color 120ms ease;
}

@media (prefers-reduced-motion: reduce) {
  .nav-item {
    transition: none;
  }
}

.nav-item:hover {
  background: var(--active-bg);
  color: var(--text);
}

.nav-item.active {
  background: var(--active-bg);
  color: var(--text);
  font-weight: 700;
}

.nav-icon {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  display: flex;
}

.nav-icon svg {
  width: 100%;
  height: 100%;
}

.sidebar-footer {
  margin-top: auto;
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.sidebar-footer .avatar {
  background: var(--active-bg);
  color: var(--text-faint);
}

.sidebar-footer-text {
  display: flex;
  flex-direction: column;
  line-height: 1.25;
  min-width: 0;
}

.sidebar-footer-name {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sidebar-footer-sub {
  font-size: 0.75rem;
  color: var(--text-faint);
}

/* ---- Content ---- */

.content {
  flex: 1;
  padding: var(--space-6);
  min-width: 0;
}

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-5);
  /* Barely-there elevation -- enough to lift cards off the page's flat
     gray background, subtle enough to stay inside the monochrome,
     no-bright-accent direction the palette already commits to. */
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
}

.panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

.panel-count {
  font-weight: 600;
  font-size: 0.95rem;
}

.lede {
  max-width: 65ch;
  line-height: 1.6;
  color: var(--text-muted);
  margin: 0;
}

/* ---- Reusable components (buttons, badges, table) for pages migrated in ---- */

.stat-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

.stat-card {
  flex: 1;
  min-width: 160px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4) var(--space-5);
  overflow: hidden;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
}

/* .table-meta is nowrap site-wide (right for table cells) -- but a stat
   tile's subtitle is free text of unpredictable length (e.g. Overview's
   Momentum/Today's P&L tiles), so scope it back to normal wrapping just
   inside a stat-card instead of letting it run past the card edge. */
.stat-card .table-meta {
  white-space: normal;
  overflow-wrap: break-word;
}

.stat-label {
  font-size: 0.85rem;
  color: var(--text-muted);
  margin: 0 0 var(--space-1);
}

.stat-value {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.6rem;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}

.search-input {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text-muted);
  max-width: 360px;
}

.search-input svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.search-input input {
  border: none;
  background: transparent;
  outline: none;
  font-size: 0.88rem;
  color: var(--text);
  width: 100%;
}

.search-input input::placeholder {
  color: var(--text-faint);
}

.table-meta {
  font-size: 0.82rem;
  color: var(--text-faint);
  white-space: nowrap;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 120ms ease, border-color 120ms ease, opacity 120ms ease;
}

@media (prefers-reduced-motion: reduce) {
  .btn, .icon-btn, .ghost-btn, .meter > span, th.sortable, th.sortable .sort-icon {
    transition: none;
  }
}

.btn:hover {
  background: var(--active-bg);
}

.btn-primary {
  background: var(--ink);
  color: var(--ink-contrast);
  border-color: var(--ink);
}

.btn-primary:hover {
  opacity: 0.88;
}

.badge {
  display: inline-flex;
  align-items: center;
  padding: 3px var(--space-3);
  border-radius: 999px;
  background: var(--active-bg);
  color: var(--text-muted);
  font-size: 0.78rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Tinted status badges (2026-07-23) -- .badge.up/.down previously only
   changed the text color, leaving the same flat neutral-gray background
   regardless of status; a colored badge reads as a status chip faster
   than colored text alone. Same color-mix technique .risk-strip/.pill
   already used elsewhere, just applied consistently here too. */
.badge.up {
  background: color-mix(in srgb, var(--success) 14%, var(--active-bg));
}
.badge.down {
  background: color-mix(in srgb, var(--danger) 14%, var(--active-bg));
}

/* Adjacent badges (e.g. the VSTOP/ADX/RSI/momentum cluster in Holdings)
   were relying on incidental whitespace between inline-flex spans for
   spacing -- explicit and consistent regardless of how the markup wraps. */
.badge + .badge {
  margin-left: 4px;
}

/* Momentum score badge (2026-07-23) -- ported the STRUCTURE of the old
   private-app's MomGauge.jsx (the X/3 score got a distinct bordered
   "headline" treatment, separate from its green/red pass/fail dots), but
   using --ink instead of that app's blue --accent -- this palette is
   deliberately monochrome-first with no bright brand accent (see the
   :root comment at the top of this file), so --ink is the equivalent
   "this is the number that matters" treatment without introducing a new
   color into an otherwise accent-free system. */
.badge-score {
  background: color-mix(in srgb, var(--ink) 8%, var(--surface));
  color: var(--ink);
  border: 1px solid var(--ink);
  font-weight: 700;
}

.status {
  font-size: 0.85rem;
  font-weight: 600;
}

/* Ask AI chat bubbles (2026-07-28 redesign) -- was inline-style-only
   left/right flex divs with a generic .panel for both roles; separating
   into real classes here so user vs. assistant read as distinct chat
   turns at a glance rather than two same-looking cards. --ink/--ink-
   contrast (already used for .badge-score's "this is the number that
   matters" treatment) doubles as "this is you speaking" for the user
   bubble -- no new color introduced into the monochrome palette. */
.chat-row {
  display: flex;
  flex-direction: column;
  max-width: 100%;
  margin-bottom: var(--space-4);
}
.chat-row.user { align-items: flex-end; }
.chat-row.assistant { align-items: flex-start; }

.chat-role {
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin-bottom: 4px;
}

.chat-bubble {
  max-width: 80%;
  min-width: 0;
  overflow-wrap: break-word;
  word-break: break-word;
  white-space: pre-wrap;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius);
  font-size: 0.88rem;
  line-height: 1.55;
}
.chat-bubble.user {
  background: var(--ink);
  color: var(--ink-contrast);
  border-bottom-right-radius: 4px;
}
.chat-bubble.assistant {
  max-width: 95%;
  background: var(--surface);
  border: 1px solid var(--border);
  border-bottom-left-radius: 4px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
}
.chat-bubble.assistant p:first-child { margin-top: 0; }
.chat-bubble.assistant :last-child { margin-bottom: 0; }

.chat-drafts {
  margin-top: var(--space-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.chat-input-bar {
  display: flex;
  gap: var(--space-2);
  align-items: flex-end;
}

/* Round icon-only button (mic) -- same visual weight as .btn but sized for
   a single icon rather than a text label. .listening swaps it to the
   --danger tint that .badge.down already established as this palette's
   "active/attention" color, so it's obvious when it's capturing audio. */
.chat-icon-btn {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 0;
  border-radius: 999px;
}
.chat-icon-btn.listening {
  color: var(--danger);
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 10%, var(--surface));
}

/* Ask AI composer (2026-07-28) -- the plain .field-input textarea read as
   just another form field, flat next to the hero's rounded pills/soft
   vignette. This merges textarea + mic + send into one bordered "bar",
   the shape a chat composer actually has (Claude.ai/ChatGPT), rather than
   three separate boxed controls sitting side by side. Scoped to this one
   class, not a .field-input change -- ordinary forms elsewhere (task/
   routine creation) keep the plain field style, which is correct there. */
.ai-composer {
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 8px 24px -16px rgba(0, 0, 0, 0.15);
  transition: border-color 0.15s, box-shadow 0.15s;
}
.ai-composer:focus-within {
  border-color: var(--ink);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 8px 28px -14px rgba(0, 0, 0, 0.2);
}
.ai-composer-input {
  flex: 1;
  border: none;
  background: transparent;
  resize: none;
  padding: var(--space-2) 0;
  font-size: 0.95rem;
  font-family: var(--font-body);
  color: var(--text);
  line-height: 1.5;
}
.ai-composer-input:focus,
.ai-composer-input:focus-visible {
  outline: none;
}
.ai-composer-input::placeholder {
  color: var(--text-faint);
}
.ai-composer .chat-icon-btn {
  border: none;
  background: transparent;
  color: var(--text-muted);
}
.ai-composer .chat-icon-btn:hover {
  background: var(--active-bg);
  color: var(--text);
}
.ai-composer .chat-icon-btn.listening {
  background: color-mix(in srgb, var(--danger) 10%, transparent);
  color: var(--danger);
}
.ai-composer .btn-primary {
  border-radius: 16px;
}

/* Ask AI landing hero (2026-07-28) -- shown on every fresh page load in
   place of the raw history dump (history is hidden by default, toggled via
   the top-right button). A soft radial vignette gives it depth without
   introducing a new hue -- color-mix off --ink, so it stays inside the
   site's deliberately monochrome palette rather than reaching for an
   accent color the rest of the app doesn't use. */
/* Landing hero that collapses into a chat composer (2026-07-28) --
   #ai-page wraps both the hero/composer and the history panel. Before any
   message, .ai-landing-wrap sits vertically centered in the open page
   like a real blank-canvas start screen (min-height transition -- see
   below). Once "started" (first send, or manually opening History), the
   SAME reference point that this app has followed for chat apps
   generally: history above, a slim composer pinned below it -- achieved
   with a `order` swap rather than moving markup, and the greeting/
   suggestion chips (.ai-hero-decor) disappear since they'd be redundant
   mid-conversation, leaving just the input bar. */
.ai-page {
  display: flex;
  flex-direction: column;
}
.ai-page .ai-landing-wrap {
  order: 1;
}
#ai-history {
  order: 2;
}
.ai-page.ai-started .ai-landing-wrap {
  order: 2;
}
.ai-page.ai-started #ai-history {
  order: 1;
}
.ai-page.ai-started .ai-hero-decor {
  display: none;
}
.ai-page.ai-started #ai-history-toggle {
  display: none; /* redundant once the conversation is already showing */
}
.ai-page.ai-started .ai-hero-panel {
  padding: var(--space-3) var(--space-4);
}
.ai-page.ai-started .ai-hero-panel::before {
  display: none; /* the soft vignette suited a big empty hero, not a slim composer bar */
}

.ai-landing-wrap {
  display: flex;
  align-items: center;
  min-height: calc(100vh - var(--topbar-height) - var(--space-6) * 2);
  transition: min-height 0.5s ease;
}
.ai-page.ai-started .ai-landing-wrap {
  min-height: 0;
}
.ai-landing-wrap > * {
  width: 100%;
}
@media (prefers-reduced-motion: reduce) {
  .ai-landing-wrap {
    transition: none;
  }
}

.ai-hero-panel {
  position: relative;
  overflow: hidden;
}
.ai-hero-panel::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(
    ellipse 640px 280px at 50% 0%,
    color-mix(in srgb, var(--ink) 5%, transparent),
    transparent 70%
  );
  pointer-events: none;
}
.ai-hero {
  position: relative;
  text-align: center;
  padding: var(--space-6) var(--space-4) var(--space-5);
  animation: ai-hero-in 0.5s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .ai-hero { animation: none; }
}
@keyframes ai-hero-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}
.ai-hero-icon {
  width: 30px;
  height: 30px;
  margin: 0 auto var(--space-2);
  color: var(--text-faint);
}
.ai-hero-headline {
  font-family: var(--font-display);
  font-size: 2.1rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  text-wrap: balance;
  margin: 0 0 var(--space-2);
}
.ai-hero-tagline {
  display: inline-block;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-muted);
  background: var(--active-bg);
  border: 1px solid var(--border);
  padding: 4px var(--space-3);
  border-radius: 999px;
  margin-bottom: var(--space-3);
}
.ai-hero-subtitle {
  color: var(--text-faint);
  font-size: 1.05rem;
  margin: 0 0 var(--space-5);
}

/* Suggestion chips -- turn "what are we working on today" from a passive
   question into a menu of things this AI can actually do (portfolio,
   routines, memory), doubling as lightweight feature discovery. Click
   fills the textarea, doesn't auto-submit -- these can trigger real
   writes (routines, memory) downstream, so the user should see and
   confirm the exact wording before it goes anywhere. */
.ai-suggestions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-2);
  max-width: 680px;
  margin: var(--space-3) auto 0;
}
.ai-suggestion-chip {
  font-size: 0.82rem;
  font-weight: 500;
  color: var(--text-muted);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 6px var(--space-3);
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}
.ai-suggestion-chip:hover {
  border-color: var(--ink);
  color: var(--ink);
}
.ai-suggestion-chip:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.status-active {
  color: var(--success);
}

.status-inactive {
  color: var(--text-faint);
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.88rem;
  font-variant-numeric: tabular-nums;
}

th {
  text-align: left;
  font-weight: 600;
  font-size: 0.78rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: var(--space-2) var(--space-3);
  /* Heavier than td's 1px -- gives the header a clearer break from the
     body instead of reading as just another row. */
  border-bottom: 2px solid var(--border);
  white-space: nowrap;
}

th.sortable {
  cursor: pointer;
  user-select: none;
  transition: color 120ms ease;
}

th.sortable:hover {
  color: var(--text);
}

th.sortable .sort-icon {
  display: inline-flex;
  width: 12px;
  height: 12px;
  vertical-align: -2px;
  margin-left: 3px;
  color: var(--text-faint);
  transition: transform 150ms ease;
}

th.sortable .sort-icon svg {
  width: 100%;
  height: 100%;
}

td {
  padding: var(--space-3);
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

tbody tr:hover {
  background: var(--active-bg);
}

.table-wrap {
  overflow-x: auto;
  /* Slim, theme-matched scrollbar for wide tables (e.g. Holdings' alpha
     tab) instead of the platform-default chunky gray bar. Firefox +
     Chromium/Safari properties both included; no-op elsewhere. */
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}

.table-wrap::-webkit-scrollbar {
  height: 8px;
}
.table-wrap::-webkit-scrollbar-track {
  background: transparent;
}
.table-wrap::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 100px;
}
.table-wrap::-webkit-scrollbar-thumb:hover {
  background: var(--text-faint);
}

/* Ask AI chat scroll -- same slim, theme-matched treatment as .table-wrap
   above (2026-07-28), just the vertical-axis version: the platform-default
   scrollbar (with its chunky up/down arrow buttons) stood out against the
   rest of this app's deliberately minimal chrome. */
.ai-chat-scroll {
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
.ai-chat-scroll::-webkit-scrollbar {
  width: 8px;
}
.ai-chat-scroll::-webkit-scrollbar-track {
  background: transparent;
}
.ai-chat-scroll::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 100px;
}
.ai-chat-scroll::-webkit-scrollbar-thumb:hover {
  background: var(--text-faint);
}

/* ---- Overview / analytics widgets (Personal Management) ---- */
/* Reuses .panel/.panel-header from the shell section above; adds a plain
   title+note pairing for panels that don't need .panel-count's styling. */

.panel + .panel {
  margin-top: var(--space-4);
}

.panel-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.95rem;
  letter-spacing: -0.01em;
}

.panel-note {
  font-size: 0.8rem;
  color: var(--text-faint);
}

.up { color: var(--success); }
.down { color: var(--danger); }
.muted { color: var(--text-faint); }

.equity-tooltip {
  position: absolute;
  z-index: 5;
  min-width: 150px;
  max-width: 220px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  padding: var(--space-3);
  pointer-events: none;
  transition: left 0.06s linear, top 0.06s linear;
}

.meter {
  height: 6px;
  border-radius: 100px;
  background: var(--active-bg);
  overflow: hidden;
}
.meter > span {
  display: block;
  height: 100%;
  border-radius: 100px;
  background: var(--ink);
  transition: width 260ms ease;
}

.pill {
  display: inline-flex;
  align-items: center;
  padding: 3px var(--space-3);
  border-radius: 999px;
  font-size: 0.76rem;
  font-weight: 600;
}

.legend-dot {
  display: inline-block;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex-shrink: 0;
}

.live-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--success);
}

.ghost-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border: none;
  background: none;
  color: var(--text-muted);
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
  padding: 2px 4px;
  transition: color 120ms ease;
}
.ghost-btn:hover { color: var(--text); }

/* Chart tile detail overlay (2026-09-05) -- a native <dialog>, opened via
   .showModal(), which centers itself and blocks outside interaction for
   free; only the box's own look and the backdrop dimming need styling. */
#chart-detail-modal {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4);
  /* fit-content, not a large fixed width (2026-09-05, corrected twice
     same day) -- the quarterly table is itself capped to ~620px (see
     holdings_chart_detail.html) and the ratio table beside it grows to
     fit its own content (icons + a Mkt Cap column added later the same
     day), so a big fixed dialog left a wide dead strip of empty
     background to the right of both. min-width keeps it from going
     cramped on a symbol with few/no peers. max-width is intentionally
     computed with real margin, not sized flush to the columns' current
     combined minimum (620 + gap + ratio col's own min-width + this
     padding) -- sitting flush against that number is exactly what broke
     twice already when the ratio column grew again for a new reason;
     120px of slack here means the next added column doesn't immediately
     retrigger the same flex-wrap regression. */
  width: fit-content;
  min-width: 700px;
  max-width: min(1200px, 96vw);
  max-height: 94vh;
  overflow-y: auto;
  background: var(--surface);
  color: var(--text);
}
#chart-detail-modal::backdrop {
  background: rgba(0, 0, 0, 0.5);
}

/* Company Detail full-window overlay (2026-09-06) -- deliberately the
   opposite sizing choice from #chart-detail-modal above: that one hugs its
   content because it's a small side-by-side pair of tables, but this one
   holds an entire page's worth of sections (chart, quarterly fundamentals,
   notebook, thesis, position history) so it needs to behave like the real
   full-page route it mirrors, just without leaving the sidebar/topbar
   behind. Fixed near-viewport size (not fit-content) so switching between
   companies doesn't reflow/resize the dialog itself, only its body. */
#company-detail-modal {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4);
  width: 96vw;
  max-width: 96vw;
  height: 92vh;
  max-height: 92vh;
  overflow-y: auto;
  background: var(--surface);
  color: var(--text);
}
#company-detail-modal::backdrop {
  background: rgba(0, 0, 0, 0.6);
}

/* Hover-preview popup (2026-09-06) -- a plain positioned div, not a
   <dialog>: it must float non-modally near the cursor while the rest of
   the page (and the row under the cursor) stays fully interactive, unlike
   #company-detail-modal which deliberately blocks everything behind it. */
.company-hover-popup {
  position: fixed;
  /* 9999, not 60 (2026-09-06) -- matching the site's own "definitely on
     top of everything" precedent (#htmx-progress). 60 was still getting
     out-ranked by the Quarterly Screener's sticky frozen columns, which
     create their own stacking context under the table rather than
     competing with this fixed-position popup on raw z-index alone. */
  z-index: 9999;
  width: min(700px, 92vw);
  max-height: 80vh;
  overflow-y: auto;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-3);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.28);
}
/* Denser than the site-wide table default (var(--space-3) = 12px/cell) --
   this table can show up to 9 companies x 4 metrics x 12 quarters at
   once, and the generous default padding meant for a normal data table
   made that read as bloated rather than dense/comparable. */
.chart-detail-table th,
.chart-detail-table td {
  padding: 4px var(--space-2);
  font-size: 0.8rem;
  /* A long symbol (TORNTPHARM, ZYDUSLIFE, ...) plus the ● held-marker and
     the two stockscans icons was enough to overflow the ratio table's
     narrow company column and wrap onto a second line -- dropping the
     icons (and, in the quarterly table, the up-arrow winner marker)
     below the text they belong beside instead of staying on it
     (2026-09-05). nowrap forces the cell to grow to fit its content in
     one line instead; see the ratio table's own widened column below for
     the other half of the actual fix. */
  white-space: nowrap;
}
.chart-detail-table .chart-detail-metric-row {
  font-weight: 700;
  background: var(--active-bg);
  font-size: 0.78rem;
  padding: 4px var(--space-2);
}
/* The gray bar itself is one <td colspan> spanning every quarter column,
   so it can't be "the sticky column" the same way the company-name cells
   are -- sticky-positioning a spanning cell doesn't pin its CONTENT, only
   an already-huge box that's already anchored at column 0. What actually
   needed to stay in view while scrolling was the LABEL TEXT, not the
   cell -- sticking this inner span instead keeps "Sales Gwt"/"Profit
   Gwt"/etc. visible over the gray background as it scrolls (2026-09-05:
   the auto-scroll-to-latest-quarter already lands scrolled away from
   column 0 by default, so without this the label was invisible from the
   very first render, not just after a manual scroll). */
.chart-detail-metric-label {
  position: sticky;
  left: var(--space-2);
  display: inline-block;
}
/* Pinned to the top of #chart-detail-modal's own vertical scroll
   (2026-09-05) -- the ratio table is much shorter than the quarterly
   one beside it (companies only, no metric-group repeats), so without
   this, scrolling down through the quarterly table's later rows took the
   ratio side with it instead of leaving it in view the whole time. */
.chart-detail-ratio-col {
  position: sticky;
  top: 0;
}
/* First column pinned while the rest of the table scrolls horizontally
   (2026-09-05) -- with up to 12 quarters, knowing WHOSE row you're
   looking at is exactly what used to scroll out of view first. Needs its
   own opaque background (not transparent, the default) so quarter
   columns don't show through underneath as they scroll past it. */
.chart-detail-sticky-col {
  position: sticky;
  left: 0;
  z-index: 2;
  background: var(--surface);
  min-width: 150px;
}
thead .chart-detail-sticky-col {
  z-index: 3;
}
/* Predictable per-quarter column width (2026-09-05) -- the wrapping div's
   own max-width (see holdings_chart_detail.html) is what actually caps
   the visible window to ~6 quarters, but that number only holds if each
   column has a KNOWN minimum width to size against; left to the browser's
   auto-sizing, wide modals (this one can be up to 1400px) gave columns
   enough natural room that all 12 quarters fit with nothing left to
   scroll, defeating the cap entirely. 150px sticky + 6*70px = 570px,
   comfortably inside the wrapper's 620px ceiling. */
.chart-detail-scroll th:not(.chart-detail-sticky-col),
.chart-detail-scroll td:not(.chart-detail-sticky-col) {
  min-width: 70px;
}
.ghost-btn svg { width: 13px; height: 13px; }

.risk-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  /* Muted/tinted border instead of a full-strength warning-colored line --
     the tinted background already reads as "warning" clearly; a
     solid-color border on top of that read as louder than the content
     warranted. */
  border: 1px solid color-mix(in srgb, var(--warning) 35%, var(--border));
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--warning) 8%, var(--surface));
  margin-bottom: var(--space-4);
  font-size: 0.82rem;
}

.risk-chip {
  padding: 2px var(--space-2);
  border-radius: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  font-weight: 600;
  font-size: 0.78rem;
}

.attrib-row {
  display: grid;
  grid-template-columns: 90px 1fr 100px;
  align-items: center;
  gap: var(--space-3);
  padding: 5px 0;
  font-size: 0.85rem;
}

.attrib-track {
  height: 8px;
  background: var(--active-bg);
  border-radius: 100px;
  position: relative;
  overflow: hidden;
}

.attrib-bar {
  position: absolute;
  top: 0;
  bottom: 0;
  border-radius: 100px;
}
.attrib-bar.up { background: var(--success); left: 50%; }
.attrib-bar.down { background: var(--danger); right: 50%; }

.weight-cell {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  white-space: nowrap;
}
.weight-cell .meter { width: 60px; }

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
  align-items: center;
}
.filter-bar .field-input {
  padding: var(--space-2) var(--space-3);
  font-size: 0.85rem;
  cursor: pointer;
}
.filter-bar .search-input { margin: 0; }

.pulse-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--space-4);
}

.pulse-seg {
  border-left: 1px solid var(--border);
  padding-left: var(--space-4);
}
.pulse-seg:first-child { border-left: none; padding-left: 0; }

/* Overview page: Market pulse as a sticky right sidebar, everything else
   (net worth + tiles, IPO watch, trend/attribution/broker/holdings) as the
   main column on the left -- same sticky-sidebar-at-900px pattern as
   .agents-layout, reused rather than reinvented. */
.overview-layout {
  display: flex;
  gap: var(--space-4);
  align-items: flex-start;
}

.overview-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.overview-side {
  flex: 0 0 300px;
  position: sticky;
  top: var(--space-4);
}

/* Pulse's own grid is 6 segments meant for a wide horizontal card -- inside
   the narrow 300px sidebar, 2 columns of divider-bordered segments reads
   as a stray line down the middle instead of a clean column split, so drop
   the border-as-divider convention here in favor of one card per segment,
   matching .stat-card elsewhere on this same page. Column COUNT is
   deliberately left to .pulse-grid's own existing auto-fit/minmax(120px)
   rule rather than a fixed repeat(2, 1fr) here -- a rigid 2 columns inside
   a 300px sidebar (minus panel padding, minus .pulse-seg's own padding)
   left barely 90-100px of text room per cell, and index values like
   "63,079.05" or FII's "-₹3,112Cr" are one unbreakable token wider than
   that -- CSS grid items default to min-width:auto (their content's
   min-content size), so that overflowed the track and, cascading up
   through .overview-side's fixed flex-basis, the whole layout. auto-fit
   naturally drops to 1 column instead of forcing an overflow; .pulse-value
   below (a smaller font + a hard min-width:0/overflow-wrap escape hatch)
   keeps 2 columns comfortable wherever they DO fit, and is the actual
   safety net if a future value is wider still. */
.overview-side .pulse-seg {
  border-left: none;
  padding-left: 0;
  background: var(--active-bg);
  border-radius: var(--radius);
  padding: var(--space-3);
  min-width: 0;
}
/* Same reasoning as .stat-card .table-meta above: .table-meta is nowrap by
   default, fine for table cells, not for a narrow card's own label/sub-line
   ("MTD +₹18.6KCr · 4d" under DII/FII is long enough to overflow a
   squeezed 2-column mobile row on its own, same failure mode .pulse-value
   already has overflow-wrap:anywhere for on the number above it). */
.overview-side .pulse-seg .table-meta {
  white-space: normal;
}
.pulse-value {
  font-size: 1.05rem;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* Plain viewport media query, deliberately generous (2026-09-05, third
   pass) -- attempt 1 used 900px and didn't account for .content's padding
   and the persistent 248px nav eating real width first, so it fired on
   ordinary desktop windows. Attempt 2 switched to a 640px viewport
   threshold computed by hand for that overhead, which left a real gap:
   nav width and .content's padding both change at their OWN separate
   768px breakpoint, so 640-768px still had a full 248px nav beside a
   rigid, non-shrinking 300px sidebar (.overview-side has flex-shrink:0
   via its flex:0 0 300px shorthand) with barely 70-90px left for
   .overview-main -- and on an actual phone, that meant .overview-side
   (sticky, unable to shrink) rendered wider than the space it had,
   overlapping .overview-main's squeezed content instead of sitting beside
   it. Attempt 3 tried a container query on .overview-layout itself to
   sidestep computing the overhead by hand entirely -- but that still
   overlapped on a real phone, and container-query behavior when the
   container is ALSO a flex parent of a position:sticky child isn't
   something to keep guessing at without a way to test it directly here.
   Back to a media query, but with NO attempt to compute the tightest
   correct number: 1000px is comfortably above every other breakpoint in
   this file (768px, 900px) and every real phone/tablet width there is --
   the tradeoff is side-by-side only kicks in on genuinely wide windows,
   in exchange for this being unambiguous rather than another guess. */
@media (max-width: 1000px) {
  .overview-layout {
    flex-direction: column;
  }
  /* .overview-layout's align-items:flex-start is for the DESKTOP row
     layout (top-aligning cards of different heights side by side). In
     this column-direction context it governs the CROSS axis instead,
     which is now horizontal -- flex-start there means each stacked item
     sizes to its own content's width instead of stretching to fill the
     column, unless told to explicitly. .overview-side got that explicit
     width below already (which is exactly why pulse rendered correctly);
     .overview-main never did, so its children -- the tables especially --
     were free to dictate their own preferred width upward past the
     viewport instead of being constrained to it, which is what was
     actually overflowing on mobile (pulse and the topbar were never the
     problem; .overview-main just never got the same fix pulse did). */
  .overview-main {
    width: 100%;
  }
  .overview-side {
    flex-basis: auto;
    width: 100%;
    position: static;
    /* DOM order puts .overview-main first specifically so desktop's
       row layout renders it on the left without needing `order` at all --
       but that same source order made pulse fall LAST once stacked into a
       column, i.e. below the portfolio content. Pulling it back to first
       visually here, on stacked layouts only, without touching the DOM
       order the desktop case depends on. */
    order: -1;
  }
  /* Match pulse's cards to the same compact sizing as the small stat-cards
     beside them once pulse is full-width instead of squeezed into a
     300px sidebar (2026-09-05) -- same value font-size, shorter padding,
     so the two card styles read as one consistent system on mobile
     instead of pulse looking like a leftover from its old,
     differently-proportioned sidebar treatment.
     min-width is deliberately LOWER than .stat-card's 130px, not the same
     number: stat-cards sit directly in .overview-main with no wrapping
     panel of their own, but every pulse-seg sits inside #overview-pulse's
     OWN .panel padding on top of that -- an extra ~32px "tax" .pulse-grid
     pays that .stat-grid never does. Matching the number outright still
     produced 2 columns for stat-cards but only 1 for pulse at the exact
     same phone width; 110px is the number that actually nets out to the
     same 2-per-row result once that extra padding is accounted for,
     while auto-fit still drops to 1 column instead of overflowing on
     anything narrower. */
  .overview-side .pulse-grid {
    grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  }
  .overview-side .pulse-seg {
    padding: var(--space-2) var(--space-3);
  }
  .pulse-value {
    font-size: 1.1rem;
  }
  .overview-side .pulse-seg .table-meta {
    font-size: 0.75rem;
  }
}

/* Compact the stat tiles in the main column (2026-09-05) -- with the
   sidebar now claiming ~300px, the same 160px-min .stat-card fit fewer per
   row than before and read as oversized for how little each one holds.
   Net worth itself was folded into this same grid the same day (2026-09-05)
   -- it used to be a separate oversized .row-hero/.hero-col panel that
   stretched to match the tiles column's height and left most of its own
   height empty; it's just the first of these compact cards now, no
   different from Total P&L or Liquid cash beside it.
   Scoped to .overview-main specifically: .stat-card is shared with
   ask_ai_chart.html and holdings.html, which keep the site-wide default. */
.overview-main .stat-card {
  min-width: 130px;
  padding: var(--space-3) var(--space-4);
}
.overview-main .stat-value {
  font-size: 1.3rem;
}

.row-split {
  display: flex;
  gap: var(--space-4);
  flex-wrap: wrap;
  align-items: stretch;
}
.row-split > .col-7 { flex: 7 1 420px; }
.row-split > .col-5 { flex: 5 1 320px; }
.row-split > * { margin-bottom: 0; }

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-2);
  padding: var(--space-6) var(--space-4);
  color: var(--text-muted);
}
.empty-state svg { width: 32px; height: 32px; color: var(--text-faint); }
.empty-state-title { font-weight: 600; color: var(--text); }

/* ---- Form fields (login, entry add/edit) ---- */

.field-label {
  display: block;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text-muted);
  margin: var(--space-3) 0 var(--space-1);
}

.field-input {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font-size: 0.9rem;
  font-family: var(--font-body);
}

.field-input:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 1px;
}

.field-input:disabled {
  color: var(--text-faint);
  background: var(--active-bg);
}

.field-mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85rem;
}

.checkbox-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  font-size: 0.9rem;
}

.schedule-toggle {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-1);
}

.schedule-option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  font-size: 0.88rem;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.schedule-option:has(input:checked) {
  border-color: var(--ink);
  background: var(--active-bg);
}

.schedule-fields {
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-2);
}

.script-editor {
  font-size: 0.82rem;
  line-height: 1.5;
  tab-size: 4;
}

.field-with-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.field-with-toggle .field-input {
  flex: 1;
  min-width: 0;
}

.field-with-toggle .icon-btn {
  flex-shrink: 0;
}

.json-textarea {
  width: 100%;
  resize: vertical;
  line-height: 1.5;
  min-height: 300px;
}

.secret-value {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85rem;
  color: var(--text-muted);
  letter-spacing: 0.05em;
}

.secret-value.revealed {
  color: var(--text);
  letter-spacing: normal;
}

.group-row {
  width: 100%;
}

.group-summary {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  cursor: pointer;
  list-style: none;
}

.group-summary::-webkit-details-marker {
  display: none;
}

.group-summary-name {
  font-weight: 600;
}

.group-summary-actions {
  margin-left: auto;
  display: flex;
  gap: var(--space-2);
  white-space: nowrap;
}

.group-children {
  padding: 0 var(--space-3) var(--space-3) var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  font-size: 0.85rem;
  color: var(--text-muted);
}

.group-child-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-1) 0;
  border-top: 1px solid var(--border);
}

.group-child-name {
  min-width: 160px;
}

/* ---- Standalone login page (no shell/sidebar) ---- */

.login-body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  font-family: var(--font-body);
  color: var(--text);
}

.login-card {
  width: 100%;
  max-width: 360px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-6);
}

.login-brand {
  padding: 0 0 var(--space-5);
}

.login-form {
  display: flex;
  flex-direction: column;
}

.login-submit {
  margin-top: var(--space-5);
  width: 100%;
  justify-content: center;
}

.login-error {
  background: color-mix(in srgb, #dc2626 12%, transparent);
  color: #dc2626;
  border-radius: 8px;
  padding: var(--space-2) var(--space-3);
  font-size: 0.85rem;
  margin: 0 0 var(--space-4);
}

.login-hint {
  margin: var(--space-5) 0 0;
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--text-faint);
}

.login-hint code {
  background: var(--active-bg);
  padding: 1px 5px;
  border-radius: 4px;
  font-size: 0.85em;
}

/* ---- Entry add/edit form ---- */

.entry-form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 var(--space-4);
}

.entry-form-extra-row {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.entry-form-actions {
  display: flex;
  gap: var(--space-3);
  margin-top: var(--space-5);
}

/* ---- Mobile layout (2026-07-28) ---- */
/* Sidebar collapses to an icon-only rail rather than an off-canvas drawer
   -- every nav item stays one tap away with no hamburger/overlay to learn,
   and the freed width goes straight to content. 768px catches phones and
   small tablets in portrait; nothing here changes above that width. */
@media (max-width: 768px) {
  .topbar {
    padding: 0 var(--space-3);
  }
  .page-title {
    font-size: 1rem;
  }
  .topbar-actions {
    gap: var(--space-2);
  }
  .session-countdown {
    display: none; /* the passkey session itself already governs access -- this just crowds a narrow row */
  }

  .sidebar {
    width: 56px;
    padding: var(--space-3) var(--space-1);
  }
  .brand {
    justify-content: center;
    padding: var(--space-2) 0 var(--space-4);
  }
  .brand-text,
  .nav-section-label,
  .nav-label,
  .sidebar-footer-text {
    display: none;
  }
  .nav-item {
    justify-content: center;
    padding: var(--space-3) 0;
    gap: 0;
  }
  .sidebar-footer {
    justify-content: center;
    padding-top: var(--space-3);
  }

  .content {
    padding: var(--space-4) var(--space-3);
  }
  .panel {
    padding: var(--space-4);
  }
  .panel-header {
    flex-wrap: wrap;
    row-gap: var(--space-2);
  }
  /* .table-meta is nowrap by default (right for table cells), but several
     panel headers (IPO GMP watch, Market pulse) put a whole status LINE in
     one -- "SME >=60% * Mainline >=35% GMP * updated 12:12" plus a Refresh
     button, as an inline-flex row. .panel-header wrapping the TITLE and
     this span onto separate lines (above) doesn't help if the span's OWN
     content still refuses to wrap -- it just overflows on its own line
     instead. flex-wrap so the button can drop under the text, white-space
     so the text itself can break, both scoped to mobile only. */
  .panel-header .table-meta {
    white-space: normal;
    flex-wrap: wrap;
  }

  /* notebook.html's fixed 260px sidebar (pmsvc-rendered, inline style --
     see the module's own comment on why an override lives here instead of
     there) would eat most of a phone's width sitting next to the note
     body; stacked full-width above it reads correctly instead. */
  .nb-layout {
    flex-direction: column !important;
  }
  .nb-sidebar {
    width: 100% !important;
  }
}

@media (max-width: 480px) {
  .ai-hero {
    padding: var(--space-5) var(--space-3) var(--space-4);
  }
  .ai-hero-headline {
    font-size: 1.5rem;
  }
  .ai-hero-subtitle {
    font-size: 0.92rem;
  }
  /* The textarea + mic + Ask button no longer fit one row at this width --
     squeezing them together made the placeholder text wrap and collide
     with the mic icon. Wrapping to a second row (textarea full-width on
     its own line, buttons on the next) fixes it cleanly instead. */
  .ai-composer {
    flex-wrap: wrap;
    justify-content: flex-end;
    padding: var(--space-3);
  }
  .ai-composer-input {
    flex-basis: 100%;
  }
  .stat-card {
    min-width: 140px;
  }
}

/* Agents org chart (tier-3 /agents page, redesigned 2026-08-03) -- a
   proper top-down connector-line tree (nested <ul><li>, the standard
   pure-CSS org-chart technique: each <li> draws its own line up to a
   shared horizontal "bus" via ::before/::after, each nested <ul> draws
   the drop-line down from its parent) instead of the earlier indented
   list, plus a two-column layout: chart on the left, a click-to-load
   detail panel on the right, so a node's full config doesn't have to
   live inline in the chart itself. The org-chart SHAPE came from a
   reference image; the COLORS stay this app's own monochrome palette --
   copying the reference's orange would break the no-bright-accent
   direction everywhere else here (see [[project-app-web-scaffold]]). */

.agents-layout {
  display: flex;
  gap: var(--space-5);
  align-items: flex-start;
}

.agents-chart-col {
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
}

.agents-panel-col {
  flex: 0 0 340px;
  position: sticky;
  top: var(--space-4);
}

@media (max-width: 900px) {
  .agents-layout {
    flex-direction: column;
  }
  .agents-panel-col {
    flex-basis: auto;
    width: 100%;
    position: static;
  }
}

.org-chart {
  padding: var(--space-3) 0;
}

.org-chart > ul {
  display: flex;
  justify-content: center;
  margin: 0;
  padding: 0;
}

.org-chart ul ul {
  display: flex;
  justify-content: center;
  margin: 0;
  padding: 20px 0 0 0;
  position: relative;
}

.org-chart li {
  list-style-type: none;
  position: relative;
  padding: 0 var(--space-3);
  text-align: center;
}

/* Horizontal run from each node up to the shared bus, plus the vertical
   tick connecting that bus down to the parent -- suppressed on an
   only-child (nothing to bus together) and clipped on the outermost
   siblings so the line doesn't run off past the first/last box. */
.org-chart ul ul li::before,
.org-chart ul ul li::after {
  content: '';
  position: absolute;
  top: 0;
  right: 50%;
  width: 50%;
  height: 20px;
  border-top: 2px solid var(--border);
}

.org-chart ul ul li::after {
  right: auto;
  left: 50%;
  border-left: 2px solid var(--border);
}

.org-chart ul ul li:only-child::before,
.org-chart ul ul li:only-child::after {
  display: none;
}

.org-chart ul ul li:first-child::before,
.org-chart ul ul li:last-child::after {
  border-color: transparent;
}

.org-chart ul ul li:last-child::before {
  border-right: 2px solid var(--border);
  border-radius: 0 6px 0 0;
}

.org-chart ul ul li:first-child::after {
  border-radius: 6px 0 0 0;
}

/* Drop-line from a parent box down into its own children's bus. */
.org-chart ul ul::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  width: 0;
  height: 20px;
  border-left: 2px solid var(--border);
}

.org-chart .org-node {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  cursor: pointer;
  min-width: 92px;
  transition: background-color 120ms ease, border-color 120ms ease;
}

.org-chart .org-node:hover {
  background: var(--active-bg);
}

.org-chart .org-node.kind-root {
  font-weight: 700;
  background: var(--active-bg);
}

.org-node-label {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-weight: 600;
  font-size: 0.85rem;
}

.org-node-sub {
  font-size: 0.65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.org-node.kind-llm .org-node-sub {
  color: var(--ink);
}

/* Right-hand detail panel, loaded via one HTMX GET per node click. */
.agent-panel-empty {
  color: var(--text-muted);
  font-size: 0.85rem;
}

.agent-panel-title {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 1rem;
  font-weight: 700;
  margin-bottom: var(--space-1);
}

.agent-kind-badge {
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--text-muted);
  display: inline-block;
  margin-bottom: var(--space-3);
}

.agent-kind-badge.kind-llm {
  color: var(--ink);
  background: var(--active-bg);
  border-color: var(--ink);
}

.agent-panel-desc {
  color: var(--text-muted);
  font-size: 0.85rem;
  margin-bottom: var(--space-4);
}

.agent-prompt-panel {
  margin: 0;
}

.agent-prompt-meta {
  font-size: 0.78rem;
  color: var(--text-muted);
  margin-bottom: var(--space-2);
}

/* ── Global htmx request progress bar (2026-08-25) ──────────────────────
   Fixed to the top of the viewport, above everything, and pointer-events:
   none so it can never intercept a click. Width is driven entirely by
   portal-watch.js; this only supplies appearance. Hidden (opacity 0) until
   .active so it takes no visual space at rest. */
#htmx-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  width: 0;
  z-index: 9999;
  /* Uses the palette's own tokens so it tracks light/dark rather than
     hardcoding a blue that only works in one theme. */
  background: linear-gradient(90deg, var(--ink), var(--success));
  box-shadow: 0 0 6px var(--success);
  opacity: 0;
  pointer-events: none;
  border-bottom-right-radius: 2px;
}

#htmx-progress.active {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  /* Still show state, just don't animate the creep. */
  #htmx-progress {
    transition: none !important;
  }
}

/* ── Ask AI prompt library (2026-08-31) ─────────────────────────────────
   Replaces the four hardcoded suggestion chips. A chip is a two-part pill:
   a muted category on the left, the prompt name as the button, and an
   optional live-data <select> welded into the same pill so a parameterised
   prompt reads as one control rather than three. */
.ai-prompt-bar {
  max-width: 760px;
  margin: var(--space-5) auto 0;
  text-align: center;
}

.ai-prompt-eyebrow {
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: var(--space-3);
  text-align: left;
}

.ai-prompt-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: center;
}

.ai-prompt-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 5px var(--space-3);
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  font-size: 0.82rem;
  transition: border-color 120ms ease, background-color 120ms ease;
}

.ai-prompt-chip:hover {
  border-color: var(--text-faint);
  background: var(--active-bg);
}

.ai-prompt-cat {
  color: var(--text-faint);
  font-size: 0.76rem;
  padding-right: var(--space-2);
  border-right: 1px solid var(--border);
}

.ai-prompt-go {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  font-weight: 600;
  color: var(--text);
  cursor: pointer;
}

/* Sits inside the pill: no border/background of its own, so the chip reads
   as a single control instead of a button next to a dropdown. */
.ai-prompt-param {
  border: 0;
  background: var(--active-bg);
  border-radius: 999px;
  padding: 2px var(--space-2);
  font: inherit;
  font-size: 0.78rem;
  color: var(--text);
  max-width: 190px;
  cursor: pointer;
}

.ai-prompt-save {
  color: var(--text-muted);
  cursor: pointer;
  border-style: dashed;
}

.ai-prompt-browse {
  display: inline-block;
  margin-top: var(--space-3);
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
  opacity: 0.75;
}

.ai-prompt-browse:hover { opacity: 1; }

.ai-prompt-library {
  margin-top: var(--space-4);
  text-align: left;
  border-top: 1px solid var(--border);
  padding-top: var(--space-4);
  max-height: 46vh;
  overflow-y: auto;
}

.ai-prompt-group { margin-bottom: var(--space-4); }

.ai-prompt-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--border);
}

.ai-prompt-row-label { white-space: nowrap; }

.ai-prompt-preview {
  font-size: 0.76rem;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ai-prompt-count { font-size: 0.7rem; white-space: nowrap; }

.ai-prompt-saveform {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
  flex-wrap: wrap;
}

.ai-prompt-saveform .field-input { flex: 1; min-width: 180px; }

@media (max-width: 640px) {
  .ai-prompt-preview, .ai-prompt-count { display: none; }
}

/* ── Ask AI: chat threads + activity trace (2026-09-01) ──────────────────
   Two columns: a left rail listing conversations, the existing chat page on
   the right. The rail is its own scroll container and sticks below the
   topbar, so a long thread list never pushes the composer off screen. */
.ai-layout {
  display: grid;
  grid-template-columns: 248px minmax(0, 1fr);
  gap: var(--space-5);
  align-items: start;
}
.ai-sidebar {
  position: sticky;
  top: calc(var(--topbar-height) + var(--space-3));
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-height: calc(100vh - var(--topbar-height) - var(--space-5));
  min-width: 0;
}
.ai-new-chat {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  text-decoration: none;
  flex-shrink: 0;
}
.ai-thread-search { flex-shrink: 0; }
.ai-thread-search .search-input { width: 100%; }

.ai-thread-list {
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-height: 0;
}
.ai-thread-empty {
  font-size: 0.78rem;
  padding: var(--space-3) var(--space-2);
  margin: 0;
}
.ai-thread {
  display: flex;
  align-items: center;
  border-radius: var(--radius);
  /* The delete button only appears on hover/focus, so the title gets the
     full width at rest -- these are long, model-derived sentences. */
  padding-right: 2px;
}
.ai-thread:hover { background: var(--active-bg); }
.ai-thread.active { background: var(--active-bg); }
.ai-thread.active .ai-thread-title { font-weight: 650; }

.ai-thread-link {
  flex: 1;
  min-width: 0;
  display: block;
  padding: var(--space-2) var(--space-3);
  text-decoration: none;
  color: inherit;
}
.ai-thread-title {
  display: block;
  font-size: 0.82rem;
  line-height: 1.35;
  /* One line, ellipsised: a wrapped 48-char title would make every row a
     different height and turn the rail into a ragged wall of text. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ai-thread-meta {
  display: block;
  font-size: 0.68rem;
  color: var(--text-faint);
  margin-top: 1px;
}
.ai-thread-del {
  opacity: 0;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-faint);
  padding: var(--space-2) var(--space-1);
  line-height: 0;
  border-radius: var(--radius);
  transition: opacity 0.12s ease, color 0.12s ease;
}
.ai-thread:hover .ai-thread-del,
.ai-thread-del:focus-visible { opacity: 1; }
.ai-thread-del:hover { color: var(--danger); }

/* Live trace of the model's real tool calls while a send is in flight. */
.ai-trace-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.ai-trace-elapsed {
  margin-left: auto;
  font-size: 0.7rem;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.ai-trace-steps {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.ai-trace-steps:not(:empty) {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--border);
}
.ai-trace-step {
  font-size: 0.72rem;
  color: var(--text-muted);
  display: flex;
  gap: 6px;
  min-width: 0;
}
.ai-trace-step-label { flex-shrink: 0; }
.ai-trace-step-detail {
  color: var(--text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (max-width: 900px) {
  .ai-layout { grid-template-columns: minmax(0, 1fr); }
  .ai-sidebar {
    position: static;
    max-height: 260px;
  }
}

/* ── Sold Watchlist (2026-09-03) ─────────────────────────────────────────
   A left edge-stripe rather than a filled row: the table already uses green
   and red on individual numbers, and tinting the whole row would fight
   those and make a "turning" row with a red vs-sell number unreadable. */
.data-table tr.row-turning td:first-child {
  box-shadow: inset 3px 0 0 var(--success);
}
.data-table tr.row-turning {
  background: color-mix(in srgb, var(--success) 5%, transparent);
}
