// Hyper · section components

function NavBar() {
  const [open, setOpen] = React.useState(false);

  // блокируем скролл фона, пока открыто мобильное меню
  React.useEffect(() => {
    document.body.classList.toggle('nav-open', open);
    return () => document.body.classList.remove('nav-open');
  }, [open]);

  // закрываем меню по Esc и при возврате на десктоп
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    const onResize = () => { if (window.innerWidth > 900) setOpen(false); };
    window.addEventListener('keydown', onKey);
    window.addEventListener('resize', onResize);
    return () => { window.removeEventListener('keydown', onKey); window.removeEventListener('resize', onResize); };
  }, []);

  const links = [
    { href: '#directions', label: 'Отличия', n: '01' },
    { href: 'services.html', label: 'Услуги', n: '02' },
    { href: '#metrics', label: 'Каналы', n: '03' },
    { href: 'work.html', label: 'Кейсы', n: '04' },
    { href: '#contact', label: 'Контакты', n: '05' },
  ];

  return (
    <>
    <nav className="nav" data-comment-anchor="6a2b07cfa4-nav-16-5">
      <a href="#top" className="nav-logo" data-cursor data-cursor-label="наверх" data-comment-anchor="15bec6db93-a-6-7">
        <img className="nav-eyes" src="assets/brand-mark-tight.png" alt="ХАЙПЕР" />
        <span className="nav-wordmark">ХАЙПЕР</span>
      </a>
      <div className="nav-links">
        {links.map((l) => <a key={l.href} href={l.href}>{l.label}</a>)}
      </div>
      <a href="https://wa.me/79886554717" target="_blank" rel="noopener" className="nav-cta magnetic" data-cursor data-cursor-label="WhatsApp">
        WhatsApp&nbsp;→
      </a>

      {/* ── мобильный бургер ── */}
      <button
        type="button"
        className={'nav-burger' + (open ? ' is-open' : '')}
        aria-label={open ? 'Закрыть меню' : 'Открыть меню'}
        aria-expanded={open}
        onClick={() => setOpen((v) => !v)}>
        <span></span><span></span><span></span>
      </button>
    </nav>

    {/* ── мобильное меню (полноэкранное) — вне <nav>, чтобы не наследовать
         containing block от backdrop-filter навбара ── */}
    <div className={'nav-drawer' + (open ? ' is-open' : '')} role="dialog" aria-modal="true" aria-hidden={!open}>
      <div className="nav-drawer-inner">
        <span className="nav-drawer-label">Навигация</span>
        <ul className="nav-drawer-links">
          {links.map((l) => (
            <li key={l.href}>
              <a href={l.href} onClick={() => setOpen(false)}>
                <span className="ndl-n">/ {l.n}</span>
                <span className="ndl-t">{l.label}</span>
                <span className="ndl-arrow" aria-hidden="true">→</span>
              </a>
            </li>
          ))}
        </ul>
        <div className="nav-drawer-foot">
          <button
            type="button"
            className="btn primary nav-drawer-cta"
            onClick={() => { setOpen(false); window.openLead({ service: 'consult', source: 'Мобильное меню · консультация' }); }}>
            <span>Получить консультацию</span>
            <span className="arrow">→</span>
          </button>
          <div className="nav-drawer-contacts">
            <a href="https://wa.me/79886554717" target="_blank" rel="noopener">WhatsApp</a>
            <a href="tel:+79886554717">+7 (988) 655-47-17</a>
          </div>
        </div>
      </div>
    </div>
    </>);

}

// ── Hero brand mark
function HeroMark() {
  return (
    <div className="hero-mark fade-up">
      <div className="hero-mark-stage">
        <img src="assets/brand-mark.png" alt="" />
      </div>
    </div>);

}

// ── Бегущий слоган: вторая строка hero печатается и сменяется по кругу (typewriter)
function TypewriterRotator({ phrases }) {
  const [index, setIndex] = React.useState(0);   // какая фраза сейчас
  const [count, setCount] = React.useState(0);   // сколько букв показано
  const [phase, setPhase] = React.useState('idle'); // idle | typing | deleting

  const TYPE = 60;    // мс на букву при печати
  const ERASE = 32;   // мс на букву при стирании
  const HOLD = 1600;  // пауза на готовой фразе
  const START = 1300; // задержка старта — даём отыграть reveal-анимации строки

  // Старт печати с небольшой задержкой — даём отыграть reveal-анимации строки.
  // (Намеренно не глушим эффект при prefers-reduced-motion: бегущий слоган —
  //  ключевой имиджевый элемент главного экрана, он должен работать у всех.)
  React.useEffect(() => {
    const t = setTimeout(() => setPhase('typing'), START);
    return () => clearTimeout(t);
  }, []);

  React.useEffect(() => {
    if (phase === 'idle') return;
    const word = phrases[index];
    let t;
    if (phase === 'typing') {
      if (count < word.length) {
        t = setTimeout(() => setCount(count + 1), TYPE);
      } else {
        t = setTimeout(() => setPhase('deleting'), HOLD);
      }
    } else if (phase === 'deleting') {
      if (count > 0) {
        t = setTimeout(() => setCount(count - 1), ERASE);
      } else {
        setIndex((index + 1) % phrases.length);
        setPhase('typing');
      }
    }
    return () => clearTimeout(t);
  }, [phase, count, index, phrases]);

  const word = phrases[index];
  return (
    <span className="accent tw-line">
      {word.slice(0, count)}
      <span className="tw-caret" aria-hidden="true"></span>
    </span>);

}

function Hero() {
  return (
    <section className="hero" id="top" data-comment-anchor="dfc359384f-section-37-5">
      <div className="hero-glow" data-parallax="0.12"></div>
      <div className="shell">
        <div className="hero-grid" data-comment-anchor="bfc02a7c63-div-40-9">
          <div className="hero-mark-wrap" data-cursor="xray" data-cursor-label="" data-comment-anchor="1b617a1eb7-div-41-11">
            <HeroMark />
          </div>
          <div className="hero-eyebrow fade-up d-1">
            <span className="pulse"></span>
            <span className="eyebrow">Хайпер · агентство маркетинга · 2026</span>
          </div>

          <h1>
            <span className="row r-lead"><span data-cursor="xray">Маркетинг, который</span></span>
            <span className="row r-punch"><TypewriterRotator phrases={["возвращает больше", "приносит лиды", "работает на результат", "считает каждый рубль"]} /></span>
          </h1>

          <div className="hero-sub-row fade-up d-2" data-comment-anchor="5c9a0abf10-div-52-11">
            <p className="hero-sub">
              Стратегия, SMM, личный бренд и продакшн.
              Каждое решение обосновано <strong>цифрами</strong>,
              каждая цифра упакована в <strong>смелую идею</strong>.
            </p>

            <div className="hero-cta-row">
              <button type="button" onClick={() => window.openLead({ service: 'consult', source: 'Главный экран · Получить консультацию' })} className="btn primary magnetic" data-cursor data-cursor-label="бесплатно">
                <span>Получить консультацию</span>
                <span className="arrow">→</span>
              </button>
              <a href="#cases" className="btn magnetic" data-cursor data-cursor-label="кейсы">
                <span>Смотреть кейсы</span>
                <span className="arrow">↓</span>
              </a>
            </div>

            <div className="hero-meta">
              <div><span className="num">100<em>+</em></span> проектов</div>
              <div><span className="num">300K<em>+</em></span> привлечённых лидов</div>
              <div><span className="num">7<em>+</em></span> лет на рынке маркетинга</div>
            </div>
          </div>
        </div>
      </div>
    </section>);

}

// ── Ticker: two counter-rotating rows (v2)
function Ticker() {
  const itemsA = [
  'Стратегия',
  'SMM',
  'Перформанс',
  'Личный бренд',
  'Продакшн',
  'Аналитика',
  'Креатив',
  'Reels'];

  const itemsB = [
  '100+ проектов',
  '300K+ лидов',
  '7 лет на рынке',
  '13,1М охват с одного Reels',
  '×30 рост Reels',
  '+489% переходов',
  '97,7% органика',
  'Махачкала · Дагестан'];

  const make = (items) =>
  <span className="item">
      {items.map((t, i) =>
    <React.Fragment key={i}>
          <span className="ti-w">{t}</span>
          <span className="sep" aria-hidden="true">·</span>
        </React.Fragment>
    )}
    </span>;

  const blockA = make(itemsA);
  const blockB = make(itemsB);

  return (
    <>
      <div className="ticker">
        <div className="ticker-track">
          {blockA}{blockA}{blockA}{blockA}
        </div>
      </div>
      <div className="ticker ticker-v2">
        <div className="ticker-track">
          {blockB}{blockB}{blockB}{blockB}
        </div>
      </div>
    </>);

}

// ──────────────────────────────────────────────
// Scroll-driven "constructor" assembly for card grids — same motion language as
// the client-logos block, with a left-to-right cascade so cards lock into the
// grid one after another. Uses the independent `translate`/`scale` CSS props so
// it never fights each card's own hover transform. Returns a cleanup fn.
// ──────────────────────────────────────────────
function runConstructor(grid, { expand = 0.1, stagger = 0.5, lift = 30, dim = 0.65 } = {}) {
  const cards = [...grid.children];
  const n = cards.length;
  if (!n) return () => {};
  const clamp = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
  const ease = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;

  // each card's offset from grid centre (layout coords, transform-independent);
  // re-measured on resize so it tracks the responsive breakpoints
  let off = [];
  const measure = () => {
    const cx = grid.clientWidth / 2, cy = grid.clientHeight / 2;
    off = cards.map((c) => ({
      x: c.offsetLeft + c.offsetWidth / 2 - cx,
      y: c.offsetTop + c.offsetHeight / 2 - cy,
    }));
  };
  measure();
  window.addEventListener('resize', measure);

  let raf = 0, last = -1;
  const tick = () => {
    const rect = grid.getBoundingClientRect();
    const vh = window.innerHeight || 1;
    const v = (rect.top + rect.height / 2) / vh;          // 1 = grid centre at viewport bottom
    const raw = clamp((1.08 - v) / (1.08 - 0.5));          // linear 0→1, fully locked by v=0.5
    if (Math.abs(raw - last) > 0.0006) {
      last = raw;
      for (let i = 0; i < n; i++) {
        const local = clamp((raw - (i / n) * stagger) / (1 - stagger));  // per-card cascade
        const k = 1 - ease(local);                         // 1 = apart, 0 = locked into slot
        const o = off[i];
        cards[i].style.translate = `${(o.x * expand * k).toFixed(2)}px ${(o.y * expand * k + lift * k).toFixed(2)}px`;
        cards[i].style.scale = (1 - 0.05 * k).toFixed(3);
        cards[i].style.opacity = (1 - dim * k).toFixed(3);
      }
    }
    raf = requestAnimationFrame(tick);
  };
  raf = requestAnimationFrame(tick);
  return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', measure); };
}

// ──────────────────────────────────────────────
// Mobile reveal — на телефоне «конструктор» требует долгой прокрутки, чтобы
// собрать карточки в сетку. Вместо него: карточки поочерёдно выезжают с боков
// (нечётные — слева, чётные — справа) по мере появления в кадре. Направление и
// смещение заданы в CSS (.m-reveal, ≤640px), здесь только триггер на скролл.
// Карточки уже несут класс .m-reveal из разметки, поэтому на мобиле скрыты
// сразу (без вспышки); на десктопе те же правила выключены медиазапросом.
// ──────────────────────────────────────────────
function runMobileReveal(grid) {
  const cards = [...grid.children];
  if (!cards.length) return () => {};
  // подчищаем инлайн-стили, если до ресайза успел отработать десктопный конструктор
  cards.forEach((c) => { c.style.translate = ''; c.style.scale = ''; c.style.opacity = ''; });
  const io = new IntersectionObserver((entries) => {
    entries.forEach((e) => {
      if (e.isIntersecting) { e.target.classList.add('m-in'); io.unobserve(e.target); }
    });
  }, { threshold: 0.25 });
  cards.forEach((c) => io.observe(c));
  return () => io.disconnect();
}

// Выбирает анимацию под ширину экрана: десктоп — «конструктор», мобайл — выезд
// с боков. Переключается при пересечении брейкпоинта (поворот экрана / ресайз).
function runGridAssembly(grid, constructorOpts) {
  const desktop = window.matchMedia('(min-width: 641px)');
  let cleanup = () => {};
  const setup = () => {
    cleanup();
    cleanup = desktop.matches ? runConstructor(grid, constructorOpts) : runMobileReveal(grid);
  };
  setup();
  desktop.addEventListener('change', setup);
  return () => { desktop.removeEventListener('change', setup); cleanup(); };
}

function Directions() {
  const bentoRef = React.useRef(null);
  React.useEffect(() => {
    if (!bentoRef.current) return;
    return runGridAssembly(bentoRef.current, { expand: 0.09, stagger: 0.5, lift: 28, dim: 0.6 });
  }, []);

  const points = [
  {
    idx: '01',
    area: 'a',
    featured: true,
    title: 'Весь маркетинг под ключ',
    body:
    'Брендинг, стратегия, SMM, performance, продакшн — одна команда вместо пяти подрядчиков. С нуля или после провального тендера.',
    tag: 'полный цикл'
  },
  {
    idx: '02',
    area: 'b',
    title: '7+ лет на рынке',
    body: 'Запустились в Махачкале одними из первых.',
    tag: 'опыт'
  },
  {
    idx: '03',
    area: 'c',
    title: 'Цифры, а не отчёты',
    body: 'KPI, ROAS, CPL. Канал не выходит — закрываем без эго.',
    tag: 'результат'
  },
  {
    idx: '04',
    area: 'd',
    title: 'Смелые идеи в метрике',
    body: 'Креатив доказывает себя цифрами, а не на дегустации.',
    tag: 'креатив'
  },
  {
    idx: '05',
    area: 'e',
    title: 'Команда, которая не рассыпается',
    body:
    'Продюсер на каждом проекте, отчёты по понедельникам, планы на квартал. Никаких «забыли, потерялись, заболели».',
    tag: 'процесс'
  }];


  return (
    <section className="sec" id="directions">
      <div className="shell">
        <div className="sec-num fade-up">01 · отличия</div>
        <div className="sec-head fade-up d-1">
          <h2>
            Чем мы <span className="accent">отличаемся</span><br />
            <span className="muted">от других.</span>
          </h2>
          <p className="lede">
            Пять опор, на&nbsp;которых держится наша работа.
            Без&nbsp;маркетинговой воды&nbsp;— только то, что мы готовы защищать на&nbsp;любом созвоне.
          </p>
        </div>
        <div className="diff-bento" ref={bentoRef}>
          {points.map((p) =>
          <article
            key={p.idx}
            className={'db-card m-reveal' + (p.featured ? ' is-featured' : '')}
            style={{ gridArea: p.area }}>

              <header className="db-head">
                <span className="db-idx">/ {p.idx}</span>
                <span className="db-tag">{p.tag}</span>
              </header>
              <h3 className="db-title">{p.title}</h3>
              <p className="db-body">{p.body}</p>
              {p.featured &&
            <span className="db-feat-mark" aria-hidden="true">
                  <span className="db-feat-line"></span>
                  <span className="db-feat-dot"></span>
                </span>
            }
              <span className="db-corner" aria-hidden="true"></span>
            </article>
          )}
        </div>
      </div>
    </section>);

}

// ──────────────────────────────────────────────
// SERVICES — компактное превью направлений (главная).
// Полный прайс и состав работ — на отдельной странице services.html.
// ──────────────────────────────────────────────
function ServicePreviewCard({ dir }) {
  return (
    <a
      href={'services.html#' + dir.id}
      className={'svc-prev m-reveal' + (dir.featured ? ' is-featured' : '')}
      data-glow
      data-cursor data-cursor-label="смотреть">

      <span className="glow"></span>
      <header className="svc-prev-head">
        <span className="svc-prev-idx">/ {dir.idx}</span>
        {dir.tag && <span className="svc-prev-tag">{dir.tag}</span>}
      </header>

      <h3 className="svc-prev-name">{dir.name}</h3>
      <p className="svc-prev-desc">{dir.desc}</p>

      <ul className="svc-prev-tariffs">
        {dir.items.map((t, i) =>
        <li key={i}><span className="svc-prev-dot">·</span>{t}</li>
        )}
      </ul>

      <div className="svc-prev-foot">
        <span className="svc-prev-more">Смотреть тарифы</span>
        <span className="svc-prev-go" aria-hidden="true">→</span>
      </div>
    </a>);

}

function Services() {
  const gridRef = React.useRef(null);
  React.useEffect(() => {
    if (!gridRef.current) return;
    return runGridAssembly(gridRef.current, { expand: 0.28, stagger: 0.5, lift: 30, dim: 0.72 });
  }, []);

  const directions = [
  {
    id: 'marketing', idx: '01', name: 'Маркетинг', tag: 'полный цикл',
    desc: 'Забираем управление маркетингом на себя и превращаем его в конвейер лидов. Считаем каждый канал — что не приносит денег, закрываем без эго.',
    items: ['Start — фокус на одной метрике', 'Boost — все каналы и стратегия']
  },
  {
    id: 'smm', idx: '02', name: 'SMM', tag: 'контент',
    desc: 'Instagram, который работает на охваты и продажи: контент по стратегии, блогеры, продвижение. От стабильного присутствия до заметного роста.',
    items: ['База — регулярное ведение', 'Pro — комплексный рост']
  },
  {
    id: 'maximum', idx: '03', name: 'Максимум', tag: 'флагман', featured: true,
    desc: 'Весь маркетинг и SMM в одной команде под единым KPI. Один подрядчик с ответственностью за результат — вместо пяти, которые кивают друг на друга.',
    items: ['Boost+ маркетинг', 'Pro+ SMM', 'Продюсер и арт-директор']
  },
  {
    id: 'branding', idx: '04', name: 'Брендинг', tag: 'айдентика',
    desc: 'Визуальный код, который раскрывает смыслы бренда и работает на продажи. Не просто «красиво» — маркетинговый подход от позиционирования.',
    items: ['Логотип и айдентика', 'Дизайн упаковки', 'Позиционирование']
  },
  {
    id: 'production', idx: '05', name: 'Продакшн', tag: 'видео',
    desc: 'Рекламные ролики, которые цепляют: от reels на телефон до павильона с режиссёром. Идея, съёмка, монтаж — под любую площадку.',
    items: ['Сценарий и раскадровка', 'Съёмка и монтаж']
  },
  {
    id: 'extra', idx: '06', name: 'Доп. услуги', tag: 'разовые',
    desc: 'Глубокий разбор бизнеса и каналов с готовой дорожной картой роста. Когда нужен не подрядчик на ведение, а ясность, что делать дальше.',
    items: ['Разработка стратегии', 'Аудит маркетинга и SMM']
  }];


  return (
    <section className="sec" id="services" data-comment-anchor="22ddb4ae7d-section-402-5">
      <div className="shell">
        <div className="sec-num fade-up">02 · услуги</div>
        <div className="sec-head fade-up d-1">
          <h2>
            Что мы&nbsp;<span className="accent">делаем</span><br />
            <span className="muted">и какой даём результат.</span>
          </h2>
          <p className="lede">
            Шесть направлений — от одной услуги до полного цикла. Здесь коротко, под какую
            задачу каждое. Подробные тарифы, состав работ и&nbsp;цены&nbsp;— на&nbsp;отдельной странице.
          </p>
        </div>

        <div className="svc-prev-grid" ref={gridRef}>
          {directions.map((d) => <ServicePreviewCard key={d.id} dir={d} />)}
        </div>

        <div className="cases-foot fade-up">
          <a href="services.html" className="btn primary magnetic" data-cursor data-cursor-label="все услуги">
            <span>Все услуги и цены</span>
            <span className="arrow">→</span>
          </a>
          <span className="cases-foot-note">
            <span className="cases-foot-dot"></span>
            6 направлений · можно собрать своё решение
          </span>
        </div>
      </div>
    </section>);

}

function Channels() {
  // "Decoder" heading: on first scroll-in every letter flickers through digits
  // (a nod to "считаем") and then resolves into the slogan, left-to-right.
  const headingRef = React.useRef(null);
  React.useEffect(() => {
    const h2 = headingRef.current;
    if (!h2) return;

    // wrap each character in its own span, preserving the red/muted colour spans
    // and the <br> line breaks; spaces stay as plain text
    const chars = [];
    const walk = (node) => {
      [...node.childNodes].forEach((child) => {
        if (child.nodeType === 3) {
          const frag = document.createDocumentFragment();
          for (const ch of child.nodeValue) {
            if (ch === ' ' || ch === ' ') { frag.appendChild(document.createTextNode(ch)); continue; }
            const s = document.createElement('span');
            s.className = 'sc';
            s.textContent = ch;
            frag.appendChild(s);
            chars.push({ el: s, fin: ch, letter: /[A-Za-zА-Яа-яЁё]/.test(ch) });
          }
          child.replaceWith(frag);
        } else if (child.nodeType === 1 && child.tagName !== 'BR') {
          walk(child);
        }
      });
    };
    walk(h2);
    if (!chars.length) return;

    const DIGITS = '0123456789';
    let raf = 0, running = false, widthsLocked = false;
    const run = () => {
      // lock each slot to its final width once so flickering digits never reflow the line
      if (!widthsLocked) {
        chars.forEach((c) => {
          c.el.style.display = 'inline-block';
          c.el.style.width = c.el.getBoundingClientRect().width + 'px';
          c.el.style.textAlign = 'center';
        });
        widthsLocked = true;
      }
      cancelAnimationFrame(raf);
      running = true;
      const DUR = 1050;
      chars.forEach((c, i) => {
        c.revealAt = c.letter ? (i / chars.length) * (DUR * 0.6) + Math.random() * (DUR * 0.4) : 0;
        c.nextFlip = 0;
        c.el.classList.remove('sc-in');   // allow the red "click" to replay
      });
      let start = 0;
      const frame = (ts) => {
        if (!start) start = ts;
        const t = ts - start;
        let done = true;
        for (const c of chars) {
          if (!c.letter || t >= c.revealAt) {
            if (c.el.textContent !== c.fin) { c.el.textContent = c.fin; c.el.classList.add('sc-in'); }
          } else {
            done = false;
            if (ts >= c.nextFlip) { c.el.textContent = DIGITS[(Math.random() * 10) | 0]; c.nextFlip = ts + 45; }
          }
        }
        if (!done) raf = requestAnimationFrame(frame);
        else { chars.forEach((c) => { c.el.textContent = c.fin; }); running = false; }
      };
      raf = requestAnimationFrame(frame);
    };

    // Replays every time the slogan re-enters view. Hysteresis (re-arm only after
    // it drops below 0.2) prevents re-firing while you linger near the edge.
    let armed = true;
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (e.intersectionRatio >= 0.4) {
          if (armed && !running) { armed = false; run(); }
        } else if (e.intersectionRatio <= 0.2) {
          armed = true;
        }
      });
    }, { threshold: [0.2, 0.4] });
    io.observe(h2);

    return () => { cancelAnimationFrame(raf); io.disconnect(); };
  }, []);

  // Channels table: the grid frame stays put while its contents "populate" —
  // column by column left→right, rows top→down within each column — like a data
  // feed filling in. Replays each time the table re-enters view.
  const tableRef = React.useRef(null);
  React.useEffect(() => {
    const table = tableRef.current;
    if (!table) return;
    const cols = [...table.querySelectorAll('.ch-col')];
    const cells = [];
    cols.forEach((col, ci) => {
      const items = [col.querySelector('.ch-col-head'), ...col.querySelectorAll('.ch-row')];
      items.forEach((el, ri) => { if (el) cells.push({ el, delay: ci * 85 + ri * 55 }); });
    });
    if (!cells.length) return;

    const clamp = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
    const ease = (t) => 1 - Math.pow(1 - t, 3);   // easeOutCubic
    const DUR = 480;
    const apply = (c, p) => {
      const e = ease(p);
      c.el.style.opacity = e.toFixed(3);
      c.el.style.transform = `translateY(${((1 - e) * 18).toFixed(2)}px)`;
    };
    const reset = () => cells.forEach((c) => { c.el.style.willChange = 'transform, opacity'; c.el.style.opacity = '0'; c.el.style.transform = 'translateY(18px)'; });
    reset();   // hidden until first reveal (prevents a flash before the trigger)

    let raf = 0, running = false;
    const run = () => {
      cancelAnimationFrame(raf);
      running = true;
      reset();
      let start = 0;
      const frame = (ts) => {
        if (!start) start = ts;
        const t = ts - start;
        let done = true;
        for (const c of cells) {
          const p = clamp((t - c.delay) / DUR);
          apply(c, p);
          if (p < 1) done = false;
        }
        if (!done) raf = requestAnimationFrame(frame);
        else running = false;
      };
      raf = requestAnimationFrame(frame);
    };

    let armed = true;
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (e.intersectionRatio >= 0.22) {
          if (armed && !running) { armed = false; run(); }
        } else if (e.intersectionRatio <= 0.06) {
          armed = true;
        }
      });
    }, { threshold: [0.06, 0.22] });
    io.observe(table);

    return () => { cancelAnimationFrame(raf); io.disconnect(); };
  }, []);

  const channels = [
  {
    idx: '01',
    name: 'Instagram',
    kicker: 'основная площадка',
    rows: [
    { k: 'Контент', v: 'Reels, ленты, истории по стратегии бренда' },
    { k: 'Блогеры', v: 'Подбор, переговоры, контроль интеграций' },
    { k: 'Паблики', v: 'Посевы и тематические сообщества' }]

  },
  {
    idx: '02',
    name: 'Telegram',
    kicker: 'охват и трафик',
    rows: [
    { k: 'Паблики', v: 'Подбор каналов, рекламные посты под ЦА' },
    { k: 'Таргет', v: 'Таргетированная реклама внутри Telegram' },
    { k: 'Кабинеты', v: 'Запуск и ведение рекламных кабинетов' }]

  },
  {
    idx: '03',
    name: 'Яндекс.​Директ',
    kicker: 'тёплая аудитория',
    rows: [
    { k: 'Семантика', v: 'Сбор и кластеризация ключевых запросов' },
    { k: 'Поиск', v: 'Объявления под горячий спрос' },
    { k: 'РСЯ', v: 'Сети, ретаргет, look-alike по базам' }]

  },
  {
    idx: '04',
    name: 'Авито',
    kicker: 'прямые заявки',
    rows: [
    { k: 'Объявления', v: 'Тексты, фото, упаковка, которая продаёт' },
    { k: 'Магазины', v: 'Оформление витрин и тарифов' },
    { k: 'Продвижение', v: 'Платное продвижение и аналитика' }]

  },
  {
    idx: '05',
    name: 'Гео-карты',
    kicker: 'локальный поиск',
    rows: [
    { k: 'Карты', v: 'Яндекс, 2ГИС, Google — карточки бизнеса' },
    { k: 'Оформление', v: 'Фото, описания, услуги, акции' },
    { k: 'Геотаргет', v: 'Реклама по радиусу и районам' }]

  },
  {
    idx: '06',
    name: 'Наружка и полиграфия',
    kicker: 'офлайн-присутствие',
    rows: [
    { k: 'Идея', v: 'Концепт под город, район, бренд' },
    { k: 'Дизайн', v: 'Билборды, вывески, носители, мерч' },
    { k: 'Размещение', v: 'Подбор площадок, печать, монтаж' }]

  }];


  return (
    <section className="sec channels" id="metrics">
      <div className="shell">
        <div className="sec-num fade-up">03 · каналы</div>
        <div className="sec-head fade-up d-1">
          <h2 ref={headingRef}>
            Считаем всё, что<br />
            <span className="accent">движется</span><span className="muted">,</span> а что не движется —<br />
            <span className="muted">двигаем и считаем.</span>
          </h2>
          <p className="lede">
            Делаем упор на креатив и нестандартные решения — иногда заходим
            через хайповую рекламу, иногда через тихую системную работу.
            Но любая идея и любой формат у нас в итоге превращаются
            в результат, измеримый в&nbsp;цифрах.
          </p>
        </div>

        <div className="channels-meta fade-up d-2">
          <span className="cm-label">Каналы, с&nbsp;которыми мы работаем</span>
          <span className="cm-count">6 направлений</span>
        </div>

        <div className="channels-table" ref={tableRef}>
          {channels.map((c) =>
          <div key={c.idx} className="ch-col">
              <div className="ch-col-head">
                <span className="ch-idx">/ {c.idx}</span>
                <h3 className="ch-name">{c.name}</h3>
                <span className="ch-kicker">{c.kicker}</span>
              </div>
              <dl className="ch-rows">
                {c.rows.map((r, i) =>
              <div key={i} className="ch-row">
                    <dt>{r.k}</dt>
                    <dd>{r.v}</dd>
                  </div>
              )}
              </dl>
            </div>
          )}
        </div>
      </div>
    </section>);

}

// ──────────────────────────────────────────────
// CASES · v3 — компактная превью-карточка
// ──────────────────────────────────────────────
function CaseDIOX() {
  return (
    <article className="case-preview fade-up" data-screen-label="case-diox">
      {/* media: tilted phones + headline badge */}
      <div className="cp-media">
        <div className="cp-grid-bg" aria-hidden="true"></div>
        <div className="cp-phones">
          <div className="cp-phone cp-phone-back" data-cursor data-cursor-label="аккаунт @diox_ru">
            <img src="assets/case-diox-account.jpg" alt="Лента аккаунта DIOX" />
          </div>
          <div className="cp-phone cp-phone-third" data-cursor data-cursor-label="эксперт в кадре">
            <img src="assets/case-diox-reels-2.jpg" alt="Reels с врачами DIOX" />
          </div>
          <div className="cp-phone cp-phone-front" data-cursor data-cursor-label="215K просмотров на Reels">
            <img src="assets/case-diox-stat.jpg" alt="Статистика Reels: 215 114 просмотров" />
            <span className="cp-phone-tag">
              <span className="cp-phone-tag-dot"></span>
              215K
            </span>
          </div>
        </div>
        <div className="cp-media-meta">
          <span>SMM полного цикла</span>
          <span className="cp-dot">·</span>
          <span>12 месяцев</span>
        </div>
      </div>

      {/* content: brand, headline, 3 stats, link */}
      <div className="cp-content">
        <div className="cp-meta">
          <span className="cp-idx">CASE / 01</span>
          <span className="cp-niche">витамины и&nbsp;БАДы</span>
        </div>

        <div className="cp-brand">DIOX</div>

        <h3 className="cp-headline">
          <span className="hl-minor">
            С <span className="accent">500</span> до <span className="accent">160&nbsp;000</span>
            {' '}<span className="muted">просмотров на&nbsp;Reels</span>
          </span>
          <br />
          <span className="muted">и&nbsp;</span><span className="accent">+489%</span>
          {' '}<span className="muted">переходов на&nbsp;маркетплейсы.</span>
        </h3>

        <p className="cp-pitch">
          Экспертный контент от&nbsp;врачей, чат-бот в&nbsp;Direct, сетка блогеров
          по&nbsp;сегментам ЦА. Из&nbsp;витрины с&nbsp;баночками — в&nbsp;канал продаж.
        </p>

        <div className="cp-stats">
          <div className="cp-stat">
            <span className="cp-stat-v">×30</span>
            <span className="cp-stat-l">охват Reels</span>
          </div>
          <div className="cp-stat">
            <span className="cp-stat-v">1M<em>+</em></span>
            <span className="cp-stat-l">охват за&nbsp;год</span>
          </div>
          <div className="cp-stat">
            <span className="cp-stat-v">+489<em>%</em></span>
            <span className="cp-stat-l" data-comment-anchor="c05282a901-span-645-13">переходов на&nbsp;маркетплейсы</span>
          </div>
        </div>

        <a href="work.html#diox" className="cp-link magnetic" data-cursor data-cursor-label="кейс">
          <span>Открыть кейс целиком</span>
          <span className="arrow">→</span>
        </a>
      </div>
    </article>);

}

function CasePavlonia() {
  return (
    <article className="case-preview is-reversed fade-up" data-screen-label="case-pavlonia">
      {/* media: tilted phones */}
      <div className="cp-media">
        <div className="cp-grid-bg" aria-hidden="true"></div>
        <div className="cp-phones">
          <div className="cp-phone cp-phone-back" data-cursor data-cursor-label="аккаунт @paulownia_cafe">
            <img src="assets/case-pav-account.jpg" alt="Лента аккаунта «Павлония»" />
          </div>
          <div className="cp-phone cp-phone-third" data-cursor data-cursor-label="курица на углях · 13,1 млн">
            <img src="assets/case-pav-reel.jpg" alt="Reels «Курица на углях»: 13,1 млн просмотров" />
          </div>
          <div className="cp-phone cp-phone-front" data-cursor data-cursor-label="17,4 млн просмотров за месяц">
            <img src="assets/case-pav-stat.jpg" alt="Статистика: 17 412 243 просмотра за месяц" />
            <span className="cp-phone-tag">
              <span className="cp-phone-tag-dot"></span>
              17,4М
            </span>
          </div>
        </div>
        <div className="cp-media-meta">
          <span>SMM · influence</span>
          <span className="cp-dot">·</span>
          <span>6 месяцев</span>
        </div>
      </div>

      {/* content */}
      <div className="cp-content">
        <div className="cp-meta">
          <span className="cp-idx">CASE / 02</span>
          <span className="cp-niche">сеть кафе · общепит</span>
        </div>

        <div className="cp-brand">Павлония</div>

        <h3 className="cp-headline">
          <span className="accent">13,1&nbsp;млн</span> просмотров
          {' '}<span className="muted">с&nbsp;одного Reels.</span>
        </h3>

        <p className="cp-pitch">
          Связали два формата, которые до&nbsp;нас никто не&nbsp;комбинировал: бытовые
          скетчи «муж и&nbsp;жена» на&nbsp;охваты и&nbsp;продающие ролики на&nbsp;заказы.
          Из&nbsp;аккаунта без&nbsp;системы — в&nbsp;контентную машину.
        </p>

        <div className="cp-stats">
          <div className="cp-stat">
            <span className="cp-stat-v">13,1<em>М</em></span>
            <span className="cp-stat-l">лучший Reels</span>
          </div>
          <div className="cp-stat">
            <span className="cp-stat-v">+1921<em>%</em></span>
            <span className="cp-stat-l">рост охвата</span>
          </div>
          <div className="cp-stat">
            <span className="cp-stat-v">97,7<em>%</em></span>
            <span className="cp-stat-l">органика</span>
          </div>
        </div>

        <a href="work.html#pavlonia" className="cp-link magnetic" data-cursor data-cursor-label="кейс">
          <span>Открыть кейс целиком</span>
          <span className="arrow">→</span>
        </a>
      </div>
    </article>);

}

function CaseSHRM() {
  return (
    <article className="case-preview fade-up" data-screen-label="case-shrm">
      {/* media: tilted phones */}
      <div className="cp-media">
        <div className="cp-grid-bg" aria-hidden="true"></div>
        <div className="cp-phones">
          <div className="cp-phone cp-phone-back" data-cursor data-cursor-label="аккаунт @prazhskaya26">
            <img src="assets/case-shrm-account.jpg" alt="Профиль @prazhskaya26: 5 106 подписчиков" />
          </div>
          <div className="cp-phone cp-phone-third" data-cursor data-cursor-label="лента: офферы и стройка">
            <img src="assets/case-shrm-grid2.jpg" alt="Лента ШРМ: офферы, материнский капитал, стройка" />
          </div>
          <div className="cp-phone cp-phone-front" data-cursor data-cursor-label="вирусный Reels собственника">
            <img src="assets/case-shrm-viral.jpg" alt="Вирусный Reels «из машины»: 7 141 комментарий" />
            <span className="cp-phone-tag">
              <span className="cp-phone-tag-dot"></span>
              7 141 комм.
            </span>
          </div>
        </div>
        <div className="cp-media-meta">
          <span>SMM + маркетинг</span>
          <span className="cp-dot">·</span>
          <span>7 месяцев</span>
        </div>
      </div>

      {/* content */}
      <div className="cp-content" data-comment-anchor="1f784011f1-div-756-7">
        <div className="cp-meta">
          <span className="cp-idx">CASE / 03</span>
          <span className="cp-niche">застройщик · недвижимость</span>
        </div>

        <div className="cp-brand">ШРМ<span className="cp-brand-sub">ЖК «Пражская»</span></div>

        <h3 className="cp-headline">
          С <span className="accent">0</span> до <span className="accent">300</span>
          {' '}<span className="muted">целевых лидов в&nbsp;месяц</span>
          <br />
          <span className="muted">и&nbsp;до</span> <span className="accent">100&nbsp;000</span>
          {' '}<span className="muted">охватов.</span>
        </h3>

        <p className="cp-pitch">
          Отказались от&nbsp;корпоративного контента&nbsp;— продвижение строится
          вокруг личного бренда собственника: лайфстайл со&nbsp;стройки, вопросы-хуки
          от&nbsp;первого лица и&nbsp;офферы-дефицит «только&nbsp;3 квартиры».
          Из&nbsp;нуля узнаваемости&nbsp;— в&nbsp;поток целевых заявок.
        </p>

        <div className="cp-stats">
          <div className="cp-stat">
            <span className="cp-stat-v">0&nbsp;→&nbsp;300</span>
            <span className="cp-stat-l">целевых лидов/мес</span>
          </div>
          <div className="cp-stat">
            <span className="cp-stat-v">100<em>K</em></span>
            <span className="cp-stat-l">охватов в&nbsp;месяц</span>
          </div>
          <div className="cp-stat">
            <span className="cp-stat-v">50<em>K ₽</em></span>
            <span className="cp-stat-l">рекламный бюджет</span>
          </div>
        </div>

        <a href="work.html#shrm" className="cp-link magnetic" data-cursor data-cursor-label="кейс">
          <span>Открыть кейс целиком</span>
          <span className="arrow">→</span>
        </a>
      </div>
    </article>);

}

function Cases() {
  return (
    <section className="sec" id="cases" data-comment-anchor="e94840d987-section-428-5">
      <div className="shell">
        <div className="sec-num fade-up">04 · кейсы</div>
        <div className="sec-head fade-up d-1">
          <h2>
            Кейсы, которые говорят<br />
            <span className="accent">сами за себя</span><span className="muted">.</span>
          </h2>
          <p className="lede">
            Коротко: бренд, цифра, что сделали. Полный разбор&nbsp;— на&nbsp;странице кейсов.
          </p>
        </div>

        <div className="cases-list-v3">
          <CaseDIOX />
          <CasePavlonia />
          <CaseSHRM />
        </div>

        <div className="cases-foot fade-up">
          <a href="work.html" className="btn magnetic" data-cursor data-cursor-label="все кейсы">
            <span>Все кейсы</span>
            <span className="arrow">→</span>
          </a>
          <span className="cases-foot-note">
            <span className="cases-foot-dot"></span>
            ещё кейсы готовятся к&nbsp;публикации
          </span>
        </div>
      </div>
    </section>);

}

function BriefForm({ status, onSubmit }) {
  const sent = status === 'sent';
  const sending = status === 'sending';
  return (
    <form className="brief brief-mini fade-up" onSubmit={onSubmit}>
      <header className="brief-mini-head">
        <span className="brief-stamp">Связаться</span>
        <h3 className="brief-mini-title">Оставьте имя и телефон —<br /><span className="muted">мы&nbsp;перезвоним.</span></h3>
      </header>

      <div className="brief-mini-row">
        <div className="field">
          <label className="field-l">Имя</label>
          <input type="text" name="name" placeholder="Как к вам обращаться" required data-cursor="text" />
        </div>
        <div className="field">
          <label className="field-l">Телефон</label>
          <input type="tel" name="phone" placeholder="+7 (___) ___-__-__" required data-cursor="text" />
        </div>
      </div>

      <button type="submit" className="btn primary magnetic brief-mini-submit" disabled={sending} data-cursor data-cursor-label={sent ? '✓ принято' : 'отправить'}>
        <span>{sent ? 'Принято · мы перезвоним' : sending ? 'Отправляем…' : 'Свяжитесь со мной'}</span>
        <span className="arrow">→</span>
      </button>

      {status === 'error' && (
        <p className="brief-mini-note" style={{ color: 'var(--red-2)' }}>
          Не удалось отправить. Напишите нам в&nbsp;
          <a href="https://wa.me/79886554717" target="_blank" rel="noopener" style={{ color: 'var(--ink)' }}>WhatsApp</a>.
        </p>
      )}

      <p className="brief-mini-note">
        Отвечаем в&nbsp;течение <span style={{ color: 'var(--red)' }}>одного&nbsp;часа</span> в&nbsp;рабочее время.
        Никакого спама — только короткий звонок по&nbsp;делу.
      </p>
    </form>);

}

function Contact() {
  const [status, setStatus] = React.useState('idle'); // idle | sending | sent | error
  const onSubmit = async (e) => {
    e.preventDefault();
    const f = e.currentTarget;
    const name = f.elements.name.value.trim();
    const phone = f.elements.phone.value.trim();
    if (!name || !phone) return;
    setStatus('sending');
    try {
      await window.sendLead({
        name,
        phone,
        service: 'Нужна консультация',
        partnership: false,
        source: 'Форма в контактах',
      });
      setStatus('sent');
      f.reset();
      setTimeout(() => setStatus('idle'), 5000);
    } catch (err) {
      setStatus('error');
    }
  };
  return (
    <section className="sec contact contact-v2" id="contact">
      <div className="contact-bg-mark" data-parallax="0.08">
        <img src="assets/brand-mark.png" alt="" style={{ width: '100%', opacity: 0.5 }} />
      </div>
      <div className="shell">
        <div className="sec-num fade-up">07 · контакты</div>
        <h2 className="fade-up d-1">
          Давайте <span className="accent">считать</span> <span className="muted">вместе.</span>
        </h2>

        {/* Форма сверху */}
        <div className="contact-form-wrap fade-up d-2">
          <BriefForm status={status} onSubmit={onSubmit} />
        </div>

        {/* Контакты снизу */}
        <div className="contact-info-grid fade-up d-3" data-comment-anchor="a6c2a52b0e-div-963-13">
          <div className="contact-info-cell">
            <span className="k">Телефон · звонок</span>
            <a className="v" href="tel:+79886554717" data-cursor data-cursor-label="позвонить">+7 (988) 655-47-17</a>
          </div>
          <div className="contact-info-cell">
            <span className="k">Telegram</span>
            <a className="v" href="#" data-cursor data-cursor-label="написать">@hyper_agency</a>
          </div>
          <div className="contact-info-cell">
            <span className="k">E-mail</span>
            <a className="v" href="mailto:hi@hyper.agency" data-cursor>hi@hyper.agency</a>
          </div>
          <div className="contact-info-cell">
            <span className="k">Офис</span>
            <span className="v">Махачкала · Дагестан</span>
          </div>
          <div className="contact-info-cell">
            <span className="k">Часы работы</span>
            <span className="v">Пн–Сб · 10:00–20:00</span>
          </div>
          <div className="contact-info-cell">
            <span className="k">Соцсети</span>
            <span className="v">
              <a href="#" data-cursor>@Hyper_Marketing_agency</a>
            </span>
          </div>
        </div>
      </div>
    </section>);

}

function Footer() {
  return (
    <footer className="foot">
      <div className="shell">
        <div className="row">
          <span><strong>HYPER</strong> · агентство маркетинга · 2026</span>
          <span>Создано на цифрах и нервах</span>
          <span>© 2018—2026</span>
        </div>
      </div>
    </footer>);

}

Object.assign(window, { NavBar, Hero, Ticker, Directions, Services, Channels, Cases, BriefForm, Contact, Footer });