// Frontend — universal channel router + dynamic PrimaryNav.
//
// Every nav item in the header is a row in yylearn.channels. When a user
// clicks one we navigate to /channel/<slug>; this component reads the
// channel by slug, switches on `channel.type`, and renders one of four
// templates:
//
//   course    — featured courses + per-category strips, admin-picked
//   system    — fixed built-in sections (membership / my-learning)
//   activity  — admin-edited block list (hero, featuredCourses, ...)
//   home      — same block list as activity, just accessed from "/"
//
// Everything is driven by data from /api/channels, /api/courses, and
// /api/categories so admins can reorder/retitle/retemplate without any
// code changes.

// ---------- data hooks ------------------------------------------------------
// Pull the current language straight off localStorage; the data hooks below
// pass it as ?locale= so the API resolves localized titles/subtitles.
function _currentLang() {
  try {
    return (typeof localStorage !== 'undefined' && localStorage.getItem('yylearn.lang'))
        || (document.documentElement.lang || '').split('-')[0]
        || 'en';
  } catch (_) { return 'en'; }
}

function useChannelList() {
  const [channels, setChannels] = React.useState([]);
  React.useEffect(() => {
    fetch('/api/channels?locale=' + encodeURIComponent(_currentLang()), { credentials: 'same-origin' })
      .then(r => r.json()).then(j => setChannels((j && j.channels) || []))
      .catch(() => setChannels([]));
  }, []);
  return channels;
}

function useCourseList() {
  const [courses, setCourses] = React.useState([]);
  React.useEffect(() => {
    fetch('/api/courses?locale=' + encodeURIComponent(_currentLang()), { credentials: 'same-origin' })
      .then(r => r.json()).then(j => setCourses((j && j.courses) || []))
      .catch(() => setCourses([]));
  }, []);
  return courses;
}

function useCategoryTree() {
  const [cats, setCats] = React.useState([]);
  React.useEffect(() => {
    fetch('/api/categories?locale=' + encodeURIComponent(_currentLang()), { credentials: 'same-origin' })
      .then(r => r.json()).then(j => setCats((j && j.categories) || []))
      .catch(() => setCats([]));
  }, []);
  return cats;
}

function pickI18n(obj, lang, fallback) {
  if (!obj) return fallback || '';
  const base = (lang || 'en').split('-')[0];
  return obj[lang] || obj[base] || obj.en || fallback || '';
}

// ---------- dynamic PrimaryNav ---------------------------------------------
// Reads channels from /api/channels and renders them left-to-right in the
// same slot the static PrimaryNav used. Home channel is excluded because
// it's already accessible via the logo click.
const DynamicPrimaryNav = ({ lang, goTo, signedIn }) => {
  const T = k => t(k, lang);
  const channels = useChannelList();
  const [open, setOpen] = React.useState(false);

  // Home is the logo — skip it in the nav row itself.
  const visible = channels.filter(c => c.type !== 'home' && c.slug !== 'home');
  // Cluster into groups based on group_key so the header can show a thin
  // divider between, say, course channels and system/account ones.
  const groups = [];
  const byGroup = {};
  visible.forEach(ch => {
    const g = ch.groupKey || '';
    if (!(g in byGroup)) { byGroup[g] = []; groups.push(g); }
    byGroup[g].push(ch);
  });

  function onNavClick(ch) {
    // System channels with well-known keys can shortcut to existing pages
    // so users land on the actual feature rather than a wrapper view.
    const sysKey = ch.templateOverrides && ch.templateOverrides.systemKey;
    if (ch.type === 'system' && sysKey === 'my-learning') return goTo('user');
    if (ch.type === 'system' && sysKey === 'membership')  return goTo('pricing');
    location.href = '/channel/' + encodeURIComponent(ch.slug || ch.id);
  }

  return (
    <nav style={{
      display:'flex', alignItems:'center', justifyContent:'space-between',
      padding:'14px 32px', borderBottom:'1px solid var(--line)',
      background:'var(--bg)', gap: 16,
    }}>
      <div style={{ display:'flex', alignItems:'center', gap: 40, minWidth: 0, flex:'1 1 0' }}>
        <button onClick={() => goTo('home')} aria-label="YYLearn home" style={{ flexShrink: 0 }}><Logo /></button>
        <div style={{ display:'flex', gap: 16, fontSize: 14, alignItems:'center' }} className="nav-links">
          {visible.length === 0 && (
            <span style={{ color:'var(--ink-4)', fontSize: 12 }}>{T('fc.loading')}</span>
          )}
          {groups.map((g, gi) => (
            <React.Fragment key={g || '__blank__' + gi}>
              {gi > 0 && (
                <span aria-hidden="true" style={{
                  width: 1, height: 16, background:'var(--line)',
                  display:'inline-block', margin:'0 2px', opacity: 0.8, flexShrink: 0,
                }}/>
              )}
              <div style={{ display:'flex', gap: 18, alignItems:'center' }}
                   data-nav-group={g || 'default'}>
                {byGroup[g].map(ch => (
                  <button key={ch.id} onClick={() => onNavClick(ch)}
                    style={{ color:'var(--ink-2)', display:'inline-flex', alignItems:'center', gap: 6 }}>
                    {ch.icon && <Icon name={ch.icon} size={13} />}
                    {pickI18n(ch.nameI18n, lang, ch.name)}
                  </button>
                ))}
              </div>
            </React.Fragment>
          ))}
        </div>
      </div>
      <div className="nav-right" style={{ display:'flex', alignItems:'center', gap: 12 }}>
        {typeof HeaderSearch !== 'undefined' ? (
          <HeaderSearch lang={lang} />
        ) : null}
        {typeof LangThemeMenu !== 'undefined' && (
          <LangThemeMenu signedIn={signedIn} />
        )}
        {signedIn ? (
          <button onClick={() => goTo('user')} style={{ display:'inline-flex', alignItems:'center', gap: 8 }}>
            <Avatar initials={(window.DEMO_USER && DEMO_USER.avatar) || 'U'} size={32} bg="var(--accent)" color="#fff"/>
          </button>
        ) : (
          <>
            <button className="btn btn-ghost btn-sm" onClick={() => goTo('auth', { authMode:'signin' })}>{T('nav.signin')}</button>
            <button className="btn btn-primary btn-sm" onClick={() => goTo('auth', { authMode:'signup' })}>{T('nav.signup')}</button>
          </>
        )}
      </div>
    </nav>
  );
};

// ---------- block renderer --------------------------------------------------
// Knows how to paint every block type that AdminChannels can emit. Used by
// both the home and activity templates — same block shapes, same layout
// rules, so admins get a predictable preview.
const BlockRenderer = ({ blocks, lang, goTo, courses, cats, channels }) => {
  if (!Array.isArray(blocks) || blocks.length === 0) return null;
  return (
    <div>
      {blocks.map((b, i) => {
        if (!b || !b.type) return null;
        const d = b.data || {};
        switch (b.type) {
          case 'hero':          return <HeroBlock key={b.id || i} data={d} lang={lang} goTo={goTo} courses={courses} />;
          case 'featuredCourses': return <FeaturedCoursesBlock key={b.id || i} data={d} lang={lang} goTo={goTo} courses={courses} />;
          case 'categoryStrip': return <CategoryStripBlock key={b.id || i} data={d} lang={lang} cats={cats} courses={courses} channels={channels} />;
          case 'richText':      return <RichTextBlock key={b.id || i} data={d} lang={lang} />;
          case 'cta':           return <CtaBlock key={b.id || i} data={d} lang={lang} goTo={goTo} />;
          default: return null;
        }
      })}
    </div>
  );
};

// href handler used by hero/cta blocks. Accepts a simple DSL:
//   "browse", "pricing", "signup", "signin" → goTo(that screen)
//   "/course/slug"                        → location.href (absolute)
//   anything else starting with "/"       → location.href
//   anything else                         → goTo('channel', { channel: href })
function resolveHref(href, goTo) {
  const v = String(href || '').trim();
  if (!v) return () => {};
  if (v === 'browse')  return () => goTo('browse');
  if (v === 'pricing') return () => goTo('pricing');
  if (v === 'signup')  return () => goTo('auth', { authMode: 'signup' });
  if (v === 'signin')  return () => goTo('auth', { authMode: 'signin' });
  if (v === 'home')    return () => goTo('home');
  if (v === 'me' || v === 'user') return () => goTo('user');
  if (v.startsWith('/')) return () => { location.href = v; };
  return () => { location.href = '/channel/' + encodeURIComponent(v); };
}

// HeroBlock — the first-screen area for home / course-channel / activity pages.
// When `data.visual !== 'none'` and we have courses to render, we paint a
// rich 2-column layout: headline on the left, tilted course-card stack on
// the right (same visual as the legacy homepage hero). When there are no
// courses to stack, or data.visual === 'none', we collapse to a simple
// single-column hero. This keeps the home page visually substantial once
// admin-authored blocks take over from the legacy hard-coded hero.
const HeroBlock = ({ data, lang, goTo, courses }) => {
  const title = pickI18n(data.titleI18n, lang, '');
  const sub   = pickI18n(data.subtitleI18n, lang, '');
  const badge = pickI18n(data.badgeI18n, lang, '');
  const cta   = pickI18n(data.ctaI18n, lang, '');
  const cta2  = pickI18n(data.ctaSecondaryI18n, lang, '');
  const stats = Array.isArray(data.stats) ? data.stats.slice(0, 4) : [];
  const stackCourses = Array.isArray(courses)
    ? courses.slice().sort((a, b) => (b.rating || 0) - (a.rating || 0)).slice(0, 3)
    : [];
  // Carousel pulls more slides (up to 6) — ranked by rating, same source.
  const carouselCourses = Array.isArray(courses)
    ? courses.slice().sort((a, b) => (b.rating || 0) - (a.rating || 0)).slice(0, 6)
    : [];
  const showVisual = data.visual !== 'none' && stackCourses.length >= 1;

  const Header = (
    <div>
      {badge && (
        <div style={{
          display:'inline-flex', alignItems:'center', gap: 8,
          padding:'5px 12px', borderRadius: 999,
          background:'var(--accent-soft)', color:'var(--accent)',
          fontSize: 12, letterSpacing:'0.08em', textTransform:'uppercase',
          marginBottom: 24, fontWeight: 500,
        }}>
          <span style={{ width:6, height:6, borderRadius:'50%', background:'var(--accent)'}} />
          {badge}
        </div>
      )}
      {title && (
        <h1 style={{
          fontFamily:'var(--font-serif)',
          fontSize: showVisual ? 'clamp(38px, 5.5vw, 68px)' : 'clamp(36px, 5vw, 60px)',
          fontWeight: 600, lineHeight: 1.05, letterSpacing:'-0.015em',
          margin:'0 0 20px', color:'var(--ink)', whiteSpace:'pre-line',
        }}>{title}</h1>
      )}
      {sub && (
        <p style={{
          fontSize: 17, lineHeight: 1.55, color:'var(--ink-2)',
          maxWidth: 520, margin:'0 0 32px',
        }}>{sub}</p>
      )}
      <div style={{ display:'flex', gap: 12, flexWrap:'wrap' }}>
        {cta && (
          <button className="btn btn-accent" onClick={resolveHref(data.ctaHref, goTo)}>
            {cta} <Icon name="arrowR" size={16} />
          </button>
        )}
        {cta2 && (
          <button className="btn btn-ghost" onClick={resolveHref(data.ctaSecondaryHref, goTo)}>
            {cta2}
          </button>
        )}
      </div>
      {stats.length > 0 && (
        <div style={{ marginTop: 48, display:'flex', gap: 32, flexWrap:'wrap' }}>
          {stats.map((s, i) => (
            <div key={i}>
              <div style={{ fontFamily:'var(--font-serif)', fontSize: 26, fontWeight: 700, color:'var(--ink)' }}>
                {pickI18n(s.valueI18n, lang, s.value || '')}
              </div>
              <div style={{ fontSize: 12, color:'var(--ink-3)', marginTop: 2 }}>
                {pickI18n(s.labelI18n, lang, s.label || '')}
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );

  if (!showVisual) {
    return (
      <section style={{ maxWidth: 1240, margin:'0 auto', padding:'72px 32px 40px' }}>
        {Header}
      </section>
    );
  }

  return (
    <section className="hero-block-grid" style={{
      maxWidth: 1240, margin:'0 auto', padding:'72px 32px 56px',
      display:'grid', gridTemplateColumns:'0.85fr 1.15fr', gap: 56,
      alignItems:'center',
    }}>
      {Header}
      <div className="hero-block-visual" style={{ position:'relative', minHeight: 560 }}>
        <ChannelCoverCarousel
          courses={carouselCourses.map(c => Object.assign({}, c, {
            channelName: c.channelName || (c.instructor && c.instructor.name) || ''
          }))}
          tint={'#8B5A3C'}
          lang={lang}
        />
      </div>
      <style>{`
        @media (max-width: 900px) {
          .hero-block-grid { grid-template-columns: 1fr !important; }
          .hero-block-visual { display: none; }
        }
      `}</style>
    </section>
  );
};

// Tilted stack of 3 course cards used by the rich hero layout. Pulls real
// courses from the DB so the home hero always reflects current content.
const HeroBlockStack = ({ courses, goTo, lang }) => {
  const positions = [
    { top: 0,   left: 40,  rot: -4, z: 3, w: 260 },
    { top: 110, left: 180, rot: 3,  z: 2, w: 240 },
    { top: 230, left: 20,  rot: -2, z: 1, w: 250 },
  ];
  return (
    <div style={{ position:'relative', width:'100%', height:'100%' }}>
      {courses.map((c, i) => {
        const p = positions[i] || positions[positions.length - 1];
        const title = pickI18n(c.titleI18n || c.i18n && c.i18n.title, lang, c.title || '');
        return (
          <div key={c.id || i} onClick={() => goTo && goTo('course', { course: c.slug || c.id })}
            style={{
              position:'absolute', top: p.top, left: p.left,
              width: p.w, zIndex: p.z,
              transform: `rotate(${p.rot}deg)`,
              transition:'transform 0.3s, z-index 0s',
              cursor:'pointer',
            }}
            onMouseEnter={e => { e.currentTarget.style.transform = `rotate(0deg) translateY(-6px) scale(1.03)`; e.currentTarget.style.zIndex = 10; }}
            onMouseLeave={e => { e.currentTarget.style.transform = `rotate(${p.rot}deg)`; e.currentTarget.style.zIndex = p.z; }}
          >
            <div className="card" style={{ overflow:'hidden', boxShadow:'0 18px 40px rgba(0,0,0,0.08)'}}>
              {typeof CourseCover !== 'undefined'
                ? <CourseCover course={c} height={140} />
                : <div style={{ height: 140, background:'var(--accent-soft)' }} />}
              <div style={{ padding: 14 }}>
                <div style={{ fontFamily:'var(--font-serif)', fontSize: 15, fontWeight: 600, color:'var(--ink)', lineHeight:1.3 }}>
                  {title}
                </div>
                <div style={{ fontSize: 11, color:'var(--ink-3)', marginTop: 4 }}>
                  {(c.hours || 0)}{t('home.heroHours', lang)} · {(c.lessons || c.lessonCount || 0)} {t('home.heroLessons', lang)}
                </div>
              </div>
            </div>
          </div>
        );
      })}
    </div>
  );
};

const FeaturedCoursesBlock = ({ data, lang, goTo, courses }) => {
  const title = pickI18n(data.titleI18n, lang, t('fc.featured', lang));
  const ids = Array.isArray(data.courseIds) ? data.courseIds : [];
  // If the admin didn't hand-pick any, fall back to the highest-rated
  // published courses so the block never renders empty.
  const pool = Array.isArray(courses) ? courses : [];
  const picked = ids.length
    ? ids.map(id => pool.find(c => c.id === id)).filter(Boolean)
    : pool.slice().sort((a, b) => (b.rating || 0) - (a.rating || 0)).slice(0, 8);
  if (!picked.length) return null;
  return (
    <section style={{ maxWidth: 1240, margin:'32px auto', padding:'0 32px' }}>
      <h2 style={{ fontFamily:'var(--font-serif)', fontSize: 26, fontWeight: 600,
                   margin:'0 0 18px', color:'var(--ink)' }}>{title}</h2>
      <FeaturedCarousel courses={picked} goTo={goTo} lang={lang} />
    </section>
  );
};

// ---------- FeaturedCarousel -----------------------------------------------
// Horizontal snap-scroll carousel used by the home page's featured block and
// every channel page's "recommended" strip, so both surfaces share the exact
// same look. Auto-advances every 6s, pauses on hover, exposes left/right
// arrow buttons once the track overflows. Cards are rendered with the shared
// CourseCard from shared.jsx so channel pages get the same cover/chrome as
// home.
const FeaturedCarousel = ({ courses, goTo, lang }) => {
  const trackRef = React.useRef(null);
  const [canLeft, setCanLeft]   = React.useState(false);
  const [canRight, setCanRight] = React.useState(false);
  const hoveringRef = React.useRef(false);

  const updateArrows = React.useCallback(() => {
    const el = trackRef.current; if (!el) return;
    setCanLeft(el.scrollLeft > 8);
    setCanRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 8);
  }, []);

  React.useEffect(() => {
    const el = trackRef.current; if (!el) return;
    updateArrows();
    el.addEventListener('scroll', updateArrows);
    window.addEventListener('resize', updateArrows);
    return () => {
      el.removeEventListener('scroll', updateArrows);
      window.removeEventListener('resize', updateArrows);
    };
  }, [courses, updateArrows]);

  const step = (dir) => {
    const el = trackRef.current; if (!el) return;
    // Advance by one "page" (≈ track width minus a peek).
    const delta = Math.max(240, el.clientWidth * 0.85) * dir;
    el.scrollBy({ left: delta, behavior: 'smooth' });
  };

  // Auto-advance; pauses on hover or when tab is hidden.
  React.useEffect(() => {
    if (!courses || courses.length <= 1) return;
    const id = setInterval(() => {
      if (hoveringRef.current || document.hidden) return;
      const el = trackRef.current; if (!el) return;
      if (el.scrollLeft + el.clientWidth >= el.scrollWidth - 8) {
        el.scrollTo({ left: 0, behavior: 'smooth' });
      } else {
        el.scrollBy({ left: Math.max(260, el.clientWidth * 0.85), behavior: 'smooth' });
      }
    }, 6000);
    return () => clearInterval(id);
  }, [courses]);

  if (!Array.isArray(courses) || !courses.length) return null;

  return (
    <div
      className="yy-carousel"
      style={{ position:'relative' }}
      onMouseEnter={() => { hoveringRef.current = true; }}
      onMouseLeave={() => { hoveringRef.current = false; }}
    >
      <div
        ref={trackRef}
        className="yy-carousel-track"
        style={{
          display:'flex', gap: 20, overflowX:'auto',
          scrollSnapType:'x mandatory', scrollBehavior:'smooth',
          paddingBottom: 6,
          scrollbarWidth:'none',
          msOverflowStyle:'none',
        }}
      >
        {courses.map(c => (
          <div key={c.id} className="yy-carousel-item" style={{
            flex:'0 0 calc((100% - 60px) / 4)',
            minWidth: 240,
            scrollSnapAlign:'start',
          }}>
            <CourseCard course={c} onClick={() => goTo('course', { course: c.slug || c.id })} />
          </div>
        ))}
      </div>
      {canLeft && (
        <button onClick={() => step(-1)} aria-label="previous" style={{
          position:'absolute', top:'38%', left: -14, zIndex: 2,
          width: 40, height: 40, borderRadius:'50%',
          background:'var(--bg)', color:'var(--ink)',
          border:'1px solid var(--line)',
          boxShadow:'0 4px 14px rgba(0,0,0,0.10)',
          display:'flex', alignItems:'center', justifyContent:'center',
          cursor:'pointer', fontSize: 20, lineHeight: 1,
        }}>‹</button>
      )}
      {canRight && (
        <button onClick={() => step(1)} aria-label="next" style={{
          position:'absolute', top:'38%', right: -14, zIndex: 2,
          width: 40, height: 40, borderRadius:'50%',
          background:'var(--bg)', color:'var(--ink)',
          border:'1px solid var(--line)',
          boxShadow:'0 4px 14px rgba(0,0,0,0.10)',
          display:'flex', alignItems:'center', justifyContent:'center',
          cursor:'pointer', fontSize: 20, lineHeight: 1,
        }}>›</button>
      )}
      <style>{`
        .yy-carousel-track::-webkit-scrollbar { display: none; }
        @media (max-width: 1100px) {
          .yy-carousel-item { flex: 0 0 calc((100% - 40px) / 3) !important; }
        }
        @media (max-width: 820px) {
          .yy-carousel-item { flex: 0 0 calc((100% - 20px) / 2) !important; }
        }
        @media (max-width: 520px) {
          .yy-carousel-item { flex: 0 0 82% !important; }
        }
      `}</style>
    </div>
  );
};

const CategoryStripBlock = ({ data, lang, cats, courses, channels }) => {
  const title = pickI18n(data.titleI18n, lang, '');
  const ids = Array.isArray(data.categoryIds) ? data.categoryIds : [];
  const pool = Array.isArray(cats) ? cats : [];
  // If none selected, show top-level categories.
  const picked = ids.length
    ? ids.map(id => pool.find(c => c.id === id)).filter(Boolean)
    : pool.filter(c => !c.parentId);
  if (!picked.length) return null;
  return (
    <section style={{ maxWidth: 1240, margin:'32px auto', padding:'0 32px' }}>
      {title && (
        <h2 style={{ fontFamily:'var(--font-serif)', fontSize: 22, fontWeight: 600,
                     margin:'0 0 16px', color:'var(--ink)' }}>{title}</h2>
      )}
      <div style={{ display:'grid', gap: 12,
                    gridTemplateColumns:'repeat(auto-fill, minmax(200px, 1fr))' }}>
        {picked.map(cat => {
          const count = (courses || []).filter(c =>
            c.categoryId === cat.id || c.channel === cat.slug).length;
          // Prefer linking to the channel that carries this category when one exists.
          const ch = (channels || []).find(x => x.slug === cat.slug || x.id === cat.id);
          const href = ch ? ('/channel/' + encodeURIComponent(ch.slug || ch.id)) : ('/browse/' + encodeURIComponent(cat.slug || cat.id));
          return (
            <a key={cat.id} href={href} className="card"
              style={{ padding: 16, display:'block', textDecoration:'none', color:'inherit', borderRadius: 10 }}>
              <div style={{ fontFamily:'var(--font-serif)', fontSize: 18, fontWeight: 600, color:'var(--ink)' }}>
                {pickI18n(cat.nameI18n, lang, cat.name)}
              </div>
              <div style={{ fontSize: 12, color:'var(--ink-3)', marginTop: 4 }}>{count} {t('home.coursesTag', lang)}</div>
            </a>
          );
        })}
      </div>
    </section>
  );
};

const RichTextBlock = ({ data, lang }) => {
  const title = pickI18n(data.titleI18n, lang, '');
  const body  = pickI18n(data.bodyI18n, lang, '');
  if (!title && !body) return null;
  return (
    <section style={{ maxWidth: 900, margin:'32px auto', padding:'0 32px' }}>
      {title && (
        <h2 style={{ fontFamily:'var(--font-serif)', fontSize: 26, fontWeight: 600,
                     margin:'0 0 10px', color:'var(--ink)' }}>{title}</h2>
      )}
      {body && (
        <div style={{ fontSize: 16, lineHeight: 1.65, color:'var(--ink-2)',
                      whiteSpace:'pre-wrap' }}>{body}</div>
      )}
    </section>
  );
};

const CtaBlock = ({ data, lang, goTo }) => {
  const title = pickI18n(data.titleI18n, lang, '');
  const body  = pickI18n(data.bodyI18n, lang, '');
  const cta   = pickI18n(data.ctaI18n, lang, '');
  return (
    <section style={{ maxWidth: 1240, margin:'40px auto', padding:'0 32px' }}>
      <div className="card" style={{
        padding: '32px 28px', display:'flex', gap: 24,
        alignItems:'center', justifyContent:'space-between', flexWrap:'wrap',
        background:'var(--accent-soft)', border:'1px solid var(--line)',
      }}>
        <div style={{ flex:'1 1 320px', minWidth: 280 }}>
          {title && <h3 style={{ fontFamily:'var(--font-serif)', fontSize: 22,
                                 fontWeight: 600, margin:'0 0 6px', color:'var(--ink)' }}>{title}</h3>}
          {body && <p style={{ margin: 0, fontSize: 15, color:'var(--ink-2)' }}>{body}</p>}
        </div>
        {cta && (
          <button className="btn btn-accent" onClick={resolveHref(data.ctaHref, goTo)}>
            {cta} <Icon name="arrowR" size={14} />
          </button>
        )}
      </div>
    </section>
  );
};

// ---------- CourseCard -----------------------------------------------------
// Channel / block surfaces reuse the global CourseCard exposed by shared.jsx
// (window.CourseCard). That component renders the full CourseCover seal + the
// channel chip + instructor + stats, so home and channel pages stay visually
// identical. We DON'T redefine a local one here because it would shadow the
// shared version via the final window assignment at the bottom of the file.

// ---------- templates -------------------------------------------------------
// Course template: magazine-style hero (pill tag + breadcrumb + serif H1 +
// tagline + stats row + CTA + cover stack), then the recommended carousel,
// then one section per admin-picked category. Styled to feel like a proper
// editorial landing page rather than a debug grid.
const ChannelHero = ({ channel, lang, stats, onBrowse, accent }) => {
  // Channel-level info (name / tagline / breadcrumb / stats / Browse-all CTA)
  // intentionally NOT rendered here. The user is already inside the channel
  // page and the full course list is right below — the hero is just for
  // featured-course rotation. Keeping `channel` only for the accent tint.
  const tint = channel.color || 'var(--accent)';

  // Featured course slides — full-bleed cover crossfade. Caption (title +
  // instructor) and a thumbnail filmstrip sit directly on the dark gradient,
  // no white card.
  const slides = ((stats && stats.coversFrom) || []).slice(0, 8);
  const n = slides.length;
  const [idx, setIdx] = React.useState(0);
  const [paused, setPaused] = React.useState(false);
  React.useEffect(() => {
    if (n < 2 || paused) return;
    const tm = setInterval(() => setIdx(i => (i + 1) % n), 6000);
    return () => clearInterval(tm);
  }, [n, paused]);
  const current = n ? slides[idx] : null;
  const courseHref = current ? '/course/' + encodeURIComponent(current.slug || current.id) : null;
  // If the channel has no featured / no courses at all, hide the hero
  // entirely instead of rendering an empty band.
  if (!current) return null;

  return (
    <section
      onMouseEnter={() => setPaused(true)}
      onMouseLeave={() => setPaused(false)}
      style={{ position:'relative', overflow:'hidden',
               background:'var(--bg-alt)' }}
    >
      {/* Stacked full-bleed cover backgrounds — only the current slide is
          opaque so crossfades are seamless with no layout jumps. The whole
          stack sits behind a clickable <a> so tapping anywhere on the cover
          opens the current course. */}
      {n > 0 ? slides.map((c, i) => (
        <div key={c.id || i} aria-hidden="true" style={{
          position:'absolute', inset: 0,
          backgroundImage: c.cover ? 'url(' + c.cover + ')'
            : 'linear-gradient(135deg, ' + tint + '33, ' + tint + '0a)',
          backgroundSize:'cover', backgroundPosition:'center',
          filter:'saturate(1.05)',
          opacity: i === idx ? 1 : 0,
          transition:'opacity 0.8s ease',
        }}/>
      )) : (
        <div aria-hidden="true" style={{
          position:'absolute', inset: 0,
          background:'radial-gradient(60% 80% at 85% 0%, ' + tint + '22 0%, transparent 60%), '
                   + 'radial-gradient(50% 60% at 5% 100%, ' + tint + '14 0%, transparent 70%)',
        }}/>
      )}
      {/* Whole-area click → current course. Sits above the covers, below the
           gradient/content layer (z-index 1). */}
      <a
        href={courseHref}
        aria-label={current.title || ''}
        style={{
          position:'absolute', inset: 0, zIndex: 1,
          textDecoration:'none', color:'inherit',
        }}
      />
      {/* Dark gradient overlay for legibility — purely decorative,
           pointer-events: none so it doesn't intercept the click on the
           cover <a> below. */}
      {n > 0 && (
        <div aria-hidden="true" style={{
          position:'absolute', inset: 0, pointerEvents:'none', zIndex: 2,
          background: 'linear-gradient(180deg, rgba(0,0,0,0.20) 0%, rgba(0,0,0,0.08) 30%, rgba(0,0,0,0.48) 70%, rgba(0,0,0,0.82) 100%), '
                    + 'linear-gradient(90deg, rgba(0,0,0,0.60) 0%, rgba(0,0,0,0.25) 45%, rgba(0,0,0,0) 70%)',
        }}/>
      )}

      {/* Content layer — only the rotating-course caption + filmstrip.
           Channel name / breadcrumb / tagline / Browse-all CTA / stats are
           NOT rendered here: the user is already in the channel and the
           full course list is right below. zIndex 3 sits above the
           click-target <a> (1) and the dark gradient (2). */}
      <div style={{ position:'relative', zIndex: 3, maxWidth: 1240, margin:'0 auto',
                    padding:'56px 32px 64px', minHeight: 460,
                    display:'flex', flexDirection:'column',
                    justifyContent:'flex-end',
                    pointerEvents:'none' }} className="ch-hero-grid">
        <div className="ch-hero-feature" style={{
          color:'#fff', maxWidth: 720,
          display:'flex', flexDirection:'column', gap: 14,
          pointerEvents:'auto',
        }}>
          {/* Glass "Now showing" pill */}
          <div style={{
            alignSelf:'flex-start',
            display:'inline-flex', alignItems:'center', gap: 8,
            padding:'5px 12px', borderRadius: 999,
            background:'rgba(255,255,255,0.14)',
            border:'1px solid rgba(255,255,255,0.28)',
            backdropFilter:'blur(10px)',
            fontSize: 11, letterSpacing:'0.14em', textTransform:'uppercase',
            fontWeight: 500,
          }}>
            <span style={{ width: 6, height: 6, borderRadius:'50%', background:'#fff' }}/>
            {t('fc.featured', lang) !== 'fc.featured' ? t('fc.featured', lang) : 'Now Showing'}
          </div>

          {/* Course title — big serif, white on dark gradient (no card!) */}
          <a href={courseHref} style={{
            color:'#fff', textDecoration:'none',
            fontFamily:'var(--font-serif)',
            fontSize:'clamp(36px, 5vw, 56px)',
            fontWeight: 600, lineHeight: 1.1, letterSpacing:'-0.015em',
            textShadow:'0 2px 24px rgba(0,0,0,0.5)',
            display:'-webkit-box', WebkitLineClamp: 2,
            WebkitBoxOrient:'vertical', overflow:'hidden',
            margin: 0,
          }}>
            {current.title || ''}
          </a>
          {current.instructor && current.instructor.name && (
            <div style={{ fontSize: 16, color:'rgba(255,255,255,0.88)',
                          textShadow:'0 1px 8px rgba(0,0,0,0.4)' }}>
              {current.instructor.name}
            </div>
          )}

          {/* Cover thumbnail filmstrip — clickable manual nav. The
               active thumbnail lifts up + brightens. Top padding gives the
               translateY(-4px) of the active tile room to lift without
               getting clipped by overflow:auto. */}
          {n > 1 && (
            <div className="ch-hero-strip" style={{
              display:'flex', alignItems:'flex-end', gap: 10,
              marginTop: 14, flexWrap:'nowrap',
              overflowX:'auto', paddingTop: 8, paddingBottom: 6,
              scrollbarWidth:'none',
            }}>
              {slides.map((c, i) => {
                const active = i === idx;
                return (
                  <button
                    key={c.id || i}
                    aria-label={'feature ' + (i+1) + ': ' + (c.title || '')}
                    onClick={(e) => { e.preventDefault(); setIdx(i); }}
                    title={c.title || ''}
                    style={{
                      flex: '0 0 auto',
                      width: active ? 110 : 76,
                      height: active ? 72 : 50,
                      padding: 0,
                      borderRadius: 10,
                      overflow: 'hidden',
                      border: active ? '2px solid #fff' : '1px solid rgba(255,255,255,0.5)',
                      background: c.cover
                        ? 'url(' + c.cover + ') center/cover'
                        : 'linear-gradient(135deg, ' + tint + ', ' + tint + 'aa)',
                      cursor: 'pointer',
                      opacity: active ? 1 : 0.7,
                      transform: active ? 'translateY(-4px)' : 'translateY(0)',
                      boxShadow: active ? '0 12px 28px rgba(0,0,0,0.5)' : '0 2px 8px rgba(0,0,0,0.25)',
                      transition: 'all 0.25s ease',
                    }}
                  />
                );
              })}
            </div>
          )}
        </div>
      </div>

      {/* Thin progress bar showing auto-rotation timing per slide. Lives at
           SECTION level (not inside the content layer) so its `bottom: 0`
           always anchors to the very bottom of the hero, regardless of
           content layer height. zIndex 4 keeps it above content (3),
           gradient (2), click target (1), and covers (auto). */}
      {n > 1 && (
        <React.Fragment>
          <div aria-hidden="true" style={{
            position:'absolute', left: 0, right: 0, bottom: 0,
            height: 3, background:'rgba(255,255,255,0.12)', zIndex: 4,
            pointerEvents:'none',
          }}>
            <div key={idx + '-' + paused} style={{
              height:'100%', width: paused ? '0%' : '100%',
              background: tint,
              animation: paused ? 'none' : 'chHeroProgress 6s linear forwards',
              transformOrigin:'left',
            }}/>
          </div>
          <style>{`
            @keyframes chHeroProgress {
              from { width: 0%; }
              to   { width: 100%; }
            }
          `}</style>
        </React.Fragment>
      )}
    </section>
  );
};

// Auto-rotating carousel of featured courses — one large cover per slide,
// with title + instructor overlay and dots / arrows for manual nav. Pauses
// on hover so the user can read without fighting the animation.
const ChannelCoverCarousel = ({ courses, tint, lang }) => {
  const slides = (courses || []).slice(0, 8);
  const [idx, setIdx] = React.useState(0);
  const [paused, setPaused] = React.useState(false);
  const n = slides.length;

  // Auto-advance every 5s, pausable on hover. The effect re-runs when n
  // changes so adding/removing slides doesn't leave a stale timer.
  React.useEffect(() => {
    if (n < 2 || paused) return;
    const t = setInterval(() => setIdx(i => (i + 1) % n), 5000);
    return () => clearInterval(t);
  }, [n, paused]);

  if (!n) {
    return (
      <div style={{ position:'relative', width:'100%', height: 400 }}>
        <div style={{
          position:'absolute', top: 40, left: '10%', right: '10%', height: 320,
          borderRadius: 20, border:'1px solid var(--line)',
          background:'linear-gradient(135deg, ' + tint + '1f, ' + tint + '06)',
          boxShadow:'0 30px 60px -40px rgba(0,0,0,0.25)',
        }}/>
      </div>
    );
  }

  const current = slides[idx];
  const href = '/course/' + encodeURIComponent(current.slug || current.id);

  return (
    <div
      onMouseEnter={() => setPaused(true)}
      onMouseLeave={() => setPaused(false)}
      style={{ position:'relative', width:'100%', height: 560 }}
    >
      {/* Stacked slides — all mounted, only the current is opaque, so the
          transition is a clean crossfade without layout jumps. */}
      {slides.map((c, i) => {
        const active = i === idx;
        const slideHref = '/course/' + encodeURIComponent(c.slug || c.id);
        return (
          <a
            key={c.id}
            href={slideHref}
            aria-hidden={!active}
            tabIndex={active ? 0 : -1}
            style={{
              position:'absolute', inset: 0,
              borderRadius: 20, overflow:'hidden',
              border:'1px solid var(--line)',
              boxShadow:'0 32px 70px -40px rgba(0,0,0,0.45), 0 4px 14px rgba(0,0,0,0.06)',
              background:'var(--bg)',
              textDecoration:'none', color:'inherit',
              opacity: active ? 1 : 0,
              transform: active ? 'translateY(0)' : 'translateY(8px)',
              transition:'opacity 0.5s ease, transform 0.5s ease',
              pointerEvents: active ? 'auto' : 'none',
            }}
          >
            {/* Cover fills the tile */}
            <div style={{ position:'absolute', inset: 0,
                          background: tint + '18',
                          display:'flex', alignItems:'center', justifyContent:'center' }}>
              {c.cover ? (
                <img
                  src={c.cover}
                  alt=""
                  style={{ width:'100%', height:'100%', objectFit:'cover', display:'block' }}
                />
              ) : (
                <div style={{ fontFamily:'var(--font-serif)', fontSize: 64, color: tint,
                              opacity: 0.45 }}>{(c.title || '')[0] || '·'}</div>
              )}
            </div>
            {/* Dark gradient for legibility on photos */}
            <div aria-hidden="true" style={{
              position:'absolute', inset: 0,
              background:'linear-gradient(180deg, rgba(0,0,0,0.05) 40%, rgba(0,0,0,0.62) 100%)',
            }}/>
            {/* Caption */}
            <div style={{
              position:'absolute', left: 0, right: 0, bottom: 0,
              padding:'28px 32px 32px', color:'#fff',
            }}>
              <div style={{ fontSize: 12, letterSpacing:'0.12em', textTransform:'uppercase',
                            opacity: 0.85, marginBottom: 10 }}>
                {c.channelName || (c.instructor && c.instructor.name) || ''}
              </div>
              <div style={{ fontFamily:'var(--font-serif)', fontSize: 30, fontWeight: 600,
                            lineHeight: 1.15, letterSpacing:'-0.01em',
                            display:'-webkit-box', WebkitLineClamp: 2,
                            WebkitBoxOrient:'vertical', overflow:'hidden',
                            textShadow:'0 2px 18px rgba(0,0,0,0.35)' }}>
                {c.title || ''}
              </div>
              {c.instructor && c.instructor.name && (
                <div style={{ fontSize: 14, opacity: 0.9, marginTop: 10 }}>
                  {c.instructor.name}
                </div>
              )}
            </div>
          </a>
        );
      })}

      {/* Arrow controls — shown only when there's something to cycle */}
      {n > 1 && (
        <React.Fragment>
          <button
            aria-label="previous"
            onClick={(e) => { e.preventDefault(); setIdx((idx - 1 + n) % n); }}
            style={{
              position:'absolute', top:'50%', left: -14, transform:'translateY(-50%)',
              width: 36, height: 36, borderRadius:'50%', border:'1px solid var(--line)',
              background:'var(--bg)', cursor:'pointer',
              display:'flex', alignItems:'center', justifyContent:'center',
              boxShadow:'0 6px 16px -8px rgba(0,0,0,0.25)',
              color:'var(--ink)',
            }}
          >‹</button>
          <button
            aria-label="next"
            onClick={(e) => { e.preventDefault(); setIdx((idx + 1) % n); }}
            style={{
              position:'absolute', top:'50%', right: -14, transform:'translateY(-50%)',
              width: 36, height: 36, borderRadius:'50%', border:'1px solid var(--line)',
              background:'var(--bg)', cursor:'pointer',
              display:'flex', alignItems:'center', justifyContent:'center',
              boxShadow:'0 6px 16px -8px rgba(0,0,0,0.25)',
              color:'var(--ink)',
            }}
          >›</button>

          {/* Progress dots */}
          <div style={{
            position:'absolute', bottom: -24, left: 0, right: 0,
            display:'flex', gap: 6, justifyContent:'center',
          }}>
            {slides.map((_, i) => (
              <button
                key={i}
                aria-label={'slide ' + (i+1)}
                onClick={(e) => { e.preventDefault(); setIdx(i); }}
                style={{
                  width: i === idx ? 22 : 8, height: 8, borderRadius: 999,
                  border:'none',
                  background: i === idx ? tint : 'var(--line)',
                  cursor:'pointer', transition:'width 0.25s, background 0.25s',
                  padding: 0,
                }}
              />
            ))}
          </div>
        </React.Fragment>
      )}
    </div>
  );
};

const CourseChannelTemplate = ({ channel, channels, courses, cats, lang, goTo }) => {
  const [detail, setDetail] = React.useState(null);
  React.useEffect(() => {
    fetch('/api/admin/channels-full', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : { channels: [] })
      .then(j => {
        const match = (j.channels || []).find(c => c.id === channel.id);
        setDetail(match || { id: channel.id, categories: [], featured: [] });
      })
      .catch(() => setDetail({ id: channel.id, categories: [], featured: [] }));
  }, [channel.id]);

  const pool = Array.isArray(courses) ? courses : [];
  const pickedCatIds = (detail && detail.categories) || [];
  const featuredIds  = (detail && detail.featured)   || [];

  // All courses belonging to this channel — the page body lists ALL of them
  // grouped by category, and the hero carousel scopes its auto-pick to this.
  const channelPool = pool.filter(c =>
    c.channel === channel.id || c.channel === channel.slug
  );

  // Hero carousel slides — admin's featured list wins. If none configured,
  // fall back to the full channel pool (capped to 8 in the carousel itself).
  const featured = featuredIds.length
    ? featuredIds.map(id => pool.find(c => c.id === id)).filter(Boolean)
    : channelPool;

  // Categories to render below the hero. Admin's picked list wins; otherwise
  // show every category that actually has at least one course in this channel,
  // in the category-tree's own sort order.
  const poolCatIds = Array.from(new Set(channelPool.map(c => c.categoryId).filter(Boolean)));
  const catIds = pickedCatIds.length
    ? pickedCatIds
    : cats.filter(c => poolCatIds.includes(c.id)).map(c => c.id);

  // Hero stats
  const hours = channelPool.reduce((s, c) => s + (Number(c.hours) || 0), 0);
  const ratings = channelPool.map(c => Number(c.rating) || 0).filter(r => r > 0);
  const avgRating = ratings.length ? (ratings.reduce((a,b)=>a+b,0)/ratings.length) : 0;
  const stats = {
    count: channelPool.length,
    hours: Math.round(hours),
    rating: avgRating,
    coversFrom: featured,
  };

  const browseHref = '/browse/' + encodeURIComponent(channel.slug || channel.id);
  const onBrowse = () => { window.location.href = browseHref; };

  return (
    <div>
      <ChannelHero channel={channel} lang={lang} stats={stats} onBrowse={onBrowse} />

      {/* One section per category — every category that has a course in
           this channel. Numbered, editorial style. No separate "recommended"
           strip above — the hero carousel already showcases featured courses. */}
      {catIds.length > 0 && catIds.map((catId, idx) => {
        const cat = cats.find(c => c.id === catId);
        if (!cat) return null;
        const inCat = channelPool.filter(c =>
          c.categoryId === catId || c.channel === (cat.slug || ''));
        if (inCat.length === 0) return null;
        const n = String(idx + 1).padStart(2, '0');
        return (
          <section key={catId} style={{ maxWidth: 1240, margin:'0 auto',
                                        padding: idx === 0 ? '72px 32px 24px' : '40px 32px 24px' }}>
            <div style={{ display:'flex', alignItems:'flex-end',
                          justifyContent:'space-between', gap: 24,
                          marginBottom: 20, flexWrap:'wrap',
                          borderBottom:'1px solid var(--line)', paddingBottom: 16 }}>
              <div>
                <div style={{ display:'flex', alignItems:'center', gap: 10,
                              fontSize: 12, color:'var(--ink-3)', letterSpacing:'0.1em',
                              textTransform:'uppercase', marginBottom: 6 }}>
                  <span style={{ fontFamily:'var(--font-serif)', fontSize: 13,
                                 color:'var(--accent)', fontWeight: 700 }}>{n}</span>
                  <span>{t('fc.catPrefix', lang)}</span>
                </div>
                <h2 style={{ fontFamily:'var(--font-serif)', fontSize: 28,
                             fontWeight: 600, margin: 0, color:'var(--ink)',
                             letterSpacing:'-0.01em' }}>
                  {pickI18n(cat.nameI18n, lang, cat.name)}
                </h2>
              </div>
              <a href={'/browse/' + encodeURIComponent(cat.slug || cat.id)}
                 className="btn btn-ghost btn-sm" style={{ textDecoration:'none' }}>
                {t('fc.viewAll', lang)}
              </a>
            </div>
            <div style={{ display:'grid', gap: 20,
                          gridTemplateColumns:'repeat(auto-fill, minmax(260px, 1fr))' }}>
              {inCat.slice(0, 8).map(c => (
                <CourseCard key={c.id} course={c}
                  onClick={() => goTo('course', { course: c.slug || c.id })} />
              ))}
            </div>
          </section>
        );
      })}

      {/* Empty state — only when neither the hero carousel nor any category
           has content (i.e. the channel truly has no courses yet). */}
      {featured.length === 0 && catIds.length === 0 && (
        <div style={{ textAlign:'center', padding:'80px 32px', color:'var(--ink-3)' }}>
          {t('fc.noCourses', lang)}
        </div>
      )}

      <style>{`
        @media (max-width: 900px) {
          .ch-hero-inner { grid-template-columns: 1fr !important; }
          .ch-hero-visual { display: none !important; }
        }
      `}</style>
    </div>
  );
};

// System template: route to the matching built-in section. The system
// systemKey on template_overrides controls which view.
const SystemChannelTemplate = ({ channel, lang, goTo, signedIn }) => {
  const key = (channel.templateOverrides && channel.templateOverrides.systemKey) || '';
  if (key === 'membership') {
    return (
      <section style={{ maxWidth: 900, margin:'80px auto', padding:'0 32px', textAlign:'center' }}>
        <h1 style={{ fontFamily:'var(--font-serif)', fontSize: 40, fontWeight: 600,
                     color:'var(--ink)', margin:'0 0 12px' }}>
          {t('fc.member.title', lang)}
        </h1>
        <p style={{ fontSize: 17, color:'var(--ink-2)', marginBottom: 28 }}>
          {t('fc.member.sub', lang)}
        </p>
        <button className="btn btn-accent" onClick={() => goTo('pricing')}>
          {t('fc.member.cta', lang)} <Icon name="arrowR" size={14} />
        </button>
      </section>
    );
  }
  if (key === 'my-learning') {
    return (
      <section style={{ maxWidth: 900, margin:'80px auto', padding:'0 32px', textAlign:'center' }}>
        <h1 style={{ fontFamily:'var(--font-serif)', fontSize: 40, fontWeight: 600,
                     color:'var(--ink)', margin:'0 0 12px' }}>
          {t('fc.mylearning.title', lang)}
        </h1>
        <p style={{ fontSize: 17, color:'var(--ink-2)', marginBottom: 28 }}>
          {signedIn ? t('fc.mylearning.sub', lang) : t('fc.mylearning.signin', lang)}
        </p>
        {signedIn ? (
          <button className="btn btn-accent" onClick={() => goTo('user')}>
            {t('fc.mylearning.cta', lang)} <Icon name="arrowR" size={14} />
          </button>
        ) : (
          <div style={{ display:'inline-flex', gap: 10 }}>
            <button className="btn btn-accent" onClick={() => goTo('auth', { authMode:'signin' })}>{t('nav.signin', lang)}</button>
            <button className="btn btn-ghost" onClick={() => goTo('auth', { authMode:'signup' })}>{t('nav.signup', lang)}</button>
          </div>
        )}
      </section>
    );
  }
  // Unknown systemKey — just show the channel name so admins realize
  // they need to pick a key.
  return (
    <section style={{ maxWidth: 900, margin:'80px auto', padding:'0 32px', textAlign:'center', color:'var(--ink-3)' }}>
      <h1 style={{ fontFamily:'var(--font-serif)', fontSize: 32, fontWeight: 600, color:'var(--ink)' }}>
        {pickI18n(channel.nameI18n, lang, channel.name)}
      </h1>
      <p style={{ marginTop: 10 }}>systemKey = "{key || '(unset)'}"</p>
    </section>
  );
};

// Activity / home templates: same block renderer.
const BlockChannelTemplate = ({ channel, channels, courses, cats, lang, goTo }) => {
  const blocks = (channel.templateOverrides && Array.isArray(channel.templateOverrides.blocks))
    ? channel.templateOverrides.blocks : [];
  return <BlockRenderer blocks={blocks} lang={lang} goTo={goTo}
                        courses={courses} cats={cats} channels={channels} />;
};

// ---------- top-level router ------------------------------------------------
const FrontChannelRouter = ({ lang, slug, goTo, signedIn }) => {
  const channels = useChannelList();
  const courses  = useCourseList();
  const cats     = useCategoryTree();

  const channel = channels.find(c => (c.slug || c.id) === slug);

  // Loading / not-found states use the same header so navigation still works.
  return (
    <div className="fade-in">
      <DynamicPrimaryNav lang={lang} goTo={goTo} signedIn={signedIn} />
      {!channels.length && (
        <div style={{ padding: 80, textAlign:'center', color:'var(--ink-3)' }}>
          {t('fc.loading', lang)}
        </div>
      )}
      {channels.length > 0 && !channel && (
        <div style={{ padding: 80, textAlign:'center', color:'var(--ink-3)' }}>
          {t('admin.noCourses', lang) || 'Not found'} · <code>{slug}</code>
        </div>
      )}
      {channel && channel.type === 'course' && (
        <CourseChannelTemplate channel={channel} channels={channels}
          courses={courses} cats={cats} lang={lang} goTo={goTo} />
      )}
      {channel && channel.type === 'system' && (
        <SystemChannelTemplate channel={channel} lang={lang} goTo={goTo} signedIn={signedIn} />
      )}
      {channel && (channel.type === 'activity' || channel.type === 'home') && (
        <BlockChannelTemplate channel={channel} channels={channels}
          courses={courses} cats={cats} lang={lang} goTo={goTo} />
      )}
      {channel && !['course','system','activity','home'].includes(channel.type) && (
        <div style={{ padding: 80, textAlign:'center', color:'var(--ink-3)' }}>
          Unknown channel type: {channel.type}
        </div>
      )}
      {typeof FooterBar !== 'undefined' && <FooterBar lang={lang} />}
    </div>
  );
};

// Expose to window so entries can mount them. We intentionally do NOT export
// CourseCard here — the canonical one lives in shared.jsx and this file
// consumes window.CourseCard from there.
Object.assign(window, {
  FrontChannelRouter,
  DynamicPrimaryNav,
  BlockRenderer,
  FeaturedCarousel,
});
