Eblo Modal Viewer

Tampermonkey-скрипт для eblo.id: открывает посты в модалке поверх ленты, поддерживает просмотр фото и видео, навигацию между постами, закрытие по клику вне окна и переход к оригинальному посту.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Eblo Modal Viewer
// @namespace    https://greasyfork.org/users/1589566
// @version      5.2.0
// @description  Tampermonkey-скрипт для eblo.id: открывает посты в модалке поверх ленты, поддерживает просмотр фото и видео, навигацию между постами, закрытие по клику вне окна и переход к оригинальному посту.
// @author       mizu299
// @match        https://eblo.id/*
// @icon         https://eblo.id/favicon.ico
// @run-at       document-start
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  const CARD_SELECTOR = '.feed-card[data-link]';
  const FEED_SELECTOR = '#feed-grid.feed-grid';

  const VOLUME_KEY = 'tm_eblo_volume';
  const MUTED_KEY = 'tm_eblo_muted';

  let overlay = null;
  let modal = null;
  let contentBox = null;
  let prevBtn = null;
  let nextBtn = null;

  let currentIndex = -1;
  let feedUrlBeforeOpen = '';
  let currentPostUrl = '';
  let currentPostId = '';
  let currentCommentSort = 'best';

  let currentVote = 0; // 1 up, -1 down, 0 none
  let currentScore = 0;

  function q(sel, root = document) {
    return root.querySelector(sel);
  }

  function qa(sel, root = document) {
    return [...root.querySelectorAll(sel)];
  }

  function esc(text) {
    const div = document.createElement('div');
    div.textContent = text ?? '';
    return div.innerHTML;
  }

  function abs(url) {
    return url ? new URL(url, location.origin).href : '';
  }

  function isFeedPage() {
    return !!q(FEED_SELECTOR);
  }

  function getCards() {
    return qa(CARD_SELECTOR).filter(el => el.offsetParent !== null);
  }

  function getUrlFromCard(card) {
    return abs(card?.dataset?.link || '');
  }

  function getIndexByUrl(url) {
    return getCards().findIndex(card => getUrlFromCard(card) === url);
  }

  function getPostIdFromUrl(url) {
    try {
      const pathname = new URL(url).pathname.replace(/^\/+|\/+$/g, '');
      return pathname.split('/')[0] || '';
    } catch {
      return '';
    }
  }

  function getSavedVolume() {
    const raw = localStorage.getItem(VOLUME_KEY);
    const num = raw == null ? 1 : Number(raw);
    return Number.isFinite(num) ? Math.max(0, Math.min(1, num)) : 1;
  }

  function getSavedMuted() {
    return localStorage.getItem(MUTED_KEY) === '1';
  }

  function saveMediaState(video) {
    if (!video) return;
    try {
      localStorage.setItem(VOLUME_KEY, String(video.volume));
      localStorage.setItem(MUTED_KEY, video.muted ? '1' : '0');
    } catch { }
  }

  function applyMediaState(video) {
    if (!video) return;
    video.volume = getSavedVolume();
    video.muted = getSavedMuted();
    video.addEventListener('volumechange', () => saveMediaState(video));
  }

  function getCsrfToken(doc = document) {
    const meta =
      doc.querySelector('meta[name="csrf-token"]') ||
      doc.querySelector('meta[name="csrf_token"]') ||
      doc.querySelector('meta[name="csrf"]');

    if (meta?.content) return meta.content;

    const el =
      doc.querySelector('[data-csrf-token]') ||
      doc.querySelector('input[name="csrf_token"]') ||
      doc.querySelector('input[name="csrf"]');

    if (el?.value) return el.value;
    if (el?.dataset?.csrfToken) return el.dataset.csrfToken;

    const html = document.documentElement.innerHTML;
    const patterns = [
      /"csrf_token"\s*:\s*"([^"]+)"/i,
      /"csrfToken"\s*:\s*"([^"]+)"/i,
      /csrf_token['"]?\s*[:=]\s*['"]([^'"]+)['"]/i,
      /csrfToken['"]?\s*[:=]\s*['"]([^'"]+)['"]/i
    ];

    for (const re of patterns) {
      const m = html.match(re);
      if (m?.[1]) return m[1];
    }

    return '';
  }

  function ensureStyles() {
    if (q('#tm-eblo-style')) return;

    const style = document.createElement('style');
    style.id = 'tm-eblo-style';
    style.textContent = `
      :root {
        --tm-accent: #789d2a;
        --tm-bg: #202020;
        --tm-card-bg: #262626;
        --tm-border: #333;
        --tm-feed-card-border: #ffffff33;
        --tm-title: #ebebeb;
        --tm-muted: #989898;
        --tm-stats: #696969;
        --tm-search-border: #444444;
      }

      #tm-eblo-overlay {
        position: fixed;
        inset: 0;
        z-index: 900;
        display: none;
        font-family: 'Handjet', Arial, sans-serif;
        color: var(--tm-title);
      }

      body > div[id*="emote"], body > div[class*="emote"], body > div[class*="picker"] {
        z-index: 99999 !important;
      }

      #tm-eblo-overlay * {
        box-sizing: border-box;
      }

      #tm-eblo-overlay .tm-eblo-backdrop {
        position: absolute;
        inset: 0;
        background: rgba(0, 0, 0, 0.84);
        backdrop-filter: blur(4px);
      }

      #tm-eblo-overlay .tm-eblo-shell {
        position: absolute;
        inset: 2vh 0;
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
      }

      #tm-eblo-overlay .tm-eblo-modal {
        position: relative;
        height: 96vh;
        width: 100%;
        max-width: 980px;
        overflow: hidden;
        border-radius: 1rem;
        background: transparent;
      }

      #tm-eblo-overlay .tm-eblo-scroll {
        height: 100%;
        overflow: auto;
        padding: 8px 0;
        scrollbar-width: thin;
        scrollbar-color: #8f8f8f transparent;
      }

      #tm-eblo-overlay .tm-eblo-scroll::-webkit-scrollbar {
        width: 10px;
      }

      #tm-eblo-overlay .tm-eblo-scroll::-webkit-scrollbar-thumb {
        background: #8f8f8f;
        border-radius: 10px;
      }

      #tm-eblo-overlay .tm-eblo-nav {
        position: fixed;
        top: 50%;
        transform: translateY(-50%);
        z-index: 10;
        border: 1px solid var(--tm-feed-card-border);
        background: rgba(38, 38, 38, 0.96);
        color: #fff;
        cursor: pointer;
        font-family: inherit;
        width: 48px;
        height: 48px;
        border-radius: 999px;
        font-size: 28px;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 0;
        padding-bottom: 2px;
        line-height: 0;
      }
      
      #tm-eblo-overlay .tm-eblo-prev { left: max(2vw, 16px); }
      #tm-eblo-overlay .tm-eblo-next { right: max(2vw, 16px); }

      #tm-eblo-overlay .tm-eblo-nav:hover {
        border-color: var(--tm-accent);
        color: var(--tm-accent);
      }

      #tm-eblo-overlay .tm-eblo-post,
      #tm-eblo-overlay .tm-eblo-comments-wrap {
        width: 100%;
        background: var(--tm-card-bg);
        border: 1px solid var(--tm-border);
        border-radius: 0.75rem;
      }

      #tm-eblo-overlay .tm-eblo-post {
        position: relative;
        padding: 1.5rem;
        margin-bottom: 1rem;
      }

      #tm-eblo-overlay .tm-eblo-author {
        display: flex;
        align-items: center;
        gap: 12px;
        margin-bottom: 1rem;
      }

      #tm-eblo-overlay .tm-eblo-author-avatar {
        width: 54px;
        height: 54px;
        border-radius: 999px;
        object-fit: cover;
        flex: 0 0 auto;
      }

      #tm-eblo-overlay .tm-eblo-author-meta {
        display: flex;
        align-items: center;
        gap: 10px;
        flex-wrap: wrap;
      }

      #tm-eblo-overlay .tm-eblo-author-name {
        color: #fff;
        text-decoration: none;
        font-size: 1.8rem;
        line-height: 1;
      }

      #tm-eblo-overlay .tm-eblo-author-date {
        color: var(--tm-muted);
        font-size: 1.2rem;
      }

      #tm-eblo-overlay .tm-eblo-title {
        margin: 0 0 1.2rem;
        color: var(--tm-title);
        font-size: 3.2rem;
        line-height: 1;
        letter-spacing: 0.02em;
      }

      #tm-eblo-overlay .tm-eblo-post-header {
        display: flex;
        align-items: center;
        gap: 12px;
        margin-bottom: 20px;
      }
      #tm-eblo-overlay .tm-eblo-post-desc {
        flex: 1;
        text-align: left;
        color: #828282;
        font-weight: 600;
        font-size: 1.25rem;
        line-height: 1.5rem;
        letter-spacing: 0.04em;
        overflow-wrap: break-word;
        word-wrap: break-word;
        word-break: break-word;
        max-width: 100%;
        min-width: 0;
        margin: 10px 0 20px 0;
        white-space: pre-wrap;
      }
      #tm-eblo-overlay .tm-eblo-post-desc p {
        margin: 0 0 0.6em;
        white-space: pre-wrap;
      }
      #tm-eblo-overlay .tm-eblo-post-desc a {
        color: var(--tm-accent);
        text-decoration: underline;
      }
      #tm-eblo-overlay .tm-eblo-post-desc a:hover {
        color: #fff;
      }
      #tm-eblo-overlay .tm-eblo-media-gallery {
        position: relative;
        width: 100%;
        border-radius: 8px;
        margin-top: 15px;
        overflow: hidden;
      }
      #tm-eblo-overlay .tm-eblo-gallery-track {
        display: flex;
        width: 100%;
        transition: transform 0.3s ease;
      }
      #tm-eblo-overlay .tm-eblo-gallery-item {
        flex: 0 0 100%;
        width: 100%;
        object-fit: contain;
        display: block;
        max-height: 80vh;
        background: #000;
      }

      /* Censorship styles start */
      #tm-eblo-overlay .tm-eblo-item-wrapper {
        position: relative;
        flex: 0 0 100%;
        width: 100%;
        display: flex;
        justify-content: center;
        align-items: center;
        background: #000;
        overflow: hidden;
      }

      #tm-eblo-overlay .tm-eblo-gallery-item.blurred {
        opacity: 0;
        pointer-events: none;
        transition: opacity 0.4s cubic-bezier(.4, 0, .2, 1);
      }

      #tm-eblo-overlay .tm-eblo-censored-overlay {
        position: absolute;
        inset: 0;
        z-index: 20;
        display: flex;
        justify-content: center;
        align-items: center;
        background: rgba(0, 0, 0, 0.2);
        cursor: pointer;
        backdrop-filter: blur(2px);
      }

      #tm-eblo-overlay .tm-eblo-censored-box {
        background: #262626;
        border-radius: 12px;
        padding: 30px 40px;
        box-shadow: 0 8px 32px rgba(0,0,0,0.5);
        border: 1px solid #333;
        text-align: center;
        max-width: 90%;
      }

      #tm-eblo-overlay .tm-eblo-censored-title {
        font-size: 2.5rem;
        color: #fff;
        margin-bottom: 8px;
        line-height: 1.1;
      }

      #tm-eblo-overlay .tm-eblo-censored-sub {
        font-size: 1.25rem;
        color: #989898;
        font-weight: 400;
      }

      #tm-eblo-overlay .is-revealed .tm-eblo-gallery-item.blurred {
        opacity: 1 !important;
        pointer-events: auto !important;
      }

      #tm-eblo-overlay .is-revealed .tm-eblo-censored-overlay {
        display: none !important;
      }

      #tm-eblo-overlay .tm-eblo-gallery-track.no-transition {
        transition: none !important;
      }
      /* Censorship styles end */

      /* Стили для кнопок навигации галереи */
      #tm-eblo-overlay .tm-gallery-nav {
         position: absolute;
         top: 50%;
         transform: translateY(-50%);
         z-index: 10;
         width: 44px;
         height: 44px;
         min-width: 44px;
         min-height: 44px;
         border-radius: 50%;
         border: none;
         cursor: pointer;
         display: flex;
         justify-content: center;
         align-items: center;
         background: #4CAF50 !important;
         box-shadow: 0 4px 12px rgba(0,0,0,0.3);
         transition: filter 0.2s;
         flex-shrink: 0;
         padding: 0;
      }
      #tm-eblo-overlay .tm-gallery-nav:hover {
        filter: brightness(1.1);
      }
      #tm-eblo-overlay .tm-gallery-prev {
        left: 10px;
      }
      #tm-eblo-overlay .tm-gallery-next {
        right: 10px;
      }
      #tm-eblo-overlay .arrow-left { transform: rotate(-90deg); }
      #tm-eblo-overlay .arrow-right { transform: rotate(90deg); }
      #tm-eblo-overlay .arrow-up { transform: rotate(0deg); }
      #tm-eblo-overlay .arrow-down { transform: rotate(180deg); }

      #tm-eblo-overlay .tm-eblo-watermark {
        position: absolute;
        top: 1.2rem;
        right: 1.5rem;
        font-size: 1rem;
        color: #4a4a4a;
        letter-spacing: 0.06em;
        pointer-events: none;
        user-select: none;
      }

      /* Fix for EmotePicker going off-screen */
      .seventv-emote-picker-container, #emote-picker, .emote-picker, .picker-container {
         position: fixed !important;
         top: auto !important;
         bottom: 20px !important;
         right: 20px !important;
         left: auto !important;
         z-index: 9999999 !important;
         max-height: calc(100vh - 40px) !important;
      }

      #tm-eblo-overlay .tm-eblo-interaction {
        margin-top: 1rem;
        border-top: 1px solid var(--tm-border);
        padding-top: 1rem;
      }

      #tm-eblo-overlay .tm-eblo-stats-row {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 1rem;
        flex-wrap: wrap;
        margin-bottom: 1rem;
      }

      #tm-eblo-overlay .tm-eblo-stats-left,
      #tm-eblo-overlay .tm-eblo-stats-right {
        display: flex;
        align-items: center;
        gap: 1.2rem;
        flex-wrap: wrap;
      }

      #tm-eblo-overlay .tm-eblo-votes {
        display: inline-flex;
        align-items: center;
        gap: 0.55rem;
      }

      #tm-eblo-overlay .tm-eblo-vote-btn,
      #tm-eblo-overlay .tm-eblo-action-btn,
      .tm-eblo-open-post-btn {
        font-family: inherit;
        border-radius: 0.5rem;
      }

      #tm-eblo-overlay .tm-eblo-vote-btn {
        background: transparent;
        border: none;
        color: #fff;
        cursor: pointer;
        padding: 0;
        line-height: 1;
        display: flex;
        align-items: center;
        justify-content: center;
      }

      #tm-eblo-overlay .tm-eblo-vote-btn img {
        width: 20px;
        height: 20px;
      }

      #tm-eblo-overlay .tm-eblo-vote-btn:hover img {
        filter: brightness(0) saturate(100%) invert(60%) sepia(60%) saturate(400%) hue-rotate(50deg);
      }

      #tm-eblo-overlay .tm-eblo-vote-btn.active img {
        filter: brightness(0) saturate(100%) invert(60%) sepia(60%) saturate(400%) hue-rotate(50deg);
      }

      #tm-eblo-overlay .tm-eblo-vote-btn:disabled {
        opacity: 0.6;
        cursor: wait;
      }

      #tm-eblo-overlay .tm-eblo-vote-score,
      #tm-eblo-overlay .tm-eblo-stat {
        font-size: 1.5rem;
        color: #fff;
      }

      #tm-eblo-overlay .tm-eblo-stat-muted {
        font-size: 1.4rem;
        color: var(--tm-stats);
      }

      #tm-eblo-overlay .tm-eblo-actions {
        display: flex;
        gap: 0.75rem;
        flex-wrap: wrap;
      }

      #tm-eblo-overlay .tm-eblo-action-btn,
      .tm-eblo-open-post-btn {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-height: 40px;
        padding: 0.7rem 1rem;
        text-decoration: none;
        cursor: pointer;
      }

      #tm-eblo-overlay .tm-eblo-action-btn {
        background: transparent;
        border: 1px solid var(--tm-search-border);
        color: var(--tm-title);
      }

      #tm-eblo-overlay .tm-eblo-action-btn:hover,
      .tm-eblo-open-post-btn:hover {
        border-color: var(--tm-accent);
        color: var(--tm-accent);
      }

      #tm-eblo-overlay .tm-eblo-comments-wrap {
        background: var(--tm-card-bg);
        border: 1px solid #333;
        border-radius: 0.75rem;
        padding: 1.5rem;
        width: 100%;
      }

      #tm-eblo-overlay .tm-eblo-comments-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 1rem;
        margin-bottom: 1.5rem;
      }

      #tm-eblo-overlay .tm-eblo-comments-title {
        margin: 0;
        font-size: 2rem;
        line-height: 1;
        color: #fff;
      }

      #tm-eblo-overlay .tm-eblo-sort-opt {
        background: transparent;
        color: var(--tm-muted);
        border: 1px solid transparent;
        cursor: pointer;
        font-family: inherit;
        font-size: 1.3rem;
        border-radius: 8px;
        padding: 4px 8px;
      }
      #tm-eblo-overlay .tm-eblo-sort-opt.active {
        color: #fff;
        background: rgba(255,255,255,0.1);
      }
      #tm-eblo-overlay .tm-eblo-sort-opt:hover {
        color: #fff;
      }

      #tm-eblo-overlay .tm-eblo-comment-form {
        display: flex;
        gap: 10px;
        margin-bottom: 20px;
        align-items: flex-start;
      }
      #tm-eblo-overlay .tm-eblo-comment-form textarea {
        flex: 1;
        background: rgba(255,255,255,0.05);
        border: 1px solid var(--tm-search-border);
        color: #fff;
        border-radius: 8px;
        padding: 10px;
        font-family: inherit;
        font-size: 1.4rem;
        min-height: 48px;
        resize: vertical;
      }
      #tm-eblo-overlay .tm-eblo-comment-form button {
        background: var(--tm-accent);
        color: #fff;
        border: none;
        border-radius: 8px;
        padding: 10px 16px;
        font-family: inherit;
        font-size: 1.4rem;
        cursor: pointer;
        min-height: 48px;
      }
      #tm-eblo-overlay .tm-eblo-comment-form button:disabled {
        opacity: 0.5;
        cursor: not-allowed;
      }

      #tm-eblo-overlay .tm-eblo-login-prompt {
        text-align: center;
        color: var(--tm-muted);
        font-size: 1.3rem;
        margin-bottom: 1.4rem;
      }

      #tm-eblo-overlay .tm-eblo-comment-item {
        display: flex;
        gap: 14px;
        padding: 0.9rem 0;
        border-bottom: 1px solid rgba(255,255,255,0.05);
      }

      #tm-eblo-overlay .tm-eblo-comment-item:last-child {
        border-bottom: none;
      }

      /* Стили для вложенных комментариев (ответов) */
      #tm-eblo-overlay .tm-eblo-comment-reply {
        margin-left: 1.5rem;
        border-left: 2px solid #333;
        padding-left: 1rem;
        margin-top: 0.5rem;
        border-bottom: none; /* Убираем полоску снизу у реплаев */
      }

      #tm-eblo-overlay .tm-eblo-comment-vote-col {
        width: 28px;
        flex: 0 0 28px;
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 0.15rem;
        color: #fff;
        padding-top: 2px;
      }

      #tm-eblo-overlay .tm-eblo-comment-vote-btn {
        border: none;
        background: transparent;
        color: #999;
        cursor: pointer;
        padding: 0;
        line-height: 1;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      #tm-eblo-overlay .tm-eblo-comment-vote-btn img {
        width: 16px;
        height: 16px;
      }
      #tm-eblo-overlay .tm-eblo-comment-vote-btn:hover img {
        filter: brightness(0) saturate(100%) invert(60%) sepia(60%) saturate(400%) hue-rotate(50deg);
      }
      #tm-eblo-overlay .tm-eblo-comment-vote-btn.active img {
        filter: brightness(0) saturate(100%) invert(60%) sepia(60%) saturate(400%) hue-rotate(50deg);
      }

      #tm-eblo-overlay .tm-eblo-comment-score {
        font-size: 1.15rem;
        color: #fff;
        line-height: 1;
        font-weight: 600;
      }

      #tm-eblo-overlay .tm-eblo-comment-body {
        min-width: 0;
        flex: 1 1 auto;
      }

      #tm-eblo-overlay .tm-eblo-comment-meta {
        display: flex;
        align-items: center;
        gap: 0.55rem;
        flex-wrap: wrap;
        margin-bottom: 0.35rem;
      }

      #tm-eblo-overlay .tm-eblo-comment-author-avatar {
        width: 28px;
        height: 28px;
        border-radius: 999px;
        object-fit: cover;
      }

      #tm-eblo-overlay .tm-eblo-comment-author {
        color: #fff;
        text-decoration: none;
        font-size: 1.45rem;
        line-height: 1;
      }

      #tm-eblo-overlay .tm-eblo-comment-time {
        color: var(--tm-muted);
        text-decoration: none;
        font-size: 1.15rem;
      }

      #tm-eblo-overlay .tm-eblo-comment-text {
        color: #fff;
        font-size: 1.35rem;
        line-height: 1.35;
        word-break: break-word;
      }

      #tm-eblo-overlay .tm-eblo-comment-text img.emote-inline {
        height: 28px;
        width: auto;
        vertical-align: middle;
      }

      #tm-eblo-overlay .tm-eblo-loading,
      #tm-eblo-overlay .tm-eblo-error {
        background: var(--tm-card-bg);
        border: 1px solid var(--tm-border);
        border-radius: 0.75rem;
        padding: 2rem;
        text-align: center;
        color: #fff;
        font-size: 1.6rem;
      }

      body.tm-eblo-lock {
        overflow: hidden !important;
      }

      .tm-eblo-open-post-btn {
        margin-top: 10px;
        background: transparent;
        border: 1px solid var(--tm-search-border);
        color: var(--tm-title) !important;
        font-size: 1.15rem;
        font-weight: 600;
        position: relative;
        z-index: 5;
      }

      @media (max-width: 900px) {
        #tm-eblo-overlay .tm-eblo-shell {
          grid-template-columns: 1fr;
          inset: 1vh 1vw;
        }

        #tm-eblo-overlay .tm-eblo-nav {
          position: fixed;
          bottom: 18px;
          z-index: 5;
        }

        #tm-eblo-overlay .tm-eblo-prev { left: 14px; }
        #tm-eblo-overlay .tm-eblo-next { right: 14px; }

        #tm-eblo-overlay .tm-eblo-title {
          font-size: 2.3rem;
        }

        #tm-eblo-overlay .tm-eblo-comments-header {
          align-items: flex-start;
          flex-direction: column;
        }
      }
    `;

    document.documentElement.appendChild(style);
  }

  function ensureModal() {
    if (overlay) return;

    ensureStyles();

    overlay = document.createElement('div');
    overlay.id = 'tm-eblo-overlay';
    overlay.innerHTML = `
      <div class="tm-eblo-backdrop"></div>
        <button class="tm-eblo-nav tm-eblo-prev" aria-label="Назад">‹</button>
        <button class="tm-eblo-nav tm-eblo-next" aria-label="Вперед">›</button>
        <div class="tm-eblo-shell">
        <div class="tm-eblo-modal">
          <div class="tm-eblo-scroll">
            <div class="tm-eblo-content">
              <div class="tm-eblo-loading">Загрузка...</div>
            </div>
          </div>
        </div>
        </div>
      </div>
    `;

    document.documentElement.appendChild(overlay);

    modal = q('.tm-eblo-modal', overlay);
    contentBox = q('.tm-eblo-content', overlay);
    prevBtn = q('.tm-eblo-prev', overlay);
    nextBtn = q('.tm-eblo-next', overlay);

    q('.tm-eblo-backdrop', overlay).addEventListener('click', closeModal);
    q('.tm-eblo-shell', overlay).addEventListener('click', closeModal);

    modal.addEventListener('click', (e) => {
      e.stopPropagation();
    });

    prevBtn.addEventListener('click', (e) => {
      e.stopPropagation();
      goRelative(-1);
    });

    nextBtn.addEventListener('click', (e) => {
      e.stopPropagation();
      goRelative(1);
    });

    document.addEventListener('keydown', (e) => {
      if (!overlay || overlay.style.display === 'none') return;
      if (e.key === 'Escape') closeModal();
      if (e.key === 'ArrowLeft') goRelative(-1);
      if (e.key === 'ArrowRight') goRelative(1);
    });
  }

  function openModalShell() {
    ensureModal();
    overlay.style.display = 'block';
    document.body.classList.add('tm-eblo-lock');
  }

  function closeModal() {
    if (!overlay) return;
    overlay.style.display = 'none';
    contentBox.innerHTML = '';
    document.body.classList.remove('tm-eblo-lock');
    currentPostUrl = '';
    currentPostId = '';
    currentVote = 0;
    currentScore = 0;

    if (feedUrlBeforeOpen && location.href !== feedUrlBeforeOpen) {
      history.replaceState({}, '', feedUrlBeforeOpen);
    }
  }

  async function sendCommentVote(commentId, direction, btn) {
    if (!commentId) return;
    const csrf = getCsrfToken(document);
    if (!csrf) return;

    btn.disabled = true;
    try {
      const res = await fetch(`/api/comment/${commentId}/vote`, {
        method: 'POST',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': csrf
        },
        body: JSON.stringify({ vote: direction })
      });
      const data = await res.json();
      if (data.success) {
        const col = btn.closest('.tm-eblo-comment-vote-col');
        if (col) {
          const scoreSpan = col.querySelector('.tm-eblo-comment-score');
          if (scoreSpan) scoreSpan.textContent = data.score;
          const up = col.querySelector('[data-action="up"]');
          const down = col.querySelector('[data-action="down"]');
          if (up) up.classList.toggle('active', data.user_vote === 'up' || data.user_vote === 1);
          if (down) down.classList.toggle('active', data.user_vote === 'down' || data.user_vote === -1);
        }
      }
    } catch (e) {
      console.error('[EbloModal] comment vote err', e);
    } finally {
      btn.disabled = false;
    }
  }

  async function submitNewComment(textContent = null, replyToId = null) {
    if (!currentPostId) return;
    const csrf = getCsrfToken(document);
    if (!csrf) {
      console.error('[EbloModal] CSRF token not found');
      return;
    }

    const text = textContent || q('#tm-eblo-new-comment-text', contentBox)?.value?.trim();
    if (!text) return;

    const mainSubmitBtn = q('#tm-eblo-submit-comment', contentBox);
    if (mainSubmitBtn && !textContent) mainSubmitBtn.disabled = true;

    const payload = { content: text };
    if (replyToId) payload.reply_to = String(replyToId);

    try {
      const res = await fetch(`/api/post/${encodeURIComponent(currentPostId)}/comments`, {
        method: 'POST',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': csrf
        },
        body: JSON.stringify(payload)
      });

      if (!res.ok) throw new Error(`HTTP ${res.status}`);

      if (!textContent) {
        const input = q('#tm-eblo-new-comment-text', contentBox);
        if (input) input.value = '';
      }
      fetchAndRenderComments(currentPostId);
    } catch (e) {
      console.error('[EbloModal] failed to submit commment', e);
    } finally {
      if (mainSubmitBtn && !textContent) mainSubmitBtn.disabled = false;
    }
  }

  function hookGlobalContentEvents() {
    if (!contentBox) return;
    contentBox.addEventListener('click', (e) => {
      const sortBtn = e.target.closest('.tm-eblo-sort-opt');
      if (sortBtn) {
        currentCommentSort = sortBtn.dataset.sort;
        qa('.tm-eblo-sort-opt', contentBox).forEach(b => b.classList.remove('active'));
        sortBtn.classList.add('active');
        if (currentPostId) fetchAndRenderComments(currentPostId);
      }

      const cvBtn = e.target.closest('.tm-eblo-comment-vote-btn');
      if (cvBtn) {
        const id = cvBtn.dataset.id;
        const action = cvBtn.dataset.action;
        sendCommentVote(id, action, cvBtn);
      }

      const submitBtn = e.target.closest('#tm-eblo-submit-comment');
      if (submitBtn) {
        submitNewComment();
      }

      const inlineSubmit = e.target.closest('.tm-eblo-inline-reply-submit');
      if (inlineSubmit) {
        const form = inlineSubmit.closest('.tm-eblo-inline-reply-form');
        const txt = form.querySelector('textarea').value.trim();
        const replyTo = inlineSubmit.getAttribute('data-reply-to');
        if (txt) {
          submitNewComment(txt, replyTo).then(() => { form.remove(); });
        }
      }

      const inlineCancel = e.target.closest('.tm-eblo-inline-reply-cancel');
      if (inlineCancel) {
        inlineCancel.closest('.tm-eblo-inline-reply-form').remove();
      }

      const replyBtn = e.target.closest('.tm-eblo-reply-btn');
      if (replyBtn) {
        const commentId = replyBtn.getAttribute('data-id');
        const login = replyBtn.getAttribute('data-login');
        const item = replyBtn.closest('.tm-eblo-comment-item');

        const oldForms = contentBox.querySelectorAll('.tm-eblo-inline-reply-form');
        oldForms.forEach(f => f.remove());

        const formHtml = `
                    <form class="reply-form tm-eblo-inline-reply-form" style="margin-top: 10px; display: flex; flex-direction: column; gap: 8px;">
                        <textarea class="reply-input tm-eblo-inline-reply-text" placeholder="${login ? 'Ответ @' + esc(login) + '...' : 'Написать ответ...'}" rows="2" maxlength="2000" autocomplete="off" style="width: 100%; background: #1a1a1a; border: 1px solid #333; color: white; padding: 10px; border-radius: 6px; resize: vertical; min-height: 60px; font-family: inherit; font-size: 1rem;"></textarea>
                        <div style="display: flex; justify-content: flex-end; gap: 8px;">
                            <button type="button" class="emote-btn tm-eblo-inline-emote-btn" title="7TV смайлики" style="background: transparent; border: none; color: #999; cursor: pointer; align-self: center; display: flex; align-items: center; justify-content: center;">
                                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M8 13s1.5 2 4 2 4-2 4-2"></path><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line></svg>
                            </button>
                            <button class="reply-cancel-btn tm-eblo-inline-reply-cancel" type="button" style="background: none; border: none; color: #999; cursor: pointer; padding: 5px 10px;">Отмена</button>
                            <button class="reply-submit-btn tm-eblo-inline-reply-submit" data-reply-to="${commentId}" type="button" style="background: var(--tm-primary); color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer;">Ответить</button>
                        </div>
                    </form>
                  `;
        item.querySelector('.tm-eblo-comment-text').insertAdjacentHTML('afterend', formHtml);

        const input = item.querySelector('.tm-eblo-inline-reply-text');
        const emoteBtn = item.querySelector('.tm-eblo-inline-emote-btn');
        if (input) {
          input.focus();
          if (window.EmotePicker) {
            window.EmotePicker.attach(input);
            if (emoteBtn) window.EmotePicker.attachPickerBtn(emoteBtn, input);
          }
        }
      }
    });

    contentBox.addEventListener('input', (e) => {
      const input = e.target.closest('#tm-eblo-new-comment-text');
      if (input) {
        const submitBtn = q('#tm-eblo-submit-comment', contentBox);
        if (submitBtn) submitBtn.disabled = !input.value.trim();
      }
    });

    contentBox.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
        const input = e.target.closest('#tm-eblo-new-comment-text');
        if (input && input.value.trim()) {
          submitNewComment();
        }
      }
    });

    // Gallery init moved to bindDynamicActions() per-post
  }

  function updateNav() {
    const cards = getCards();
    prevBtn.style.visibility = currentIndex > 0 ? 'visible' : 'hidden';
    nextBtn.style.visibility = currentIndex >= 0 && currentIndex < cards.length - 1 ? 'visible' : 'hidden';
  }

  async function fetchDoc(url) {
    const res = await fetch(url, { credentials: 'include' });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const html = await res.text();
    return new DOMParser().parseFromString(html, 'text/html');
  }

  function parseVoteState(root) {
    const scoreText = root.querySelector('#vote-score')?.textContent?.trim() || '0';
    const score = parseInt(scoreText, 10) || 0;

    let vote = 0;
    const upBtn = root.querySelector('#vote-up-btn');
    const downBtn = root.querySelector('#vote-down-btn');

    const isUpActive =
      upBtn?.classList.contains('active') ||
      upBtn?.getAttribute('aria-pressed') === 'true' ||
      /active|selected|voted/i.test(upBtn?.className || '');

    const isDownActive =
      downBtn?.classList.contains('active') ||
      downBtn?.getAttribute('aria-pressed') === 'true' ||
      /active|selected|voted/i.test(downBtn?.className || '');

    if (isUpActive) vote = 1;
    else if (isDownActive) vote = -1;

    return { score, vote };
  }

  function extractImageFromPost(root, doc) {
    const candidates = [
      '#media-container img',
      '.post-card img[src*="/uploads/"]',
      '.post-card img:not(.author-avatar):not(.action-icon):not(.stat-img-sm)',
      '#page-wrap img[src*="/uploads/"]'
    ];

    for (const sel of candidates) {
      const img = q(sel, root) || q(sel, doc);
      if (!img) continue;
      const src = img.getAttribute('src') || img.getAttribute('data-src') || '';
      if (!src) continue;
      return abs(src);
    }

    return '';
  }

  function buildCommentsSection(doc) {
    const commentsRoot = doc.querySelector('#comments-section');
    if (!commentsRoot) return '';

    const title = commentsRoot?.querySelector('.comments-title')?.textContent?.trim() || 'Комментарии';
    const sortLabel = commentsRoot?.querySelector('#comments-sort-label')?.textContent?.trim() || 'По рейтингу';
    const loginPrompt = commentsRoot?.querySelector('.comment-login-prompt span')?.textContent?.trim() || '';

    const csrf = getCsrfToken(document);
    let formStr = '';
    if (csrf && !loginPrompt) {
      formStr = `
              <div class="tm-eblo-comment-form">
                <textarea id="tm-eblo-new-comment-text" placeholder="Написать комментарий..."></textarea>
                <div style="display: flex; flex-direction: column; gap: 8px;">
                  <button id="tm-eblo-submit-comment" type="button" disabled>Отправить</button>
                  <button type="button" class="emote-btn" id="tm-eblo-emote-picker-btn" title="7TV смайлики" style="background: transparent; border: none; color: #999; cursor: pointer; align-self: center;">
                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M8 13s1.5 2 4 2 4-2 4-2"></path><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line></svg>
                  </button>
                </div>
              </div>
            `;
    }

    return `
      <section class="tm-eblo-comments-wrap">
        <div class="tm-eblo-comments-header">
          <h3 class="tm-eblo-comments-title" id="tm-eblo-comments-title">${esc(title)}</h3>
          <div class="tm-eblo-sort-wrap">
             <button class="tm-eblo-sort-opt ${currentCommentSort === 'best' ? 'active' : ''}" data-sort="best" type="button">По рейтингу</button>
             <button class="tm-eblo-sort-opt ${currentCommentSort === 'new' ? 'active' : ''}" data-sort="new" type="button">Сначала новые</button>
             <button class="tm-eblo-sort-opt ${currentCommentSort === 'controversial' ? 'active' : ''}" data-sort="controversial" type="button">Спорные</button>
          </div>
        </div>
        ${formStr}
        ${loginPrompt ? `<div class="tm-eblo-login-prompt">${esc(loginPrompt)}</div>` : ''}
        <div class="tm-eblo-comments-list" id="tm-eblo-comments-list">
          <div class="tm-eblo-login-prompt">Загрузка комментариев...</div>
        </div>
      </section>
    `;
  }

  function buildInteraction(root, url) {
    const interaction = root.querySelector('#interaction-panel');
    if (!interaction) return '';

    const commentsCount = interaction.querySelector('#comments-count-label')?.textContent?.trim() || '0';
    const views = interaction.querySelector('.stat-value-muted')?.textContent?.trim() || '0';

    const voteState = parseVoteState(root);
    currentScore = voteState.score;
    currentVote = voteState.vote;

    return `
      <div class="tm-eblo-interaction">
        <div class="tm-eblo-stats-row">
          <div class="tm-eblo-stats-left">
            <div class="tm-eblo-votes">
              <button class="tm-eblo-vote-btn tm-eblo-vote-up ${currentVote === 1 ? 'active' : ''}" type="button" aria-label="Апвоут"><img src="/static/image/btn-arrow.svg" alt="up" class="btn-arrow arrow-up"></button>
              <span class="tm-eblo-vote-score" id="tm-eblo-vote-score">${esc(String(currentScore))}</span>
              <button class="tm-eblo-vote-btn tm-eblo-vote-down ${currentVote === -1 ? 'active' : ''}" type="button" aria-label="Даунвоут"><img src="/static/image/btn-arrow.svg" alt="down" class="btn-arrow arrow-down"></button>
            </div>
            <div class="tm-eblo-stat">💬 ${esc(commentsCount)}</div>
          </div>
          <div class="tm-eblo-stats-right">
            <div class="tm-eblo-stat-muted">👁 ${esc(views)}</div>
          </div>
        </div>

        <div class="tm-eblo-actions">
          <a class="tm-eblo-action-btn" href="${esc(url)}" target="_blank" rel="noopener noreferrer">Открыть пост</a>
          <button class="tm-eblo-action-btn tm-eblo-copy-link" data-url="${esc(url)}" type="button">Скопировать ссылку</button>
        </div>
      </div>
    `;
  }

  function buildPostHtml(doc, url, overrideCard = null) {
    const root = doc.querySelector('.post-card') || doc.querySelector('#page-wrap.preview-content') || doc;
    if (!root) throw new Error('Не найден пост');

    const authorLink = root.querySelector('.author-link');
    const authorNameEl = root.querySelector('.author-name');
    const authorAvatar = root.querySelector('.author-avatar')?.getAttribute('src') || '';
    const authorName = authorNameEl?.textContent?.trim() || '';
    const authorHref = authorLink?.getAttribute('href') || '#';
    const authorDate = root.querySelector('.author-date')?.textContent?.trim() || '';
    const title = root.querySelector('#post-title')?.textContent?.trim() || 'Пост';

    const descSelectors = [
      '#post-desc', '#post-description', '#post-text',
      '.post-description', '.post-text', '.post-content',
      '.media-description', '.content-text', '.preview-text',
      '.post-body', '.desc-text', '.post-desc',
      '[data-role="description"]', '.description',
      '.post-card-text', '.card-text', '.post-caption',
      '.caption', '.text-content', '.media-caption'
    ];

    let descNode = null;
    for (const sel of descSelectors) {
      descNode = root.querySelector(sel);
      if (descNode) break;
    }

    if (!descNode && overrideCard) {
      for (const sel of descSelectors) {
        descNode = overrideCard.querySelector(sel);
        if (descNode) break;
      }
    }

    let descHtml = '';
    if (descNode) {
      // data-original contains raw text with \n — the site's JS uses this to build the HTML
      const rawOriginal = descNode.getAttribute('data-original')?.trim() || '';
      let descInner = rawOriginal || descNode.innerHTML?.trim() || '';

      // If #post-desc is empty, fall back to og:description meta tag
      if (!descInner) {
        const ogDesc = doc.querySelector('meta[property="og:description"]')?.getAttribute('content')
          || doc.querySelector('meta[name="description"]')?.getAttribute('content')
          || '';
        if (ogDesc) descInner = ogDesc;
      }

      if (descInner) {
        const hasBlocks = /<(p|div|li|ul|ol|h[1-6])[\s>\/]/i.test(descInner);
        const hasBr = /<br\s*\/?>/i.test(descInner);

        if (hasBlocks || hasBr) {
          // Already structured HTML — linkify any plain-text URLs not already wrapped
          descInner = descInner.replace(/(^|[^"'>])(https?:\/\/[^\s<>"']+)/g, (_, pre, url) => {
            return `${pre}<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
          });
        } else {
          // Plain text (e.g. data-original or og:description) — linkify + convert newlines
          descInner = descInner.replace(/\r\n|\r/g, '\n');
          descInner = descInner.replace(/\n{3,}/g, '\n\n');
          descInner = descInner.replace(/(https?:\/\/[^\s<>"']+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
          descInner = descInner.replace(/\n/g, '<br>');
        }
        descHtml = `<div class="tm-eblo-post-desc">${descInner}</div>`;
      }
    }

    const mediaNodes = Array.from(root.querySelectorAll('#media-container img, #media-container video, #media-container .media-video, .post-media img, .post-media video, .post-media .media-video'));
    const uniqueMedia = [];

    for (const node of mediaNodes) {
      const src = node.getAttribute('src') || node.getAttribute('data-src');
      if (!src) continue;
      if (src.includes('/static/')) continue;

      if (!uniqueMedia.find(m => (m.getAttribute('src') || m.getAttribute('data-src')) === src)) {
        uniqueMedia.push(node);
      }
    }

    let mediaHtml = '';
    if (uniqueMedia.length > 0) {
      mediaHtml += '<div class="tm-eblo-media-gallery">';
      if (uniqueMedia.length > 1) {
        mediaHtml += `<button class="tm-gallery-nav tm-gallery-prev" aria-label="Prev" type="button"><img src="/static/image/btn-arrow.svg" alt="prev" class="btn-arrow arrow-left"></button>`;
      }
      mediaHtml += '<div class="tm-eblo-gallery-track">';
      for (const node of uniqueMedia) {
        const isBlurred = node.classList.contains('blurred') || node.style.opacity === '0' || node.getAttribute('style')?.includes('opacity: 0');
        
        mediaHtml += `<div class="tm-eblo-item-wrapper ${isBlurred ? 'tm-eblo-censored' : ''}">`;
        
        if (node.tagName === 'IMG') {
          const src = node.getAttribute('src') || node.getAttribute('data-src');
          mediaHtml += `<img class="tm-eblo-image tm-eblo-gallery-item ${isBlurred ? 'blurred' : ''}" src="${esc(abs(src))}" alt="">`;
        } else {
          const src = node.getAttribute('src') || node.getAttribute('data-src');
          const poster = node.getAttribute('poster') || node.getAttribute('data-poster') || '';
          mediaHtml += `
                         <video
                             class="tm-eblo-video tm-eblo-gallery-item ${isBlurred ? 'blurred' : ''}"
                             controls
                             playsinline
                             preload="metadata"
                             poster="${esc(abs(poster))}"
                             src="${esc(abs(src))}"
                         ></video>
                     `;
        }

        if (isBlurred) {
          mediaHtml += `
            <div class="tm-eblo-censored-overlay">
              <div class="tm-eblo-censored-box">
                <div class="tm-eblo-censored-title">Контент скрыт цензурой</div>
                <div class="tm-eblo-censored-sub">Нажмите, чтобы открыть материалы</div>
              </div>
            </div>
          `;
        }

        mediaHtml += '</div>';
      }
      mediaHtml += '</div>';
      if (uniqueMedia.length > 1) {
        mediaHtml += `<button class="tm-gallery-nav tm-gallery-next" aria-label="Next" type="button"><img src="/static/image/btn-arrow.svg" alt="next" class="btn-arrow arrow-right"></button>`;
      }
      mediaHtml += '</div>';
    } else {
      const fallbackImg = extractImageFromPost(root, doc);
      if (fallbackImg && !fallbackImg.includes('/static/')) {
        mediaHtml = `<div class="tm-eblo-media-wrap" style="margin-top: 15px;"><img class="tm-eblo-image" src="${esc(fallbackImg)}" alt="" style="max-width: 100%; border-radius: 8px;"></div>`;
      }
    }

    return `
      <article class="tm-eblo-post">
        <span class="tm-eblo-watermark">Modal Window by mizu299</span>
        <div class="tm-eblo-author">
          <img class="tm-eblo-author-avatar" src="${esc(authorAvatar)}" alt="">
          <div class="tm-eblo-author-meta">
            <a class="tm-eblo-author-name" href="${esc(authorHref)}" target="_blank" rel="noopener noreferrer">${esc(authorName)}</a>
            <span class="tm-eblo-author-date">${esc(authorDate)}</span>
          </div>
        </div>

        <h1 class="tm-eblo-title">${esc(title)}</h1>
        ${descHtml}

        ${mediaHtml}

        ${buildInteraction(root, url)}
      </article>

      ${buildCommentsSection(doc)}
    `;
  }

  function updateVoteUi() {
    const scoreEl = q('#tm-eblo-vote-score', contentBox);
    const upBtn = q('.tm-eblo-vote-up', contentBox);
    const downBtn = q('.tm-eblo-vote-down', contentBox);

    if (scoreEl) scoreEl.textContent = String(currentScore);

    if (upBtn) upBtn.classList.toggle('active', currentVote === 1);
    if (downBtn) downBtn.classList.toggle('active', currentVote === -1);
  }

  function setVoteButtonsDisabled(disabled) {
    const upBtn = q('.tm-eblo-vote-up', contentBox);
    const downBtn = q('.tm-eblo-vote-down', contentBox);
    if (upBtn) upBtn.disabled = disabled;
    if (downBtn) downBtn.disabled = disabled;
  }

  async function sendVote(direction) {
    if (!currentPostId) return;

    const csrf = getCsrfToken(document);
    if (!csrf) {
      console.error('[EbloModal] CSRF token not found');
      return;
    }

    const prevVote = currentVote;
    const prevScore = currentScore;

    let nextVote = prevVote;
    let nextScore = prevScore;

    if (direction === 'up') {
      if (prevVote === 1) {
        nextVote = 0;
        nextScore = prevScore - 1;
      } else if (prevVote === 0) {
        nextVote = 1;
        nextScore = prevScore + 1;
      } else if (prevVote === -1) {
        nextVote = 1;
        nextScore = prevScore + 2;
      }
    } else if (direction === 'down') {
      if (prevVote === -1) {
        nextVote = 0;
        nextScore = prevScore + 1;
      } else if (prevVote === 0) {
        nextVote = -1;
        nextScore = prevScore - 1;
      } else if (prevVote === 1) {
        nextVote = -1;
        nextScore = prevScore - 2;
      }
    }

    currentVote = nextVote;
    currentScore = nextScore;
    updateVoteUi();
    setVoteButtonsDisabled(true);

    try {
      const res = await fetch(`/api/post/${encodeURIComponent(currentPostId)}/vote`, {
        method: 'POST',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': csrf
        },
        body: JSON.stringify({ vote: direction })
      });

      if (!res.ok) {
        throw new Error(`Vote request failed: ${res.status}`);
      }

      let data = null;
      try {
        data = await res.json();
      } catch { }

      if (data && typeof data === 'object') {
        const maybeScore =
          data.score ??
          data.vote_score ??
          data.rating ??
          data.post_score;

        const maybeVote =
          data.user_vote ??
          data.vote_state ??
          data.current_vote;

        if (typeof maybeScore === 'number') {
          currentScore = maybeScore;
        }

        if (maybeVote === 'up' || maybeVote === 1) currentVote = 1;
        else if (maybeVote === 'down' || maybeVote === -1) currentVote = -1;
        else if (maybeVote === null || maybeVote === 0 || maybeVote === 'none') currentVote = 0;

        updateVoteUi();
      }
    } catch (err) {
      console.error('[EbloModal] vote error', err);
      currentVote = prevVote;
      currentScore = prevScore;
      updateVoteUi();
    } finally {
      setVoteButtonsDisabled(false);
    }
  }

  function bindDynamicActions() {
    const copyBtn = q('.tm-eblo-copy-link', contentBox);
    if (copyBtn) {
      copyBtn.addEventListener('click', async () => {
        try {
          await navigator.clipboard.writeText(copyBtn.dataset.url);
          copyBtn.textContent = 'Скопировано';
          setTimeout(() => {
            copyBtn.textContent = 'Скопировать ссылку';
          }, 1000);
        } catch (e) {
          console.error('[EbloModal] clipboard error', e);
        }
      });
    }

    const upBtn = q('.tm-eblo-vote-up', contentBox);
    const downBtn = q('.tm-eblo-vote-down', contentBox);

    if (upBtn) upBtn.addEventListener('click', () => sendVote('up'));
    if (downBtn) downBtn.addEventListener('click', () => sendVote('down'));

    // --- Gallery navigation (wired per-post) ---
    const track = q('.tm-eblo-gallery-track', contentBox);
    if (track) {
      let galIndex = 0;
      const galItems = Array.from(track.querySelectorAll('.tm-eblo-gallery-item'));
      const galWrappers = Array.from(track.querySelectorAll('.tm-eblo-item-wrapper'));
      const galPrev = q('.tm-gallery-prev', contentBox);
      const galNext = q('.tm-gallery-next', contentBox);

      const updateGallery = () => {
        track.style.transform = `translateX(-${galIndex * 100}%)`;
        if (galPrev) galPrev.style.display = galIndex > 0 ? 'flex' : 'none';
        if (galNext) galNext.style.display = galIndex < galItems.length - 1 ? 'flex' : 'none';
        galItems.forEach((item, i) => {
          if (item.tagName === 'VIDEO' && i !== galIndex) item.pause();
        });
        // autoplay video when it becomes active AND not blurred
        const activeItem = galItems[galIndex];
        const activeWrapper = galWrappers[galIndex];
        const isRevealed = activeWrapper && activeWrapper.classList.contains('is-revealed');
        const isBlurred = activeItem && activeItem.classList.contains('blurred');

        if (activeItem && activeItem.tagName === 'VIDEO' && (!isBlurred || isRevealed)) {
          applyMediaState(activeItem);
          activeItem.play().catch(() => { });
        }
      };

      if (galPrev) {
        galPrev.addEventListener('click', (e) => {
          e.preventDefault(); e.stopPropagation();
          if (galIndex > 0) { galIndex--; updateGallery(); }
        });
      }
      if (galNext) {
        galNext.addEventListener('click', (e) => {
          e.preventDefault(); e.stopPropagation();
          if (galIndex < galItems.length - 1) { galIndex++; updateGallery(); }
        });
      }

      // Handle Censorship Clicks
      track.addEventListener('click', (e) => {
        const censoredOverlay = e.target.closest('.tm-eblo-censored-overlay');
        if (censoredOverlay) {
          e.preventDefault(); e.stopPropagation();
          const wrapper = censoredOverlay.closest('.tm-eblo-item-wrapper');
          if (wrapper) {
            wrapper.classList.add('is-revealed');
            const video = wrapper.querySelector('video');
            if (video) {
              applyMediaState(video);
              video.play().catch(() => { });
            }
          }
        }
      });

      updateGallery();
    }

    // --- Video autoplay ---
    const video = q('.tm-eblo-video', contentBox);
    if (video && !track) {
      applyMediaState(video);
      video.play().catch(() => { });
    }
  }

  function timeAgo(ts) {
    if (!ts) return '';
    var now = Math.floor(Date.now() / 1000);
    var diff = now - ts;
    if (diff < 60) return 'только что';
    if (diff < 3600) return Math.floor(diff / 60) + ' мин назад';
    if (diff < 86400) return Math.floor(diff / 3600) + ' ч назад';
    if (diff < 604800) return Math.floor(diff / 86400) + ' дн назад';
    var d = new Date(ts * 1000);
    return d.toLocaleDateString('ru-RU');
  }

  async function fetchAndRenderComments(postId) {
    const listEl = q('#tm-eblo-comments-list', contentBox);
    const titleEl = q('#tm-eblo-comments-title', contentBox);
    if (!listEl) return;

    try {
      const res = await fetch(`/api/post/${encodeURIComponent(postId)}/comments?sort=${currentCommentSort}`);
      const data = await res.json();
      if (data.success) {
        const comments = data.comments || [];
        const total = data.total || 0;
        if (titleEl) titleEl.textContent = `Комментарии (${total})`;

        if (comments.length === 0) {
          listEl.innerHTML = '<div class="tm-eblo-login-prompt">Комментариев пока нет. Будьте первым!</div>';
          return;
        }

        let itemsHtml = '';
        function renderCommentObj(c, isReply = false) {
          if (c.is_deleted) return '';
          const avatar = c.author?.avatar || '/static/image/user.svg';
          const author = c.author?.name || c.author?.login || 'Пользователь';
          const authorHref = c.author?.login ? `/@${c.author.login}` : '#';
          const timeStr = timeAgo(c.created_at);

          let html = `
                        <div class="tm-eblo-comment-item ${isReply ? 'tm-eblo-comment-reply' : ''}">
                          <div class="tm-eblo-comment-vote-col">
                            <button class="tm-eblo-comment-vote-btn ${c.user_vote === 'up' || c.user_vote === 1 ? 'active' : ''}" data-action="up" data-id="${c.id}" type="button" title="Апвоут"><img src="/static/image/btn-arrow.svg" alt="up" class="btn-arrow arrow-up"></button>
                            <span class="tm-eblo-comment-score">${esc(String(c.score || 0))}</span>
                            <button class="tm-eblo-comment-vote-btn ${c.user_vote === 'down' || c.user_vote === -1 ? 'active' : ''}" data-action="down" data-id="${c.id}" type="button" title="Даунвоут"><img src="/static/image/btn-arrow.svg" alt="down" class="btn-arrow arrow-down"></button>
                          </div>
                          <div class="tm-eblo-comment-body">
                            <div class="tm-eblo-comment-meta">
                              <img class="tm-eblo-comment-author-avatar" src="${esc(avatar)}" alt="">
                              <a class="tm-eblo-comment-author" href="${esc(abs(authorHref))}" target="_blank" rel="noopener noreferrer">${esc(author)}</a>
                              <span class="tm-eblo-comment-time">${esc(timeStr)}</span>
                            </div>
                            <div class="tm-eblo-comment-text">${c.content || ''}</div>
                            <div class="tm-eblo-comment-actions" style="margin-top: 8px; display: flex; align-items: center; gap: 16px;">
                               <button class="tm-eblo-comment-action-btn tm-eblo-reply-btn" data-id="${c.id}" data-login="${c.author?.login || ''}" type="button" style="background: none; border: none; color: #a0a0a0; font-size: 0.9rem; cursor: pointer; padding: 0;">Ответить</button>
                            </div>
                          </div>
                        </div>
                    `;

          if (c.replies && c.replies.length > 0) {
            for (const rep of c.replies) {
              html += renderCommentObj(rep, true);
            }
          }
          return html;
        }

        for (const c of comments) {
          itemsHtml += renderCommentObj(c, false);
        }
        listEl.innerHTML = itemsHtml;

        listEl.querySelectorAll('.tm-eblo-comment-text').forEach(el => {
          const raw = el.textContent;
          if (window.EmotePicker && window.EmotePicker.loaded && raw.includes(':')) {
            window.EmotePicker.applyRender(el, raw);
          }
          if (window.HashtagUtils) {
            try { window.HashtagUtils.renderInElement(el); } catch (err) { }
          }
        });
      } else {
        listEl.innerHTML = '<div class="tm-eblo-login-prompt">Не удалось загрузить комментарии</div>';
      }
    } catch (e) {
      console.error('[EbloModal] load comments error', e);
      listEl.innerHTML = '<div class="tm-eblo-login-prompt">Ошибка сети при загрузке</div>';
    }
  }

  async function openPost(url, index) {
    openModalShell();
    currentIndex = index;

    const cards = getCards();
    const card = cards[index] || null;

    currentPostUrl = url;
    currentPostId = getPostIdFromUrl(url);
    updateNav();
    contentBox.innerHTML = '<div class="tm-eblo-loading">Загрузка поста...</div>';

    try {
      const doc = await fetchDoc(url);
      contentBox.innerHTML = buildPostHtml(doc, url, card);
      bindDynamicActions();

      if (window.EmotePicker) {
        const input = q('#tm-eblo-new-comment-text', contentBox);
        const emoteBtn = q('#tm-eblo-emote-picker-btn', contentBox);
        if (input && emoteBtn) {
          window.EmotePicker.attach(input);
          window.EmotePicker.attachPickerBtn(emoteBtn, input);
        }
      }

      history.replaceState({}, '', url);
      fetchAndRenderComments(currentPostId);
    } catch (e) {
      console.error('[EbloModal]', e);
      contentBox.innerHTML = `
        <div class="tm-eblo-error">
          Не удалось загрузить пост.<br><br>
          <a class="tm-eblo-action-btn" href="${esc(url)}" target="_blank" rel="noopener noreferrer">Открыть отдельно</a>
        </div>
      `;
    }
  }

  function goRelative(delta) {
    const currentVideo = q('.tm-eblo-video', contentBox);
    if (currentVideo) saveMediaState(currentVideo);

    const cards = getCards();
    const newIndex = currentIndex + delta;
    if (newIndex < 0 || newIndex >= cards.length) return;

    const url = getUrlFromCard(cards[newIndex]);
    if (!url) return;

    openPost(url, newIndex);
  }

  function injectOpenButtons() {
    for (const card of getCards()) {
      if (card.querySelector('.tm-eblo-open-post-btn')) continue;

      const url = getUrlFromCard(card);
      if (!url) continue;

      const target = card.querySelector('.feed-card-info') || card;
      const btn = document.createElement('a');
      btn.className = 'tm-eblo-open-post-btn';
      btn.href = url;
      btn.target = '_blank';
      btn.rel = 'noopener noreferrer';
      btn.textContent = 'Открыть пост';

      btn.addEventListener('click', (e) => {
        e.stopPropagation();
      });

      target.appendChild(btn);
    }
  }

  function handleCardOpen(event) {
    if (!isFeedPage()) return;
    if (event.button !== 0) return;
    if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;

    const openBtn = event.target.closest('.tm-eblo-open-post-btn');
    if (openBtn) return;

    const card = event.target.closest(CARD_SELECTOR);
    if (!card) return;

    event.preventDefault();
    event.stopPropagation();
    event.stopImmediatePropagation();

    const url = getUrlFromCard(card);
    if (!url) return;

    feedUrlBeforeOpen = location.href;
    openPost(url, getIndexByUrl(url));
  }

  function installInterceptors() {
    const types = ['click', 'auxclick', 'mousedown', 'mouseup', 'pointerdown', 'pointerup'];
    for (const type of types) {
      document.addEventListener(type, handleCardOpen, true);
    }
  }

  function observeFeed() {
    const mo = new MutationObserver(() => injectOpenButtons());
    mo.observe(document.documentElement, { childList: true, subtree: true });
    injectOpenButtons();
  }

  function init() {
    ensureModal();
    hookGlobalContentEvents();
    installInterceptors();
    observeFeed();
    console.log('[EbloModal] ready v5.1');
  }

  init();
})();