:root {
  --bg: #f2f4fb;
  --card-bg: #ffffff;
  --border: #e3e7f3;
  --text: #1f2437;
  --text-muted: #6b7280;
  --accent: #5b5bf6;
  --accent-2: #8b5cf6;
  --ok: #16a34a;
  --fail: #e0384d;
  --warn: #d97706;
  --running: #2f6feb;
  --shadow: 0 1px 3px rgba(31, 36, 55, 0.06), 0 1px 2px rgba(31, 36, 55, 0.08);
  --shadow-hover: 0 6px 16px rgba(31, 36, 55, 0.10);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.9rem 1.75rem;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  box-shadow: var(--shadow);
}

.topbar .brand {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 800;
  font-size: 1.2rem;
  color: #fff;
  text-decoration: none;
  letter-spacing: 0.02em;
}
.topbar .brand-logo {
  display: block;
  border-radius: 6px;
}

.topbar nav a {
  margin-left: 1.4rem;
  color: rgba(255, 255, 255, 0.85);
  text-decoration: none;
  font-size: 0.95rem;
  font-weight: 500;
  padding-bottom: 2px;
  border-bottom: 2px solid transparent;
}

.topbar nav a:hover,
.topbar nav a.active {
  color: #fff;
  border-bottom-color: #fff;
}

/* H3 ("заглушки TikTok/Instagram"): видимий, але явно неактивний пункт меню --
   той самий "opacity, не приховати зовсім" принцип, що вже .card.locked
   застосовує до заблокованих карток вище у файлі. */
.topbar nav a.nav-soon {
  opacity: 0.55;
  cursor: default;
}
.topbar nav a.nav-soon:hover {
  color: rgba(255, 255, 255, 0.85);
  border-bottom-color: transparent;
}

/* Задача (7 вересня, "channel under autopilot management" -- перегрупування
   навігації): Content/Growth -- <details>/<summary> дропдауни у тому самому
   topbar nav, поруч зі звичайними <a> лінками (Dashboard/Review Queue/
   Pricing/About лишаються прямими посиланнями). summary виглядає як
   решта пунктів меню (та сама margin-left/color/font, що .topbar nav a) --
   різниця лише в маленькій ▾-стрілці й розкривній панелі нижче. */
.topbar nav details.nav-group {
  display: inline-block;
  position: relative;
  vertical-align: middle;
}

.topbar nav details.nav-group summary {
  margin-left: 1.4rem;
  color: rgba(255, 255, 255, 0.85);
  font-size: 0.95rem;
  font-weight: 500;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.topbar nav details.nav-group summary::-webkit-details-marker { display: none; }
.topbar nav details.nav-group summary::after { content: " \25BE"; font-size: 0.75em; }

.topbar nav details.nav-group summary:hover {
  color: #fff;
}

.topbar nav details.nav-group[open] summary {
  color: #fff;
}

.topbar nav .nav-dropdown {
  position: absolute;
  top: calc(100% + 0.6rem);
  left: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  min-width: 170px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow-hover);
  padding: 0.4rem;
}

.topbar nav .nav-dropdown a {
  margin-left: 0;
  color: var(--text);
  font-size: 0.9rem;
  font-weight: 500;
  padding: 0.5rem 0.6rem;
  border-radius: 6px;
  border-bottom: none;
}

.topbar nav .nav-dropdown a:hover {
  background: var(--bg);
  border-bottom: none;
}

.content {
  max-width: 1040px;
  margin: 0 auto;
  padding: 2rem 1.5rem;
}

h1 { margin-top: 0; font-size: 1.7rem; }

.hint { color: var(--text-muted); font-size: 0.85rem; }

.empty-state {
  background: var(--card-bg);
  border: 1px dashed var(--border);
  border-radius: 10px;
  padding: 1.5rem;
  text-align: center;
  color: var(--text-muted);
}

/* docs/08_CHANNEL_BRAIN_DESIGN.md §7 -- /analytics' "Channel Brain" section */
.channel-brain-divider { border: none; border-top: 1px solid var(--border); margin: 2rem 0 1.5rem; }
.channel-brain-locked {
  background: var(--card-bg);
  border: 1px dashed var(--border);
  border-radius: 10px;
  padding: 1.5rem;
  text-align: center;
}
.channel-brain-locked .btn { display: inline-block; margin-top: 0.75rem; text-decoration: none; }
.channel-brain-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.25rem;
}
.channel-brain-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1rem 1.25rem;
}
.channel-brain-card h3 { margin-top: 0; font-size: 1rem; }
/* Fаза 2 (8 вересня) -- rule-based recommendations card, same card shell
   as the three chart cards above, colored by rec.kind the same way the
   subtopic bar chart already colors above/below-average bars. */
.channel-brain-recommendations { margin: 0; padding-left: 1.1rem; }
.channel-brain-recommendation { margin-bottom: 0.6rem; line-height: 1.4; }
.channel-brain-recommendation:last-child { margin-bottom: 0; }
.channel-brain-recommendation-top_subtopic { color: #16a34a; }
.channel-brain-recommendation-weak_subtopic { color: #dc2626; }
.channel-brain-recommendation-duration { color: var(--text); }

/* Publish-status patch (Aug 2026): videos stuck Private past their
   scheduled publishAt, surfaced instead of silently sitting there --
   see backend/services/publish_health.py. */
.publish-health-banner {
  background: #fdf1f1;
  border: 1px solid var(--fail);
  border-radius: 10px;
  padding: 1rem 1.25rem;
  margin-bottom: 1.25rem;
}

.publish-health-banner h3 {
  margin: 0 0 0.5rem 0;
  font-size: 1rem;
  color: var(--fail);
}

.publish-health-list {
  margin: 0;
  padding-left: 1.25rem;
  font-size: 0.9rem;
}

.publish-health-list li {
  margin-bottom: 0.3rem;
}

.btn {
  display: inline-block;
  padding: 0.55rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--card-bg);
  color: var(--text);
  cursor: pointer;
  font-size: 0.95rem;
  font-weight: 500;
  text-decoration: none;
  transition: transform 0.1s ease, box-shadow 0.15s ease;
}

.btn:hover { border-color: var(--accent); box-shadow: var(--shadow-hover); transform: translateY(-1px); }
.btn:disabled { opacity: 0.5; cursor: default; transform: none; box-shadow: none; }

.btn-primary { background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: #fff; border-color: transparent; }
.btn-primary:hover { filter: brightness(1.08); }
.btn-link { color: var(--accent); border-color: var(--accent); }
.btn-danger { color: var(--fail); border-color: var(--fail); }
.btn-danger:hover { background: var(--fail); color: #fff; }

/* ---- Stats bar (Dashboard) ---- */

.stats-bar {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 1rem;
  margin-bottom: 1.75rem;
}

.stat-tile {
  background: var(--card-bg);
  border-radius: 12px;
  padding: 1.25rem 1.5rem;
  box-shadow: var(--shadow);
  border-top: 4px solid var(--accent);
}

.stat-tile.stat-review { border-top-color: var(--warn); }
.stat-tile.stat-running { border-top-color: var(--running); }
.stat-tile.stat-quota { border-top-color: var(--ok); }

.stat-number {
  font-size: 2.6rem;
  font-weight: 800;
  line-height: 1;
  color: var(--accent);
}

/* Gradient fill is opt-in via this modifier, applied only to the one tile
   that wants it — NOT applied-then-undone per tile, since resetting
   -webkit-background-clip/-webkit-text-fill-color reliably across browsers
   is fragile (undoing it incorrectly leaves genuinely invisible text,
   which is worse than no gradient at all). */
.stat-number.gradient-fill {
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}

.stat-review .stat-number { color: var(--warn); }
.stat-running .stat-number { color: var(--running); }
.stat-quota .stat-number { color: var(--ok); }

.stat-label { color: var(--text-muted); font-size: 0.9rem; margin-top: 0.25rem; }
.stat-link { font-size: 0.8rem; color: var(--accent); text-decoration: none; }

/* ---- Provider call counter (5 серпня, "легкий cost-tracking") ---- */

.cost-tracking-details { margin-bottom: 1.75rem; }
.cost-tracking-details summary { cursor: pointer; font-size: 0.85rem; color: var(--text-muted); }
.cost-tracking-grid { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.6rem; }
.cost-tracking-chip {
  display: flex; align-items: baseline; gap: 0.35rem;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px;
  padding: 0.35rem 0.65rem; box-shadow: var(--shadow);
}
.cost-tracking-chip-count { font-weight: 700; color: var(--accent); font-variant-numeric: tabular-nums; }
.cost-tracking-chip-name { font-size: 0.8rem; color: var(--text-muted); }

/* ---- Setup screen ---- */

.progress-bar {
  width: 100%;
  height: 12px;
  border-radius: 6px;
  background: var(--border);
  overflow: hidden;
  margin-top: 0.5rem;
}

.progress-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  transition: width 0.3s ease;
}

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}

.card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1rem;
  box-shadow: var(--shadow);
  transition: opacity 0.3s ease;
}

.card.done {
  opacity: 0.6;
}

.card.done .card-body {
  display: none;
}

/* Step-sequencing (add-channel wizard): a later step's card is visually and
   functionally inert until the step before it is actually complete -- fields
   are real <input>/<select>/<button disabled> (not just faded), so nothing
   in a locked card can be interacted with, not just "looks unavailable". */
.card.locked { opacity: 0.55; }
.card:not(.locked) .locked-note { display: none; }
.card.locked .locked-note { display: block; font-weight: 600; }

.post-create-checklist { list-style: none; padding: 0; margin: 0.75rem 0 0; }
.post-create-checklist li { padding: 0.3rem 0; font-size: 0.9rem; }
.post-create-checklist .check-done { color: var(--ok); }
.post-create-checklist .check-done::before { content: "\2713  "; font-weight: 700; }
.post-create-checklist .check-todo { color: var(--warn); }
.post-create-checklist .check-todo::before { content: "\2610  "; font-weight: 700; }

.path-display { background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
                 padding: 0.4rem 0.6rem; font-size: 0.85rem; word-break: break-all; margin: 0.3rem 0; }
.path-picker-actions { display: flex; gap: 0.5rem; margin-bottom: 0.4rem; }

/* ---- Server-side folder browser modal (reusable, folder_browser.js) ---- */

.modal-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-box {
  background: var(--card-bg);
  border-radius: 12px;
  padding: 1.5rem;
  width: min(420px, 90vw);
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-hover);
}

.modal-box h3 { margin-top: 0; }

.folder-list {
  list-style: none;
  padding: 0;
  margin: 0.5rem 0;
  overflow-y: auto;
  flex: 1;
  min-height: 120px;
  border: 1px solid var(--border);
  border-radius: 8px;
}

.folder-item { padding: 0.5rem 0.75rem; cursor: pointer; border-bottom: 1px solid var(--border); }
.folder-item:last-child { border-bottom: none; }
.folder-item:hover { background: var(--bg); }
.folder-up { font-weight: 600; color: var(--accent); }

.modal-actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 0.75rem; }

.card-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.card-header h3 { margin: 0; flex: 1; font-size: 1rem; }

.badge {
  font-size: 0.7rem;
  padding: 0.15rem 0.4rem;
  border-radius: 4px;
  background: #fff3cd;
  color: var(--warn);
  white-space: nowrap;
}

.steps { padding-left: 1.1rem; font-size: 0.9rem; color: var(--text-muted); }

.save-row { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
.key-input { flex: 1; padding: 0.4rem; border: 1px solid var(--border); border-radius: 6px; }
.save-status { font-size: 0.85rem; color: var(--text-muted); min-height: 1.2em; }

.oauth-log { background: #10141c; color: #d7dee8; padding: 0.5rem; border-radius: 6px;
             font-size: 0.8rem; white-space: pre-wrap; max-height: 200px; overflow-y: auto; }

.device-oauth-panel { margin-top: 0.5rem; padding: 0.6rem 0.75rem; background: var(--bg);
                       border: 1px solid var(--border); border-radius: 6px; }
.device-oauth-panel p { margin: 0.35rem 0; font-size: 0.9rem; }
.device-oauth-code { font-size: 1.1rem; font-weight: 600; letter-spacing: 0.05em; }
.channel-quota { margin: 0.2rem 0 0.6rem; }

.check-results { list-style: none; padding: 0; }
.check-results li { padding: 0.25rem 0; font-size: 0.9rem; }
.check-ok { color: var(--ok); }
.check-fail { color: var(--fail); }

/* ---- Dashboard ---- */

.channel-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 1.25rem;
  /* Grid's default (align-items: stretch) makes every card in a row match the
     tallest one -- so the "+ Add channel" tile (short, fixed min-height) and a
     real channel card (tall, variable content) sitting in the same row would
     visibly stretch/resize together. Cards must size to their own content,
     independent of whatever else shares their row. */
  align-items: start;
}

.channel-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-left: 5px solid var(--text-muted);
  border-radius: 12px;
  padding: 1.1rem 1.25rem;
  box-shadow: var(--shadow);
  transition: box-shadow 0.15s ease;
}

.channel-card:hover { box-shadow: var(--shadow-hover); }

.add-channel-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border: 2px dashed var(--border);
  border-left: 2px dashed var(--border);
  color: var(--text-muted);
  text-decoration: none;
  font-weight: 600;
  min-height: 140px;
  transition: border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}

.add-channel-card:hover {
  border-color: var(--accent);
  color: var(--accent);
  box-shadow: var(--shadow-hover);
}

.add-channel-plus {
  font-size: 2.2rem;
  font-weight: 800;
  line-height: 1;
  margin-bottom: 0.25rem;
}

.status-border-completed { border-left-color: var(--ok); }
.status-border-failed { border-left-color: var(--fail); }
.status-border-running { border-left-color: var(--running); }
.status-border-none { border-left-color: var(--border); }
.status-border-cancelled { border-left-color: var(--warn); }

.channel-card-header { display: flex; align-items: center; justify-content: space-between; }
.channel-card-header h3 { margin: 0; font-size: 1.15rem; }

.status-badge {
  display: inline-block;
  padding: 0.15rem 0.6rem;
  border-radius: 12px;
  font-size: 0.78rem;
  font-weight: 600;
  color: #fff;
  background: var(--text-muted);
}

.status-completed { background: var(--ok); }
.status-failed { background: var(--fail); }
.status-running { background: var(--running); }
.status-none { background: var(--text-muted); }
.status-cancelled { background: var(--warn); }
.status-cancelling { background: var(--warn); animation: stage-pulse 1.2s ease-in-out infinite; }

.voice-line { margin: 0.4rem 0; }

.social-connect-row { margin: 0.4rem 0; display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center; }
.social-connect-item { display: inline-flex; align-items: center; gap: 0.35rem; }
.social-connect-btn { padding: 0.3rem 0.7rem; font-size: 0.85rem; }
.social-connect-connected-text { font-size: 0.85rem; color: var(--text-muted); }

/* Задача (10 вересня, "чекбокси TikTok/Facebook/Instagram окремо на
   дашборді") -- та сама inline-row верстка, що .social-connect-row вище. */
.multi-platform-publish-row { margin: 0.4rem 0; display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center; }
.multi-platform-publish-item { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.85rem; cursor: pointer; }
.multi-platform-publish-save-btn { padding: 0.3rem 0.7rem; font-size: 0.85rem; }
.multi-platform-publish-status { font-size: 0.85rem; color: var(--text-muted); }

.niche-edit-row { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; margin: 0.3rem 0 0.5rem; }
.niche-edit-row select, .niche-edit-row input {
  padding: 0.3rem; border: 1px solid var(--border); border-radius: 6px;
}

/* Задача (5 серпня): checkbox-чіпи замість comma-separated текстового поля
   для duration_choices_sec -- підтверджений UI-макет (Option A). */
.duration-choices-edit-row {
  margin: 0.3rem 0 0.5rem; padding: 0.65rem 0.75rem;
  border: 1px dashed var(--border); border-radius: 8px;
  background: rgba(107, 114, 128, 0.04);
}
.duration-chip-group-label {
  font-size: 0.74rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--text-muted); margin: 0.6rem 0 0.4rem;
}
.duration-chip-group-label:first-child { margin-top: 0; }
.duration-chip-grid { display: flex; flex-wrap: wrap; gap: 0.45rem; }
.duration-chip { position: relative; }
.duration-chip input {
  position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer;
}
.duration-chip label {
  display: inline-block; padding: 0.35rem 0.7rem; border-radius: 999px;
  border: 1px solid var(--border); background: var(--card-bg); color: var(--text);
  font-size: 0.85rem; font-weight: 600; cursor: pointer; user-select: none;
  transition: border-color 0.12s ease, background 0.12s ease, color 0.12s ease;
}
.duration-chip input:checked + label {
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  border-color: transparent; color: #fff;
}
.duration-chip input:focus-visible + label { box-shadow: 0 0 0 3px rgba(91, 91, 246, 0.3); }
.duration-chip label:hover { border-color: var(--accent); }
.duration-chip-disabled input { cursor: not-allowed; }
.duration-chip-disabled label {
  cursor: not-allowed; opacity: 0.5; background: var(--card-bg);
}
.duration-chip-disabled label:hover { border-color: var(--border); }
.duration-choices-summary { margin: 0.7rem 0 0.6rem; }
.duration-choices-summary-warn { color: var(--warn); font-weight: 600; }

/* Патч #94 (1 августа): .batch-setup (config + schedule preview) is visually
   a dashed, muted "this is just a preview" box -- deliberately NOT styled
   like .batch-launch below (solid border-top, no dashing) so the two read as
   distinct zones: "things you're configuring" vs "the actual running batch
   and its real status". */
.batch-setup {
  margin-top: 0.85rem;
  padding: 0.65rem 0.75rem;
  border: 1px dashed var(--border);
  border-radius: 8px;
  background: rgba(107, 114, 128, 0.04);
}

.batch-form { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.batch-form label { font-size: 0.85rem; }
.batch-count { width: 4rem; padding: 0.3rem; border: 1px solid var(--border); border-radius: 6px; }

.schedule-preview-block { margin-top: 0.6rem; padding-top: 0.5rem; border-top: 1px dashed var(--border); }
.schedule-preview-caption {
  margin: 0 0 0.3rem; font-size: 0.78rem; font-weight: 600; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.02em;
}
.schedule-preview { list-style: none; padding: 0; margin: 0; font-size: 0.82rem; color: var(--text-muted); }
.schedule-preview-title { font-weight: 600; color: var(--text); }

/* Задача (9 серпня, "компактний пікер content_variant"): за замовчуванням
   видно лише один рядок "whole batch" -- список per-slot <select>'ів
   ховається за <details>, щоб не рости повноширинними рядками вниз при
   збільшенні Count, коли власнику реально треба лише загальний вибір.
   Розгорнутий список -- інлайн-чипи в один потік (flex-wrap), а не
   блочні <li> на всю ширину картки. */
.content-variant-slot-details { margin-top: 0.4rem; }
.content-variant-slot-details summary { cursor: pointer; font-size: 0.8rem; }
.content-variant-slot-picker {
  list-style: none; padding: 0; margin: 0.4rem 0 0;
  display: flex; flex-wrap: wrap; gap: 0.4rem 0.75rem;
}
.content-variant-slot-picker li { display: block; }
.content-variant-slot-picker label {
  display: inline-flex; align-items: center; gap: 0.3rem;
  font-size: 0.78rem; color: var(--text-muted); white-space: nowrap;
}
.content-variant-slot-select { padding: 0.15rem; font-size: 0.78rem; border: 1px solid var(--border); border-radius: 4px; }

.batch-launch { margin-top: 0.85rem; padding-top: 0.75rem; border-top: 1px solid var(--border); }

/* Задача (5 серпня, "виправити позиціонування кнопки Cancel batch"): Start
   batch + Cancel batch раніше були двома голими inline-block <button>, без
   спільного flex-контейнера -- на типовій ширині картки каналу їм двом
   разом (довший текст Cancel batch + вкладена info-tip іконка) не
   вистачало місця в одному "рядку" inline-контенту, і Cancel batch
   переносився під Start, зсунутий і нерівний, замість стояти поруч. Той
   самий display:flex + align-items:center + gap приём, що вже є в
   .batch-form вище -- єдиний ряд кнопок, які завжди вирівняні одна з
   одною незалежно від довжини вмісту кожної. */
.batch-launch-buttons { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.cancel-batch-btn { align-items: center; }

.batch-log { margin-top: 0.5rem; background: #10141c; color: #d7dee8; padding: 0.5rem;
             border-radius: 6px; font-size: 0.75rem; white-space: pre-wrap; max-height: 220px; overflow-y: auto; }

.batch-progress { margin-top: 0.75rem; }
.batch-progress-text { margin: 0 0 0.35rem; font-weight: 600; color: var(--text); }
.batch-log-details { margin-top: 0.5rem; }
.batch-log-details summary { cursor: pointer; font-size: 0.8rem; }

.analytics-link-line { margin-top: 0.85rem; padding-top: 0.75rem; border-top: 1px solid var(--border); }

/* Задача 2 (8 серпня): аналітика тепер окрема сторінка (/analytics), не
   вбудований блок на дашборді -- див. analytics.html. */
.analytics-filter-row { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 1rem; }
.analytics-table { width: 100%; border-collapse: collapse; margin-top: 0.5rem; font-size: 0.88rem; }
.analytics-table th, .analytics-table td { text-align: left; padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--border); }
.analytics-table th { color: var(--text-muted); font-weight: 600; }
.analytics-table td a { color: var(--accent); text-decoration: none; }
.analytics-table td a:hover { text-decoration: underline; }
.verdict-chip { display: inline-block; padding: 0.15rem 0.6rem; border-radius: 999px; font-size: 0.78rem; font-weight: 600; }
.verdict-chip-good { background: #1c3d2a; color: #6fd48c; }
.verdict-chip-medium { background: #40391a; color: #e0c34a; }
.verdict-chip-bad { background: #452323; color: #e88585; }
.verdict-chip-none { background: var(--border); color: var(--text-muted); }

/* Задача (19 серпня, "owner-visibility: /subscriptions"): subscription_status
   chips -- переиспользует .verdict-chip's форму, свої відтінки за
   SubscriptionStatus.ALL (backend/models/billing.py). */
.verdict-chip-status-active { background: #1c3d2a; color: #6fd48c; }
.verdict-chip-status-trialing { background: #1a3340; color: #6fc0e0; }
.verdict-chip-status-past_due { background: #40391a; color: #e0c34a; }
.verdict-chip-status-canceled { background: #452323; color: #e88585; }

/* Задача (20 серпня, "Admin-панель"): /admin's status-колонка додає два
   значення, яких не показує /subscriptions -- "exempt" (channel.is_exempt,
   явний override, не реальний SubscriptionStatus) і "no subscription"
   (канал без жодного рядка Subscription взагалі). Той самий .verdict-chip
   формат, що вище. */
.verdict-chip-status-exempt { background: #1a3340; color: #8bc7f0; }
.verdict-chip-status-no-subscription { background: var(--border); color: var(--text-muted); }

.stage-checklist { list-style: none; padding: 0; margin: 0.6rem 0 0; display: flex; flex-direction: column; gap: 0.3rem; }
.stage-item { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-muted); }
.stage-indicator {
  width: 1.1rem; height: 1.1rem; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%; font-size: 0.68rem; font-weight: 700;
  background: var(--border); color: var(--text-muted);
}
.stage-item.stage-done { color: var(--text); }
.stage-item.stage-done .stage-indicator { background: var(--ok); color: #fff; }
.stage-item.stage-running { color: var(--text); font-weight: 600; }
.stage-item.stage-running .stage-indicator { background: var(--running); color: #fff; animation: stage-pulse 1.2s ease-in-out infinite; }
.stage-item.stage-failed, .stage-item.stage-rejected { color: var(--fail); }
.stage-item.stage-failed .stage-indicator, .stage-item.stage-rejected .stage-indicator { background: var(--fail); color: #fff; }

@keyframes stage-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}

.batch-result-video { margin-top: 0.75rem; }

/* Задача (9 серпня, "живий preview під час генерації батчу"): per-slot
   панель поверх існуючого /batch/status-поллінгу (dashboard.js's
   renderBatchSlots()) -- один рядок на слот (done/current/pending), активний
   слот розгортає повний .stage-item чек-ліст (перевикористовує стилі вище)
   + опційне превью поточного кліпу. */
.batch-slots-panel { list-style: none; padding: 0; margin: 0.6rem 0 0; display: flex; flex-direction: column; gap: 0.5rem; }
.batch-slot { border: 1px solid var(--border); border-radius: 8px; padding: 0.5rem 0.7rem; }
.batch-slot-current { border-color: var(--running); background: rgba(59, 130, 246, 0.06); }
.batch-slot-done { opacity: 0.8; }
.batch-slot-pending { opacity: 0.55; }
.batch-slot-header { display: flex; align-items: center; gap: 0.6rem; font-size: 0.85rem; }
.batch-slot-index { font-weight: 600; color: var(--text); flex-shrink: 0; }
.batch-slot-stage { color: var(--text-muted); text-transform: capitalize; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.batch-slot-elapsed { margin-left: auto; flex-shrink: 0; color: var(--text-muted); font-size: 0.78rem; font-variant-numeric: tabular-nums; }
.batch-slot-stage-checklist { list-style: none; padding: 0; margin: 0.5rem 0 0; display: flex; flex-direction: column; gap: 0.3rem; }

/* ---- Reusable info tooltip (pure CSS, no JS) ---- */

.info-tip {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.05rem;
  height: 1.05rem;
  border-radius: 50%;
  background: var(--border);
  color: var(--text-muted);
  font-size: 0.68rem;
  font-weight: 700;
  cursor: help;
  margin-left: 0.3rem;
  vertical-align: middle;
}

.info-tip-text {
  visibility: hidden;
  opacity: 0;
  position: absolute;
  bottom: 140%;
  left: 50%;
  transform: translateX(-50%);
  width: 230px;
  background: #10141c;
  color: #d7dee8;
  padding: 0.5rem 0.6rem;
  border-radius: 6px;
  font-size: 0.78rem;
  font-weight: 400;
  line-height: 1.35;
  text-align: left;
  transition: opacity 0.15s ease;
  z-index: 10;
  pointer-events: none;
}

.info-tip:hover .info-tip-text,
.info-tip:focus .info-tip-text {
  visibility: visible;
  opacity: 1;
}

/* ---- Review queue ---- */

.review-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1.1rem;
}

.review-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1rem;
  box-shadow: var(--shadow);
}

.review-card.resolved { opacity: 0.5; }

.review-card .thumb { width: 100%; border-radius: 8px; background: #10141c; }
/* Unscoped so this same finished-video player look is reusable outside the
   review queue too (the dashboard's per-batch result video reuses it as-is,
   rather than redefining its own version). */
.preview-video { width: 100%; border-radius: 8px; background: #10141c; }
.review-actions { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
.btn-approve { border-color: var(--ok); color: var(--ok); }
.btn-reject { border-color: var(--fail); color: var(--fail); }
.review-status { font-size: 0.85rem; color: var(--text-muted); }
/* Патч #69 (30 июля): маленький мелкий текст "Approved."/"Rejected." не
   был заметен рядом с всё ещё активными кнопками -- реальный баг:
   владелец нажимал Approve повторно на уже одобренное видео, отсюда серия
   invalidPublishAt. Крупный, заметный, цветной баннер вместо мелкого
   текста -- см. review.js, который добавляет этот класс + .status-approved/
   .status-rejected после успешного действия (и одновременно отключает обе
   кнопки, чтобы повторный клик был физически невозможен, не только
   визуально нежелателен). */
.review-status-final {
  font-size: 1.05rem;
  font-weight: 700;
  padding: 0.6rem 0.85rem;
  border-radius: 8px;
  margin-top: 0.5rem;
}
.review-status-final.status-approved { background: #eaf7ee; color: var(--ok); }
.review-status-final.status-rejected { background: #fdeceb; color: var(--fail); }

.approval-decision { font-size: 0.85rem; font-weight: 600; padding: 0.3rem 0.5rem; border-radius: 6px; }
.approval-decision.slot-ok { background: #eaf7ee; color: var(--ok); }
.approval-decision.slot-immediate { background: #fff3cd; color: var(--warn); }

/* Патч #70 (30 июля): реальная жалоба -- владелец узнавал об исчерпанной
   дневной квоте бесплатного API только из сырой технической ошибки в
   логе. Крупный, заметный баннер вместо этого — тот же визуальный язык,
   что уже используют .review-status-final/.approval-decision выше
   (цветной фон + var(--warn), не просто текст). */
.batch-quota-banner {
  font-size: 0.95rem;
  font-weight: 600;
  padding: 0.6rem 0.85rem;
  border-radius: 8px;
  margin: 0.5rem 0;
  background: #fff3cd;
  color: var(--warn);
}

/* Задача (12 серпня, "перший крок міграції subprocess.Popen -> RQ"): той
   самий візуальний патерн, що .batch-quota-banner вище, але var(--fail) --
   на відміну від "квота скоро відновиться сама" (жовтий/warn), "черга
   недоступна" -- активна проблема з інфраструктурою, яка сама не пройде,
   і саме тому статус батчу зараз невідомий (requirement 5). */
.batch-queue-error-banner {
  font-size: 0.95rem;
  font-weight: 600;
  padding: 0.6rem 0.85rem;
  border-radius: 8px;
  margin: 0.5rem 0;
  background: #fdf1f1;
  color: var(--fail);
}

/* Задача (16 вересня, "фінальне зведення після завершення батчу"): той
   самий "кольоровий фон + читабельний текст" патерн, що .batch-quota-
   banner/.batch-queue-error-banner вище -- нейтрально-інформаційний
   (не помилка/попередження сам по собі, навіть коли всередині є провальні
   слоти -- самі провали вже перелічені текстом), тож var(--ok)/світлий
   фон, а не warn/fail. */
.batch-summary-banner {
  font-size: 0.95rem;
  font-weight: 600;
  padding: 0.6rem 0.85rem;
  border-radius: 8px;
  margin: 0.5rem 0;
  background: #eaf7ee;
  color: var(--ok);
  white-space: pre-line;
}
.batch-summary-banner .batch-summary-failure {
  display: block;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--fail);
  margin-top: 0.25rem;
}

.tiktok-export { font-size: 0.78rem; color: var(--text-muted); background: var(--bg); padding: 0.4rem 0.5rem; border-radius: 6px; }
.tiktok-export code { font-size: 0.75rem; word-break: break-all; }

/* Задача (18 серпня, "thumbnail A/B"): candidate frames PublisherWorker
   generated at publish time -- click one to make it the live thumbnail (see
   review.js's activateThumbnailVariant()). Same card/button visual language
   as .review-actions above, not a new component style. */
.thumbnail-variants { margin-top: 0.5rem; }
.thumbnail-variant-list { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.thumbnail-variant {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  padding: 0.3rem;
  border: 2px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  cursor: pointer;
  font-size: 0.75rem;
  color: var(--text-muted);
}
.thumbnail-variant img { width: 96px; border-radius: 4px; background: #10141c; }
.thumbnail-variant:disabled { cursor: default; }
.thumbnail-variant-active { border-color: var(--ok); color: var(--ok); font-weight: 600; }
.thumbnail-variant-pending { opacity: 0.5; }

/* Title+thumbnail-as-a-pair v1 (22 вересня): title_variant candidates shown
   side-by-side with thumbnail candidates -- same card/button visual
   language as .thumbnail-variant above, not a new component style. */
.title-thumbnail-pair { margin-top: 0.5rem; }
.pair-columns { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-start; }
.title-variants { flex: 1 1 200px; min-width: 180px; }
.thumbnail-variants { flex: 1 1 200px; margin-top: 0; }
.title-variant-list { display: flex; flex-direction: column; gap: 0.4rem; }
.title-variant {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.4rem 0.6rem;
  border: 2px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  cursor: pointer;
  font-size: 0.8rem;
  color: var(--text-muted);
  text-align: left;
}
.title-variant:disabled { cursor: default; }
.title-variant-active { border-color: var(--ok); color: var(--ok); font-weight: 600; }
.title-variant-pending { opacity: 0.5; }

.custom-publish-row { margin: 0.5rem 0; }
.custom-publish-row label { display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.85rem; }
.custom-publish-time { padding: 0.35rem; border: 1px solid var(--border); border-radius: 6px; align-self: flex-start; }
.custom-publish-note { margin: 0.3rem 0 0; }

/* ---- Sign-in / sign-up (Задача 20 серпня, "полірування сторінки реєстрації"):
   раніше -- голий .card з inline max-width/margin, без заголовка/копірайту,
   ніякого бренд-акценту -- Clerk's віджет просто "плавав" у загальній
   картці. Той самий візуальний язик, що вже є в topbar/.stat-tile (accent
   gradient, .hint), а не новий стиль з нуля. Сама логіка/розмітка форми
   Clerk (mountSignIn/mountSignUp) не змінюється -- лише обгортка. ---- */
.auth-shell {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 55vh;
  text-align: center;
  padding: 1rem 0;
  max-width: 480px;
  margin: 0 auto;
}

.auth-brand {
  font-weight: 800;
  font-size: 1.5rem;
  letter-spacing: 0.01em;
  margin-bottom: 0.6rem;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}

.auth-shell h1 { font-size: 1.5rem; margin: 0 0 0.35rem; }
.auth-shell .hint { margin: 0 0 1.4rem; max-width: 360px; }

.auth-card {
  width: 100%;
  max-width: 420px;
}

/* ---- Voice page ---- */

#channel-input, #lang-select, #voice-select { padding: 0.4rem; border: 1px solid var(--border);
                                               border-radius: 6px; margin-left: 0.4rem; }
.voice-actions { display: flex; align-items: center; gap: 0.75rem; margin: 0.75rem 0; }
label { display: block; margin-bottom: 0.75rem; }

/* ---- Niche Research page: tree (Патч #72, 31 июля) ---- */

.niche-toolbar { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; flex-wrap: wrap; }

.niche-node { margin: 0.35rem 0; }
.niche-node-children { margin-left: 1.5rem; border-left: 2px solid var(--border); padding-left: 0.75rem; }

.niche-node-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 0.35rem 0.5rem;
  border-radius: 6px;
}
.niche-node-row:hover { background: var(--bg); }

.niche-node-checkbox { flex: 0 0 auto; }

.niche-node-label { font-weight: 600; min-width: 10rem; }
.niche-depth-1 .niche-node-label, .niche-depth-2 .niche-node-label { font-weight: 500; font-size: 0.92rem; }

.niche-node-analyze { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; font-size: 0.85rem; color: var(--text-muted); flex: 1 1 auto; }
.niche-research-summary b { color: var(--text); }

.niche-use-link, .niche-expand-btn, .niche-refresh-btn, .niche-analyze-btn {
  padding: 0.25rem 0.6rem;
  font-size: 0.8rem;
  flex: 0 0 auto;
}

/* Задача ("Niche Research -- кликабельні посилання на топ-результати"):
   .niche-node-analyze -- flex ряд (summary + Refresh поруч), список
   top_videos-посилань падає на власний рядок під ним, не тисне туди ж. */
.niche-top-videos {
  flex-basis: 100%;
  margin: 0.25rem 0 0;
  padding-left: 1.1rem;
  font-size: 0.82rem;
}
.niche-top-videos li { margin: 0.15rem 0; }

/* Задача ("подсказки при наборе текста -- дешёвая версия"): вхідне поле
   пошуку/додавання ніші + випадаючий список substring-match підказок під
   ним, і короткий "flash"-хайлайт рядка дерева, куди веде вибір підказки. */
.niche-search-row { position: relative; margin-bottom: 0.75rem; max-width: 28rem; }
.niche-search-row input { width: 100%; padding: 0.45rem 0.6rem; border: 1px solid var(--border); border-radius: 6px; }
.niche-search-suggestions {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 5;
  background: var(--card-bg, #fff); border: 1px solid var(--border); border-top: none;
  border-radius: 0 0 6px 6px; max-height: 14rem; overflow-y: auto;
}
.niche-search-suggestion { padding: 0.4rem 0.6rem; cursor: pointer; font-size: 0.9rem; }
.niche-search-suggestion:hover { background: var(--bg); }
.niche-node-highlight { background: var(--bg-accent, #fff3cd); transition: background 1.8s ease; }

/* Задача (26 серпня, "Google Cloud TTS як другий voice provider") -- /voice's
   provider toggle (Kokoro/Google) + gender filter chips + speed/pitch
   sliders. Provider tab reuses the same input:checked+label chip pattern as
   .duration-chip above (radio, not checkbox -- exactly one provider active
   at a time); .provider-tab.locked reuses .card.locked's "real disabled
   control, not just faded" contract (see channels_new.html) -- a locked
   Google tab is a genuine <input disabled>, not a CSS-only fake-out. */
.provider-tabs { display: flex; gap: 0.6rem; margin: 0.6rem 0 1rem; }
.provider-tab { position: relative; flex: 1 1 0; }
.provider-tab input {
  position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer;
}
.provider-tab label {
  display: block; text-align: center; padding: 0.6rem 0.8rem; border-radius: 10px;
  border: 1px solid var(--border); background: var(--card-bg); color: var(--text);
  font-weight: 700; cursor: pointer; user-select: none;
  transition: border-color 0.12s ease, background 0.12s ease, color 0.12s ease;
}
.provider-tab input:checked + label {
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  border-color: transparent; color: #fff;
}
.provider-tab label:hover { border-color: var(--accent); }
.provider-tab .lock-hint {
  display: block; font-size: 0.72rem; font-weight: 500; margin-top: 0.15rem;
  color: inherit; opacity: 0.85;
}
.provider-tab.locked input { cursor: not-allowed; }
.provider-tab.locked label { cursor: not-allowed; opacity: 0.55; }
.provider-tab.locked label:hover { border-color: var(--border); }

/* Задача (26 серпня, "пресети субтитрів") -- /subtitles's preset picker,
   same input:checked+label chip pattern as .provider-tab above, laid out as
   a grid (4 presets) instead of a row of 2. */
.preset-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0.6rem; margin: 0.6rem 0 1rem; }
.preset-card { position: relative; }
.preset-card input {
  position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer;
}
.preset-card label {
  display: block; padding: 0.7rem 0.9rem; border-radius: 10px;
  border: 1px solid var(--border); background: var(--card-bg); color: var(--text);
  cursor: pointer; user-select: none;
  transition: border-color 0.12s ease, background 0.12s ease, color 0.12s ease;
}
.preset-card label strong { display: block; margin-bottom: 0.2rem; }
.preset-card.selected label,
.preset-card input:checked + label {
  border-color: var(--accent); background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: #fff;
}
.preset-card.selected label .hint,
.preset-card input:checked + label .hint { color: inherit; opacity: 0.85; }
.preset-card label:hover { border-color: var(--accent); }

.voice-filter-row { display: flex; flex-wrap: wrap; gap: 1.4rem; margin: 0.4rem 0 1rem; }
.voice-filter-row label { font-size: 0.85rem; font-weight: 600; }

.gender-chip-grid { display: inline-flex; gap: 0.4rem; margin-left: 0.5rem; vertical-align: middle; }
.gender-chip { position: relative; }
.gender-chip input {
  position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer;
}
.gender-chip label {
  display: inline-block; padding: 0.3rem 0.65rem; border-radius: 999px;
  border: 1px solid var(--border); background: var(--card-bg); color: var(--text);
  font-size: 0.8rem; font-weight: 600; cursor: pointer; user-select: none;
}
.gender-chip input:checked + label {
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  border-color: transparent; color: #fff;
}

.slider-row { margin: 0.9rem 0; }
.slider-row label { display: block; font-weight: 600; font-size: 0.85rem; margin-bottom: 0.3rem; }
.slider-row input[type="range"] { width: 100%; max-width: 360px; vertical-align: middle; }
.slider-row .slider-value { margin-left: 0.6rem; font-variant-numeric: tabular-nums; color: var(--text-muted); }
.pitch-row { display: none; }
.pitch-row.pitch-row-visible { display: block; }
.slider-row.slider-row-hidden { display: none; }

/* Задача H1 (27 серпня, "voice cloning"): upload widget, hidden by default,
   toggled the same way .pitch-row above is (provider-specific visible field) --
   Chatterbox shows this INSTEAD of #voice-select-row's catalog dropdown. */
.voice-upload-row { display: none; margin: 0.9rem 0; }
.voice-upload-row.voice-upload-row-visible { display: block; }
.voice-upload-row label { display: block; font-weight: 600; font-size: 0.85rem; margin-bottom: 0.3rem; }
.voice-select-row.voice-select-row-hidden { display: none; }
/* Задача C (4 вересня, "людський вибір голосу"): #catalog-picker-row (країна+
   стать) ховається тим самим voice-select-row-hidden класом, коли активний
   режим клонування -- окреме правило, бо цей рядок несе свій власний базовий
   клас (.voice-filter-row), не .voice-select-row вище. */
.voice-filter-row.voice-select-row-hidden { display: none; }

/* Задача (9 жовтня, "табло"): вкладки /analytics + таблиця табло. */
.analytics-tabs { display: flex; gap: 0.4rem; margin: 0.5rem 0 1rem; border-bottom: 1px solid var(--border); }
.analytics-tab { padding: 0.45rem 0.9rem; color: var(--text-muted); text-decoration: none; border-bottom: 2px solid transparent; }
.analytics-tab-active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.scoreboard-live { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.6rem; margin-bottom: 1rem; }
.scoreboard-live-card { border: 1px solid var(--border); border-radius: 8px; padding: 0.6rem 0.8rem; }
.scoreboard-live-card .value { font-size: 1.3rem; font-weight: 700; }
.scoreboard-live-card .label { color: var(--text-muted); font-size: 0.8rem; }
.scoreboard-scroll { overflow-x: auto; }
.scoreboard-table td { white-space: nowrap; max-width: 260px; overflow: hidden; text-overflow: ellipsis; }
.scoreboard-table th a { color: inherit; text-decoration: none; }
.scoreboard-table .engaged-rate { font-weight: 700; }

/* Shared site footer (_site_footer.html): welcome.html and every base.html page. */
.site-footer {
  border-top: 1px solid var(--border);
  padding: 1.75rem 1.5rem;
  text-align: center;
  color: var(--text-muted);
  font-size: 0.85rem;
}
.site-footer .footer-links {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  margin-bottom: 0.5rem;
}
.site-footer a { color: var(--text-muted); text-decoration: none; }
.site-footer a:hover { color: var(--accent); text-decoration: underline; }
.site-footer p { margin: 0; }
.consent-note { font-size: 0.85rem; color: var(--text-muted); margin: 0.4rem 0 0; }

/* /privacy, /terms (legal pages, 13 October). */
.legal-doc { max-width: 780px; line-height: 1.6; }
.legal-doc h2 { margin-top: 1.75rem; }
.legal-doc li { margin-bottom: 0.35rem; }
