/* global React */
// Postilo app-specific components (Russian UI) — builds on Vuexy primitives.

const POSTILO_ICONS = {
  "chart-pie":       "M10 3.2a9 9 0 1 0 10.8 10.8a1 1 0 0 0 -1 -1h-6.8a2 2 0 0 1 -2 -2v-7a.9 .9 0 0 0 -1 -.8M15 3.5a9 9 0 0 1 5.5 5.5h-4.5a1 1 0 0 1 -1 -1z",
  "hash":            "M5 9l14 0M5 15l14 0M11 4l-4 16M17 4l-4 16",
  "repeat":          "M17 2l4 4l-4 4M3 11v-1a4 4 0 0 1 4 -4h14M7 22l-4 -4l4 -4M21 13v1a4 4 0 0 1 -4 4h-14",
  "shield-check":    "M11.46 20.846a12 12 0 0 1 -7.96 -14.846a12 12 0 0 0 8.5 -3a12 12 0 0 0 8.5 3a12 12 0 0 1 -.09 7.06M15 19l2 2l4 -4",
  "trash":           "M4 7l16 0M10 11l0 6M14 11l0 6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3",
  "calendar-event":  "M4 5m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2zM16 3v4M8 3v4M4 11h16M8 15h2v2h-2z",
  "brand-telegram":  "M15 10l-4 4l6 6l4 -16l-18 7l4 2l2 6l3 -4",
  "circle-check":    "M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0M9 12l2 2l4 -4",
  "refresh":         "M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4",
  "eye":             "M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7",
  "lock":            "M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2zM11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0M8 11v-4a4 4 0 1 1 8 0v4",
  "users-group":     "M10 13a2 2 0 1 0 -4 0a2 2 0 0 0 4 0M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1M15 5a2 2 0 1 0 -4 0a2 2 0 0 0 4 0M17 10v-1a2 2 0 0 0 -2 -2h-4a2 2 0 0 0 -2 2v1M21 13a2 2 0 1 0 -4 0a2 2 0 0 0 4 0",
  "users":           "M9 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2M16 3.13a4 4 0 0 1 0 7.75M21 21v-2a4 4 0 0 0 -3 -3.85",
  "filter":          "M4 4h16v2.172a2 2 0 0 1 -.586 1.414l-4.414 4.414v7l-6 2v-8.5l-4.48 -4.928a2 2 0 0 1 -.52 -1.345v-2.227z",
  "chevron-down":    "M6 9l6 6l6 -6",
  "chevron-right":   "M9 6l6 6l-6 6",
  "chevron-left":    "M15 6l-6 6l6 6",
  "x":               "M18 6l-12 12M6 6l12 12",
  "sparkles":        "M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zM16 6a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zM9 18a6 6 0 0 1 6 -6a6 6 0 0 1 -6 -6a6 6 0 0 1 -6 6a6 6 0 0 1 6 6z",
  "menu-2":          "M4 6h16M4 12h16M4 18h16",
  "adjustments":     "M4 10a2 2 0 1 0 4 0a2 2 0 0 0 -4 0M6 4v4M6 12v8M10 16a2 2 0 1 0 4 0a2 2 0 0 0 -4 0M12 4v10M12 18v2M16 7a2 2 0 1 0 4 0a2 2 0 0 0 -4 0M18 4v1M18 9v11",
  "plus":            "M12 5l0 14M5 12l14 0",
  "clock":           "M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0M12 7v5l3 3",
  "settings":        "M12 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06 .06a2 2 0 0 1 0 2.83a2 2 0 0 1 -2.83 0l-.06 -.06a1.65 1.65 0 0 0 -1.82 -.33a1.65 1.65 0 0 0 -1 1.51v.17a2 2 0 0 1 -2 2a2 2 0 0 1 -2 -2v-.09a1.65 1.65 0 0 0 -1 -1.51a1.65 1.65 0 0 0 -1.82 .33l-.06 .06a2 2 0 0 1 -2.83 0a2 2 0 0 1 0 -2.83l.06 -.06a1.65 1.65 0 0 0 .33 -1.82a1.65 1.65 0 0 0 -1.51 -1h-.17a2 2 0 0 1 -2 -2a2 2 0 0 1 2 -2h.09a1.65 1.65 0 0 0 1.51 -1a1.65 1.65 0 0 0 -.33 -1.82l-.06 -.06a2 2 0 0 1 0 -2.83a2 2 0 0 1 2.83 0l.06 .06a1.65 1.65 0 0 0 1.82 .33h0a1.65 1.65 0 0 0 1 -1.51v-.17a2 2 0 0 1 2 -2a2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51h0a1.65 1.65 0 0 0 1.82 -.33l.06 -.06a2 2 0 0 1 2.83 0a2 2 0 0 1 0 2.83l-.06 .06a1.65 1.65 0 0 0 -.33 1.82v0a1.65 1.65 0 0 0 1.51 1h.17a2 2 0 0 1 2 2a2 2 0 0 1 -2 2h-.09a1.65 1.65 0 0 0 -1.51 1z",
  "help-circle":     "M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0M12 16v.01M12 13a2 2 0 0 0 .914 -3.782a1.98 1.98 0 0 0 -2.414 .483",
  "bell":            "M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2 -3v-3a7 7 0 0 1 4 -6M9 17v1a3 3 0 0 0 6 0v-1",
  "dots-vertical":   "M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M12 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M12 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0",
  "search":          "M21 21l-6 -6M3 10a7 7 0 1 0 14 0a7 7 0 0 0 -14 0",
  "send":            "M10 14l11 -11M21 3l-6.5 18a.55 .55 0 0 1 -1 0l-3.5 -8l-8 -3.5a.55 .55 0 0 1 0 -1z",
  "photo":           "M15 8h.01M3 6a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3zM3 16l5 -5c.928 -.893 2.072 -.893 3 0l5 5M14 14l1 -1c.928 -.893 2.072 -.893 3 0l3 3",
  "alert-triangle":  "M12 9v4M10.363 3.591l-8.106 13.534a1.914 1.914 0 0 0 1.636 2.871h16.214a1.914 1.914 0 0 0 1.636 -2.87l-8.106 -13.536a1.914 1.914 0 0 0 -3.274 0zM12 16h.01",
  "chevron-up":      "M6 15l6 -6l6 6",
  "user":            "M12 11m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2",
  "logout":          "M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2M9 12h12l-3 -3M18 15l3 -3",
  "info-circle":     "M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0M12 9h.01M11 12h1v4h1",
  "key":             "M16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1 -4.069 0l-.301 -.301l-6.558 6.558a2 2 0 0 1 -1.239 .578l-.175 .008h-1.172a1 1 0 0 1 -.993 -.883l-.007 -.117v-1.172a2 2 0 0 1 .467 -1.284l.119 -.13l.414 -.414h2v-2h2v-2l2.144 -2.144l-.301 -.301a2.877 2.877 0 0 1 0 -4.069l2.643 -2.643a2.877 2.877 0 0 1 4.069 0zM15 9h.01",
  "monitor":         "M3 5a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1zM7 20h10M9 16v4M15 16v4",
  "upload":          "M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2M7 9l5 -5l5 5M12 4v12",
  "download":        "M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2M7 11l5 5l5 -5M12 4v12",
  "plug":            "M9.785 6l8.215 8.215l-2.054 2.054a5.81 5.81 0 1 1 -8.215 -8.215l2.054 -2.054zM4 20l3.5 -3.5M15 4l-3.5 3.5M20 9l-3.5 3.5",
  "arrow-down":      "M12 5v14M18 13l-6 6M6 13l6 6",
  "arrow-right":     "M5 12l14 0M13 18l6 -6M13 6l6 6",
  "external-link":   "M12 6h-6a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-6M11 13l9 -9M15 4h5v5",
  "file-text":       "M14 3v4a1 1 0 0 0 1 1h4M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2zM9 9h1M9 13h6M9 17h6",
  "code":            "M7 8l-4 4l4 4M17 8l4 4l-4 4M14 4l-4 16",
  "credit-card":     "M3 5m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3zM3 10l18 0M7 15l.01 0M11 15l2 0",
  "home":            "M5 12l-2 0l9 -9l9 9l-2 0M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6",
};

const TIP = ({ n, size = 18, color, strokeWidth = 2 }) => {
  const d = POSTILO_ICONS[n];
  if (d) {
    return (
      <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
        stroke={color || "currentColor"} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round"
        style={{ flexShrink: 0, display: "inline-block", verticalAlign: "middle" }}>
        <path d={d} />
      </svg>
    );
  }
  return <window.TI n={n} size={size} color={color} strokeWidth={strokeWidth} />;
};

function PostiloMark({ size = 32, gradientFrom = "#7367F0", gradientTo = "#8F85F3" }) {
  return (
    <div style={{
      width: size, height: size, borderRadius: 8,
      background: `linear-gradient(135deg, ${gradientFrom} 0%, ${gradientTo} 100%)`,
      boxShadow: `0 2px 4px 0 ${gradientFrom}4D`,
      display: "flex", alignItems: "center", justifyContent: "center",
      color: "#fff", fontFamily: "var(--font-sans)", fontWeight: 700,
      fontSize: size * 0.5, letterSpacing: "-0.04em", flexShrink: 0,
    }}>P</div>
  );
}

function PlatformGlyph({ platform, size = 20 }) {
  // window.STATIC_BASE = "/static/design-v2" injected by react_page_template;
  // falls back to "" for local prototype (python -m http.server).
  const base = (typeof window !== "undefined" && typeof window.STATIC_BASE === "string")
    ? window.STATIC_BASE : "";
  // tg — официальная иконка с web.telegram.org/k
  if (platform === "tg") {
    return (
      <img src={`${base}/assets/tg-logo.png`} alt="Telegram"
        width={size} height={size}
        style={{ flexShrink: 0, display: "inline-block", verticalAlign: "middle", objectFit: "contain" }}
      />
    );
  }
  // MAX — официальный фавикон с dev.max.ru
  return (
    <img src={`${base}/assets/max-logo.png`} alt="MAX"
      width={size} height={size}
      style={{ flexShrink: 0, display: "inline-block", verticalAlign: "middle", objectFit: "contain" }}
    />
  );
}

function ConnectionPill({ platform, account, status = "ok" }) {
  const statusColors = {
    ok: { bg: "rgba(40,199,111,.12)", fg: "#28C76F" },
    warn: { bg: "rgba(255,159,67,.12)", fg: "#FF9F43" },
    err: { bg: "rgba(255,76,81,.12)", fg: "#FF4C51" },
  };
  const s = statusColors[status];
  return (
    <div style={{
      display: "inline-flex", alignItems: "center", gap: 10,
      padding: "6px 12px 6px 6px", background: "#fff",
      border: "1px solid var(--divider)", borderRadius: 999,
    }}>
      <PlatformGlyph platform={platform} size={24} />
      <div style={{ display: "flex", flexDirection: "column", lineHeight: 1.2 }}>
        <span style={{ fontSize: 13, fontWeight: 500, color: "var(--text-primary)" }}>
          {platform === "tg" ? "Telegram бот" : "MAX аккаунт"}
        </span>
        <span style={{ fontSize: 11, color: "var(--text-secondary)" }}>{account}</span>
      </div>
      <span style={{ width: 8, height: 8, borderRadius: "50%", background: s.fg, boxShadow: `0 0 0 3px ${s.bg}`, marginLeft: 4 }} />
    </div>
  );
}

function SegmentedFilter({ value, onChange, options }) {
  return (
    <div style={{
      display: "inline-flex", padding: 4, background: "var(--bg-body)",
      border: "1px solid var(--divider)", borderRadius: 8, gap: 2,
    }}>
      {options.map(o => {
        const active = o.value === value;
        return (
          <button key={o.value} onClick={() => onChange(o.value)} style={{
            display: "inline-flex", alignItems: "center", gap: 6,
            padding: "6px 14px", border: "none", cursor: "pointer",
            borderRadius: 6, fontFamily: "var(--font-sans)",
            fontSize: 13, fontWeight: 500,
            background: active ? "#fff" : "transparent",
            color: active ? "var(--text-primary)" : "var(--text-secondary)",
            boxShadow: active ? "0 2px 4px rgba(165,163,174,.3)" : "none",
            transition: "all .15s",
          }}
            onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = "rgba(47,43,61,.04)"; }}
            onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = "transparent"; }}>
            {o.icon}
            {o.label}
            {o.count != null && (
              <span style={{
                fontSize: 11, fontWeight: 500, padding: "1px 6px", borderRadius: 10,
                background: active ? "rgba(115,103,240,.12)" : "rgba(47,43,61,.06)",
                color: active ? "#7367F0" : "var(--text-secondary)",
                minWidth: 18, textAlign: "center",
              }}>{o.count}</span>
            )}
          </button>
        );
      })}
    </div>
  );
}

function ChannelAvatar({ initials, color, platform, size = 48, radius = 10, avatar }) {
  return (
    <div style={{ position: "relative", width: size, height: size, flexShrink: 0 }}>
      {avatar ? (
        <img src={avatar} alt={initials} style={{
          width: size, height: size, borderRadius: "50%",
          objectFit: "cover", display: "block",
        }} />
      ) : (
        <div style={{
          width: size, height: size, borderRadius: "50%",
          background: `linear-gradient(135deg, ${color}, ${color}cc)`,
          color: "#fff", display: "flex", alignItems: "center", justifyContent: "center",
          fontFamily: "var(--font-sans)", fontWeight: 600, fontSize: size * 0.4,
          letterSpacing: "-0.02em",
        }}>{initials}</div>
      )}
      <div style={{
        position: "absolute", right: -4, bottom: -4,
        borderRadius: platform === "tg" ? "50%" : 6, background: "#fff", padding: 2,
        boxShadow: "0 1px 2px rgba(47,43,61,.16)",
        display: "flex", alignItems: "center", justifyContent: "center",
      }}>
        <PlatformGlyph platform={platform} size={16} />
      </div>
    </div>
  );
}

function StatusDot({ status }) {
  const variants = {
    active: { dot: "#28C76F", label: "Отправлен", bg: "rgba(40,199,111,.12)", fg: "#1E9553" },
    paused: { dot: "#FF9F43", label: "Пауза", bg: "rgba(255,159,67,.12)", fg: "#BF7732" },
    error:  { dot: "#FF4C51", label: "Ошибка", bg: "rgba(255,76,81,.12)", fg: "#BF393D" },
    idle:   { dot: "#808390", label: "Ожидание", bg: "rgba(128,131,144,.12)", fg: "#60626C" },
  };
  const v = variants[status];
  return (
    <span style={{
      display: "inline-flex", alignItems: "center", gap: 6,
      padding: "3px 10px", borderRadius: 4,
      background: v.bg, color: v.fg,
      fontFamily: "var(--font-sans)", fontSize: 13, fontWeight: 500, lineHeight: "20px",
    }}>
      <span style={{ width: 6, height: 6, borderRadius: "50%", background: v.dot }} />
      {v.label}
    </span>
  );
}

function ChannelSpark({ data, color = "#7367F0", width = 84, height = 28 }) {
  const w = width, h = height;
  const max = Math.max(...data, 1);
  const pts = data.map((d, i) => {
    const x = (i / (data.length - 1)) * w;
    const y = h - (d / max) * (h - 2) - 1;
    return `${x},${y}`;
  });
  const area = `0,${h} ${pts.join(" ")} ${w},${h}`;
  const gid = `spark-${color.replace("#","")}`;
  return (
    <svg width={w} height={h} style={{ display: "block" }}>
      <defs>
        <linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor={color} stopOpacity="0.24" />
          <stop offset="100%" stopColor={color} stopOpacity="0" />
        </linearGradient>
      </defs>
      <polygon points={area} fill={`url(#${gid})`} />
      <polyline points={pts.join(" ")} fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function truncateWords(text, maxWords) {
  if (!text) return text;
  const words = text.split(/\s+/);
  if (words.length <= maxWords) return text;
  return words.slice(0, maxWords).join(" ") + "…";
}

function formatCompact(n) {
  if (n == null) return "";
  if (n >= 1000000) return (n / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
  if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k";
  return String(n);
}

function renderGrowth(value, sparkData) {
  if (value == null) return null;
  const positive = value > 0;
  const negative = value < 0;
  const color = positive ? "#28C76F" : negative ? "#FF4C51" : "var(--text-secondary)";
  return (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 8, height: 20 }}>
      <span style={{ fontSize: 14, fontWeight: 500, color, fontVariantNumeric: "tabular-nums", lineHeight: 1 }}>
        {positive ? "+" : ""}{value}
      </span>
      {sparkData && <ChannelSpark data={sparkData} color={negative ? "#FF4C51" : "#7367F0"} width={60} height={20} />}
    </span>
  );
}

function SortIndicator({ active, dir }) {
  const color = active ? "#7367F0" : "var(--text-secondary)";
  const opacity = active ? 1 : 0.5;
  return (
    <span style={{ display: "inline-flex", alignItems: "center", marginLeft: 4, color, opacity, fontSize: 10, lineHeight: 1, fontWeight: 700 }}>
      {active ? (dir === "asc" ? "↑" : "↓") : "↕"}
    </span>
  );
}

function MobileChannelCard({ ch, onMappingClick, onAddMapping, isLast }) {
  const handleNav = () => { window.location.href = `/channel/${ch.id}`; };
  const mappings = ch.mappings || (ch.pairedWith ? [ch.pairedWith] : []);
  const displayTitle = truncateWords(ch.title, 5);
  const subText = ch.platform === "max"
    ? (ch.chatId || (ch.id ? String(ch.id) : ""))
    : (ch.username ? `@${ch.username}` : "");

  const Stat = ({ label, value, sparkData, color }) => (
    <div style={{
      display: "flex", alignItems: "center", justifyContent: "space-between", gap: 6,
      padding: "8px 12px",
      background: "rgba(47,43,61,.03)",
      borderRadius: 6,
      minWidth: 0,
    }}>
      <span style={{ fontSize: 10, color: "var(--text-secondary)", textTransform: "uppercase", letterSpacing: "0.04em", fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", flexShrink: 1 }}>{label}</span>
      <span style={{ display: "inline-flex", alignItems: "center", gap: 6, flexShrink: 0 }}>
        <span style={{ fontSize: 13, fontWeight: 500, color: color || "var(--text-primary)", fontVariantNumeric: "tabular-nums", lineHeight: 1 }}>{value}</span>
        {sparkData && <ChannelSpark data={sparkData} color={color === "#FF4C51" ? "#FF4C51" : "#7367F0"} width={40} height={16} />}
      </span>
    </div>
  );

  const fmtGrowth = (v) => v == null ? "—" : (v > 0 ? `+${v}` : String(v));
  const colorGrowth = (v) => v > 0 ? "#28C76F" : v < 0 ? "#FF4C51" : "var(--text-secondary)";

  return (
    <div onClick={handleNav} style={{
      padding: "14px 16px", borderBottom: isLast ? "none" : "1px solid var(--divider)",
      cursor: "pointer", display: "flex", flexDirection: "column", gap: 12,
    }}>
      {/* Header: avatar + title + dots */}
      <div style={{ display: "flex", alignItems: "flex-start", gap: 12 }}>
        <ChannelAvatar initials={ch.initials} color={ch.color} platform={ch.platform} radius={10} avatar={ch.avatar} size={44} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div title={ch.title} style={{ fontSize: 15, fontWeight: 500, color: "var(--text-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", lineHeight: 1.25 }}>{displayTitle}</div>
          <div style={{ fontSize: 13, color: "var(--text-secondary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", marginTop: 2, fontFamily: ch.platform === "max" ? "var(--font-mono)" : "inherit" }}>{subText}</div>
        </div>
        <div onClick={e => e.stopPropagation()}>
          <Dropdown align="right" width={200} trigger={<window.IconButton icon="dots-vertical" size={34} />}>
            <DropdownItem icon="eye" onClick={() => { window.location.href = `/channel/${ch.id}`; }}>Открыть канал</DropdownItem>
            <DropdownItem icon="repeat" onClick={() => { window.location.href = `/channel/${ch.id}?action=create-mapping`; }}>Создать связку</DropdownItem>
            <DropdownItem icon="settings" onClick={() => { window.location.href = `/channel/${ch.id}?tab=settings`; }}>Настройки</DropdownItem>
            <DropdownDivider />
            <DropdownItem icon="trash" danger onClick={() => { if (window.confirm(`Удалить канал «${ch.title}» из Postilo?`)) alert("Удаление (демо)"); }}>Удалить канал</DropdownItem>
          </Dropdown>
        </div>
      </div>

      {/* Mappings row */}
      <div onClick={e => e.stopPropagation()} style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
        <span style={{ fontSize: 11, color: "var(--text-secondary)", textTransform: "uppercase", letterSpacing: "0.04em", fontWeight: 500, marginRight: 4 }}>Кросспостинг:</span>
        {mappings.length > 0 ? mappings.map(m => (
          <button key={m.id} onClick={e => { e.stopPropagation(); if (onMappingClick) onMappingClick(ch, m); }}
            style={{
              display: "inline-flex", alignItems: "center", gap: 6, padding: "2px 8px 2px 2px",
              background: "rgba(47,43,61,.05)", border: "none", borderRadius: 4, height: 22, cursor: "pointer",
              maxWidth: "100%",
            }}>
            <PlatformGlyph platform={m.platform} size={16} />
            <span style={{ fontSize: 12, fontWeight: 500, color: "var(--text-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", lineHeight: 1 }}>{truncateWords(m.title, 5)}</span>
            {m.status === "paused" && (
              <svg width="9" height="9" viewBox="0 0 24 24" fill="#FF4C51" style={{ flexShrink: 0 }} aria-label="Связка на паузе">
                <rect x="6" y="4" width="4" height="16" rx="1" />
                <rect x="14" y="4" width="4" height="16" rx="1" />
              </svg>
            )}
          </button>
        )) : <AddMappingButton ch={ch} onAddMapping={onAddMapping} />}
      </div>

      {/* Subs + views — 2 cols */}
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }}>
        <Stat label="Подписчики" value={ch.subscribers} />
        <Stat label="Просмотров / сутки" value={(function(n){ if (n>=1000) return (n/1000).toFixed(1).replace(/\.0$/,"")+"k"; return n; })(ch.views24h||0)} />
      </div>
      {/* Growth — 3 cols в один ряд */}
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 6 }}>
        <Stat label="Сегодня" value={fmtGrowth(ch.growth?.today)} color={colorGrowth(ch.growth?.today)} sparkData={ch.spark?.today} />
        <Stat label="Неделя"  value={fmtGrowth(ch.growth?.week)}  color={colorGrowth(ch.growth?.week)}  sparkData={ch.spark?.week} />
        <Stat label="Месяц"   value={fmtGrowth(ch.growth?.month)} color={colorGrowth(ch.growth?.month)} sparkData={ch.spark?.month} />
      </div>
    </div>
  );
}

function AddMappingButton({ ch, onAddMapping }) {
  const [hover, setHover] = React.useState(false);
  return (
    <button
      onClick={(e) => { e.stopPropagation(); if (onAddMapping) onAddMapping(ch); }}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      title="Создать связку"
      aria-label="Создать связку"
      style={{
        display: "inline-flex", alignItems: "center", justifyContent: "center",
        width: 22, height: 22, padding: 0,
        background: hover ? "rgba(115,103,240,.16)" : "rgba(47,43,61,.05)",
        border: "none", borderRadius: 4, cursor: "pointer",
        color: hover ? "#7367F0" : "var(--text-secondary)",
        transition: "background .12s, color .12s",
      }}
    >
      <TIP n="plus" size={14} strokeWidth={2.4} />
    </button>
  );
}

function ChannelRow({ ch, density, radius, striped, onClick, isLast, onMappingClick, onAddMapping }) {
  const vpad = density === "compact" ? 10 : density === "spacious" ? 18 : 14;
  const [hover, setHover] = React.useState(false);
  const [pillHoverId, setPillHoverId] = React.useState(null);
  const bg = hover ? "rgba(47,43,61,0.03)" : (striped ? "rgba(47,43,61,0.02)" : "transparent");
  const cell = { padding: `${vpad}px 20px`, borderBottom: isLast ? "none" : "1px solid var(--divider)", verticalAlign: "middle" };
  const handleClick = onClick || (() => { window.location.href = `/channel/${ch.id}`; });
  const mappings = ch.mappings || (ch.pairedWith ? [ch.pairedWith] : []);
  const displayTitle = truncateWords(ch.title, 5);
  const MAPPING_VISIBLE = 1;
  const [mappingsExpanded, setMappingsExpanded] = React.useState(false);
  const [moreHover, setMoreHover] = React.useState(false);
  const visibleMappings = mappingsExpanded ? mappings : mappings.slice(0, MAPPING_VISIBLE);
  const hiddenCount = Math.max(0, mappings.length - MAPPING_VISIBLE);
  return (
    <tr
      onClick={handleClick}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      style={{ transition: "background .12s", cursor: "pointer", background: bg }}
    >
      <td style={cell}>
        <div style={{ display: "flex", alignItems: "center", gap: 14, minWidth: 0 }}>
          <ChannelAvatar initials={ch.initials} color={ch.color} platform={ch.platform} radius={radius} avatar={ch.avatar} />
          <div style={{ minWidth: 0 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
              <span title={ch.title} style={{ fontSize: 15, fontWeight: 500, color: "var(--text-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{displayTitle}</span>
            </div>
            <div style={{ fontSize: 13, color: "var(--text-secondary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: ch.platform === "max" ? "var(--font-mono)" : "inherit" }}>
              {ch.platform === "max"
                ? (ch.chatId || (ch.id ? String(ch.id) : ""))
                : (ch.username ? `@${ch.username}` : "")}
            </div>
          </div>
        </div>
      </td>
      <td style={cell} onClick={e => e.stopPropagation()}>
        {mappings.length > 0 ? (
          <div style={{
            display: "flex",
            flexDirection: mappingsExpanded ? "column" : "row",
            alignItems: mappingsExpanded ? "flex-start" : "center",
            gap: mappingsExpanded ? 3 : 6,
            maxWidth: "100%",
          }}>
            {visibleMappings.map((m) => {
              const isHover = pillHoverId === m.id;
              return (
                <button
                  key={m.id}
                  onClick={(e) => { e.stopPropagation(); if (onMappingClick) onMappingClick(ch, m); }}
                  onMouseEnter={() => setPillHoverId(m.id)}
                  onMouseLeave={() => setPillHoverId(null)}
                  style={{
                    display: "inline-flex", alignItems: "center", gap: 6, padding: "2px 8px 2px 2px",
                    background: isHover ? "rgba(115,103,240,.16)" : "rgba(47,43,61,.05)",
                    border: "none",
                    borderRadius: 4, maxWidth: "100%", minWidth: 0, cursor: "pointer", height: 22,
                    transition: "background .12s",
                    fontFamily: "var(--font-sans)",
                  }}
                  title="Открыть настройки связки"
                >
                  <PlatformGlyph platform={m.platform} size={16} />
                  <span title={m.title} style={{ fontSize: 12, fontWeight: 500, color: "var(--text-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", lineHeight: 1 }}>{truncateWords(m.title, 5)}</span>
                  {m.status === "paused" && (
                    <svg width="9" height="9" viewBox="0 0 24 24" fill="#FF4C51" style={{ flexShrink: 0 }} aria-label="Связка на паузе">
                      <rect x="6" y="4" width="4" height="16" rx="1" />
                      <rect x="14" y="4" width="4" height="16" rx="1" />
                    </svg>
                  )}
                </button>
              );
            })}
            {hiddenCount > 0 && !mappingsExpanded && (
              <button
                onClick={(e) => { e.stopPropagation(); setMappingsExpanded(true); }}
                onMouseEnter={() => setMoreHover(true)}
                onMouseLeave={() => setMoreHover(false)}
                title={`Показать ещё ${hiddenCount}: ` + mappings.slice(MAPPING_VISIBLE).map(m => m.title).join(", ")}
                style={{
                  flexShrink: 0, border: "none", cursor: "pointer",
                  display: "inline-flex", alignItems: "center", justifyContent: "center",
                  padding: "0 8px", height: 22, minWidth: 22,
                  background: moreHover ? "rgba(115,103,240,.2)" : "rgba(115,103,240,.12)",
                  color: "#7367F0", borderRadius: 4,
                  fontFamily: "var(--font-sans)", fontSize: 11, fontWeight: 600,
                  letterSpacing: "0.02em", lineHeight: 1,
                  transition: "background .12s",
                }}
              >+{hiddenCount}</button>
            )}
            {mappingsExpanded && mappings.length > MAPPING_VISIBLE && (
              <button
                onClick={(e) => { e.stopPropagation(); setMappingsExpanded(false); }}
                onMouseEnter={() => setMoreHover(true)}
                onMouseLeave={() => setMoreHover(false)}
                style={{
                  display: "inline-flex", alignItems: "center", gap: 4,
                  padding: "2px 6px", border: "none", background: "transparent",
                  color: moreHover ? "#7367F0" : "var(--text-secondary)",
                  fontFamily: "var(--font-sans)", fontSize: 11, fontWeight: 500,
                  cursor: "pointer", transition: "color .12s",
                }}
              >свернуть</button>
            )}
          </div>
        ) : (
          <AddMappingButton ch={ch} onAddMapping={onAddMapping} />
        )}
      </td>
      <td style={cell}>
        <span style={{ display: "inline-flex", alignItems: "center", gap: 6, height: 20, fontSize: 14, fontWeight: 500, color: "var(--text-primary)", lineHeight: 1, fontVariantNumeric: "tabular-nums" }}>
          <span style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", width: 16, height: 20 }}>
            <TIP n="user" size={16} color="var(--text-secondary)" strokeWidth={2.4} />
          </span>
          <span style={{ lineHeight: 1, position: "relative", top: 2 }}>{ch.subscribers}</span>
        </span>
      </td>
      <td style={cell}>{renderGrowth(ch.growth?.today, ch.spark?.today || ch.activity)}</td>
      <td style={cell}>{renderGrowth(ch.growth?.week,  ch.spark?.week  || ch.activity)}</td>
      <td style={cell}>{renderGrowth(ch.growth?.month, ch.spark?.month || ch.activity)}</td>
      <td style={cell} className="vx-hide-mobile">
        <span title="Постов опубликовано за сутки" style={{ display: "inline-flex", alignItems: "center", height: 20, fontSize: 14, fontWeight: 500, color: ch.views24h != null ? "var(--text-primary)" : "var(--text-secondary)", fontVariantNumeric: "tabular-nums", lineHeight: 1 }}>
          {ch.views24h != null ? formatCompact(ch.views24h) : "—"}
        </span>
      </td>
      <td style={{ ...cell, padding: `${vpad - 6}px 16px`, textAlign: "right" }} onClick={e => e.stopPropagation()}>
        <Dropdown align="right" width={200} trigger={
          <window.IconButton icon="dots-vertical" size={34} />
        }>
          <DropdownItem icon="eye" onClick={() => { window.location.href = `/channel/${ch.id}`; }}>Открыть канал</DropdownItem>
          <DropdownItem icon="repeat" onClick={() => { window.location.href = `/channel/${ch.id}?action=create-mapping`; }}>Создать связку</DropdownItem>
          <DropdownItem icon="settings" onClick={() => { window.location.href = `/channel/${ch.id}?tab=settings`; }}>Настройки</DropdownItem>
          <DropdownDivider />
          <DropdownItem icon="trash" danger onClick={() => { if (window.confirm(`Удалить канал «${ch.title}» из Postilo?`)) alert("Удаление (демо)"); }}>Удалить канал</DropdownItem>
        </Dropdown>
      </td>
    </tr>
  );
}

// ==================== SHARED SHELL ====================

const DEFAULT_NOTIFICATIONS = [
  { id: 1, kind: "error",   icon: "alert-triangle", iconBg: "rgba(255,76,81,.14)",  iconFg: "#FF4C51",
    title: "Crypto Insights — ошибка репоста",
    desc: "MAX API вернул 429 Too Many Requests. Повторная попытка через 5 мин.",
    time: "5 мин назад", unread: true, href: "/channel/6" },
  { id: 2, kind: "success", icon: "circle-check",   iconBg: "rgba(40,199,111,.14)", iconFg: "#28C76F",
    title: "Новый канал подключён",
    desc: "Daily Deals успешно синхронизирован и готов к работе.",
    time: "1 ч назад",    unread: true, href: "/channel/10" },
  { id: 3, kind: "info",    icon: "repeat",         iconBg: "rgba(115,103,240,.14)", iconFg: "#7367F0",
    title: "48 репостов за сутки",
    desc: "Рекорд за последние 30 дней. Посмотреть статистику?",
    time: "6 ч назад",    unread: false, href: "/calendar" },
  { id: 4, kind: "warning", icon: "shield-check",   iconBg: "rgba(255,159,67,.14)", iconFg: "#FF9F43",
    title: "Антиспам сработал 12 раз",
    desc: "За сегодня во всех каналах. 3 флуда, 7 ссылок, 2 бота.",
    time: "вчера",        unread: false, href: "/channel/1#antispam" },
  { id: 5, kind: "info",    icon: "brand-telegram", iconBg: "rgba(34,158,217,.14)", iconFg: "#229ED9",
    title: "Бот обновлён",
    desc: "Postilo v2.4 — улучшенная обработка медиа-групп.",
    time: "2 дня назад",  unread: false, href: "/settings" },
];

function NotificationBell({ size = 34, placement = "auto" }) {
  const [open, setOpen] = React.useState(false);
  const [items, setItems] = React.useState(DEFAULT_NOTIFICATIONS);
  const [flipUp, setFlipUp] = React.useState(false);
  const [anchorLeft, setAnchorLeft] = React.useState(false);
  const ref = React.useRef();
  const unread = items.filter(n => n.unread).length;

  React.useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, [open]);

  // spec 004 — live fetch errors из /api/log_notifications. Preview-mode → mock.
  React.useEffect(() => {
    const apiKey = (typeof localStorage !== "undefined") ? (localStorage.getItem("api_key") || "") : "";
    if (!apiKey || !window.INITIAL_DATA) return;
    fetch("/api/log_notifications?limit=10", { headers: { "X-API-Key": apiKey } })
      .then(r => r.ok ? r.json() : Promise.reject(r.status))
      .then(d => {
        if (!Array.isArray(d.items)) return;
        setItems(d.items.map(it => ({
          id: it.id,
          kind: "error",
          icon: "alert-triangle",
          iconBg: "rgba(255,76,81,.14)",
          iconFg: "#FF4C51",
          title: `Ошибка репоста: ${it.mapping_label || `Связка #${it.mapping_id}`}`,
          desc: it.error_text || "Без описания",
          time: it.created_at ? String(it.created_at).slice(5, 16) : "—",
          unread: true,
          href: "/logs",
        })));
      })
      .catch(e => console.error("[design-v2] NotificationBell fetch failed:", e));
  }, []);

  // Auto-detect vertical flip + horizontal anchor based on viewport space
  React.useEffect(() => {
    if (!open || !ref.current) return;
    const rect = ref.current.getBoundingClientRect();
    const dropdownWidth = 360;
    const dropdownHeight = 480;

    // Vertical: flip upward if not enough space below
    if (placement === "auto") {
      const spaceBelow = window.innerHeight - rect.bottom;
      setFlipUp(spaceBelow < dropdownHeight && rect.top > spaceBelow);
    }

    // Horizontal: pick side with less overflow.
    //   right-anchored: dropdown spans [rect.right - W, rect.right]
    //   left-anchored:  dropdown spans [rect.left, rect.left + W]
    const overflowWithRight = Math.max(0, 8 - (rect.right - dropdownWidth));
    const overflowWithLeft  = Math.max(0, (rect.left + dropdownWidth) - (window.innerWidth - 8));
    setAnchorLeft(overflowWithLeft < overflowWithRight);
  }, [open, placement]);

  const dropUp = placement === "up" || (placement === "auto" && flipUp);

  const markAllRead = () => setItems(items.map(n => ({ ...n, unread: false })));

  return (
    <div ref={ref} style={{ position: "relative", flexShrink: 0 }}>
      <button onClick={() => setOpen(v => !v)}
        aria-label={unread > 0 ? `Уведомления (${unread} новых)` : "Уведомления"}
        aria-expanded={open} style={{
        width: size, height: size, padding: 0,
        background: open ? "rgba(115,103,240,.08)" : "transparent",
        border: "1px solid var(--divider)", borderRadius: 6,
        color: "var(--text-primary)", cursor: "pointer", position: "relative",
        display: "inline-flex", alignItems: "center", justifyContent: "center",
        transition: "background .12s",
      }}
        onMouseEnter={(e) => { if (!open) e.currentTarget.style.background = "rgba(47,43,61,.04)"; }}
        onMouseLeave={(e) => { if (!open) e.currentTarget.style.background = "transparent"; }}>
        <TIP n="bell" size={Math.max(16, size - 18)} />
        {unread > 0 && (
          <span style={{
            position: "absolute", top: 4, right: 4, minWidth: 14, height: 14,
            padding: "0 4px", borderRadius: 7, background: "#FF4C51",
            color: "#fff", fontSize: 9, fontWeight: 600, lineHeight: "14px",
            textAlign: "center", boxShadow: "0 0 0 2px #fff",
          }}>{unread > 9 ? "9+" : unread}</span>
        )}
      </button>

      {open && (
        <div style={{
          position: "absolute",
          [dropUp ? "bottom" : "top"]: "calc(100% + 8px)",
          [anchorLeft ? "left" : "right"]: 0,
          zIndex: 250,
          width: 360, maxWidth: "calc(100vw - 24px)", maxHeight: 480,
          background: "#fff", borderRadius: 10,
          boxShadow: "0 16px 40px -8px rgba(47,43,61,.18), 0 0 0 1px rgba(47,43,61,.06)",
          display: "flex", flexDirection: "column", overflow: "hidden",
          animation: "fadeIn .15s linear",
        }}>
          <div style={{
            padding: "14px 16px", borderBottom: "1px solid var(--divider)",
            display: "flex", alignItems: "center", justifyContent: "space-between",
          }}>
            <div>
              <div style={{ fontSize: 15, fontWeight: 600, color: "var(--text-primary)" }}>Уведомления</div>
              <div style={{ fontSize: 12, color: "var(--text-secondary)", marginTop: 2 }}>
                {unread > 0 ? `${unread} новых` : "Всё прочитано"}
              </div>
            </div>
            {unread > 0 && (
              <button onClick={markAllRead} style={{
                background: "transparent", border: "none", cursor: "pointer",
                color: "#7367F0", fontSize: 12, fontWeight: 500, padding: 4,
                borderRadius: 4, transition: "background .12s",
              }}
                onMouseEnter={(e) => e.currentTarget.style.background = "rgba(115,103,240,.08)"}
                onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}
              >Прочитать все</button>
            )}
          </div>

          <div style={{ flex: 1, overflowY: "auto" }}>
            {items.length === 0 ? (
              <div style={{ padding: "40px 20px", textAlign: "center" }}>
                <TIP n="bell" size={36} color="var(--text-secondary)" />
                <div style={{ fontSize: 14, fontWeight: 500, color: "var(--text-primary)", marginTop: 10 }}>
                  Пока тихо
                </div>
                <div style={{ fontSize: 12, color: "var(--text-secondary)", marginTop: 4 }}>
                  Здесь будут события по вашим каналам
                </div>
              </div>
            ) : items.map((n, i) => (
              <a key={n.id} href={n.href} style={{
                display: "flex", gap: 12, padding: "12px 16px",
                borderTop: i === 0 ? "none" : "1px solid var(--divider)",
                textDecoration: "none", color: "inherit",
                background: n.unread ? "rgba(115,103,240,.04)" : "transparent",
                position: "relative", transition: "background .12s",
              }}
              onMouseEnter={e => e.currentTarget.style.background = "rgba(47,43,61,.03)"}
              onMouseLeave={e => e.currentTarget.style.background = n.unread ? "rgba(115,103,240,.04)" : "transparent"}
              >
                {n.unread && (
                  <span style={{
                    position: "absolute", top: "50%", left: 5, transform: "translateY(-50%)",
                    width: 6, height: 6, borderRadius: "50%", background: "#7367F0",
                  }} />
                )}
                <div style={{
                  width: 36, height: 36, borderRadius: 8, flexShrink: 0,
                  background: n.iconBg, color: n.iconFg,
                  display: "flex", alignItems: "center", justifyContent: "center",
                }}>
                  <TIP n={n.icon} size={18} color={n.iconFg} />
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{
                    fontSize: 13, fontWeight: n.unread ? 500 : 400,
                    color: "var(--text-primary)", lineHeight: 1.35, marginBottom: 2,
                  }}>{n.title}</div>
                  <div style={{ fontSize: 12, color: "var(--text-secondary)", lineHeight: 1.4,
                    overflow: "hidden", display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical",
                  }}>{n.desc}</div>
                  <div style={{ fontSize: 11, color: "var(--text-secondary)", marginTop: 4 }}>{n.time}</div>
                </div>
              </a>
            ))}
          </div>

          <div style={{
            padding: "10px 16px", borderTop: "1px solid var(--divider)",
            background: "rgba(47,43,61,0.02)", textAlign: "center",
          }}>
            <a href="/notifications" style={{
              fontSize: 13, color: "#7367F0", textDecoration: "none", fontWeight: 500,
            }}
              onMouseEnter={(e) => e.currentTarget.style.textDecoration = "underline"}
              onMouseLeave={(e) => e.currentTarget.style.textDecoration = "none"}
            >Все уведомления →</a>
          </div>
        </div>
      )}
    </div>
  );
}

function PostiloSidebar({ active, onNavigate, counts = {}, accent = "#7367F0", user = { name: "Артём Ковалёв", role: "Владелец · Pro", initials: "АК" } }) {
  const [mobileOpen, setMobileOpen] = React.useState(false);
  // spec 004 — counts из backend (TG + MAX каналы суммарно).
  // Preview-режим (без INITIAL_DATA) — fallback на mock counts из props.
  const [liveCounts, setLiveCounts] = React.useState(null);
  const [liveUser, setLiveUser] = React.useState(null);
  React.useEffect(() => {
    const apiKey = (typeof localStorage !== "undefined") ? (localStorage.getItem("api_key") || "") : "";
    if (!apiKey || !window.INITIAL_DATA) return;
    const headers = { "X-API-Key": apiKey };
    Promise.all([
      fetch("/api/tg_channels?fast=1", { headers }).then(r => r.ok ? r.json() : []).catch(() => []),
      fetch("/api/max_channels?fast=1", { headers }).then(r => r.ok ? r.json() : []).catch(() => []),
    ]).then(([tg, mx]) => {
      setLiveCounts({
        channels: (Array.isArray(tg) ? tg.length : 0) + (Array.isArray(mx) ? mx.length : 0),
      });
    }).catch(() => {});
    fetch("/api/me", { headers }).then(r => r.ok ? r.json() : null)
      .then(d => {
        if (!d) return;
        const initials = (d.tg_first_name || d.username || d.email || "?").trim();
        const ini = initials.split(/\s+/).map(w => w[0] || "").join("").slice(0, 2).toUpperCase() || "?";
        setLiveUser({
          name: d.tg_first_name || d.username || d.email || "",
          role: d.is_admin ? "Владелец · " + (d.plan_tier || "free").toUpperCase() : (d.plan_tier || "free").toUpperCase(),
          initials: ini,
        });
      }).catch(() => {});
  }, []);
  const c = { channels: counts.channels, calendar: counts.calendar, ...(liveCounts || {}) };
  const u = liveUser || user;
  // Main workspace navigation — задачи, с которыми пользователь работает каждый день
  const items = [
    { id: "channels",  icon: "hash",           label: "Каналы",    count: c.channels, href: "/channels" },
    { id: "calendar",  icon: "calendar-event", label: "Календарь", count: c.calendar, href: "/calendar" },
  ];
  return (
    <>
      {/* Mobile topbar (only <900px) */}
      <div className="mobile-topbar" style={{
        display: "none",
        position: "fixed", top: 0, left: 0, right: 0, zIndex: 40,
        maxWidth: "100%",
        height: 56, background: "#fff",
        borderBottom: "1px solid var(--divider)",
        padding: "0 16px", alignItems: "center", gap: 12,
      }}>
        <button onClick={() => setMobileOpen(true)} aria-label="Открыть меню" style={{
          width: 40, height: 40, borderRadius: 6, border: "none",
          background: "transparent", cursor: "pointer", color: "var(--text-primary)",
          display: "flex", alignItems: "center", justifyContent: "center",
          flexShrink: 0, transition: "background .12s",
        }}
          onMouseEnter={(e) => e.currentTarget.style.background = "rgba(47,43,61,.06)"}
          onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}
        ><TIP n="menu-2" size={22} /></button>
        <PostiloMark size={32} gradientFrom={accent} gradientTo={accent + "cc"} />
        <div style={{ display: "flex", alignItems: "center", lineHeight: 1.1, flex: 1, minWidth: 0 }}>
          <span style={{ fontSize: 18, fontWeight: 700, letterSpacing: "-0.02em", color: "var(--text-primary)" }}>Postilo</span>
        </div>
        <NotificationBell size={36} />
      </div>

      {/* Backdrop when drawer open */}
      {mobileOpen && (
        <div onClick={() => setMobileOpen(false)} className="sidebar-backdrop" style={{
          position: "fixed", inset: 0, zIndex: 48,
          background: "rgba(47,43,61,.48)", backdropFilter: "blur(2px)",
          animation: "fadeIn .15s linear",
        }} />
      )}

      <aside className={`postilo-sidebar ${mobileOpen ? "open" : ""}`} style={{
        width: 264, background: "#fff",
        borderRight: "1px solid var(--divider)",
        display: "flex", flexDirection: "column",
        flexShrink: 0, height: "100vh",
        position: "sticky", top: 0, zIndex: 50,
      }}>
      <div style={{ display: "flex", alignItems: "center", gap: 12, padding: "22px 20px 18px" }}>
        <PostiloMark size={36} gradientFrom={accent} gradientTo={accent + "cc"} />
        <div style={{ display: "flex", alignItems: "center", lineHeight: 1.1 }}>
          <span style={{ fontSize: 22, fontWeight: 700, letterSpacing: "-0.02em", color: "var(--text-primary)" }}>Postilo</span>
        </div>
      </div>

      <div style={{ padding: "4px 14px 8px" }}>
        {items.map(it => <SidebarLink key={it.id} item={it} active={active === it.id} onNavigate={onNavigate} accent={accent} />)}
      </div>

      <div style={{ flex: 1 }} />

      <UserMenu user={u} accent={accent} />
      </aside>
    </>
  );
}

function UserMenu({ user, accent = "#7367F0" }) {
  const [open, setOpen]     = React.useState(false);
  const [logout, setLogout] = React.useState(false);
  const ref = React.useRef();

  React.useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, [open]);

  const itemSt = {
    display: "flex", alignItems: "center", gap: 10, width: "100%",
    padding: "9px 12px", background: "transparent", border: "none",
    borderRadius: 4, cursor: "pointer", textAlign: "left",
    fontFamily: "var(--font-sans)", fontSize: 13, fontWeight: 400,
    color: "var(--text-primary)", textDecoration: "none",
  };

  return (
    <div style={{
      borderTop: "1px solid var(--divider)",
      background: "#fff",
      display: "flex", alignItems: "center", gap: 6, padding: "10px 12px",
    }}>
      <div ref={ref} style={{ position: "relative", flex: 1, minWidth: 0 }}>
        <button onClick={() => setOpen(v => !v)} style={{
          display: "flex", alignItems: "center", gap: 10, width: "100%",
          padding: "6px 8px", background: open ? "rgba(115,103,240,.06)" : "transparent",
          border: "none", cursor: "pointer", textAlign: "left", borderRadius: 6,
          transition: "background .12s",
          fontFamily: "var(--font-sans)",
        }}
          onMouseEnter={e => { if (!open) e.currentTarget.style.background = "rgba(47,43,61,.03)"; }}
          onMouseLeave={e => { if (!open) e.currentTarget.style.background = "transparent"; }}
        >
          <Avatar initials={user.initials} size={34} color={accent} status="#28C76F" />
          <div style={{ flex: 1, minWidth: 0, lineHeight: 1.2 }}>
            <div style={{ fontSize: 13, fontWeight: 500, color: "var(--text-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{user.name}</div>
            <div style={{ fontSize: 11, color: "var(--text-secondary)" }}>{user.role}</div>
          </div>
          <TIP n={open ? "chevron-down" : "chevron-up"} size={14} color="var(--text-secondary)" />
        </button>

        {open && (
          <div style={{
            position: "absolute", bottom: "calc(100% + 6px)", left: 0, right: 0, zIndex: 250,
            background: "#fff", borderRadius: 8, padding: 6,
            boxShadow: "0 16px 40px -8px rgba(47,43,61,.18), 0 0 0 1px rgba(47,43,61,.06)",
            animation: "fadeIn .15s linear",
          }}>
            <div style={{
              padding: "10px 12px 8px", borderBottom: "1px solid var(--divider)", marginBottom: 4,
            }}>
              <div style={{ fontSize: 13, fontWeight: 500, color: "var(--text-primary)" }}>{user.name}</div>
              <div style={{ fontSize: 11, color: "var(--text-secondary)", marginTop: 2 }}>admin@postilo.app</div>
            </div>
            <a href="/profile" style={itemSt}
              onMouseEnter={e => e.currentTarget.style.background = "rgba(47,43,61,.05)"}
              onMouseLeave={e => e.currentTarget.style.background = "transparent"}>
              <TIP n="user" size={14} color="var(--text-secondary)" />
              Профиль
            </a>
            <a href="/settings" style={itemSt}
              onMouseEnter={e => e.currentTarget.style.background = "rgba(47,43,61,.05)"}
              onMouseLeave={e => e.currentTarget.style.background = "transparent"}>
              <TIP n="settings" size={14} color="var(--text-secondary)" />
              Настройки
            </a>
            <a href="/help" style={itemSt}
              onMouseEnter={e => e.currentTarget.style.background = "rgba(47,43,61,.05)"}
              onMouseLeave={e => e.currentTarget.style.background = "transparent"}>
              <TIP n="help-circle" size={14} color="var(--text-secondary)" />
              Справка
            </a>
            <div style={{ height: 1, background: "var(--divider)", margin: "4px 0" }} />
            <button style={{ ...itemSt, color: "#BF393D" }}
              onClick={() => { setOpen(false); setLogout(true); }}
              onMouseEnter={e => e.currentTarget.style.background = "rgba(255,76,81,.06)"}
              onMouseLeave={e => e.currentTarget.style.background = "transparent"}>
              <TIP n="logout" size={14} color="#FF4C51" />
              Выйти
            </button>
          </div>
        )}
      </div>

      <NotificationBell size={36} />

      {logout && <LogoutModal onClose={() => setLogout(false)} />}
    </div>
  );
}

function LogoutModal({ onClose }) {
  return (
    <div className="modal-scrim" style={{
      position: "fixed", inset: 0, zIndex: 300,
      background: "rgba(47,43,61,.48)", backdropFilter: "blur(4px)",
      display: "flex", alignItems: "center", justifyContent: "center", padding: 20,
      animation: "fadeIn .15s linear",
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        width: "100%", maxWidth: 420,
        background: "#fff", borderRadius: 6,
        boxShadow: "0 24px 48px -12px rgba(47,43,61,.32), 0 0 0 1px rgba(47,43,61,.06)",
        overflow: "hidden",
        animation: "modalIn .22s cubic-bezier(0.4,0,0.2,1)",
      }}>
        <div style={{ padding: "22px 24px 18px", textAlign: "center" }}>
          <div style={{
            width: 56, height: 56, borderRadius: 14, margin: "0 auto 14px",
            background: "rgba(255,76,81,.12)", color: "#FF4C51",
            display: "flex", alignItems: "center", justifyContent: "center",
          }}>
            <TIP n="logout" size={26} color="#FF4C51" />
          </div>
          <div style={{ fontSize: 17, fontWeight: 600, color: "var(--text-primary)", marginBottom: 6 }}>
            Выйти из аккаунта?
          </div>
          <div style={{ fontSize: 13, color: "var(--text-secondary)", lineHeight: 1.5 }}>
            Репосты продолжат работать в фоне. Вы сможете войти снова в любой момент.
          </div>
        </div>
        <div style={{
          padding: "14px 24px", borderTop: "1px solid var(--divider)",
          display: "flex", gap: 10, background: "var(--bg-body)",
        }}>
          <button onClick={onClose} style={{
            flex: 1, padding: "10px 14px", background: "#fff",
            border: "1px solid var(--divider)", borderRadius: 6,
            color: "var(--text-primary)", fontFamily: "var(--font-sans)",
            fontSize: 13, fontWeight: 500, cursor: "pointer",
            transition: "background .12s",
          }}
            onMouseEnter={(e) => e.currentTarget.style.background = "rgba(47,43,61,.03)"}
            onMouseLeave={(e) => e.currentTarget.style.background = "#fff"}
          >Отмена</button>
          <a href="/login" style={{
            flex: 1, padding: "10px 14px", background: "#FF4C51",
            border: "1px solid #FF4C51", borderRadius: 6,
            color: "#fff", fontFamily: "var(--font-sans)",
            fontSize: 13, fontWeight: 500, textDecoration: "none",
            textAlign: "center",
            boxShadow: "0 2px 4px rgba(255,76,81,.3)",
            transition: "background .12s, border-color .12s",
          }}
            onMouseEnter={(e) => { e.currentTarget.style.background = "#E54248"; e.currentTarget.style.borderColor = "#E54248"; }}
            onMouseLeave={(e) => { e.currentTarget.style.background = "#FF4C51"; e.currentTarget.style.borderColor = "#FF4C51"; }}
          >Выйти</a>
        </div>
      </div>
    </div>
  );
}

function SidebarLink({ item, active, onNavigate, accent }) {
  const [hover, setHover] = React.useState(false);
  const clickHandler = item.href ? undefined : (e) => { if (onNavigate) onNavigate(item.id); };
  return (
    <a href={item.href || "#"}
      onClick={clickHandler}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{
        display: "flex", alignItems: "center", gap: 12,
        padding: "10px 14px", borderRadius: 8, cursor: "pointer",
        color: active ? "#fff" : "var(--text-primary)",
        background: active ? `linear-gradient(270deg, ${accent} 0%, ${accent}cc 100%)`
                   : hover ? "rgba(47,43,61,0.06)" : "transparent",
        boxShadow: active ? `0 2px 4px ${accent}4D` : "none",
        marginBottom: 2, transition: "background .15s, color .15s",
        fontSize: 15, lineHeight: "22px", fontWeight: active ? 500 : 400,
        textDecoration: "none",
      }}>
      <TIP n={item.icon} size={20} />
      <span style={{ flex: 1 }}>{item.label}</span>
      {item.count != null && (
        <span style={{
          fontSize: 11, fontWeight: 500, padding: "1px 8px", borderRadius: 10,
          background: active ? "rgba(255,255,255,.18)" : "rgba(47,43,61,.08)",
          color: active ? "#fff" : "var(--text-secondary)",
        }}>{item.count}</span>
      )}
      {item.badge && (
        <span style={{
          background: active ? "rgba(255,255,255,.22)" : "rgba(40,199,111,.16)",
          color: active ? "#fff" : "#1E9553",
          fontSize: 10, fontWeight: 600, padding: "2px 7px", borderRadius: 10, letterSpacing: ".3px",
        }}>{item.badge}</span>
      )}
    </a>
  );
}

function PageHeader({ title, subtitle, right, children }) {
  return (
    <div style={{ marginBottom: 20, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 20, flexWrap: "wrap" }}>
      <div>
        <h1 style={{
          fontSize: 28, fontWeight: 600, letterSpacing: "-0.02em",
          color: "var(--text-primary)", lineHeight: 1.2, margin: 0,
        }}>{title}</h1>
        {subtitle && <div style={{ fontSize: 14, color: "var(--text-secondary)", marginTop: 6, maxWidth: 640 }}>{subtitle}</div>}
        {children}
      </div>
      {right && <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>{right}</div>}
    </div>
  );
}

// Inline stats row without card wrappers — matches Vuexy Academy / Learning dashboard pattern
// (icon + label + value + delta inline, no white plashkas, dividers between items)
function StatsInline({ items, marginBottom = 24 }) {
  return (
    <div className="stats-inline-wrap" style={{
      display: "grid",
      gridTemplateColumns: `repeat(${items.length}, minmax(0, 1fr))`,
      marginBottom,
    }}>
      {items.map((s, i) => (
        <div key={i} style={{
          display: "flex", alignItems: "center", gap: 14,
          padding: "6px 20px 6px 0",
          borderRight: i < items.length - 1 ? "1px solid var(--divider)" : "none",
          paddingLeft: i === 0 ? 0 : 20,
          minWidth: 0,
        }}>
          <div style={{
            width: 42, height: 42, borderRadius: 6,
            background: `${s.iconColor}1F`, color: s.iconColor,
            display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
          }}>
            <TIP n={s.icon} size={22} color={s.iconColor} />
          </div>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontSize: 13, color: "var(--text-secondary)", lineHeight: 1.3 }}>{s.label}</div>
            <div style={{ display: "flex", alignItems: "baseline", gap: 8, marginTop: 2 }}>
              <div style={{ fontSize: 22, fontWeight: 600, letterSpacing: "-0.02em", color: "var(--text-primary)", lineHeight: 1.15, fontVariantNumeric: "tabular-nums" }}>{s.value}</div>
              {s.delta && <div style={{ fontSize: 12, color: "var(--text-secondary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{s.delta}</div>}
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}

// Shared Dropdown — click-outside closes, Vuexy 6px radius
function Dropdown({ trigger, children, width = 220, align = "right" }) {
  const [open, setOpen] = React.useState(false);
  const [coords, setCoords] = React.useState(null);
  const ref = React.useRef();
  const triggerRef = React.useRef();

  const compute = React.useCallback(() => {
    const tr = triggerRef.current;
    if (!tr) return;
    const r = tr.getBoundingClientRect();
    const estHeight = 280;
    const spaceBelow = window.innerHeight - r.bottom;
    const spaceAbove = r.top;
    const flipUp = spaceBelow < estHeight && spaceAbove > spaceBelow;
    const top  = flipUp ? Math.max(8, r.top - estHeight - 4) : r.bottom + 4;
    const left = align === "right" ? Math.max(8, r.right - width) : Math.min(window.innerWidth - width - 8, r.left);
    setCoords({ top, left, flipUp });
  }, [align, width]);

  React.useEffect(() => {
    if (!open) return;
    compute();
    const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onScroll = () => setOpen(false);
    document.addEventListener("mousedown", handler);
    window.addEventListener("scroll", onScroll, true);
    window.addEventListener("resize", compute);
    return () => {
      document.removeEventListener("mousedown", handler);
      window.removeEventListener("scroll", onScroll, true);
      window.removeEventListener("resize", compute);
    };
  }, [open, compute]);

  return (
    <>
      <div ref={triggerRef} style={{ display: "inline-block" }} onClick={() => setOpen(!open)}>{trigger}</div>
      {open && coords && (
        <div ref={ref} style={{
          position: "fixed",
          top: coords.top, left: coords.left, width,
          background: "#fff", border: "1px solid var(--divider)", borderRadius: 6,
          boxShadow: "0px 3px 12px 0px rgba(47,43,61,0.14)",
          padding: 4, zIndex: 1000,
          animation: "fadeIn .12s linear",
        }} onClick={() => setOpen(false)}>{children}</div>
      )}
    </>
  );
}

function DropdownItem({ icon, children, onClick, danger, active }) {
  const [hover, setHover] = React.useState(false);
  return (
    <button onClick={onClick}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{
        display: "flex", alignItems: "center", gap: 10, width: "100%",
        padding: "8px 12px", border: "none", cursor: "pointer", borderRadius: 4,
        background: active ? "rgba(115,103,240,.08)" : hover ? "rgba(47,43,61,.05)" : "transparent",
        color: danger ? "#BF393D" : active ? "#5A52C4" : "var(--text-primary)",
        fontFamily: "var(--font-sans)", fontSize: 13, fontWeight: 500,
        textAlign: "left", transition: "background .1s",
      }}>
      {icon && <TIP n={icon} size={14} />}
      <span style={{ flex: 1 }}>{children}</span>
      {active && <TIP n="circle-check" size={14} color="#5A52C4" />}
    </button>
  );
}

function DropdownDivider() {
  return <div style={{ height: 1, background: "var(--divider)", margin: "4px 0" }} />;
}

// Channel picker — dropdown select with channel list + platform glyphs
function ChannelPicker({ value, onChange, channels, placeholder = "Выбрать канал", width = "100%", searchable = true }) {
  const selected = channels.find(c => c.id === value);
  const [open, setOpen] = React.useState(false);
  const [query, setQuery] = React.useState("");
  const [coords, setCoords] = React.useState({ top: 0, left: 0, width: 0, flipUp: false });
  const ref = React.useRef();
  const searchRef = React.useRef();
  React.useEffect(() => {
    if (!open) return;
    const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", handler);
    // Auto-focus search when открывается
    if (searchable && searchRef.current) {
      setTimeout(() => searchRef.current && searchRef.current.focus(), 30);
    }
    return () => document.removeEventListener("mousedown", handler);
  }, [open, searchable]);

  // Position dropdown via fixed coords (избегает clipping в overflow:hidden модалках).
  React.useEffect(() => {
    if (!open || !ref.current) return;
    const compute = () => {
      const r = ref.current.getBoundingClientRect();
      const dropH = 360; // approx max
      const spaceBelow = window.innerHeight - r.bottom;
      const flipUp = spaceBelow < dropH && r.top > spaceBelow;
      setCoords({ top: flipUp ? r.top - 4 : r.bottom + 4, left: r.left, width: r.width, flipUp });
    };
    compute();
    window.addEventListener("scroll", compute, true);
    window.addEventListener("resize", compute);
    return () => {
      window.removeEventListener("scroll", compute, true);
      window.removeEventListener("resize", compute);
    };
  }, [open]);
  React.useEffect(() => { if (!open) setQuery(""); }, [open]);
  const filtered = React.useMemo(() => {
    const q = query.trim().toLowerCase();
    if (!q) return channels;
    return channels.filter(ch =>
      String(ch.title || "").toLowerCase().includes(q) ||
      String(ch.username || "").toLowerCase().includes(q)
    );
  }, [query, channels]);
  return (
    <div ref={ref} style={{ position: "relative", width }}>
      <div onClick={() => setOpen(!open)} style={{
        display: "flex", alignItems: "center", gap: 10, padding: "10px 12px",
        background: "#fff", border: `1px solid ${open ? "#7367F0" : "var(--input-border)"}`,
        borderRadius: 6, cursor: "pointer",
        boxShadow: open ? "0 0 0 3px rgba(115,103,240,.16)" : "none",
        transition: "border-color .12s, box-shadow .12s",
      }}>
        {selected ? (
          <>
            <PlatformGlyph platform={selected.platform} size={24} />
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 14, fontWeight: 500, color: "var(--text-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{selected.title}</div>
              {selected.username && <div style={{ fontSize: 12, color: "var(--text-secondary)" }}>@{selected.username}</div>}
            </div>
          </>
        ) : (
          <span style={{ flex: 1, fontSize: 14, color: "var(--text-secondary)" }}>{placeholder}</span>
        )}
        <TIP n="chevron-down" size={18} color="var(--text-secondary)" />
      </div>
      {open && (
        <div style={{
          position: "fixed",
          top: coords.flipUp ? "auto" : coords.top,
          bottom: coords.flipUp ? (window.innerHeight - coords.top) : "auto",
          left: coords.left, width: coords.width,
          background: "#fff", border: "1px solid var(--divider)", borderRadius: 6,
          boxShadow: "0px 3px 12px 0px rgba(47,43,61,0.14)",
          padding: 4, zIndex: 9999, maxHeight: 320, overflowY: "auto",
          animation: "fadeIn .12s linear",
        }}>
          {searchable && channels.length > 4 && (
            <div style={{ padding: "6px 6px 8px", position: "sticky", top: -4, background: "#fff", zIndex: 1, borderBottom: "1px solid var(--divider)", marginBottom: 4 }}>
              <input
                ref={searchRef}
                type="text"
                value={query}
                onChange={(e) => setQuery(e.target.value)}
                placeholder="Поиск канала…"
                style={{
                  width: "100%", padding: "8px 10px", fontSize: 13,
                  border: "1px solid #7367F0", borderRadius: 6, outline: "none",
                  fontFamily: "var(--font-sans)", color: "var(--text-primary)",
                  boxShadow: "0 0 0 3px rgba(115,103,240,.10)",
                  boxSizing: "border-box",
                }}
              />
            </div>
          )}
          {filtered.length === 0 ? (
            <div style={{ padding: "16px 12px", fontSize: 13, color: "var(--text-secondary)", textAlign: "center" }}>
              Ничего не найдено
            </div>
          ) : filtered.map(ch => {
            const isActive = ch.id === value;
            return (
              <div key={ch.id} onClick={() => { onChange(ch.id); setOpen(false); }}
                style={{
                  display: "flex", alignItems: "center", gap: 10, padding: "8px 10px",
                  borderRadius: 4, cursor: "pointer",
                  background: isActive ? "rgba(115,103,240,.08)" : "transparent",
                  transition: "background .1s",
                }}
                onMouseEnter={e => !isActive && (e.currentTarget.style.background = "rgba(47,43,61,.04)")}
                onMouseLeave={e => !isActive && (e.currentTarget.style.background = "transparent")}
              >
                <PlatformGlyph platform={ch.platform} size={22} />
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 13, fontWeight: 500, color: "var(--text-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{ch.title}</div>
                  {ch.username && <div style={{ fontSize: 12, color: "var(--text-secondary)" }}>@{ch.username}</div>}
                </div>
                {isActive && <TIP n="circle-check" size={16} color="#7367F0" />}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

function StatusChip({ platform, label, status = "ok", href }) {
  // Multi-tenant statuses (spec 001):
  //   ok   — провайдер подключён (зелёная точка)
  //   warn — внимание, частично работает (оранжевая)
  //   err  — ошибка (красная)
  //   off  — не подключён (нейтральный, dashed border, идёт по href)
  const palette = {
    ok:   { dot: "#28C76F", bg: "rgba(40,199,111,.16)" },
    warn: { dot: "#FF9F43", bg: "rgba(255,159,67,.16)" },
    err:  { dot: "#FF4C51", bg: "rgba(255,76,81,.16)" },
    off:  { dot: "#A8A8B3", bg: "rgba(168,168,179,.20)" },
  };
  const { dot, bg } = palette[status] || palette.ok;
  const isOff = status === "off";

  const baseStyle = {
    display: "inline-flex", alignItems: "center", gap: 6,
    padding: "4px 10px 4px 4px",
    background: "#fff",
    border: isOff ? "1px dashed var(--divider)" : "1px solid var(--divider)",
    borderRadius: 999,
    fontFamily: "var(--font-sans)",
    color: isOff ? "var(--text-secondary)" : "var(--text-primary)",
    textDecoration: "none",
    cursor: href ? "pointer" : "default",
    transition: "border-color .12s, background .12s",
  };
  const labelStyle = {
    fontSize: 12,
    color: isOff ? "var(--text-secondary)" : "var(--text-primary)",
    fontFamily: isOff ? "var(--font-sans)" : "var(--font-mono)",
    fontWeight: isOff ? 500 : 400,
  };
  const dotStyle = {
    width: 6, height: 6, borderRadius: "50%",
    background: dot,
    boxShadow: `0 0 0 2px ${bg}`,
  };

  const inner = (
    <>
      <PlatformGlyph platform={platform} size={20} muted={isOff} />
      <span style={labelStyle}>{label}</span>
      <span style={dotStyle} />
    </>
  );

  if (href) {
    return (
      <a href={href} style={baseStyle}
         onMouseEnter={e => { e.currentTarget.style.borderColor = isOff ? "rgba(115,103,240,.5)" : "var(--divider)"; }}
         onMouseLeave={e => { e.currentTarget.style.borderColor = "var(--divider)"; }}>
        {inner}
      </a>
    );
  }
  return <div style={baseStyle}>{inner}</div>;
}

// Reusable modal with Vuexy-matched radius (6px from --border-radius-md)
function Modal({ title, subtitle, onClose, children, width = 520, footer }) {
  // Клик по фону НЕ закрывает модалку — только крестик и кнопки в футере.
  // Это спасает от случайной потери введённых данных.
  // Escape закрывает модалку (все окна на базе Modal).
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape" && onClose) onClose(); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);
  return (
    <div className="modal-scrim" style={{
      position: "fixed", inset: 0, zIndex: 200,
      background: "rgba(47,43,61,.48)", backdropFilter: "blur(4px)",
      display: "flex", alignItems: "center", justifyContent: "center", padding: 20,
      animation: "fadeIn .15s linear",
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        width: "100%", maxWidth: width, maxHeight: "calc(100vh - 40px)",
        background: "#fff", borderRadius: 6,
        boxShadow: "0 24px 48px -12px rgba(47,43,61,.32), 0 0 0 1px rgba(47,43,61,.06)",
        display: "flex", flexDirection: "column", overflow: "hidden",
        animation: "modalIn .22s cubic-bezier(0.4,0,0.2,1)",
      }}>
        <div style={{ position: "relative", padding: "18px 48px 14px 24px", borderBottom: "1px solid var(--divider)" }}>
          <div style={{ fontSize: 17, fontWeight: 600, color: "var(--text-primary)", letterSpacing: "-0.01em" }}>{title}</div>
          {subtitle && <div style={{ fontSize: 13, color: "var(--text-secondary)", marginTop: 3 }}>{subtitle}</div>}
          <button onClick={onClose} aria-label="Закрыть" style={{
            position: "absolute", top: 8, right: 8,
            width: 32, height: 32, borderRadius: 6, border: "none", background: "transparent",
            cursor: "pointer", color: "var(--text-secondary)",
            display: "flex", alignItems: "center", justifyContent: "center",
            transition: "background .12s",
          }}
            onMouseEnter={(e) => e.currentTarget.style.background = "rgba(47,43,61,.06)"}
            onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}
          ><TIP n="x" size={18} /></button>
        </div>
        <div style={{ flex: 1, overflowY: "auto" }}>{children}</div>
        {footer && (
          <div style={{ padding: "14px 24px", borderTop: "1px solid var(--divider)",
            display: "flex", justifyContent: "flex-end", gap: 10, background: "var(--bg-body)" }}>
            {footer}
          </div>
        )}
      </div>
    </div>
  );
}

function Card({ radius = 6, pad = 0, style, children, ...rest }) {
  // Figma: Card node 6640:52133, Light/elevation/gray/shadow-md
  // border-radius: var(--border-radius-md, 6px)
  // box-shadow: 0px 3px 12px 0px rgba(47, 43, 61, 0.06)
  return (
    <div style={{
      background: "#fff", borderRadius: radius,
      boxShadow: "0px 3px 12px 0px rgba(47, 43, 61, 0.06)",
      padding: pad, ...style,
    }} {...rest}>{children}</div>
  );
}

// ==================== CALENDAR ====================

function EventChip({ ev, onClick }) {
  const variants = {
    pending:   { bg: "rgba(115,103,240,.12)", bgHover: "rgba(115,103,240,.20)", fg: "#5A52C4", dot: "#7367F0" },
    published: { bg: "rgba(47,43,61,.06)",    bgHover: "rgba(47,43,61,.12)",    fg: "var(--text-secondary)", dot: "#A5A3AE" },
    error:     { bg: "rgba(255,76,81,.12)",   bgHover: "rgba(255,76,81,.20)",   fg: "#BF393D", dot: "#FF4C51" },
  };
  const v = variants[ev.status] || variants.pending;
  const [hover, setHover] = React.useState(false);
  // Если задан кастомный цвет — hover делаем чуть насыщеннее (2A vs 1F).
  const bgDefault = ev.color ? `${ev.color}1F` : v.bg;
  const bgHovered = ev.color ? `${ev.color}33` : v.bgHover;
  return (
    <div onClick={onClick}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      style={{
        display: "flex", alignItems: "center", gap: 6,
        padding: "3px 8px", borderRadius: 4,
        background: hover ? bgHovered : bgDefault,
        color: ev.color || v.fg, fontSize: 11.5, fontWeight: 500,
        lineHeight: 1.35, cursor: "pointer", overflow: "hidden",
        textDecoration: ev.status === "published" ? "line-through" : "none",
        opacity: ev.status === "published" ? (hover ? 0.95 : 0.7) : 1,
        transition: "background .12s, opacity .12s",
      }}>
      <span style={{ width: 5, height: 5, borderRadius: "50%", background: ev.color || v.dot, flexShrink: 0 }} />
      <span style={{ fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>{ev.time}</span>
      <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{ev.channel}</span>
    </div>
  );
}

function MonthGrid({ year, month, today, events, radius = 10, onDayClick, onEventClick, onShowMore }) {
  // month: 0-11
  const first = new Date(year, month, 1);
  const firstWeekday = (first.getDay() + 6) % 7; // Monday=0
  const daysInMonth = new Date(year, month + 1, 0).getDate();
  const prevMonthDays = new Date(year, month, 0).getDate();

  const cells = [];
  // Preceding month tail
  for (let i = firstWeekday - 1; i >= 0; i--) {
    cells.push({ d: prevMonthDays - i, out: true, key: `p${i}`, mo: month - 1 });
  }
  for (let d = 1; d <= daysInMonth; d++) {
    cells.push({ d, out: false, key: `c${d}`, mo: month });
  }
  while (cells.length % 7 !== 0 || cells.length < 42) {
    const n = cells.length - (firstWeekday + daysInMonth) + 1;
    cells.push({ d: n, out: true, key: `n${n}`, mo: month + 1 });
    if (cells.length >= 42) break;
  }

  const eventsByDay = {};
  for (const ev of events) {
    if (ev.month === month && ev.year === year) {
      (eventsByDay[ev.day] = eventsByDay[ev.day] || []).push(ev);
    }
  }

  const weekdays = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"];
  return (
    <div style={{ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", borderTop: "1px solid var(--divider)", borderLeft: "1px solid var(--divider)" }}>
      {weekdays.map((w, i) => (
        <div key={w} style={{
          padding: "10px 12px", fontSize: 11, fontWeight: 500,
          letterSpacing: ".6px", textTransform: "uppercase",
          color: "var(--text-secondary)",
          background: "rgba(47,43,61,0.03)",
          borderRight: "1px solid var(--divider)", borderBottom: "1px solid var(--divider)",
          textAlign: i >= 5 ? "left" : "left",
        }}>{w}</div>
      ))}
      {cells.map((cell, i) => {
        const isToday = !cell.out && cell.d === today.day && month === today.month && year === today.year;
        const isWeekend = i % 7 >= 5;
        const evs = cell.out ? [] : (eventsByDay[cell.d] || []);
        const maxShown = 3;
        const shown = evs.slice(0, maxShown);
        const overflow = evs.length - shown.length;
        return (
          <div key={cell.key} onClick={() => !cell.out && onDayClick && onDayClick(cell.d)} style={{
            minHeight: 118, padding: "8px 8px 10px",
            minWidth: 0, overflow: "hidden",
            borderRight: "1px solid var(--divider)",
            borderBottom: "1px solid var(--divider)",
            background: cell.out ? "rgba(47,43,61,0.015)"
                       : isWeekend ? "rgba(47,43,61,0.02)" : "#fff",
            cursor: cell.out ? "default" : "pointer",
            transition: "background .12s",
            position: "relative",
          }}
            onMouseEnter={e => !cell.out && (e.currentTarget.style.background = "rgba(115,103,240,0.04)")}
            onMouseLeave={e => {
              if (cell.out) return;
              e.currentTarget.style.background = isWeekend ? "rgba(47,43,61,0.02)" : "#fff";
            }}
          >
            <div style={{
              display: "inline-flex", alignItems: "center", justifyContent: "center",
              minWidth: 26, height: 26, borderRadius: "50%",
              marginBottom: 6,
              fontSize: 13, fontWeight: isToday ? 600 : 500,
              fontVariantNumeric: "tabular-nums",
              color: isToday ? "#fff" : cell.out ? "rgba(47,43,61,0.35)" : "var(--text-primary)",
              background: isToday ? "#7367F0" : "transparent",
              boxShadow: isToday ? "0 2px 4px rgba(115,103,240,.4)" : "none",
            }}>{cell.d}</div>
            <div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
              {shown.map((ev, j) => (
                <EventChip key={j} ev={ev}
                  onClick={e => { e.stopPropagation(); onEventClick && onEventClick(ev); }} />
              ))}
              {overflow > 0 && (
                <div
                  onClick={e => { e.stopPropagation(); onShowMore && onShowMore(cell.d); }}
                  onMouseEnter={e => (e.currentTarget.style.color = "#5A52C4")}
                  onMouseLeave={e => (e.currentTarget.style.color = "var(--text-secondary)")}
                  style={{
                    fontSize: 11, color: "var(--text-secondary)", fontWeight: 500,
                    padding: "2px 8px", cursor: "pointer",
                    transition: "color .12s",
                  }}>ещё {overflow}</div>
              )}
            </div>
          </div>
        );
      })}
    </div>
  );
}

function UpcomingPostRow({ ev, onClick }) {
  const [hover, setHover] = React.useState(false);
  return (
    <div
      onClick={onClick}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{
        display: "flex", alignItems: "center", gap: 12,
        padding: "12px 14px", borderRadius: 8, cursor: "pointer",
        background: hover ? "rgba(47,43,61,0.03)" : "transparent",
        transition: "background .12s",
      }}>
      <div style={{
        width: 44, minWidth: 44, padding: "6px 4px", borderRadius: 8,
        background: `${ev.color}1A`, color: ev.color,
        textAlign: "center", fontFamily: "var(--font-sans)",
      }}>
        <div style={{ fontSize: 10, fontWeight: 500, letterSpacing: ".4px", textTransform: "uppercase", lineHeight: 1 }}>{ev.monthShort}</div>
        <div style={{ fontSize: 18, fontWeight: 600, lineHeight: 1.1, marginTop: 2 }}>{ev.day}</div>
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 2 }}>
          <PlatformGlyph platform={ev.platform} size={16} />
          <span style={{ fontSize: 13, fontWeight: 500, color: "var(--text-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{ev.channel}</span>
        </div>
        <div style={{ fontSize: 13, color: "var(--text-secondary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{ev.preview}</div>
      </div>
      <div style={{ textAlign: "right", flexShrink: 0 }}>
        <div style={{ fontSize: 13, fontWeight: 500, color: "var(--text-primary)", fontVariantNumeric: "tabular-nums" }}>{ev.time}</div>
        <div style={{ fontSize: 11, color: "var(--text-secondary)" }}>{ev.relative}</div>
      </div>
    </div>
  );
}

// === Multi-tenant Provider chips (spec 001-multi-tenant-auth) ===
// Shared компонент: пара status-chips (TG + MAX) в header странички, driven от window.USER.
// Использование: <ProviderStatusChips /> — всё остальное берётся из window.USER.connections.
// Пример override в Settings.html / Onboarding.html: window.USER = {...window.USER, connections: {tg:{connected:false,...}}}
function ProviderStatusChips({ gap = 8 }) {
  // spec 004 — fetch /api/me. Preview-режим (без INITIAL_DATA) — fallback на window.USER mock.
  const [user, setUser] = React.useState(null);
  React.useEffect(() => {
    const apiKey = (typeof localStorage !== "undefined") ? (localStorage.getItem("api_key") || "") : "";
    if (!apiKey || !window.INITIAL_DATA) return;
    fetch("/api/me", { headers: { "X-API-Key": apiKey } })
      .then(r => r.ok ? r.json() : null)
      .then(setUser)
      .catch(() => {});
  }, []);
  let tg = { connected: false, identity: "" };
  let mx = { connected: false, identity: "" };
  if (user) {
    tg = {
      connected: !!(user.tg_user_id || user.tg_bot_connected),
      identity: user.tg_username ? `@${user.tg_username}` : (user.tg_first_name || user.tg_bot_username || ""),
    };
    mx = {
      connected: !!user.max_connected,
      identity: user.max_phone || "",
    };
  } else if (typeof window !== "undefined" && window.USER) {
    tg = window.USER.connections?.tg || tg;
    mx = window.USER.connections?.max || mx;
  }
  return (
    <div style={{ display: "flex", alignItems: "center", gap }}>
      <StatusChip
        platform="tg"
        label={tg.connected ? (tg.identity || "@—") : "Подключить Telegram"}
        status={tg.connected ? "ok" : "off"}
        href="/settings#connect-tg"
      />
      <StatusChip
        platform="max"
        label={mx.connected ? (mx.identity || "+7 —") : "Подключить MAX"}
        status={mx.connected ? "ok" : "off"}
        href="/settings#connect-max"
      />
    </div>
  );
}

// === Multi-tenant USER mock (spec 001-multi-tenant-auth) ===
// Shared mock state для всех страниц прототипа. На live-версии driven от
// /api/me + /api/max_me + tg_widget state.
// Чтобы продемонстрировать "не подключён" на конкретной странице — в начале
// её скрипта переопредели: window.USER = { ...window.USER, connections: {tg: {connected:false}, ...} }
const POSTILO_USER_DEFAULT = {
  email: "artem@postilo.ru",
  username: "artem_kovalev",
  display_name: "Артём Ковалёв",
  plan: "Pro",
  is_admin: false,  // admin = self-host owner; для multi-tenant production обычные юзеры is_admin=false
  connections: {
    tg:  { connected: true, identity: "@artem_kovalev", since: "21 апр", tg_user_id: 482931, first_name: "Артём" },
    max: { connected: true, identity: "+7 ···· 4821",   since: "22 апр", phone_e164: "+79991234821", me_id: 152119897 },
  },
};
if (!window.USER) window.USER = POSTILO_USER_DEFAULT;

// ── Vuexy form-dropdowns (spec 005) — заменяют нативные <select>/<input date|time>,
//    т.к. браузерные поповеры не стилизуются. Позиционирование fixed (анти-clip в модалках).
function useDropdownCoords(open, ref, dropH = 280) {
  const [coords, setCoords] = React.useState({ top: 0, left: 0, width: 0, flipUp: false });
  React.useEffect(() => {
    if (!open || !ref.current) return;
    const compute = () => {
      const r = ref.current.getBoundingClientRect();
      const spaceBelow = window.innerHeight - r.bottom;
      const flipUp = spaceBelow < dropH && r.top > spaceBelow;
      setCoords({ top: flipUp ? r.top - 4 : r.bottom + 4, left: r.left, width: r.width, flipUp });
    };
    compute();
    window.addEventListener("scroll", compute, true);
    window.addEventListener("resize", compute);
    return () => { window.removeEventListener("scroll", compute, true); window.removeEventListener("resize", compute); };
  }, [open]);
  return coords;
}

function VuexySelect({ value, onChange, options, width = "100%", searchable = false, placeholder = "Выбрать", icon = null }) {
  const [open, setOpen] = React.useState(false);
  const [query, setQuery] = React.useState("");
  const ref = React.useRef();
  const searchRef = React.useRef();
  const coords = useDropdownCoords(open, ref);
  const selected = options.find(o => String(o.value) === String(value));
  React.useEffect(() => {
    if (!open) return;
    const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", handler);
    if (searchable && searchRef.current) setTimeout(() => searchRef.current && searchRef.current.focus(), 30);
    return () => document.removeEventListener("mousedown", handler);
  }, [open, searchable]);
  React.useEffect(() => { if (!open) setQuery(""); }, [open]);
  const filtered = React.useMemo(() => {
    const q = query.trim().toLowerCase();
    if (!q) return options;
    return options.filter(o => String(o.label).toLowerCase().includes(q));
  }, [query, options]);
  return (
    <div ref={ref} style={{ position: "relative", width }}>
      <div onClick={() => setOpen(!open)} style={{
        display: "flex", alignItems: "center", gap: 8, padding: "10px 12px",
        background: "#fff", border: `1px solid ${open ? "#7367F0" : "var(--input-border)"}`,
        borderRadius: 6, cursor: "pointer", minHeight: 40, boxSizing: "border-box",
        boxShadow: open ? "0 0 0 3px rgba(115,103,240,.16)" : "none",
        transition: "border-color .12s, box-shadow .12s",
      }}>
        {icon && <TIP n={icon} size={16} color="var(--text-secondary)" />}
        <span style={{ flex: 1, fontSize: 14, color: selected ? "var(--text-primary)" : "var(--text-secondary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
          {selected ? selected.label : placeholder}
        </span>
        <TIP n="chevron-down" size={18} color="var(--text-secondary)" />
      </div>
      {open && (
        <div style={{
          position: "fixed",
          top: coords.flipUp ? "auto" : coords.top,
          bottom: coords.flipUp ? (window.innerHeight - coords.top) : "auto",
          left: coords.left, width: coords.width,
          background: "#fff", border: "1px solid var(--divider)", borderRadius: 6,
          boxShadow: "0px 3px 12px 0px rgba(47,43,61,0.14)",
          padding: 4, zIndex: 9999, maxHeight: 280, overflowY: "auto",
          animation: "fadeIn .12s linear",
        }}>
          {searchable && options.length > 6 && (
            <div style={{ padding: "6px 6px 8px", position: "sticky", top: -4, background: "#fff", zIndex: 1, borderBottom: "1px solid var(--divider)", marginBottom: 4 }}>
              <input ref={searchRef} type="text" value={query} onChange={e => setQuery(e.target.value)} placeholder="Поиск…"
                style={{ width: "100%", padding: "8px 10px", fontSize: 13, border: "1px solid #7367F0", borderRadius: 6, outline: "none", fontFamily: "var(--font-sans)", color: "var(--text-primary)", boxShadow: "0 0 0 3px rgba(115,103,240,.10)", boxSizing: "border-box" }} />
            </div>
          )}
          {filtered.length === 0 ? (
            <div style={{ padding: "16px 12px", fontSize: 13, color: "var(--text-secondary)", textAlign: "center" }}>Ничего не найдено</div>
          ) : filtered.map(o => {
            const isActive = String(o.value) === String(value);
            return (
              <div key={o.value} onClick={() => { onChange(o.value); setOpen(false); }}
                style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 10px", borderRadius: 4, cursor: "pointer", background: isActive ? "rgba(115,103,240,.08)" : "transparent", transition: "background .1s" }}
                onMouseEnter={e => !isActive && (e.currentTarget.style.background = "rgba(47,43,61,.04)")}
                onMouseLeave={e => !isActive && (e.currentTarget.style.background = "transparent")}>
                <span style={{ flex: 1, fontSize: 13, fontWeight: isActive ? 500 : 400, color: "var(--text-primary)" }}>{o.label}</span>
                {isActive && <TIP n="circle-check" size={16} color="#7367F0" />}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

function VuexyDatePicker({ value, onChange, width = "100%" }) {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef();
  const coords = useDropdownCoords(open, ref, 320);
  const pad = n => String(n).padStart(2, "0");
  const parsed = React.useMemo(() => {
    const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value || "");
    if (m) return { y: +m[1], mo: +m[2] - 1, d: +m[3] };
    const t = new Date(); return { y: t.getFullYear(), mo: t.getMonth(), d: t.getDate() };
  }, [value]);
  const [viewY, setViewY] = React.useState(parsed.y);
  const [viewM, setViewM] = React.useState(parsed.mo);
  React.useEffect(() => { if (open) { setViewY(parsed.y); setViewM(parsed.mo); } }, [open]);
  React.useEffect(() => {
    if (!open) return;
    const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, [open]);
  const MONTHS = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"];
  const WD = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"];
  const firstDow = (new Date(viewY, viewM, 1).getDay() + 6) % 7;
  const daysInMonth = new Date(viewY, viewM + 1, 0).getDate();
  const cells = [];
  for (let i = 0; i < firstDow; i++) cells.push(null);
  for (let d = 1; d <= daysInMonth; d++) cells.push(d);
  const today = new Date();
  const isToday = d => d && viewY === today.getFullYear() && viewM === today.getMonth() && d === today.getDate();
  const isSel = d => d && value && viewY === parsed.y && viewM === parsed.mo && d === parsed.d;
  const navBtn = { width: 28, height: 28, border: "none", background: "transparent", borderRadius: 6, cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center", color: "var(--text-secondary)" };
  const fmtDisplay = value ? `${pad(parsed.d)}.${pad(parsed.mo + 1)}.${parsed.y}` : "Выбрать дату";
  return (
    <div ref={ref} style={{ position: "relative", width }}>
      <div onClick={() => setOpen(!open)} style={{
        display: "flex", alignItems: "center", gap: 8, padding: "10px 12px",
        background: "#fff", border: `1px solid ${open ? "#7367F0" : "var(--input-border)"}`,
        borderRadius: 6, cursor: "pointer", minHeight: 40, boxSizing: "border-box",
        boxShadow: open ? "0 0 0 3px rgba(115,103,240,.16)" : "none", transition: "border-color .12s, box-shadow .12s",
      }}>
        <TIP n="calendar-event" size={16} color="var(--text-secondary)" />
        <span style={{ flex: 1, fontSize: 14, color: value ? "var(--text-primary)" : "var(--text-secondary)", fontVariantNumeric: "tabular-nums" }}>{fmtDisplay}</span>
      </div>
      {open && (
        <div style={{
          position: "fixed",
          top: coords.flipUp ? "auto" : coords.top,
          bottom: coords.flipUp ? (window.innerHeight - coords.top) : "auto",
          left: coords.left, width: 280,
          background: "#fff", border: "1px solid var(--divider)", borderRadius: 6,
          boxShadow: "0px 3px 12px 0px rgba(47,43,61,0.14)", padding: 12, zIndex: 9999, animation: "fadeIn .12s linear",
        }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 10 }}>
            <button onClick={() => { let m = viewM - 1, y = viewY; if (m < 0) { m = 11; y--; } setViewM(m); setViewY(y); }} style={navBtn}><TIP n="chevron-left" size={16} /></button>
            <div style={{ fontSize: 14, fontWeight: 600, color: "var(--text-primary)" }}>{MONTHS[viewM]} {viewY}</div>
            <button onClick={() => { let m = viewM + 1, y = viewY; if (m > 11) { m = 0; y++; } setViewM(m); setViewY(y); }} style={navBtn}><TIP n="chevron-right" size={16} /></button>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: 2, marginBottom: 4 }}>
            {WD.map(w => <div key={w} style={{ textAlign: "center", fontSize: 11, color: "var(--text-secondary)", fontWeight: 500 }}>{w}</div>)}
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: 2 }}>
            {cells.map((d, i) => d === null ? <div key={i} /> : (
              <div key={i} onClick={() => { onChange(`${viewY}-${pad(viewM + 1)}-${pad(d)}`); setOpen(false); }}
                style={{
                  height: 32, display: "flex", alignItems: "center", justifyContent: "center",
                  fontSize: 13, borderRadius: 6, cursor: "pointer",
                  background: isSel(d) ? "#7367F0" : "transparent",
                  color: isSel(d) ? "#fff" : isToday(d) ? "#7367F0" : "var(--text-primary)",
                  fontWeight: (isSel(d) || isToday(d)) ? 600 : 400,
                  border: isToday(d) && !isSel(d) ? "1px solid rgba(115,103,240,.4)" : "1px solid transparent",
                  transition: "background .1s",
                }}
                onMouseEnter={e => { if (!isSel(d)) e.currentTarget.style.background = "rgba(115,103,240,.10)"; }}
                onMouseLeave={e => { if (!isSel(d)) e.currentTarget.style.background = "transparent"; }}>
                {d}
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

function VuexyTimePicker({ value, onChange, width = "100%" }) {
  const [open, setOpen] = React.useState(false);
  const [inputVal, setInputVal] = React.useState(value || "");
  const ref = React.useRef();
  const inputRef = React.useRef();
  const coords = useDropdownCoords(open, ref);

  // Sync when parent changes value
  React.useEffect(() => { setInputVal(value || ""); }, [value]);

  // Close on outside click
  React.useEffect(() => {
    if (!open) return;
    const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, [open]);

  // 30-min presets for quick pick
  const presets = React.useMemo(() => {
    const out = [];
    for (let h = 0; h < 24; h++) for (const m of [0]) {
      out.push(`${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`);
    }
    return out;
  }, []);

  const isValid = (s) => /^([01]\d|2[0-3]):([0-5]\d)$/.test(s);

  const handleInputChange = (e) => {
    let v = e.target.value;
    // auto-insert colon after 2 digits
    if (/^\d{2}$/.test(v) && (inputVal.length < 2)) v = v + ":";
    setInputVal(v);
    if (isValid(v)) onChange(v);
  };

  const handleKeyDown = (e) => {
    if (e.key === "Enter") {
      if (isValid(inputVal)) { onChange(inputVal); setOpen(false); }
    }
    if (e.key === "Escape") { setInputVal(value || ""); setOpen(false); }
  };

  const handleBlur = () => {
    // restore last valid value if input is invalid
    if (!isValid(inputVal)) setInputVal(value || "");
  };

  const handleSelect = (t) => { setInputVal(t); onChange(t); setOpen(false); };

  return (
    <div ref={ref} style={{ position: "relative", width }}>
      <div style={{
        display: "flex", alignItems: "center", gap: 8, padding: "0 12px",
        background: "#fff", border: `1px solid ${open ? "#7367F0" : "var(--input-border)"}`,
        borderRadius: 6, cursor: "text", minHeight: 40, boxSizing: "border-box",
        boxShadow: open ? "0 0 0 3px rgba(115,103,240,.16)" : "none",
        transition: "border-color .12s, box-shadow .12s",
      }} onClick={() => { setOpen(true); setTimeout(() => inputRef.current?.focus(), 20); }}>
        <TIP n="clock" size={16} color="var(--text-secondary)" />
        <input
          ref={inputRef}
          type="text"
          value={inputVal}
          onChange={handleInputChange}
          onFocus={() => setOpen(true)}
          onKeyDown={handleKeyDown}
          onBlur={handleBlur}
          placeholder="ЧЧ:ММ"
          maxLength={5}
          style={{
            flex: 1, border: "none", outline: "none", fontSize: 14,
            color: "var(--text-primary)", fontFamily: "var(--font-sans)",
            background: "transparent", padding: "10px 0", cursor: "text",
          }}
          onClick={e => e.stopPropagation()}
        />
        <TIP n="chevron-down" size={18} color="var(--text-secondary)"
          style={{ cursor: "pointer" }}
          onClick={e => { e.stopPropagation(); setOpen(v => !v); }} />
      </div>
      {open && (
        <div style={{
          position: "fixed",
          top: coords.flipUp ? "auto" : coords.top,
          bottom: coords.flipUp ? (window.innerHeight - coords.top) : "auto",
          left: coords.left, width: coords.width,
          background: "#fff", border: "1px solid var(--divider)", borderRadius: 6,
          boxShadow: "0px 3px 12px 0px rgba(47,43,61,0.14)",
          padding: 4, zIndex: 9999, maxHeight: 240, overflowY: "auto",
          animation: "fadeIn .12s linear",
        }}>
          {presets.map(t => {
            const isActive = t === value;
            return (
              <div key={t} onClick={() => handleSelect(t)}
                style={{ display: "flex", alignItems: "center", gap: 10, padding: "7px 10px", borderRadius: 4, cursor: "pointer", background: isActive ? "rgba(115,103,240,.08)" : "transparent", transition: "background .1s" }}
                onMouseEnter={e => !isActive && (e.currentTarget.style.background = "rgba(47,43,61,.04)")}
                onMouseLeave={e => !isActive && (e.currentTarget.style.background = "transparent")}>
                <span style={{ flex: 1, fontSize: 13, fontWeight: isActive ? 500 : 400, color: "var(--text-primary)" }}>{t}</span>
                {isActive && <TIP n="circle-check" size={16} color="#7367F0" />}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// Русский плюрал: 1 час / 2 часа / 5 часов.
function pluralRu(n, one, few, many) {
  const m10 = Math.abs(n) % 10, m100 = Math.abs(n) % 100;
  if (m100 >= 11 && m100 <= 19) return many;
  if (m10 === 1) return one;
  if (m10 >= 2 && m10 <= 4) return few;
  return many;
}

// Человеческое описание интервала авто-удаления в секундах.
function humanAutoDelete(s) {
  s = Number(s) || 0;
  if (s <= 0) return "";
  const presets = {
    3600: "через 1 час", 7200: "через 2 часа", 43200: "через 12 часов",
    86400: "через 1 день", 172800: "через 2 дня",
    604800: "через неделю", 2592000: "через месяц",
  };
  if (presets[s]) return presets[s];
  let days = Math.floor(s / 86400);
  let hours = Math.round((s - days * 86400) / 3600);
  if (hours >= 24) { days += Math.floor(hours / 24); hours = hours % 24; }
  if (days > 0 && hours > 0) {
    return `через ${days} ${pluralRu(days, "день", "дня", "дней")} ${hours} ${pluralRu(hours, "час", "часа", "часов")}`;
  }
  if (days > 0) return `через ${days} ${pluralRu(days, "день", "дня", "дней")}`;
  const h = Math.max(1, Math.round(s / 3600));
  return `через ${h} ${pluralRu(h, "час", "часа", "часов")}`;
}

// Селектор «Авто-удаление» с пресетами + опция «Своё значение…»
// (разворачивает inline-редактор «N [часы/дни]»). value=секунды; 0 = выключено.
function AutoDeleteField({ value, onChange, maxSeconds }) {
  const lim = Number(maxSeconds) || 0;  // 0 = без лимита
  const ALL_PRESETS = [
    { v: 0, label: "Не удалять" },
    { v: 3600, label: "Через 1 час" },
    { v: 7200, label: "Через 2 часа" },
    { v: 43200, label: "Через 12 часов" },
    { v: 86400, label: "Через 1 день" },
    { v: 172800, label: "Через 2 дня" },
    { v: 604800, label: "Через неделю" },
    { v: 2592000, label: "Через месяц" },
  ];
  const PRESETS = React.useMemo(() => {
    if (!lim) return ALL_PRESETS;
    const filtered = ALL_PRESETS.filter(p => p.v === 0 || p.v <= lim);
    // Добавляем сам лимит как опцию «максимум», если он не совпал с пресетом.
    if (!filtered.some(p => p.v === lim)) {
      const lbl = humanAutoDelete(lim).replace(/^через /, "Через ");
      filtered.push({ v: lim, label: lbl });
    }
    return filtered;
  }, [lim]);
  const presetVals = React.useMemo(() => new Set(PRESETS.map(p => p.v)), [PRESETS]);
  const isCustom = value > 0 && !presetVals.has(Number(value));
  const initDays  = isCustom ? Math.floor(value / 86400) : 0;
  const initHours = isCustom ? Math.round((value % 86400) / 3600) : 1;
  const [showCustom, setShowCustom] = React.useState(isCustom);
  const [daysStr, setDaysStr]   = React.useState(String(initDays));
  const [hoursStr, setHoursStr] = React.useState(String(initHours));
  const dropdownOptions = [
    ...PRESETS.map(p => ({ value: p.v, label: p.label })),
    { value: "custom", label: "Своё значение…" },
  ];
  const selectValue = isCustom || showCustom ? "custom" : Number(value);
  // Лимиты: дни ограничены lim; часы 0–23 (сверх — уже сутки).
  const customMaxDays  = lim ? Math.floor(lim / 86400) : 9999;
  const customMaxHours = (lim && customMaxDays === 0) ? Math.floor(lim / 3600) : 23;
  const propagate = (d, h) => {
    let secs = d * 86400 + h * 3600;
    if (lim && secs > lim) secs = lim;
    onChange(secs);
  };
  const onDaysChange = (s) => {
    setDaysStr(s);
    const d = parseInt(s, 10) || 0, h = parseInt(hoursStr, 10) || 0;
    if (d * 86400 + h * 3600 >= 3600) propagate(d, h);
  };
  const onHoursChange = (s) => {
    setHoursStr(s);
    const d = parseInt(daysStr, 10) || 0, h = parseInt(s, 10) || 0;
    if (d * 86400 + h * 3600 >= 3600) propagate(d, h);
  };
  // Нормализация на blur: пусто/0 → минимум 1 час; кламп по лимитам.
  const normalize = () => {
    let d = parseInt(daysStr, 10) || 0, h = parseInt(hoursStr, 10) || 0;
    if (d > customMaxDays) d = customMaxDays;
    if (h > customMaxHours) h = customMaxHours;
    if (d * 86400 + h * 3600 < 3600) { d = 0; h = 1; }
    setDaysStr(String(d)); setHoursStr(String(h));
    propagate(d, h);
  };
  const numInput = {
    width: 64, padding: "9px 10px", fontSize: 14,
    border: "1px solid var(--input-border)", borderRadius: 6,
    background: "#fff", outline: "none",
    fontFamily: "var(--font-sans)", color: "var(--text-primary)",
    fontVariantNumeric: "tabular-nums", boxSizing: "border-box",
  };
  const unitLbl = { fontSize: 13, color: "var(--text-secondary)" };
  return (
    <div>
      <VuexySelect
        value={selectValue}
        onChange={(v) => {
          if (v === "custom") {
            setShowCustom(true);
            const d = parseInt(daysStr, 10) || 0, h = parseInt(hoursStr, 10) || 1;
            propagate(d, h);
          } else {
            setShowCustom(false);
            onChange(Number(v));
          }
        }}
        options={dropdownOptions}
      />
      {showCustom && (
        <div style={{ display: "flex", gap: 6, marginTop: 8, alignItems: "center", flexWrap: "wrap" }}>
          <span style={{ fontSize: 12, color: "var(--text-secondary)" }}>Через</span>
          <input type="number" min="0" max={customMaxDays} value={daysStr}
            onChange={e => onDaysChange(e.target.value)} onBlur={normalize} style={numInput} />
          <span style={unitLbl}>дн</span>
          <input type="number" min="0" max={customMaxHours} value={hoursStr}
            onChange={e => onHoursChange(e.target.value)} onBlur={normalize} style={numInput} />
          <span style={unitLbl}>ч</span>
        </div>
      )}
    </div>
  );
}

function VuexyCheckbox({ checked, onChange, label, size = 16, disabled = false }) {
  const toggle = (e) => {
    if (e) { e.preventDefault(); e.stopPropagation(); }
    if (!disabled && onChange) onChange(!checked);
  };
  const box = (
    <span style={{
      width: size, height: size, borderRadius: 4, flexShrink: 0, boxSizing: "border-box",
      background: checked ? "#7367F0" : "#fff",
      border: checked ? "1px solid #7367F0" : "1px solid var(--input-border)",
      display: "inline-flex", alignItems: "center", justifyContent: "center",
      transition: "background .12s, border-color .12s, box-shadow .12s",
      boxShadow: checked ? "0 2px 4px rgba(115,103,240,.28)" : "none",
      opacity: disabled ? 0.55 : 1,
    }}>
      {checked && (
        <svg width={Math.round(size * 0.72)} height={Math.round(size * 0.72)} viewBox="0 0 24 24"
             fill="none" stroke="#fff" strokeWidth="3.2" strokeLinecap="round" strokeLinejoin="round">
          <path d="M5 12l5 5L20 7" />
        </svg>
      )}
    </span>
  );
  if (label === undefined || label === null) {
    return <span onClick={toggle} style={{ display: "inline-flex", cursor: disabled ? "not-allowed" : "pointer" }}>{box}</span>;
  }
  return (
    <span onClick={toggle} style={{
      display: "inline-flex", alignItems: "center", gap: 8, fontSize: 13,
      cursor: disabled ? "not-allowed" : "pointer",
      color: disabled ? "var(--text-secondary)" : "var(--text-primary)",
      userSelect: "none",
    }}>
      {box}
      {label}
    </span>
  );
}

// === TelegramEditor — contenteditable с постоянной панелью форматирования сверху ===
// value: Telegram HTML строка; onChange(tgHtml) при каждом изменении.
function TelegramEditor({ value, onChange, placeholder, maxLength = 4096 }) {
  const edRef    = React.useRef(null);
  const savedSel = React.useRef(null);
  const [lnkDlg, setLnkDlg] = React.useState(false);
  const [lnkVal, setLnkVal] = React.useState("");
  const [cnt, setCnt]       = React.useState(0);

  React.useEffect(() => {
    const el = edRef.current;
    if (el) { el.innerHTML = value || ""; setCnt((el.innerText || "").replace(/\n$/, "").length); }
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  const toTg = (html) => {
    const tmp = document.createElement("div"); tmp.innerHTML = html;
    const esc = s => s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
    const atr = s => String(s||"").replace(/"/g,"&quot;");
    function wk(n) {
      if (n.nodeType===3) return esc(n.textContent);
      if (n.nodeType!==1) return "";
      const t=n.tagName.toLowerCase(), k=Array.from(n.childNodes).map(wk).join("");
      if(t==="b"||t==="strong")   return `<b>${k}</b>`;
      if(t==="i"||t==="em")       return `<i>${k}</i>`;
      if(t==="u")                 return `<u>${k}</u>`;
      if(t==="s"||t==="strike"||t==="del") return `<s>${k}</s>`;
      if(t==="code")              return `<code>${k}</code>`;
      if(t==="pre")               return `<pre>${k}</pre>`;
      if(t==="tg-spoiler")        return `<tg-spoiler>${k}</tg-spoiler>`;
      if(t==="blockquote")        return `<blockquote>${k}</blockquote>`;
      if(t==="a") return `<a href="${atr(n.getAttribute("href"))}">${k}</a>`;
      if(t==="br") return "\n";
      if(t==="div"||t==="p") return k ? k+"\n" : "\n";
      return k;
    }
    return Array.from(tmp.childNodes).map(wk).join("").replace(/\n+$/,"");
  };

  const emit = () => {
    const el = edRef.current; if (!el) return;
    setCnt((el.innerText||"").replace(/\n$/,"").length);
    if (onChange) onChange(toTg(el.innerHTML));
  };

  const savSel = () => { const s=window.getSelection(); if(s&&!s.isCollapsed) savedSel.current=s.getRangeAt(0).cloneRange(); };
  const rstSel = () => { const s=window.getSelection(); if(savedSel.current&&s){s.removeAllRanges();s.addRange(savedSel.current);} };

  const cmd = (c,v) => { edRef.current?.focus(); document.execCommand(c,false,v||null); emit(); };

  const wrap = (tag) => {
    const sel=window.getSelection(); if(!sel||sel.isCollapsed) return;
    const range=sel.getRangeAt(0);
    try { const frag=range.extractContents(); const el=document.createElement(tag); el.appendChild(frag); range.insertNode(el); }
    catch(e) {}
    emit();
  };

  const openLnk = () => { savSel(); setLnkVal(""); setLnkDlg(true); };
  const insLnk  = () => {
    if (!lnkVal.trim()) { setLnkDlg(false); return; }
    rstSel(); edRef.current?.focus();
    document.execCommand("createLink", false, lnkVal.trim());
    setLnkDlg(false); emit();
  };

  const onKd = (e) => {
    const C = e.ctrlKey||e.metaKey; if (!C) return;
    if      (e.key==="b")               { e.preventDefault(); cmd("bold"); }
    else if (e.key==="i")               { e.preventDefault(); cmd("italic"); }
    else if (e.key==="u")               { e.preventDefault(); cmd("underline"); }
    else if (e.key==="k")               { e.preventDefault(); openLnk(); }
    else if (e.shiftKey && e.key==="X") { e.preventDefault(); cmd("strikeThrough"); }
    else if (e.shiftKey && e.key==="M") { e.preventDefault(); wrap("code"); }
    else if (e.shiftKey && e.key==="P") { e.preventDefault(); wrap("tg-spoiler"); }
    else if (e.shiftKey && e.key===".") { e.preventDefault(); wrap("blockquote"); }
    else if (e.shiftKey && e.key==="N") { e.preventDefault(); cmd("removeFormat"); }
  };

  const BTNS = [
    { l:"B",   t:"Жирный (Ctrl+B)",                   s:{fontWeight:700},                     fn:()=>cmd("bold") },
    { l:"I",   t:"Курсив (Ctrl+I)",                    s:{fontStyle:"italic"},                 fn:()=>cmd("italic") },
    { l:"U",   t:"Подчёркнутый (Ctrl+U)",              s:{textDecoration:"underline"},          fn:()=>cmd("underline") },
    { l:"S",   t:"Зачёркнутый (Ctrl+Shift+X)",         s:{textDecoration:"line-through"},       fn:()=>cmd("strikeThrough") },
    null,
    { l:"</>", t:"Моноширинный (Ctrl+Shift+M)",        s:{fontFamily:"monospace",fontSize:10}, fn:()=>wrap("code") },
    { l:"‖‖",  t:"Спойлер (Ctrl+Shift+P)",             s:{fontFamily:"monospace",fontSize:10}, fn:()=>wrap("tg-spoiler") },
    { l:"❝",   t:"Цитата (Ctrl+Shift+.)",              s:{fontSize:14},                        fn:()=>wrap("blockquote") },
    { l:"🔗",  t:"Ссылка (Ctrl+K)",                   s:{},                                   fn:openLnk },
    null,
    { l:"✕",   t:"Убрать форматирование (Ctrl+Shift+N)", s:{opacity:.55},                     fn:()=>cmd("removeFormat") },
  ];

  const tbBtn = {
    background:"transparent", border:"none", cursor:"pointer",
    color:"var(--text-secondary)", padding:"2px 7px", borderRadius:4,
    fontSize:12, minWidth:26, height:26,
    display:"inline-flex", alignItems:"center", justifyContent:"center", lineHeight:1,
  };

  return (
    <div style={{position:"relative"}}>
      <style>{`
        .tg-ed tg-spoiler{background:rgba(0,0,0,.13);border-radius:3px;padding:0 2px;cursor:default}
        .tg-ed code{background:rgba(115,103,240,.09);border-radius:3px;padding:0 3px;font-family:monospace;font-size:.88em}
        .tg-ed blockquote{margin:2px 0;padding:2px 10px;border-left:3px solid #7367F0;border-radius:0 3px 3px 0;display:block}
        .tg-ed a[href]{color:#7367F0;text-decoration:underline;cursor:pointer}
      `}</style>

      {/* Постоянная панель сверху */}
      <div style={{
        display:"flex", alignItems:"center", flexWrap:"wrap",
        padding:"3px 6px", gap:0,
        border:"1px solid var(--divider)", borderBottom:"1px solid var(--divider)",
        borderRadius:"6px 6px 0 0",
        background:"rgba(47,43,61,.025)",
      }}>
        {BTNS.map((b,i) => b===null
          ? <div key={i} style={{width:1,height:16,background:"rgba(47,43,61,.14)",margin:"0 4px",flexShrink:0}} />
          : <button key={i} title={b.t}
              onMouseDown={e=>{e.preventDefault();b.fn();}}
              style={{...tbBtn,...b.s}}
              onMouseEnter={e=>{e.currentTarget.style.background="rgba(115,103,240,.10)";e.currentTarget.style.color="#7367F0";}}
              onMouseLeave={e=>{e.currentTarget.style.background="transparent";e.currentTarget.style.color="var(--text-secondary)";}}>
              {b.l}
            </button>
        )}
      </div>

      {/* Поле ввода */}
      <div style={{position:"relative"}}>
        {cnt===0 && (
          <div style={{position:"absolute",top:9,left:12,color:"var(--text-secondary)",
            fontSize:14,pointerEvents:"none",userSelect:"none",lineHeight:1.6}}>
            {placeholder||"Текст публикации…"}
          </div>
        )}
        <div ref={edRef} contentEditable suppressContentEditableWarning className="tg-ed"
          onInput={emit} onKeyDown={onKd}
          style={{width:"100%",padding:"9px 12px",
            border:"1px solid var(--divider)", borderTop:"none",
            borderRadius:"0 0 6px 6px",
            fontSize:14,color:"var(--text-primary)",background:"transparent",display:"block",
            minHeight:110,lineHeight:1.6,outline:"none",whiteSpace:"pre-wrap",
            wordBreak:"break-word",cursor:"text",fontFamily:"var(--font-sans)"}} />
      </div>

      {/* Диалог вставки ссылки */}
      {lnkDlg && (
        <div style={{
          position:"absolute", top:34, left:0, right:0, zIndex:200,
          background:"var(--bg-card,#fff)", border:"1px solid var(--divider)",
          borderTop:"none", borderRadius:"0 0 6px 6px",
          padding:"7px 10px", display:"flex", gap:6, alignItems:"center",
          boxShadow:"0 6px 16px rgba(47,43,61,.14)",
        }}>
          <input autoFocus value={lnkVal} onChange={e=>setLnkVal(e.target.value)}
            placeholder="https://..."
            onKeyDown={e=>{if(e.key==="Enter")insLnk();if(e.key==="Escape")setLnkDlg(false);}}
            style={{flex:1,border:"1px solid var(--divider)",borderRadius:6,
              padding:"5px 10px",fontSize:13,outline:"none"}} />
          <button onClick={insLnk}
            style={{padding:"5px 14px",borderRadius:6,background:"#7367F0",
              color:"#fff",border:"none",cursor:"pointer",fontSize:13}}>OK</button>
          <button onClick={()=>setLnkDlg(false)}
            style={{padding:"5px 10px",borderRadius:6,background:"none",
              border:"1px solid var(--divider)",cursor:"pointer",fontSize:13}}>✕</button>
        </div>
      )}

      <div style={{textAlign:"right",fontSize:12,color:"var(--text-secondary)",marginTop:4}}>
        {cnt} / {maxLength}
      </div>
    </div>
  );
}

Object.assign(window, {
  TIP, PostiloMark, PlatformGlyph, ConnectionPill, SegmentedFilter,
  ChannelAvatar, StatusDot, ChannelSpark, ChannelRow,
  PostiloSidebar, SidebarLink, PageHeader, Card, StatsInline,
  StatusChip, ProviderStatusChips, Modal, Dropdown, DropdownItem, DropdownDivider, ChannelPicker,
  EventChip, MonthGrid, UpcomingPostRow,
  VuexySelect, VuexyDatePicker, VuexyTimePicker, VuexyCheckbox,
  AutoDeleteField, humanAutoDelete, pluralRu,
  TelegramEditor,
  POSTILO_USER_DEFAULT,
});
