@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");

/* jup.ag's dark palette — monorepo libs/ui/global.css, documented in
   apps/jupiter-ui/docs/COLORS.md (token name in each comment). Shared by every
   tenant. The accent is the one BRAND colour, so it stays a neutral amber here
   and each tenant sets its own via Brand.css_vars (Jupiter: lime #C7F284). */
:root {
  --bg: #090d10;       /* background */
  --panel: #0d151e;    /* card */
  --panel-2: #151e28;  /* big-input */
  --border: #19242e;   /* border */
  --border-strong: #212a36; /* border-strong */
  --text: #e2e8f0;     /* foreground */
  --muted: #90a1b9;    /* muted-foreground */
  --accent: #f0a830;
  --accent-fg: #090d10; /* text on an accent fill */
  --warn: #f0a830;     /* warning / "partial" status. NOT the brand accent:
                          tenants override --accent, never this. */
  --green: #37b24d;
  --red: #f03e3e;
  --blue: #1c7ed6;
  --purple: #9775fa;
  --orange: #f76707;
  --gray: #67778e;     /* faint-foreground */
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.5 "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

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

/* ---- Top bar ---- */
.topbar {
  position: sticky; top: 0; z-index: 10;
  display: flex; align-items: center; justify-content: space-between;
  /* wrap: at narrow widths the brand + filter controls exceed the
     viewport (min-content ~780px on pages with the project + source
     filters) and would otherwise stretch the whole page sideways. */
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem; padding: 0.7rem 1.5rem;
  background: color-mix(in srgb, var(--bg) 85%, transparent); backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border);
}
.brand { display: flex; align-items: center; gap: 0.75rem; }
.brand .logo { font-size: 1.8rem; }
/* A logo image sizes off the same font-size the emoji mark uses. */
img.logo { width: 1em; height: 1em; display: block; }
.brand h1 { margin: 0; font-size: 1.1rem; letter-spacing: -0.01em; }
.brand .sub { margin: 0; font-size: 0.78rem; color: var(--muted); }

.controls { display: flex; align-items: flex-end; gap: 1rem; flex-wrap: wrap; }
.controls label { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
.controls select {
  background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
  border-radius: 7px; padding: 0.45rem 0.6rem; font-size: 0.9rem; min-width: 150px;
}
.htmx-indicator { opacity: 0; transition: opacity 0.2s; color: var(--accent); font-size: 0.8rem; align-self: center; }
.htmx-request .htmx-indicator, .htmx-indicator.htmx-request { opacity: 1; }

/* Sources dropdown (base.html): labelled like the selects beside it. */
.controls .src-field { display: flex; flex-direction: column; gap: 0.2rem; }
.controls .src-field-label { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
.src-menu { position: relative; }
.src-menu > summary {
  list-style: none; cursor: pointer; min-width: 120px;
  background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
  border-radius: 7px; padding: 0.45rem 0.6rem; font-size: 0.9rem; line-height: normal;
  display: flex; justify-content: space-between; gap: 0.75rem;
}
.src-menu > summary::-webkit-details-marker { display: none; }
.src-menu > summary::after { content: "\25BE"; color: var(--muted); }
.src-menu[open] > summary { border-color: var(--accent); }
.src-pop {
  position: absolute; z-index: 20; top: calc(100% + 4px); right: 0;
  display: grid; grid-template-columns: repeat(2, max-content); gap: 0.45rem 1.1rem;
  background: var(--panel); border: 1px solid var(--border-strong); border-radius: 10px;
  padding: 0.7rem 0.85rem; box-shadow: 0 8px 28px rgba(0,0,0,0.45);
}
.controls .src-chk {
  flex-direction: row !important; align-items: center; gap: 0.3rem !important;
  font-size: 0.82rem; color: var(--text); text-transform: none;
  letter-spacing: normal; cursor: pointer;
}
.controls .src-chk input { accent-color: var(--accent); cursor: pointer; }

/* ---- Layout ---- */
main { max-width: 1200px; margin: 0 auto; padding: 1.5rem; display: flex; flex-direction: column; gap: 1.25rem; }

.card {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: 12px; padding: 1.1rem 1.25rem;
}
/* `.card-head` is GONE from the templates — a card's header row is its
 * `<summary>` now, styled by the collapsible-card rules below. The layout and
 * heading rules that used to live here were dead the moment the last card was
 * converted, and leaving dead rules that look authoritative is how the next
 * author writes a card in the old shape. Only `.meta` survived; it is used far
 * beyond card headers. */
.meta { color: var(--muted); font-size: 0.78rem; }

/* Collapsible card (e.g. Campaigns): a <details> styled like a .card whose
   <summary> is the header row, with a ▸/▾ disclosure marker. */
.collapsible-card > summary {
  display: flex; align-items: baseline; gap: 0.5rem; cursor: pointer;
  list-style: none;
}
.collapsible-card > summary::-webkit-details-marker { display: none; }
.collapsible-card > summary::before {
  content: "▸"; color: var(--muted); font-size: 0.85rem; align-self: center;
}
.collapsible-card[open] > summary::before { content: "▾"; }
.collapsible-card[open] > summary { margin-bottom: 0.9rem; }
.collapsible-card > summary h2 { margin: 0; font-size: 1rem; font-weight: 600; }
.collapsible-card > summary .meta { margin-left: auto; text-align: right; }
.empty { color: var(--muted); font-style: italic; padding: 0.5rem 0; }

/* ---- Weekly summary markdown ----
 * The weekly-summary card used to carry its own nested <details> and its own
 * ▾/▴ marker (.summary-toggle). It is now a plain .collapsible-card like every
 * other panel, so only the markdown measure below is still specific to it —
 * the disclosure styling comes from the shared rules above. */
.summary .markdown { max-width: 78ch; margin-top: 0.6rem; }
.markdown h1 { font-size: 1.25rem; margin: 0.4rem 0 0.8rem; }
.markdown h2 { font-size: 1.05rem; margin: 1.2rem 0 0.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; }
.markdown h3 { font-size: 0.95rem; margin: 1rem 0 0.4rem; color: var(--accent); }
.markdown table { width: 100%; border-collapse: collapse; margin: 0.6rem 0; font-size: 0.85rem; }
.markdown th, .markdown td { border: 1px solid var(--border); padding: 0.35rem 0.55rem; text-align: left; }
.markdown code { background: var(--panel-2); padding: 0.1rem 0.35rem; border-radius: 4px; font-size: 0.85em; }
.markdown blockquote { border-left: 3px solid var(--accent); margin: 0.6rem 0; padding-left: 0.8rem; color: var(--muted); }

/* The base .markdown rules above only set headings/tables/code — paragraphs
 * and lists fell back to browser defaults (tight leading, muted-by-inheritance
 * text). These give the recommendations + summary prose readable leading,
 * spacing, and full-contrast body text per the "better text colors/weight"
 * feedback. Scoped to .markdown so it covers both the recommendations card
 * and the weekly summary. */
.markdown p { margin: 0.5rem 0; line-height: 1.6; color: var(--text); }
.markdown ul, .markdown ol { margin: 0.5rem 0; padding-left: 1.4rem; }
.markdown li { margin: 0.3rem 0; line-height: 1.55; color: var(--text); }
.markdown li::marker { color: var(--accent); }
.markdown strong { color: var(--text); font-weight: 600; }

/* The recommendations card is the most-read block on the page, so pin it
 * visually (accent left-border, like the vibe banner) and cap line length
 * for readability — the 78ch cap in `.summary .markdown` doesn't reach here. */
.card.recommendations { border-left: 3px solid var(--accent); }
.card.recommendations .markdown { max-width: 80ch; }

/* ---- Stat cards ---- */
.cards-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1rem; }
.card.stat { display: flex; flex-direction: column; gap: 0.25rem; padding: 0.9rem 1rem; border-left: 3px solid var(--gray); }
/* Make the kpi cards visibly clickable. */
a.card.stat.kpi-link { color: inherit; text-decoration: none; cursor: pointer; transition: transform 0.05s ease, background-color 0.1s ease; }
a.card.stat.kpi-link:hover { background-color: var(--panel-2); transform: translateY(-1px); }

/* Caveat note — small amber-tinted callout under a chart title that
   warns the reader the data has a known interpretation gotcha. */
.caveat { margin: 0.4rem 0 1rem; padding: 0.55rem 0.8rem;
          background: rgba(245,159,0,0.10); border-left: 3px solid #f59f00;
          border-radius: 5px; font-size: 0.82rem; color: var(--muted); line-height: 1.45; }
.caveat strong { color: #f59f00; }
.caveat code { background: rgba(245,159,0,0.18); padding: 0 0.3em; border-radius: 3px;
               font-size: 0.92em; color: var(--text); }
.caveat-marker { color: #f59f00; font-weight: 700; }

/* Vibe banner — single-line sentiment verdict at top of dashboard. */
.vibe-banner { padding: 0.6rem 1rem; border-radius: 7px; margin-bottom: 0.75rem;
               display: flex; gap: 0.8rem; align-items: baseline; font-size: 0.92rem; }
.vibe-banner .vibe-label { font-size: 1rem; }
.vibe-banner .vibe-detail { color: var(--muted); font-size: 0.82rem; }
.vibe-banner.tone-green { background: rgba(55,178,77,0.10); border-left: 3px solid #37b24d; }
.vibe-banner.tone-red   { background: rgba(240,62,62,0.10); border-left: 3px solid #f03e3e; }
.vibe-banner.tone-gray  { background: var(--panel-2); border-left: 3px solid #90a1b9; }
.vibe-banner.tone-muted { background: var(--panel-2); border-left: 3px solid #555; color: var(--muted); }

/* Project filter — give it a bit more visual weight so users find it. */
.controls label.primary-filter { font-size: 0.78rem; color: var(--accent); font-weight: 600; }
.controls label.primary-filter select { border-color: var(--accent); }

/* Sortable table headers (used by the /mentions drilldown). */
th.sortable { user-select: none; }
th.sortable:hover { color: var(--accent); }
th.sortable[data-sort-active="asc"]::after { content: " ↑"; color: var(--accent); }
th.sortable[data-sort-active="desc"]::after { content: " ↓"; color: var(--accent); }

/* Marketing account filter (tier-1 handles) — chip-styled checkboxes in the
   controls form, so they serialize with range on change (server-side filter). */
.account-filter {
  display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center;
  border: 1px solid var(--border); border-radius: 8px; padding: 0.35rem 0.5rem;
}
.account-filter legend { font-size: 0.7rem; color: var(--muted); padding: 0 0.3rem; }
.acct-chip { display: inline-flex; cursor: pointer; }
.acct-chip input { position: absolute; opacity: 0; width: 0; height: 0; }
.acct-chip span {
  font-size: 0.72rem; padding: 0.16rem 0.5rem; line-height: 1.4;
  border: 1px solid var(--border); border-radius: 999px; color: var(--muted);
}
.acct-chip:hover span { border-color: var(--accent); color: var(--text); }
.acct-chip input:checked + span {
  background: var(--accent); border-color: var(--accent); color: var(--accent-fg); font-weight: 600;
}
.acct-chip input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 1px; }
.acct-clear {
  font-size: 0.7rem; color: var(--muted); background: transparent;
  border: none; cursor: pointer; text-decoration: underline;
}

/* Routing panel badges — "→ docs / Nicholas" / "→ comms team" inline tag */
.route-badge { display: inline-block; margin-left: 0.5rem; padding: 0.1rem 0.5rem;
               background: var(--panel-2); border: 1px solid var(--border); border-radius: 999px;
               color: var(--muted); font-size: 0.7rem; font-weight: normal; text-transform: uppercase; letter-spacing: 0.05em; }
.stat-label { font-size: 0.75rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.stat-value { font-size: 1.8rem; font-weight: 700; letter-spacing: -0.02em; }
.stat-delta { font-size: 0.75rem; color: var(--muted); }
.tone-blue { border-left-color: var(--blue); }
.tone-purple { border-left-color: var(--purple); }
.tone-red { border-left-color: var(--red); }
.tone-green { border-left-color: var(--green); }
.tone-gray { border-left-color: var(--gray); }
/* Cross-source strip: intake big, kept/filtered under it. Auto-fill so eight
 * tiles wrap 4+4 on a laptop instead of the 5+3 a fixed .cards-row gives, and
 * the strip still steps down to one column on a phone. */
.cards-row.cross-source-row { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
.stat-kept { font-size: 0.82rem; color: var(--text); }
.cross-source-section > summary { flex-wrap: wrap; }
.cross-source-headline { font-size: 0.9rem; color: var(--muted); }
.cross-source-headline strong { color: var(--text); }

/* ---- Chart ---- */
.chart-wrap { height: 280px; }
.sentiment-sources { display: flex; flex-wrap: wrap; gap: 0.35rem 0.9rem; margin: 0.25rem 0 0.6rem; }
.sentiment-sources .src-chk { display: flex; align-items: center; gap: 0.3rem; font-size: 0.82rem; color: var(--text); cursor: pointer; }
.sentiment-sources .src-chk input { accent-color: var(--accent); cursor: pointer; }

/* ---- Tables ---- */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
/* One column, but still a grid, so a group of cards keeps `main`'s own gap
 * when it is nested inside a wrapper instead of being a direct child of it.
 * Used by the feature-requests + complaints pair, which was a `.grid-2` until
 * the description column got too narrow to read a sentence in. */
.stack { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
/* Stack two-column grids sooner — at 901-1100px the two panels share row
 * width but the fixed-width chip/category/score/count columns leave
 * almost nothing for the description. Better to drop to single column
 * earlier so each panel gets the full content width. */
@media (max-width: 1100px) { .grid-2, .cards-row { grid-template-columns: 1fr; } }
/* overflow-wrap on every td lets long unbreakable tokens (e.g. hex-encoded
 * spam blobs) wrap inside their column instead of inflating it. By itself
 * this only helps in tables whose columns are width-constrained — but
 * since most tables here have width-constrained columns either via the
 * `.card > table { table-layout: fixed }` rule below or via per-class
 * max-widths on .tweet / .bullets, this is enough. We deliberately do
 * NOT make `table-layout: fixed` the global default because markdown
 * tables inside the weekly summary block (.markdown table) and drilldown
 * sub-tables (.detail-table) look better with auto-sized columns. */
table { width: 100%; border-collapse: collapse; font-size: 0.86rem; }
thead th { text-align: left; color: var(--muted); font-weight: 600; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; padding: 0.4rem 0.5rem; border-bottom: 1px solid var(--border); }
tbody td { padding: 0.5rem 0.5rem; border-bottom: 1px solid var(--border); vertical-align: top; overflow-wrap: anywhere; }
tbody tr:hover { background: var(--panel-2); }
.num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
/* The tweet/question text is the row's whole point. Full-contrast text +
 * line-height apply everywhere a tweet cell appears (a readability win in the
 * drilldowns too; it was previously muted gray). The width hint is scoped to
 * the main panel tables — `.card > table` is table-layout:fixed, so width:52%
 * is honored directly and min-width keeps the column from collapsing on narrow
 * viewports. We deliberately don't widen the auto-layout .detail-table
 * drilldowns, which carry their own .detail-table .tweet sizing. */
td.tweet { color: var(--text); line-height: 1.5; }

/* Main panel tables size columns to content (auto layout). Number / link /
 * author columns shrink to what they need (see the width:1% rule below) and
 * the text column (tweet / question / description) absorbs the rest — instead
 * of `table-layout: fixed` giving every column an equal slice (which left the
 * actual text cramped while single-number columns hogged width). Long
 * unbreakable tokens still wrap via `overflow-wrap: anywhere` (set on every
 * td above), so auto layout can't push the table past its container. */
.card > table { table-layout: auto; }

/* The width:1% trick: under auto layout this collapses a column to its
 * minimum content width, donating the slack to the wrap-able text column.
 * Applied to numeric columns (always single short numbers). Author / link
 * columns are single unbroken tokens ("@handle", "open ↗") so auto layout
 * already sizes them tight; the text column wraps and absorbs the rest. */
.card > table th.num, .card > table td.num { width: 1%; }
/* Floor so the text column stays substantial even in many-column tables. */
.card > table td.tweet, .card > table td.desc { min-width: 22rem; }

/* The FR / complaints tables carry a hand-tuned colgroup, so keep them on
 * fixed layout (higher specificity than the auto rule above) — the colgroup
 * widths are exact and the description column absorbs the remainder. */
.card > table.fr-table, .card > table.cmp-table { table-layout: fixed; }
.card > table.fr-table td.desc, .card > table.cmp-table td.desc { min-width: 0; }

/* Auto layout + the global `overflow-wrap: anywhere` together let short
 * columns collapse to a single character (a ticket-id wrapping vertically as
 * "5/1/3/5/7", an issue slug breaking mid-word). Restrict mid-token breaking
 * to the primary long-text columns — everything else breaks only at normal
 * opportunities (spaces, hyphens), keeping ids/slugs at their compact natural
 * width while the text column still wraps long URLs. */
.card > table td { overflow-wrap: normal; }
.card > table td.tweet, .card > table td.desc { overflow-wrap: anywhere; }

/* Column widths for the Top feature requests / Top complaints tables (these
 * live in the 2-col grid, so space is tight). The non-description columns are
 * pinned narrow via the colgroup so the description — which has no explicit
 * width — absorbs the bulk of the row. Trimmed cats/sources from 9/6.5rem so
 * the description gets more room (it was the most-cramped column). */
col.col-expand  { width: 2.9rem; }   /* ▸ on one line, ✓ ✕ side by side under it */
col.col-cats    { width: 7rem; }
col.col-sources { width: 5rem; }   /* pills wrap to a second line if needed */
/* Inside the full-width `.stack` pair only. Going full width gave every extra
 * pixel to the description column, because these two are fixed — so a row
 * carrying nine categories still wrapped into a nine-line-tall cell and set
 * the row height for a one-line complaint. Wider here, still narrower than
 * the description, and left alone on the half-width tables (Exec, Docs) where
 * this much would starve the text. */
.stack col.col-cats    { width: 11rem; }
.stack col.col-sources { width: 6.5rem; }
col.col-score   { width: 2.6rem; }
col.col-count   { width: 2.8rem; }

/* Description cell gets a bit more vertical breathing room than the
 * trailing chip/badge cells, since it's the only cell that can wrap to
 * multiple lines on long entries. */
.fr-table td.desc, .cmp-table td.desc { padding: 0.55rem 0.75rem 0.55rem 0.25rem; line-height: 1.35; }

/* Tighten trailing chip/badge cells so Sources / Score / Count read as a
 * group rather than spread across half the table. */
.fr-table tbody td:not(.desc):not(.expand-col),
.cmp-table tbody td:not(.desc):not(.expand-col) { padding: 0.5rem 0.25rem; }

/* Match the head row to the trimmer body cells so column alignment doesn't
 * drift. */
.fr-table thead th:not(:nth-child(2)),
.cmp-table thead th:not(:nth-child(2)) { padding: 0.4rem 0.25rem; }
a { color: var(--blue); text-decoration: none; }
a:hover { text-decoration: underline; }

.tags { color: var(--muted); font-size: 0.8rem; }
.pill { background: var(--panel-2); border: 1px solid var(--border); border-radius: 999px; padding: 0.1rem 0.55rem; font-size: 0.72rem; color: var(--muted); }

/* Source provenance badges in feedback tables. Each source gets its own
 * accent so PMs can see at a glance whether a complaint surfaced on X,
 * the chatbot, or both. The bold count chip sits inside the pill. */
.src-pill {
  display: inline-flex; align-items: center; gap: 0.25rem;
  font-size: 0.7rem; padding: 0.1rem 0.45rem; border-radius: 6px;
  margin-right: 0.25rem;
  background: var(--panel-2); border: 1px solid var(--border);
  text-transform: lowercase; letter-spacing: 0.02em;
}
.src-pill b { font-weight: 600; font-variant-numeric: tabular-nums; }
.src-pill.src-twitter { color: #6dabff; border-color: rgba(109,171,255,0.35); }
.src-pill.src-chatbot { color: #ffa94d; border-color: rgba(255,169,77,0.35); }
.src-pill.src-reddit  { color: #ff7a59; border-color: rgba(255,122,89,0.35); }
.src-pill.src-discord { color: #b18cff; border-color: rgba(177,140,255,0.35); }
.src-pill.src-telegram{ color: #4dc3ff; border-color: rgba(77,195,255,0.35); }
.src-pill.src-zendesk { color: #ffd166; border-color: rgba(255,209,102,0.35); }
.src-pill.src-widget  { color: #69db7c; border-color: rgba(105,219,124,0.35); }
.src-pill.src-uninstall { color: #ff8787; border-color: rgba(255,135,135,0.35); }
.delta.down { color: var(--red); font-weight: 600; }

/* --- Page navigation between / and /discord --- */
.page-nav { display: flex; align-items: center; gap: 1rem; margin: 0 1rem; }
.page-nav a {
  color: var(--muted);
  text-decoration: none;
  font-size: 0.85rem;
  padding: 0.35rem 0.7rem;
  border: 1px solid var(--border, #19242e);
  border-radius: 6px;
  transition: color 0.15s, border-color 0.15s;
}
.page-nav a:hover { color: #cad5e2; border-color: #212a36; }

/* --- /discord page: daily bug-report grouping --- */
.bug-day { margin: 1.25rem 0 0.5rem; }
.bug-day h3 {
  margin: 0 0 0.4rem;
  font-size: 0.95rem;
  font-weight: 600;
  color: #cad5e2;
}
.delta.up { color: var(--green); font-weight: 600; }
.delta.flat { color: var(--muted); }

/* score chip: red (low) → green (high) */
.score { padding: 0.1rem 0.45rem; border-radius: 5px; font-weight: 600; font-size: 0.8rem; }
.score-1,.score-2,.score-3 { background: rgba(240,62,62,0.18); color: #ff8787; }
.score-4,.score-5,.score-6 { background: rgba(247,103,7,0.18); color: #ffa94d; }
.score-7,.score-8,.score-9,.score-10 { background: rgba(55,178,77,0.18); color: #69db7c; }

/* Drilldown — ▸ expand button on top feature_requests / complaints rows.
 * Clicking fetches /detail and inserts a sibling <tr class="detail-row">
 * below the original row. ✕ in the detail row removes itself. */
/* width is owned by `col.col-expand` (2.9rem) under table-layout:fixed;
 * keeping only padding + alignment here. */
.expand-col { padding: 0; text-align: center; }
/* Three actions in a 2.9rem column. ▸ takes its own line and the two row marks
 * share the next one, so adding ✕ did NOT make every row a line taller — which
 * on a 40-row list is most of a screen. `.row-marks` is nowrap so they never
 * split across two lines and undo that. */
.expand-col > .expand-btn { display: block; margin: 0 auto; }
.row-marks { display: block; white-space: nowrap; }
.row-marks > * + * { margin-left: 0.1rem; }
.expand-btn {
  background: transparent; border: 1px solid var(--border);
  color: var(--muted); border-radius: 4px;
  padding: 0 0.35rem; font-size: 0.75rem; line-height: 1.4;
  cursor: pointer;
}
.expand-btn:hover { color: var(--fg); border-color: var(--fg); background: var(--panel-2); }
.expand-btn.htmx-request { color: var(--accent); border-color: var(--accent); }
.expand-btn.htmx-request::after { content: " …"; }

.detail-row > td {
  background: var(--panel-2); border-top: 0; padding: 0.5rem 0.75rem 1rem;
}
.detail-head {
  display: flex; align-items: center; gap: 0.75rem;
  margin-bottom: 0.5rem;
}
.detail-title { display: flex; align-items: center; gap: 0.5rem; flex: 1; }
.detail-desc { color: var(--muted); font-size: 0.85rem; }
.detail-counts { display: inline-flex; gap: 0.25rem; }
.detail-close {
  background: transparent; border: 1px solid var(--border);
  color: var(--muted); border-radius: 4px;
  padding: 0.1rem 0.45rem; font-size: 0.75rem; cursor: pointer;
}
.detail-close:hover { color: var(--fg); border-color: var(--fg); }
.detail-section { margin-top: 0.5rem; }
.detail-section h4 {
  font-size: 0.75rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--muted); margin: 0.5rem 0 0.25rem;
}
.detail-table { font-size: 0.85rem; width: 100%; }
.detail-table thead th {
  font-size: 0.7rem; font-weight: 500; color: var(--muted);
  padding: 0.25rem 0.4rem;
}
.detail-table tbody td { padding: 0.35rem 0.4rem; vertical-align: top; }
.detail-table .tweet { white-space: pre-wrap; max-width: 36rem; }
.detail-table .bullets {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.75rem; color: var(--muted);
  max-height: 8rem; overflow-y: auto;
}
.muted { color: var(--muted); }

/* Rotate ▸ to ▾ when the row is expanded. */
.expand-btn.open { color: var(--accent); border-color: var(--accent); }
.expand-btn.open::before { content: "▾"; }
.expand-btn.open { font-size: 0; }
.expand-btn.open::before { font-size: 0.75rem; }

/* ===================================================================
   Sidebar navigation (shared shell across /, /discord, /zendesk)
   =================================================================== */
.app-shell { display: flex; align-items: stretch; min-height: 100vh; }
/* min-width:0 is load-bearing: without it the flex child refuses to shrink
   below its content width and the fixed-layout tables overflow the viewport. */
.app-main { flex: 1 1 auto; min-width: 0; }

.sidebar {
  position: sticky; top: 0; align-self: flex-start;
  height: 100vh; width: 200px; flex: 0 0 200px;
  background: var(--panel); border-right: 1px solid var(--border);
  display: flex; flex-direction: column; gap: 0.4rem; padding: 1rem 0.75rem;
}
.sidebar-brand {
  display: flex; align-items: center; gap: 0.5rem;
  text-decoration: none; color: var(--text); font-weight: 600;
  padding: 0.3rem 0.6rem 0.7rem; border-bottom: 1px solid var(--border);
}
.sidebar-brand .logo { font-size: 1.5rem; }
.sidebar-brand-text { font-size: 0.95rem; line-height: 1.15; }
.sidebar-nav { display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.5rem; }
.sidebar-nav a {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.55rem 0.7rem; border-radius: 8px;
  color: var(--muted); text-decoration: none; font-size: 0.9rem;
  transition: background-color 0.12s, color 0.12s;
}
.sidebar-nav a:hover { background: var(--panel-2); color: var(--text); }
.sidebar-nav a.active { background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--accent); font-weight: 600; }
.sidebar-nav a .ico { font-size: 1.05rem; }
.sidebar-foot { margin-top: auto; padding: 0.5rem 0.7rem; color: var(--muted); font-size: 0.72rem; }

/* On narrow screens the sidebar becomes a horizontal scroll strip on top. */
@media (max-width: 900px) {
  /* The bar wraps here, so the Sources summary can sit at the left edge;
     right-anchored, its popover would open off-screen. */
  .src-pop { left: 0; right: auto; }
  .app-shell { flex-direction: column; }
  /* Same call as the strip below: a wrapped 2-3-row header pinned to the
     top would cover ~38% of a phone viewport — let it scroll away.
     relative (not static) so the absolutely-anchored spinner stays put. */
  .topbar { position: relative; }
  /* Idle spinner is opacity:0 but still occupies layout — wrapped, it
     reserved a near-empty row inside the header. Anchor it instead. */
  .controls .htmx-indicator { position: absolute; top: 0.65rem; right: 1rem; }
  .sidebar {
    position: static; height: auto; width: auto; flex: none;
    flex-direction: row; gap: 0.5rem;
    border-right: none; border-bottom: 1px solid var(--border); overflow-x: auto;
    /* The desktop ruleset's align-self: flex-start used to leak into this
       row mode, so the strip shrink-to-fit and got floored at its
       min-content width (~945px with 7 nav items), stretching every page
       wider than the viewport. align-self: stretch makes it track the
       column shell's width; max-width: 100% is belt-and-suspenders.
       (min-width:0 would NOT fix this — that mechanism is main-axis
       only, unlike .app-main's case.) */
    align-self: stretch; align-items: center; max-width: 100%;
  }
  .sidebar-brand { border-bottom: none; padding: 0.3rem 0.5rem; white-space: nowrap; }
  .sidebar-nav { flex-direction: row; margin-top: 0; }
  .sidebar-nav a { white-space: nowrap; }
  .sidebar-foot, .sidebar-divider { display: none; }
}

/* ===================================================================
   Sentiment gauge (Fear & Greed style dial)
   =================================================================== */
.gauge-wrap { display: flex; align-items: center; gap: 1.75rem; flex-wrap: wrap; }
.gauge-chart { position: relative; width: 260px; max-width: 100%; }
.gauge-chart svg { width: 100%; height: auto; display: block; }
.gauge-readout {
  margin-top: -0.35rem;
  display: flex; flex-direction: column; align-items: center;
  text-align: center; pointer-events: none;
}
.gauge-score { font-size: 2.1rem; font-weight: 700; line-height: 1; letter-spacing: -0.02em; }
.gauge-vlabel { font-size: 0.8rem; color: var(--muted); text-transform: capitalize; margin-top: 0.1rem; }
.gauge-compare { list-style: none; margin: 0; padding: 0; min-width: 170px;
  display: flex; flex-direction: column; gap: 0.4rem; }
.gauge-compare li { display: flex; justify-content: space-between; gap: 1rem;
  border-bottom: 1px solid var(--border); padding-bottom: 0.35rem; font-size: 0.85rem; }
.gauge-compare .gc-k { color: var(--muted); }
.gauge-compare .gc-v { font-weight: 600; font-variant-numeric: tabular-nums; }
.gauge-detail { margin: 0.85rem 0 0; color: var(--muted); font-size: 0.82rem; }
/* "What's behind it" driver line — green for praise, red for complaints. */
.gauge-why { margin: 0.4rem 0 0; font-size: 0.85rem; color: var(--text); line-height: 1.5; }
.gauge-why strong { color: var(--text); }
.gauge-why .up { color: var(--green); }
.gauge-why .down { color: var(--red); }
.gauge-why .why-sep { color: var(--muted); }
/* Text-color tone helpers (the existing .tone-* set only colors card borders). */
.gauge-score.tone-green, .gc-v.tone-green { color: var(--green); }
.gauge-score.tone-red,   .gc-v.tone-red   { color: var(--red); }
.gauge-score.tone-gray,  .gc-v.tone-gray  { color: var(--muted); }
.gauge-score.tone-muted, .gc-v.tone-muted { color: var(--muted); }

/* ===================================================================
   "What's new since last visit" catchup banner + per-row new markers
   =================================================================== */
.catchup-banner {
  margin-bottom: 0.9rem; padding: 0.6rem 1rem; border-radius: 8px;
  background: rgba(55,178,77,0.10); border-left: 3px solid var(--green);
  font-size: 0.9rem; display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
}
.catchup-banner.first-visit { background: var(--panel-2); border-left-color: var(--muted); color: var(--muted); }
.catchup-btn {
  background: var(--green); color: #07210f; border: none; border-radius: 6px;
  padding: 0.3rem 0.75rem; font-size: 0.8rem; font-weight: 600; cursor: pointer;
}
.catchup-btn:hover { filter: brightness(1.08); }
tr.is-new { background: rgba(55,178,77,0.07); }
tr.is-new td:first-child { border-left: 2px solid var(--green); }
.new-badge {
  display: inline-block; margin-right: 0.45rem; padding: 0.05rem 0.4rem;
  background: var(--green); color: #07210f; border-radius: 999px;
  font-size: 0.62rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; vertical-align: middle;
}

/* ===================================================================
   App Store reviews — expandable raw review + device metadata (/zendesk)
   =================================================================== */
.review-list { display: flex; flex-direction: column; }
.review { border-bottom: 1px solid var(--border); }
.review:last-child { border-bottom: none; }
.review > summary {
  cursor: pointer; list-style: none; padding: 0.55rem 0.25rem;
  display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap;
}
.review > summary::-webkit-details-marker { display: none; }
.review > summary::before { content: "▸"; color: var(--muted); font-size: 0.8rem; }
.review[open] > summary::before { content: "▾"; }
.review > summary:hover { background: var(--panel-2); }
.review-stars { color: var(--warn); letter-spacing: 1px; white-space: nowrap; font-size: 0.85rem; }
.review-title { font-weight: 600; }
.review-meta { color: var(--muted); font-size: 0.8rem; margin-left: auto; white-space: nowrap; }
.review-body { padding: 0.2rem 0.5rem 0.9rem 1.4rem; }
.review-text { margin: 0.2rem 0 0.8rem; line-height: 1.55; white-space: pre-wrap; max-width: 80ch; }
.device-info {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 0.5rem 1.25rem; margin: 0; padding: 0.6rem 0.8rem;
  background: var(--panel-2); border-radius: 8px;
}
.device-info div { display: flex; flex-direction: column; gap: 0.1rem; }
.device-info dt { font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
.device-info dd { margin: 0; font-size: 0.85rem; font-variant-numeric: tabular-nums; }
.device-none { font-size: 0.82rem; margin: 0.2rem 0 0; }

/* Sentiment pill color variants (base .pill defined above). */
.pill.sent-positive { background: rgba(55,178,77,0.15);  color: var(--green);  border-color: transparent; }
.pill.sent-negative { background: rgba(240,62,62,0.15);  color: var(--red);    border-color: transparent; }
.pill.sent-neutral  { background: var(--panel-2);        color: var(--muted); }
.pill.sent-mixed    { background: rgba(247,103,7,0.15);  color: var(--orange); border-color: transparent; }

/* ---------------------------------------------------------------------------
   Read/seen (client-side, localStorage) — ✓/↩ toggle, resurface badge,
   collapsed Read section. Logic lives in base.html applyReadState().
   --------------------------------------------------------------------------- */
/* The row marks (✓ read, ✕ dismiss) — laid out by `.row-marks` above, side
   by side on the line under the ▸. See the note at `.expand-col`. */
.read-btn {
  background: transparent; border: 1px solid var(--border);
  color: var(--muted); border-radius: 4px;
  padding: 0 0.3rem; font-size: 0.7rem; line-height: 1.4;
  cursor: pointer; margin-top: 0.2rem;
}
.read-btn:hover { color: #69db7c; border-color: #69db7c; background: var(--panel-2); }

/* "↻ +N since read" — read earlier, but more people reported it since. */
.resurfaced-badge {
  display: inline-block; margin-left: 0.4rem;
  padding: 0.05rem 0.4rem; border-radius: 9px;
  font-size: 0.72rem; font-weight: 600; white-space: nowrap;
  background: rgba(255,169,77,0.15); color: #ffa94d;
  border: 1px solid rgba(255,169,77,0.35);
}

/* Collapsed "Read (N)" list under each table. */
.read-section { margin-top: 0.5rem; }
.read-section > summary {
  cursor: pointer; color: var(--muted); font-size: 0.8rem;
  padding: 0.25rem 0;
}
.read-section > summary:hover { color: var(--fg); }
/* ---------------------------------------------------------------------------
   Dismiss — "not worth acting on", with a reason, recorded for the team.
   Sibling of read/seen above; the visual language is deliberately heavier
   (a red-ish accent, a persisted reason chip) because a dismissal is a
   judgement someone re-reads weeks later, not a per-browser tidy-up.
   --------------------------------------------------------------------------- */
.dismiss-btn {
  background: transparent; border: 1px solid var(--border);
  color: var(--muted); border-radius: 4px;
  padding: 0 0.3rem; font-size: 0.7rem; line-height: 1.4;
  cursor: pointer; margin-top: 0.2rem;
}
.dismiss-btn:hover { color: #ff8787; border-color: #ff8787; background: var(--panel-2); }

/* The stored reason, rendered on the row inside the Dismissed list. */
.dismiss-badge {
  display: inline-block; margin-left: 0.4rem;
  padding: 0.05rem 0.4rem; border-radius: 9px;
  font-size: 0.72rem; font-weight: 600;
  background: rgba(255,135,135,0.12); color: #ff8787;
  border: 1px solid rgba(255,135,135,0.32);
}

.dismissed-section { margin-top: 0.35rem; }
.dismissed-section > summary {
  cursor: pointer; color: var(--muted); font-size: 0.8rem;
  padding: 0.25rem 0;
}
.dismissed-section > summary:hover { color: var(--fg); }
.dismissed-hint { color: var(--muted); font-size: 0.75rem; margin: 0.1rem 0 0.4rem; }
tr.dismissed-row td { color: var(--muted); }
tr.dismissed-row .desc { text-decoration: line-through; text-decoration-color: rgba(255,135,135,0.35); }

/* The reason tray. position:fixed and positioned from JS against the ✕ that
   opened it, so it is never clipped by the card's own overflow-x scroller
   (`section.card, details.card { overflow-x: auto }`) — an absolutely
   positioned tray inside a card would be. */
.dismiss-tray {
  position: fixed; z-index: 60; width: 22rem; max-width: calc(100vw - 1rem);
  background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
  box-shadow: 0 8px 28px rgba(0,0,0,0.45); padding: 0.6rem 0.7rem;
}
.dismiss-tray-head { font-size: 0.8rem; font-weight: 600; margin-bottom: 0.45rem; }
.dismiss-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.dismiss-chip {
  background: var(--panel-2); border: 1px solid var(--border); color: var(--fg);
  border-radius: 999px; padding: 0.15rem 0.55rem; font-size: 0.72rem; cursor: pointer;
}
.dismiss-chip:hover { border-color: #ff8787; color: #ff8787; }
.dismiss-chip.armed { border-color: #ffa94d; color: #ffa94d; background: rgba(255,169,77,0.12); }
.dismiss-note {
  width: 100%; margin-top: 0.45rem; box-sizing: border-box;
  background: var(--panel-2); border: 1px solid var(--border); color: var(--fg);
  border-radius: 6px; padding: 0.3rem 0.4rem; font-size: 0.75rem; font-family: inherit;
  resize: vertical;
}
.dismiss-tray-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.5rem; margin-top: 0.35rem;
}
.dismiss-hint { color: #ffa94d; font-size: 0.7rem; }
.dismiss-cancel {
  background: transparent; border: 1px solid var(--border); color: var(--muted);
  border-radius: 4px; padding: 0.1rem 0.5rem; font-size: 0.72rem; cursor: pointer;
}
.dismiss-cancel:hover { color: var(--fg); border-color: var(--fg); }

/* /dismissed — the review pile. */
.dismiss-reason-chip {
  display: inline-block; padding: 0.05rem 0.45rem; border-radius: 9px;
  font-size: 0.72rem; font-weight: 600; white-space: nowrap;
  background: rgba(255,135,135,0.12); color: #ff8787;
  border: 1px solid rgba(255,135,135,0.32);
}
.dismiss-reason-chip.workflow {
  background: var(--panel-2); color: var(--muted); border-color: var(--border);
}
.dismiss-note-cell { color: var(--fg); font-size: 0.8rem; }
.dismiss-scope, .dismiss-age { color: var(--muted); font-size: 0.75rem; white-space: nowrap; }
.dismissed-foot { color: var(--muted); font-size: 0.78rem; margin: 0.75rem 0 0; max-width: 60rem; }
.dismissed-truncated { color: #ffa94d; font-size: 0.78rem; margin: 0.75rem 0 0; }
.dismiss-undo {
  background: transparent; border: 1px solid var(--border); color: var(--muted);
  border-radius: 4px; padding: 0.05rem 0.4rem; font-size: 0.72rem;
  cursor: pointer; white-space: nowrap;
}
.dismiss-undo:hover { color: var(--fg); border-color: var(--fg); background: var(--panel-2); }

tr.read-row td { color: var(--muted); }
tr.read-row .desc { text-decoration: line-through; text-decoration-color: rgba(255,255,255,0.25); }
.score-none { background: transparent; color: var(--muted); border: 1px dashed var(--border); }

/* ---------------------------------------------------------------------------
   Role tabs (PM / Exec)
   --------------------------------------------------------------------------- */
.sidebar-divider {
  margin: 0.75rem 0.75rem 0.25rem; padding-top: 0.5rem;
  border-top: 1px solid var(--border);
  color: var(--muted); font-size: 0.65rem; letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* Owning-project chip on exec pain points — links into /pm?project=… */
a.proj-chip { text-decoration: none; cursor: pointer; }
a.proj-chip:hover { color: var(--fg); border-color: var(--fg); }

/* "▲ rising" — pain point trending above its 4-week average. */
.rising-badge {
  display: inline-block; margin-left: 0.4rem;
  padding: 0.05rem 0.4rem; border-radius: 9px;
  font-size: 0.72rem; font-weight: 600; white-space: nowrap;
  background: rgba(240,62,62,0.12); color: #ff8787;
  border: 1px solid rgba(240,62,62,0.3);
}

/* Collapsed [ops] items under the exec action-items card. */
.ops-recs { margin-top: 0.5rem; }
.ops-recs > summary {
  cursor: pointer; color: var(--muted); font-size: 0.8rem; padding: 0.25rem 0;
}
.ops-recs > summary:hover { color: var(--fg); }

/* Docs triage — Gap / Done / Skip status buttons. */
/* Wrappable: nowrap here gave the column a ~13rem minimum width that
 * auto table layout must honor — the /docs grid-2 tables overflowed the
 * page horizontally. Compact labels + wrap keep min-content ~3.5rem. */
.triage-ctl { display: inline-flex; gap: 2px; flex-wrap: wrap; }
.triage-btn {
  background: transparent; border: 1px solid var(--border);
  color: var(--muted); padding: 0.1rem 0.45rem; font-size: 0.72rem;
  cursor: pointer;
}
/* Full border + radius per button (not a shared-edge segmented strip):
 * the control wraps to 2-3 rows in narrow columns, and shared edges left
 * wrapped buttons with missing borders / wrong corner radii. */
.triage-btn { border-radius: 4px; }
.triage-btn:hover { color: var(--fg); background: var(--panel-2); }
.triage-btn.active[data-status="gap"] { color: #ffa94d; border-color: rgba(255,169,77,0.6); background: rgba(255,169,77,0.12); }
.triage-btn.active[data-status="in_progress"] { color: #74c0fc; border-color: rgba(77,171,247,0.6); background: rgba(77,171,247,0.12); }
.triage-btn.active[data-status="done"] { color: #69db7c; border-color: rgba(55,178,77,0.6); background: rgba(55,178,77,0.12); }
.triage-btn.active[data-status="wontfix"] { color: var(--muted); border-color: var(--fg); background: var(--panel-2); text-decoration: line-through; }
.gap-badge {
  display: inline-block; margin-right: 0.4rem;
  padding: 0.05rem 0.4rem; border-radius: 9px;
  font-size: 0.72rem; font-weight: 600; white-space: nowrap;
  background: rgba(255,169,77,0.15); color: #ffa94d;
  border: 1px solid rgba(255,169,77,0.35);
}
tr.gap-row { background: rgba(255,169,77,0.05); }
.gap-badge.progress-badge {
  background: rgba(77,171,247,0.15); color: #74c0fc;
  border-color: rgba(77,171,247,0.35);
}
tr.progress-row { background: rgba(77,171,247,0.05); }
.carried-chip { font-style: italic; }

/* Marketing tab */
.campaign-card { min-width: 16rem; text-align: left; }
.campaign-card .stat-sub { font-size: 0.8rem; color: var(--muted); font-weight: 400; }
.campaign-top { margin-top: 0.4rem; }
.campaign-top > summary { cursor: pointer; color: var(--muted); font-size: 0.75rem; }
.campaign-top ul { margin: 0.3rem 0 0; padding-left: 1rem; font-size: 0.8rem; }
.campaign-top li { margin-bottom: 0.25rem; }
tr.qt-yes td:first-child { border-left: 2px solid #69db7c; }
.draft-qt { color: #b18cff; font-style: italic; }
/* Campaign cards: own auto-fit row — .cards-row's fixed 5-track grid
 * overflows with min-width cards on mid-size viewports. */
.campaign-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 0.75rem;
}
.campaign-needles { display: block; margin-top: 0.3rem; font-size: 0.72rem; }

/* Containment guard: a table whose min-content width exceeds its card
 * scrolls WITHIN the card instead of stretching the whole page sideways
 * (the /docs report: "you can scroll too much to the right").
 * ⚠️ Both elements, and NOT a bare `.card`: a collapsible card is a <details>
 * while a KPI tile is an <a class="card stat">, and giving a tile its own
 * scroll container is not what this guard is for. Every selector naming
 * `section.card` in this file and in the templates' JS has the same reason to
 * name `details.card` beside it. */
section.card, details.card { overflow-x: auto; }
/* Docs grid-2 tables: fixed layout so they honor width:100% — auto layout
 * kept their min-content (~680px) wider than a half-width card. The
 * Question cells (td.tweet) wrap via overflow-wrap: anywhere; the other
 * cells hold short tokens and break at spaces. min-width: 0 mirrors the
 * fr/cmp fixed-layout tables' override of the 22rem td.tweet floor. */
.card > table.docs-table { table-layout: fixed; }
.card > table.docs-table td.tweet { min-width: 0; }

/* "N×" — paraphrase variants collapsed into one canonical row (021). */
.variant-chip {
  display: inline-block; margin-left: 0.35rem;
  padding: 0 0.35rem; border-radius: 8px;
  font-size: 0.7rem; font-weight: 600;
  color: var(--purple); border: 1px solid rgba(151,117,250,0.4);
}

/* AppStore digest + iOS/Android comparison (/zendesk) */
.trend-meter { display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; align-items: baseline;
               margin: 0.2rem 0 0.7rem; font-size: 0.85rem; }
.trend { font-weight: 600; }
.trend.up { color: #69db7c; }
.trend.down { color: #ff8787; }
.digest-bullets { margin: 0.3rem 0 0; padding-left: 1.2rem; }
.digest-bullets li { margin: 0.35rem 0; line-height: 1.5; }
.digest-bullets li::marker { color: var(--accent); }
.platform-row { margin: 0.5rem 0; }
.platform-label { display: block; font-size: 0.85rem; margin-bottom: 0.25rem; }
.sentiment-bar { display: flex; height: 14px; border-radius: 7px; overflow: hidden;
                 background: var(--panel-2); }
.sentiment-bar .seg { display: block; height: 100%; }
.seg-pos { background: #37b24d; }
.seg-neu { background: #1c7ed6; }
.seg-mix { background: #f76707; }
.seg-neg { background: #f03e3e; }
.seg-chip { display: inline-block; width: 10px; height: 10px; border-radius: 2px;
            vertical-align: baseline; }

/* Docs-coverage suggestion chips (docs_coverage, weekly auto-check). */
.cov-chip {
  display: inline-block; margin-left: 0.35rem;
  padding: 0 0.4rem; border-radius: 8px;
  font-size: 0.72rem; font-weight: 600; text-decoration: none;
  white-space: nowrap;
}
a.cov-chip:hover { filter: brightness(1.2); }
.cov-covered { color: #69db7c; border: 1px solid rgba(55,178,77,0.4); }
.cov-partial { color: #ffa94d; border: 1px solid rgba(255,169,77,0.4); }
.cov-gap { color: #ff8787; border: 1px solid rgba(240,62,62,0.35); }

/* ---- /docs Analytics sub-tab (migration 056) ------------------------------
   Scoped additions only (dv- prefix). Data colors: cyan = human, lime = AI —
   the two Jupiter brand data hues; statuses reuse the app palette
   (--red / --warn / --green) so the tab stays coherent with the rest. */
:root { --dv-cyan: #00BEF0; --dv-lime: #C7F284; }

.docs-subtabs { display: flex; align-items: center; gap: 0.4rem;
  border-bottom: 1px solid var(--border); margin-bottom: 1.1rem; }
.docs-subtab { background: none; border: none; border-bottom: 2px solid transparent;
  color: var(--muted); font: inherit; font-size: 0.95rem; font-weight: 600;
  padding: 0.55rem 0.9rem; cursor: pointer; }
.docs-subtab:hover { color: var(--text); }
.docs-subtab.active { color: var(--dv-lime); border-bottom-color: var(--dv-lime); }
.docs-subtab-meta { margin-left: auto; color: var(--muted); font-size: 0.75rem; }
.docs-view > * + * { margin-top: 1.25rem; }
.docs-queue-banner { background: rgba(199,242,132,0.07);
  border: 1px solid rgba(199,242,132,0.25); border-radius: 8px;
  padding: 0.5rem 0.9rem; font-size: 0.8rem; color: var(--muted); }

.docs-period-bar { display: flex; align-items: center; gap: 0.8rem; }
.docs-period-label { display: flex; align-items: center; gap: 0.5rem;
  font-size: 0.78rem; color: var(--muted); text-transform: uppercase;
  letter-spacing: 0.06em; }
.docs-period-bar select { background: var(--panel-2); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px; padding: 0.35rem 0.6rem; }
.docs-export-btn { margin-left: auto; font-size: 0.78rem; color: var(--muted);
  border: 1px solid var(--border); border-radius: 8px; padding: 0.35rem 0.8rem;
  text-decoration: none; }
.docs-export-btn:hover { color: var(--text); background: var(--panel-2); }
.docs-stale-strip { background: color-mix(in srgb, var(--warn) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 35%, transparent); border-radius: 8px;
  padding: 0.7rem 1rem; font-size: 0.85rem; }

.docs-kpis { display: grid; grid-template-columns: repeat(6, 1fr); gap: 0.9rem; }
@media (max-width: 1250px) { .docs-kpis { grid-template-columns: repeat(3, 1fr); } }
.dv-kpi { display: flex; flex-direction: column; gap: 0.15rem;
  border-left: 3px solid var(--border); }
.dv-kpi .stat-value { font-variant-numeric: tabular-nums; }
.dv-kpi-conv { background: rgba(151,117,250,0.05); }
.dv-unit { font-size: 0.68rem; color: var(--muted); }
.dv-unit-caption { font-size: 0.75rem; color: var(--muted); margin: 0.4rem 0 0; }
.dv-violet { color: var(--purple); }
.dv-dot { display: inline-block; width: 8px; height: 8px; border-radius: 2px;
  margin-right: 2px; vertical-align: baseline; }
.dv-dot-cyan { background: var(--dv-cyan); }
.dv-dot-lime { background: var(--dv-lime); }
.dv-dot-violet { background: var(--purple); }
.dv-delta.dv-up { color: var(--green); }
.dv-delta.dv-down { color: var(--red); }
.dv-delta.dv-flat { color: var(--muted); }
.dv-vs { color: var(--muted); font-size: 0.72rem; }
.dv-chart { position: relative; }
.dv-footnote { font-size: 0.72rem; color: var(--muted); margin: 0.6rem 0 0; }
.dv-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.78rem; }
.dv-cyan { color: var(--dv-cyan); } .dv-lime { color: var(--dv-lime); }

.dv-ai-rows { display: flex; flex-direction: column; }
.dv-ai-row { border-bottom: 1px solid var(--border); }
.dv-ai-row:last-child { border-bottom: none; }
.dv-ai-row summary { display: flex; align-items: baseline; gap: 0.6rem;
  padding: 0.6rem 0; cursor: pointer; list-style: none; }
.dv-ai-row summary::before { content: '▸'; color: var(--muted); font-size: 0.7rem; }
.dv-ai-row[open] summary::before { content: '▾'; }
.dv-ai-name { font-weight: 600; }
.dv-ai-secondary .dv-ai-name { font-weight: 400; color: var(--muted); }
/* wraps rather than ellipsizes: "built-in chat on docs.jup.ag — sepa…" was
   cutting exactly the part that explains the row */
.dv-ai-desc { color: var(--muted); font-size: 0.72rem; flex: 1; min-width: 0;
  line-height: 1.35; }
.dv-ai-val { font-variant-numeric: tabular-nums; font-weight: 600; }
/* The "of which" row: our own crawls are a CEILING on the row above it — and
   since the 2026-09-17 experiment that row is External LLMs & agents, not
   the crawler row this shipped under. Never a peer to be added, and not a
   SUBSET either: the ceiling counts requests nobody appears to count
   (migration 073). Indented and hairline-marked so the nesting is visible
   before the copy is read — the copy says it too, because a reader
   comparing two numbers may never open either drill-down. */
.dv-ai-subrow > summary { padding-left: 0.9rem;
  border-left: 2px solid var(--border); }
.dv-ai-subrow .dv-ai-detail { padding-left: 2rem; }
.dv-ai-detail { padding: 0 0 0.7rem 1.1rem; }
.dv-ai-detail-head { font-size: 0.7rem; color: var(--muted);
  letter-spacing: 0.04em; padding-bottom: 0.25rem; }
.dv-ai-detail-row { display: flex; justify-content: space-between; gap: 0.8rem;
  font-size: 0.8rem; padding: 0.15rem 0; }
.dv-pill-violet { border-color: rgba(151,117,250,0.5); color: var(--purple); }
.dv-pill-rose { border-color: rgba(240,62,62,0.5); color: var(--red); }
.dv-pill-lime { border-color: rgba(199,242,132,0.5); color: var(--dv-lime); }

/* minmax(0,1fr), not bare 1fr: a bare 1fr floors at the column's
   min-content, so one unbreakable token in a question (a URL, a tx hash)
   widened the table column past the card, crushed the layout and stretched
   the chart. Seen live on Resolved month. */
.dv-matrix { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1.25rem; }
.dv-matrix-table td.desc { overflow-wrap: anywhere; }
@media (max-width: 1100px) { .dv-matrix { grid-template-columns: 1fr; } }
.dv-toggle { display: flex; gap: 0.25rem; margin-bottom: 0.5rem; }
.dv-toggle button { background: none; border: 1px solid var(--border);
  border-radius: 6px; color: var(--muted); font-size: 0.75rem;
  padding: 0.25rem 0.7rem; cursor: pointer; }
.dv-toggle button.active { color: var(--text); background: var(--panel-2); }
.dv-bar-row { display: flex; align-items: center; gap: 0.6rem;
  font-size: 0.78rem; padding: 0.2rem 0; }
.dv-bar-label { width: 200px; flex: none; color: var(--text); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.dv-bar-track { flex: 1; height: 9px; border-radius: 3px;
  background: var(--panel-2); overflow: hidden; }
.dv-bar-fill { display: block; height: 100%; border-radius: 3px; }
.dv-fill-gap { background: var(--red); }
.dv-fill-partial { background: var(--warn); }
.dv-fill-covered { background: var(--green); }
.dv-bar-val { width: 56px; flex: none; text-align: right;
  font-variant-numeric: tabular-nums; }
.dv-task-btn { background: none; border: 1px solid rgba(199,242,132,0.5);
  border-radius: 6px; color: var(--dv-lime); font-size: 0.72rem;
  padding: 0.15rem 0.55rem; cursor: pointer; white-space: nowrap; }
.dv-task-btn.dv-task-added { border-color: var(--border); color: var(--muted); }
.dv-task-tag { background: rgba(199,242,132,0.12); border-radius: 999px;
  color: var(--dv-lime); font-size: 0.65rem; padding: 0.05rem 0.5rem;
  margin-right: 0.4rem; }
tr.dv-task-row > td { background: rgba(199,242,132,0.04); }

.dv-health-strip { display: grid; grid-template-columns: repeat(4, 1fr);
  border-top: 1px solid var(--border); }
.dv-health-cell { background: none; border: none; border-right: 1px solid var(--border);
  text-align: left; padding: 0.8rem 1rem; cursor: pointer; font: inherit;
  color: var(--text); display: flex; flex-direction: column; gap: 0.1rem; }
.dv-health-cell:last-child { border-right: none; }
.dv-health-cell:hover, .dv-health-cell.active { background: var(--panel-2); }
.dv-h-num { font-size: 1.4rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.dv-h-red { color: var(--red); } .dv-h-amber { color: var(--warn); }
.dv-h-gray { color: var(--muted); }
.dv-h-name { font-size: 0.8rem; }
.dv-h-sub { font-size: 0.7rem; color: var(--muted); }
.dv-health-pill.dv-h-good { border-color: rgba(55,178,77,0.5); color: var(--green); }
.dv-health-pill.dv-h-warn { border-color: color-mix(in srgb, var(--warn) 50%, transparent); color: var(--warn); }
.dv-health-pill.dv-h-bad { border-color: rgba(240,62,62,0.5); color: var(--red); }
.dv-health-drill { border-top: 1px solid var(--border); padding: 0.5rem 0.2rem; }
.dv-drill-row { display: grid; grid-template-columns: minmax(0,1fr) 110px minmax(0,1fr);
  gap: 0.8rem; padding: 0.3rem 0; border-bottom: 1px solid var(--panel-2);
  font-size: 0.8rem; align-items: baseline; }
.dv-drill-row:last-child { border-bottom: none; }

tr.dv-zero-ctr > td { background: rgba(240,62,62,0.05); }
.dv-themes { display: flex; flex-direction: column; }
.dv-theme { border-bottom: 1px solid var(--border); }
.dv-theme:last-child { border-bottom: none; }
.dv-theme summary { display: flex; align-items: baseline; gap: 0.6rem;
  padding: 0.55rem 0; cursor: pointer; list-style: none; }
.dv-theme summary::before { content: '▸'; color: var(--muted); font-size: 0.7rem; }
.dv-theme[open] summary::before { content: '▾'; }
.dv-theme-name { font-weight: 600; }
.dv-theme-hint { color: var(--muted); font-size: 0.75rem; flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.dv-pages-filter { width: 100%; box-sizing: border-box; background: var(--panel-2);
  border: 1px solid var(--border); border-radius: 8px; color: var(--text);
  padding: 0.45rem 0.7rem; margin-bottom: 0.6rem; font: inherit; font-size: 0.85rem; }
.dv-pages-scroll { max-height: 440px; overflow-y: auto; }
.dv-pages th.js-pt-sort { cursor: pointer; white-space: nowrap; }
.dv-pages th.js-pt-sort:hover, .dv-pages th.dv-sorted { color: var(--text); }
.dv-caret { color: var(--muted); font-size: 0.7rem; display: inline-block; width: 0.9rem; }
tr.dv-pt-sec > td { background: var(--panel-2); font-weight: 600;
  text-transform: uppercase; font-size: 0.78rem; letter-spacing: 0.04em;
  cursor: pointer; }
tr.dv-pt-prod > td { font-weight: 600; cursor: pointer; }
tr.dv-pt-prod > td:first-child { padding-left: 1.4rem; }
tr.dv-pt-page > td:first-child { padding-left: 2.8rem; }
tr.dv-pt-page > td { color: var(--muted); }
.dv-sort { font-size: 0.65rem; }

/* ---------------------------------------------------------------------------
   Uninstall survey (/overview, project = wallet_ext). Migration 061.

   Server-rendered ranked bars, deliberately NOT a Chart.js pie:
   * The measure is a RANKING across 7 buckets. Seven slices can't be compared
     by eye; seven bars can. (The source Google Form's own pie needs six pages
     of legend, because every free-text "Other" answer becomes its own sliver —
     bucketing is what fixes that, not a different chart library.)
   * No canvas means no htmx re-render dance and nothing to tear down when the
     project filter moves off wallet_ext and the panel disappears — the class of
     bug base.html's scheduleChartRender()/destroy() branches exist to handle.
   * It renders identically in webview/build_static.py, which has no browser.

   Two colors, not seven: --warn marks a reason the product can act on, the
   muted one marks churn with no grievance ("no longer use this wallet", the
   single largest answer). That is a status distinction, not series identity —
   so there is no legend, and every bar carries its own count + share, so the
   meaning never rests on color alone.
   --------------------------------------------------------------------------- */
.un-row { display: flex; align-items: center; gap: 0.6rem;
  font-size: 0.8rem; padding: 0.22rem 0; }
.un-label { width: 190px; flex: none; color: var(--text); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.un-track { flex: 1; height: 10px; border-radius: 3px;
  background: var(--panel-2); overflow: hidden; }
.un-fill { display: block; height: 100%; border-radius: 3px;
  background: var(--warn); }
/* Churn with no product grievance — present, ranked, and visibly not a
   defect. Applied to `no_longer_use` ONLY: 'other' is free text, and most
   of it IS a grievance, just one the form had no button for. */
.un-fill-neutral { background: var(--muted); }
.un-val { width: 92px; flex: none; text-align: right; color: var(--muted);
  font-variant-numeric: tabular-nums; }
.un-val b { color: var(--text); font-weight: 600; }
.un-flag { background: rgba(240,62,62,0.15); border: 1px solid transparent;
  border-radius: 999px; color: var(--red); font-size: 0.72rem;
  font-weight: 600; padding: 0.1rem 0.55rem; }
.un-quote { border-left: 2px solid var(--border); color: var(--muted);
  font-size: 0.78rem; margin: 0.35rem 0; padding: 0.1rem 0 0.1rem 0.6rem; }
.un-quote b { color: var(--text); font-weight: 500; }

/* --- docs analytics polish (design audit, PR #256) ------------------------
   Icons: inline SVGs in headings/sub-tabs inherit currentColor; aligned
   without touching the h2 layout other tabs share. */
.h2-icon { vertical-align: -3px; margin-right: 2px; opacity: .9; }
.docs-subtab .h2-icon { vertical-align: -3.5px; }

/* Native white scrollbars were the brightest thing on the page. */
section.card, details.card, .dv-pages-scroll {
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
section.card::-webkit-scrollbar, details.card::-webkit-scrollbar,
.dv-pages-scroll::-webkit-scrollbar {
  width: 8px; height: 8px; }
section.card::-webkit-scrollbar-thumb, details.card::-webkit-scrollbar-thumb,
.dv-pages-scroll::-webkit-scrollbar-thumb {
  background: var(--border-strong); border-radius: 4px; }
section.card::-webkit-scrollbar-track, details.card::-webkit-scrollbar-track,
.dv-pages-scroll::-webkit-scrollbar-track {
  background: transparent; }

/* Long titles ("Where human visitors arrive from") stay on one line; the
   head wraps the subtitle under instead of breaking the title. Scoped to
   the docs tab — other tabs share the header row.
   ⚠️ Re-scoped to `> summary` when the cards became collapsible: the header is
   no longer a `.card-head` div, so these two rules had stopped matching
   anything at all and those titles were being squeezed onto one row with
   their subtitle. `.collapsible-card > summary` is a flex with no wrap. */
#docs-view-analytics .collapsible-card > summary { flex-wrap: wrap; }
#docs-view-analytics .collapsible-card > summary h2 { white-space: nowrap; }

/* Digits column-align in the drill-down rows. */
.dv-ai-detail-row .num, .dv-ref-key b, .docs-gaps-count {
  font-variant-numeric: tabular-nums; }

/* Expandable rows advertise themselves on hover. */
.dv-ai-row summary { cursor: pointer; transition: background-color .2s; }
.dv-ai-row summary:hover { background: rgba(21, 30, 40, .6); }
tr.dv-pt-sec:hover > td, tr.dv-pt-prod:hover > td {
  background: rgba(21, 30, 40, .6); }

/* Keyboard focus gets a neutral ring — lime stays a data color. */
.dv-task-btn:focus-visible, .docs-export-btn:focus-visible,
.docs-subtab:focus-visible, .dv-toggle button:focus-visible,
.js-goto-resolved-month:focus-visible, .dv-ai-row summary:focus-visible {
  outline: 1px solid #67778E; outline-offset: 2px; }

/* KPI tiles are anchors to their detail section (PR: jump-to-detail). */
a.dv-kpi { color: inherit; text-decoration: none; cursor: pointer;
  transition: border-color .2s, transform .1s; }
a.dv-kpi:hover { border-color: #212a36; }
a.dv-kpi:focus-visible { outline: 1px solid #67778E; outline-offset: 2px; }
/* Anchor jumps must clear the sticky .topbar (~64px) or the section heading
   lands hidden behind it; a little breathing room on top of that.
   Both elements: six docs-analytics KPI tiles jump to a card id, and every one
   of those cards is a <details> now. */
section.card[id], details.card[id] { scroll-margin-top: 84px; }
/* Resolved week/month ◀ ▶ stepper — snug pair; the shared .docs-export-btn
   margin-left:auto would scatter them across the bar. */
.dv-period-step { display: inline-flex; gap: 0.3rem; }
.dv-period-step .docs-export-btn { margin-left: 0; padding: 0.35rem 0.55rem;
  cursor: pointer; background: none; }
.dv-period-step .docs-export-btn:hover:not(:disabled) { color: var(--text);
  background: var(--panel-2); }
.dv-period-step .docs-export-btn:disabled { opacity: 0.35; cursor: default; }
