/* global React, ReactDOM, TweaksPanel, TweakSection, TweakRadio, TweakColor, TweakToggle, useTweaks */
const { useState, useEffect, useMemo, useRef } = React;

// ---------- TWITCH API HELPERS ----------
// Fetches /api/twitch/<endpoint>. Returns {data, loading, error}.
// On 5xx or network error: keeps `fallback` as data so the UI still has something to render.
// Auto-refreshes every `refreshMs` if provided.
function useTwitchData(endpoint, fallback, refreshMs) {
  const [data, setData] = useState(fallback);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  useEffect(() => {
    let cancelled = false;
    let timerId = null;
    const load = () => {
      fetch(`/api/twitch/${endpoint}`, { headers: { Accept: 'application/json' } })
        .then((r) => r.json().then((j) => ({ ok: r.ok, status: r.status, json: j })))
        .then(({ ok, status, json }) => {
          if (cancelled) return;
          if (ok) {
            setData(json);
            setError(null);
          } else {
            setError({ status, ...json });
            // Keep fallback as `data` so UI still renders.
          }
        })
        .catch((e) => { if (!cancelled) setError({ message: String(e) }); })
        .finally(() => { if (!cancelled) setLoading(false); });
    };
    load();
    if (refreshMs) timerId = setInterval(load, refreshMs);
    return () => { cancelled = true; if (timerId) clearInterval(timerId); };
  }, [endpoint, refreshMs]);
  return { data, loading, error };
}

// Twitch returns durations like "4h21m08s" — render as "4:21:08" or "21:08".
function fmtTwitchDuration(s) {
  if (!s) return '';
  const h = parseInt(/(\d+)h/.exec(s)?.[1] || '0', 10);
  const m = parseInt(/(\d+)m/.exec(s)?.[1] || '0', 10);
  const sec = parseInt(/(\d+)s/.exec(s)?.[1] || '0', 10);
  const pad = (n) => String(n).padStart(2, '0');
  return h ? `${h}:${pad(m)}:${pad(sec)}` : `${m}:${pad(sec)}`;
}

function fmtViews(n) {
  if (n == null) return '';
  if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
  if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, '') + 'K';
  return String(n);
}

function fmtTimeAgo(iso) {
  if (!iso) return '';
  const diff = Date.now() - new Date(iso).getTime();
  if (diff < 0) return '';
  const mins = Math.round(diff / 60000);
  if (mins < 60) return `${mins}m ago`;
  const hrs = Math.round(mins / 60);
  if (hrs < 24) return `${hrs}h ago`;
  const days = Math.round(hrs / 24);
  if (days < 7) return `${days}d ago`;
  const weeks = Math.round(days / 7);
  if (weeks < 4) return `${weeks}w ago`;
  const months = Math.round(days / 30);
  return `${months}mo ago`;
}

function fmtUptime(iso) {
  if (!iso) return '';
  const diff = Date.now() - new Date(iso).getTime();
  if (diff < 0) return '';
  const mins = Math.floor(diff / 60000);
  const h = Math.floor(mins / 60);
  const m = mins % 60;
  return h ? `${h}h ${m}m` : `${m}m`;
}

function fmtThumb(url, w = 640, h = 360) {
  // Twitch thumbnail URLs have {width}x{height} placeholders.
  if (!url) return '';
  return url.replace('{width}', w).replace('{height}', h);
}

// Categorize a Twitch game into one of our visual tones / kinds.
function categorize(gameName) {
  const g = (gameName || '').toLowerCase();
  if (/just chatting|chat/.test(g))               return { kind: 'chat', tag: 'CHAT',  tone: 'cyan'  };
  if (/karaoke|music|singing/.test(g))            return { kind: 'sing', tag: 'SING',  tone: 'gold'  };
  if (/baldur|elden|rpg|final fantasy|persona|witcher|dragon|skyrim|fallout|legend of zelda|fire emblem|pokemon/.test(g))
                                                  return { kind: 'rpg',  tag: 'RPG',   tone: 'royal' };
  if (/horror|resident evil|silent hill|outlast|amnesia|alien|phasmo/.test(g))
                                                  return { kind: 'mys',  tag: 'HORROR',tone: 'red'   };
  if (/lore|storytelling|story/.test(g))          return { kind: 'lore', tag: 'LORE',  tone: 'gem'   };
  if (/cooking|asmr|art|drawing|crafts/.test(g))  return { kind: 'chat', tag: 'CHILL', tone: 'cyan'  };
  if (!g)                                         return { kind: 'off',  tag: 'TBA',   tone: 'gem'   };
  return { kind: 'rpg', tag: 'PLAY', tone: 'royal' };
}

// ---------- SVG MOTIFS ----------
const WeaveMotif = ({ className = "", color = "currentColor" }) =>
<svg viewBox="0 0 80 240" className={className} fill="none" stroke={color} strokeWidth="2" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
    {[0, 60, 120, 180].map((y) =>
  <g key={y} transform={`translate(0 ${y})`}>
        <path d="M40 6 L70 30 L40 54 L10 30 Z" />
        <path d="M40 16 L60 30 L40 44 L20 30 Z" />
        <path d="M40 26 L50 30 L40 34 L30 30 Z" fill={color} />
        <path d="M8 30 L0 30 M72 30 L80 30" />
      </g>
  )}
  </svg>;


const KeyMotif = ({ className = "", color = "currentColor" }) =>
<svg viewBox="0 0 60 60" className={className} fill="none" stroke={color} strokeWidth="3" strokeLinejoin="miter" aria-hidden="true">
    <path d="M6 6 H54 V54 H30 V30 H42 V42 H36" />
  </svg>;


const DiamondGem = ({ size = 24, color = "#4d2bd4", glow = "#62c7e8" }) =>
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
    <defs>
      <linearGradient id={`gemg-${color.replace('#', '')}`} x1="0" y1="0" x2="1" y2="1">
        <stop offset="0" stopColor={glow} stopOpacity="0.9" />
        <stop offset="1" stopColor={color} />
      </linearGradient>
    </defs>
    <path d="M12 1 L23 12 L12 23 L1 12 Z" fill={`url(#gemg-${color.replace('#', '')})`} stroke="#d4a629" strokeWidth="1.2" />
    <path d="M12 1 L12 23 M1 12 L23 12" stroke="#ffffff" strokeOpacity="0.45" strokeWidth="0.7" />
  </svg>;


// ---------- TOP NAV ----------
// Try to produce a short timezone abbreviation (e.g. EST, GMT, JST) from the visitor's
// resolved timezone. Falls back to the offset (e.g. UTC+8) if the runtime won't give a name.
function getLocalTzShort(date) {
  try {
    const parts = new Intl.DateTimeFormat(undefined, { timeZoneName: 'short' }).formatToParts(date);
    const tz = parts.find((p) => p.type === 'timeZoneName')?.value;
    if (tz && /[A-Za-z]/.test(tz)) return tz;
  } catch (e) {}
  const offMin = -date.getTimezoneOffset();
  const sign = offMin >= 0 ? '+' : '-';
  const abs = Math.abs(offMin);
  const h = Math.floor(abs / 60);
  const m = abs % 60;
  return `UTC${sign}${h}${m ? ':' + String(m).padStart(2, '0') : ''}`;
}

function TopNav({ accent }) {
  const [now, setNow] = useState(() => new Date());
  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(id);
  }, []);
  const timeStr = now.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
  const tzShort = useMemo(() => getLocalTzShort(now), [now.getMinutes()]);
  return (
    <div className="topnav-wrap">
      <nav className="topnav">
        <a className="brand" href="#top" aria-label="GipTB home">
          <img className="brand-mark brand-mark-light" src="assets/Logo_GipTB_White_Watermark.png" alt="" aria-hidden="true" />
          <img className="brand-mark brand-mark-dark" src="assets/Logo_GipTB_Black_Watermark.png" alt="" aria-hidden="true" />
          <span className="brand-word">GipTB</span>
        </a>
        <div className="topnav-links">
          <a href="#watch">Live</a>
          <a href="#about">About</a>
          <a href="#schedule">Schedule</a>
          <a href="#vods">VODs</a>
          <a href="#socials">Socials</a>
        </div>
        <div className="topnav-meta">
          <span className="mono">{tzShort} · {timeStr}</span>
          <a className="btn-pill btn-pill-accent" href="#watch" style={{ '--btn-accent': accent }}>
            <span className="live-dot" />
            Watch live
          </a>
        </div>
      </nav>
    </div>
  );
}

// ---------- HERO ----------
function Hero({ accent }) {
  return (
    <section className="hero" data-screen-label="01 Hero">
      <div className="hero-bg" aria-hidden="true">
        <div className="hero-bg-gradient" />
        <div className="hero-bg-vignette" />
        <div className="hero-bg-motif-l">
          <WeaveMotif color="rgba(212,166,41,0.45)" />
        </div>
        <div className="hero-bg-motif-r">
          <WeaveMotif color="rgba(212,166,41,0.35)" />
        </div>
        <div className="hero-bg-keys">
          {Array.from({ length: 14 }).map((_, i) =>
          <KeyMotif key={i} color="rgba(245,214,58,0.18)" />
          )}
        </div>
      </div>

      <div className="hero-grid">
        <div className="hero-text">
          <div className="hero-eyebrow">
            <span className="mono">VTUBER · GEN 01</span>
            <span className="hero-dash" />
            <span className="mono">EST. 2024</span>
          </div>
          <h1 className="hero-title">
            <span className="hero-title-row">
              <span className="hero-title-word" style={{ color: 'var(--ink)' }}>Gip</span>
              <span className="hero-title-word" style={{ color: 'var(--accent)' }}>TB</span>
            </span>
            <span className="hero-title-sub">the gem-cursed cartographer</span>
          </h1>

          <p className="hero-lede">
            Storyteller, gold-keeper, occasional menace. Streams cozy
            adventure RPGs, weaving sessions with the chat, and late-night
            karaoke from a workshop somewhere off the trade road.
          </p>

          <div className="hero-ctas">
            <a className="btn-primary" href="#watch" style={{ '--btn-accent': accent }}>
              <span className="live-dot" /> Watch the stream
            </a>
            <a className="btn-ghost" href="#about">Read the lore →</a>
          </div>

          <div className="hero-stats">
            <Stat k="Channel" v="@giptb" />
            <Stat k="Followers" v="42.8K" />
            <Stat k="Hours streamed" v="1,204" />
            <Stat k="Language" v="English" />
          </div>
        </div>

        <div className="hero-art">
          <div className="hero-art-frame">
            <div className="hero-art-bg" aria-hidden="true" />
            <img src="assets/char-front.png" alt="GipTB full body" className="hero-art-img" />
            <div className="hero-art-tag">
              <span className="mono">CARD · 001 / GEN-01</span>
              <DiamondGem size={16} color="#4d2bd4" glow="#62c7e8" />
            </div>
            <div className="hero-art-corner tl"></div>
            <div className="hero-art-corner tr"></div>
            <div className="hero-art-corner bl"></div>
            <div className="hero-art-corner br"></div>
          </div>
          <div className="hero-art-caption">
            <span className="mono">FIG. A · REFERENCE CARD</span>
          </div>
        </div>
      </div>

    </section>);

}

// ---------- ROTATING CARD ----------
const RCARD_FACES = [
  { id: 'front', src: 'assets/char-front.png', alt: 'GipTB front',     tag: 'FRONT · 01' },
  { id: 'back',  src: 'assets/char-back.png',  alt: 'GipTB back',      tag: 'BACK · 02' },
  { id: 'fit',   src: 'assets/GipTB.PNG',      alt: 'GipTB alt outfit', tag: 'OUTFIT · 03' },
];

function RotatingCard() {
  const [idx, setIdx] = useState(0);
  const next = () => setIdx((i) => (i + 1) % RCARD_FACES.length);
  const cur = RCARD_FACES[idx];

  return (
    <section className="rcard-section" data-screen-label="01b Reference">
      <div className="rcard-grid">
        <div className="rcard-text">
          <span className="mono dim">FIG. B · TURNAROUND</span>
          <h2 className="rcard-headline">
            Hand-drawn. <em>Hand-stitched.</em><br />
            Every gem is a memory.
          </h2>
          <p className="rcard-body">
            The reference sheet exists in physical form too, a four-panel
            woven card the studio passes around at cons. Click the card
            to walk around him.
          </p>
          <div className="rcard-controls">
            <button className="rcard-btn" onClick={next}>
              <span className="rcard-btn-arrow">↻</span>
              Next view
            </button>
            <div className="rcard-dots" role="tablist" aria-label="Card view">
              {RCARD_FACES.map((f, i) => (
                <button
                  key={f.id}
                  type="button"
                  role="tab"
                  aria-selected={i === idx}
                  className={`rcard-dot ${i === idx ? 'is-active' : ''}`}
                  onClick={() => setIdx(i)}
                  aria-label={f.tag}
                />
              ))}
            </div>
            <span className="rcard-hint-inline mono">{cur.tag}</span>
          </div>
          <dl className="rcard-data">
            <div><dt className="mono">CANVAS</dt><dd>4096 × 2612</dd></div>
            <div><dt className="mono">PALETTE</dt><dd>6 colors · gold-led</dd></div>
            <div><dt className="mono">MOTIFS</dt><dd>woven panel · meander · diamond</dd></div>
          </dl>
        </div>

        <div
          className="rcard-stage"
          onClick={next}
          role="button"
          tabIndex={0}
          aria-label={`Show next view (currently ${cur.tag})`}
          onKeyDown={(e) => {if (e.key === 'Enter' || e.key === ' ') {e.preventDefault();next();}}}>
          <div className="rcard-shadow" />
          <div className="rcard">
            {RCARD_FACES.map((f, i) => (
              <div
                key={f.id}
                className={`rcard-face rcard-face-${f.id} ${i === idx ? 'is-active' : 'is-hidden'}`}
                aria-hidden={i !== idx}
              >
                <div className="rcard-face-bg" />
                <div className="rcard-face-corner tl"></div>
                <div className="rcard-face-corner tr"></div>
                <div className="rcard-face-corner bl"></div>
                <div className="rcard-face-corner br"></div>
                <img src={f.src} alt={f.alt} loading={i === 0 ? 'eager' : 'lazy'} />
                <div className="rcard-face-tag">
                  <span className="mono">{f.tag}</span>
                  <DiamondGem size={14} color="#4d2bd4" glow="#62c7e8" />
                </div>
              </div>
            ))}
          </div>
          <div className="rcard-hint mono">click to rotate</div>
        </div>
      </div>
    </section>);

}

function Stat({ k, v }) {
  return (
    <div className="stat">
      <div className="stat-k mono">{k}</div>
      <div className="stat-v">{v}</div>
    </div>);

}

// ---------- TWITCH LIVE ----------
function Live({ channel = 'giptb' }) {
  const [parent, setParent] = useState('localhost');
  const [active, setActive] = useState(false);
  useEffect(() => {
    const host = window.location && window.location.hostname || 'localhost';
    setParent(host);
  }, []);

  // Refresh live status every 60s.
  const { data: liveData, loading, error } = useTwitchData('live', null, 60_000);
  const isLive = !!liveData?.live;
  const setupNeeded = error?.setup_needed;

  const playerSrc = `https://player.twitch.tv/?channel=${channel}&parent=${parent}&muted=true&autoplay=false`;

  // Per-minute re-render so uptime stays current without thrashing.
  const [, tick] = useState(0);
  useEffect(() => {
    if (!isLive) return;
    const id = setInterval(() => tick((n) => n + 1), 60_000);
    return () => clearInterval(id);
  }, [isLive]);

  return (
    <section id="watch" className="watch" data-screen-label="01a Live">
      <SectionHeader
        index="01"
        eyebrow={isLive ? <><span className="live-dot" />&nbsp;Now</> : <span className="mono dim">Offline</span>}
        title={`<em>twitch.tv/</em>${channel}`}
        trailing={
          <div className="watch-trailing">
            <a className="link mono" href={`https://twitch.tv/${channel}`} target="_blank" rel="noreferrer">open on twitch ↗</a>
          </div>
        }
      />

      <div className="watch-grid">
        <div className="watch-player-wrap">
          <div className={`watch-player-frame ${active ? 'is-active' : ''}`}>
            <iframe
              src={playerSrc}
              title="Twitch stream"
              allow="autoplay; fullscreen"
              allowFullScreen
              className="watch-player-iframe" />

            {!active && (
              <button
                type="button"
                className="watch-player-shield"
                onClick={() => setActive(true)}
                aria-label="Activate stream player"
              >
                <span className="watch-player-shield-bg" />
                <img className="watch-player-shield-yap" src="assets/GipTB_Yap.gif" alt="" aria-hidden="true" />
                <span className="watch-player-shield-content">
                  <span className="watch-player-shield-icon">▶</span>
                  <span className="watch-player-shield-label">{isLive ? 'click to activate' : 'click to load player'}</span>
                  <span className="watch-player-shield-hint mono">
                    {isLive ? 'scroll stays smooth until you click' : 'currently offline · player loads on click'}
                  </span>
                </span>
              </button>
            )}

            <div className="watch-player-corner tl"></div>
            <div className="watch-player-corner tr"></div>
            <div className="watch-player-corner bl"></div>
            <div className="watch-player-corner br"></div>
          </div>
          <div className="watch-player-caption mono">
            {isLive ? (
              <>
                <span><span className="live-dot" />&nbsp;LIVE · {fmtViews(liveData.viewers)} watching</span>
                <span>{(liveData.game || 'STREAMING').toUpperCase()}</span>
              </>
            ) : (
              <>
                <span className="dim">OFFLINE{loading && !setupNeeded ? ' · checking...' : ''}</span>
                <span className="dim">follow to be notified</span>
              </>
            )}
          </div>
        </div>

        <aside className="watch-side">
          {isLive ? (
            <>
              <div className="watch-side-row">
                <div className="watch-side-k mono">Title</div>
                <div className="watch-side-v">{liveData.title}</div>
              </div>
              <div className="watch-side-row">
                <div className="watch-side-k mono">Category</div>
                <div className="watch-side-v">{liveData.game || '—'}</div>
              </div>
              <div className="watch-side-row">
                <div className="watch-side-k mono">Uptime</div>
                <div className="watch-side-v">{fmtUptime(liveData.started_at)}</div>
              </div>
              <div className="watch-side-row">
                <div className="watch-side-k mono">Viewers</div>
                <div className="watch-side-v">{Number(liveData.viewers || 0).toLocaleString()}</div>
              </div>
              {liveData.tags && liveData.tags.length > 0 && (
                <div className="watch-side-row">
                  <div className="watch-side-k mono">Tags</div>
                  <div className="watch-side-v watch-tags">
                    {liveData.tags.slice(0, 5).map((t) => <span key={t} className="watch-tag">{t}</span>)}
                  </div>
                </div>
              )}
            </>
          ) : (
            <>
              <div className="watch-side-row">
                <div className="watch-side-k mono">Status</div>
                <div className="watch-side-v">
                  {setupNeeded
                    ? 'Live status unavailable'
                    : loading
                      ? 'Checking Twitch…'
                      : 'Offline — see schedule below'}
                </div>
              </div>
              {setupNeeded && (
                <div className="watch-side-row">
                  <div className="watch-side-k mono">Setup</div>
                  <div className="watch-side-v">
                    Twitch API credentials missing. Set TWITCH_CLIENT_ID and TWITCH_CLIENT_SECRET in Cloudflare Pages env vars to enable live status.
                  </div>
                </div>
              )}
            </>
          )}
          <div className="watch-side-actions">
            <a className="btn-primary watch-follow" href={`https://twitch.tv/${channel}`} target="_blank" rel="noreferrer">
              ♥&nbsp;&nbsp;Follow on Twitch
            </a>
            <a className="btn-ghost" href={`https://twitch.tv/popout/${channel}/chat`} target="_blank" rel="noreferrer">
              Open chat ↗
            </a>
          </div>
        </aside>
      </div>
    </section>);

}

// ---------- ABOUT ----------
function About() {
  return (
    <section id="about" className="about" data-screen-label="02 About">
      <SectionHeader index="02" eyebrow="Profile" title="A trader who can't stop telling stories" />
      <div className="about-grid">
        <article className="lore">
          <p className="lore-drop">
            <span className="drop-cap">G</span>ipTB grew up running caravan
            errands along the upland trade routes, picking up six dialects,
            twelve card games, and a habit of monologuing about jewelry he
            doesn't own yet.
          </p>
          <p>
            The cuffs are real. He inherited them from a grandfather who
            insisted every stone has an opinion. Chat agrees the violet
            diamond has been louder than ever this patch.
          </p>
          <ul className="lore-traits">
            <li><span className="mono">VIBE</span> warm dusk · gold lamp · woven blanket</li>
            <li><span className="mono">LIKES</span> ginger tea, antique maps, your dog</li>
            <li><span className="mono">DISLIKES</span> liars, soggy noodles, the number 13</li>
          </ul>
        </article>

        <aside className="profile-card">
          <div className="profile-card-top">
            <div className="profile-card-tag mono">ID · GTB·001</div>
            <DiamondGem size={22} color="#4d2bd4" glow="#62c7e8" />
          </div>
          <div className="profile-card-portrait">
            <img src="assets/GipTB_PFP_1.png" alt="GipTB portrait" />
            <img src="assets/GipTB_Yap.gif" alt="" aria-hidden="true" className="profile-card-yap" />
            <div className="profile-card-watermark mono">GipTB</div>
          </div>
          <dl className="profile-card-data">
            <ProfileRow k="Pronouns" v="he / they" />
            <ProfileRow k="Height" v="186 cm" />
            <ProfileRow k="Debut" v="2024 · 08 · 17" />
            <ProfileRow k="Fans" v="Goldkeepers" />
          </dl>
          <div className="profile-card-palette" aria-hidden="true">
            {['#0a0b2e', '#62c7e8', '#3a3eb0', '#6e5a8c', '#d4a629', '#f5d63a'].map((c) =>
            <span key={c} className="swatch" style={{ background: c }} />
            )}
          </div>
        </aside>
      </div>
    </section>);

}

function ProfileRow({ k, v }) {
  return (
    <div className="profile-row">
      <dt className="mono">{k}</dt>
      <dd>{v}</dd>
    </div>);

}

// ---------- SCHEDULE ----------
const DAY_NAMES = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];

// Convert a Twitch schedule segment → render-ready row.
function segmentToRow(seg) {
  const start = new Date(seg.start_time);
  const end = seg.end_time ? new Date(seg.end_time) : null;
  const canceled = !!seg.canceled_until;
  const cat = categorize(seg.category?.name);
  const time = start.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
  let dur = '';
  if (end) {
    const mins = Math.round((end - start) / 60000);
    const h = Math.floor(mins / 60), m = mins % 60;
    dur = h ? `~${h}h${m ? ' ' + m + 'm' : ''}` : `~${m}m`;
  }
  return {
    id: seg.id,
    day: DAY_NAMES[start.getDay()],
    date: start.getDate(),
    title: canceled ? `${seg.title || 'Stream'} (canceled)` : (seg.title || cat.tag),
    tag: canceled ? 'OFF' : cat.tag,
    time,
    dur,
    kind: canceled ? 'off' : cat.kind,
    sortKey: start.getTime(),
  };
}

function Schedule({ accent }) {
  const { data, loading, error } = useTwitchData('schedule', null, 300_000);
  const setupNeeded = error?.setup_needed;

  // Compute rows: pick the next 7 days of segments from now.
  const rows = useMemo(() => {
    if (!data?.has_schedule || !Array.isArray(data.segments)) return [];
    const cutoff = Date.now() + 7 * 86400_000;
    return data.segments
      .map(segmentToRow)
      .filter((r) => r.sortKey >= Date.now() - 3 * 3600_000 && r.sortKey <= cutoff)
      .sort((a, b) => a.sortKey - b.sortKey);
  }, [data]);

  const weekLabel = useMemo(() => {
    const now = new Date();
    const end = new Date(now.getTime() + 6 * 86400_000);
    const fmt = (d) => d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }).toUpperCase();
    return `${fmt(now)} / ${fmt(end)}`;
  }, []);

  return (
    <section id="schedule" className="schedule" data-screen-label="03 Schedule">
      <SectionHeader
        index="03"
        eyebrow="Upcoming"
        title="Stream schedule"
        trailing={<span className="mono dim">{weekLabel}</span>}
      />

      {rows.length > 0 ? (
        <ol className="schedule-list">
          {rows.map((s) => (
            <li key={s.id} className={`sch-row sch-${s.kind}`}>
              <div className="sch-row-day">
                <span className="sch-row-dayname mono">{s.day}</span>
                <span className="sch-row-daynum">{s.date}</span>
              </div>
              <div className="sch-row-tag mono">{s.tag}</div>
              <div className="sch-row-title">{s.title}</div>
              <div className="sch-row-time mono">{s.time}</div>
              <div className="sch-row-dur mono">{s.dur || ''}</div>
            </li>
          ))}
        </ol>
      ) : (
        <div className="schedule-empty">
          {loading && !data ? (
            <p className="schedule-empty-msg">Loading schedule from Twitch…</p>
          ) : setupNeeded ? (
            <p className="schedule-empty-msg">
              Schedule will appear here once Twitch API credentials are set in Cloudflare Pages env vars.
            </p>
          ) : data && !data.has_schedule ? (
            <p className="schedule-empty-msg">
              No schedule set on Twitch yet. <a className="link" href={`https://dashboard.twitch.tv/u/giptb/content/schedule`} target="_blank" rel="noreferrer">Add one in the Twitch dashboard ↗</a>
            </p>
          ) : (
            <p className="schedule-empty-msg">No upcoming streams this week.</p>
          )}
        </div>
      )}

      <div className="schedule-note">
        <span className="mono">NOTE</span>
        <span>
          Times pulled live from Twitch and shown in your local timezone
          ({Intl.DateTimeFormat().resolvedOptions().timeZone}).
          Mystery streams announced 1h before going live on Discord and X.
        </span>
      </div>
    </section>);

}

// ---------- VODS ----------
// Convert a Twitch video → render-ready VOD.
function videoToVod(v) {
  const cat = categorize(/* twitch /videos doesn't return category, infer from title */ v.title);
  return {
    id: v.id,
    title: v.title,
    cat: cat.tag,
    tone: cat.tone,
    dur: fmtTwitchDuration(v.duration),
    views: fmtViews(v.view_count),
    age: fmtTimeAgo(v.published_at),
    url: v.url,
    thumb: fmtThumb(v.thumbnail, 640, 360),
  };
}

function VODs() {
  const { data, loading, error } = useTwitchData('vods', null, 300_000);
  const setupNeeded = error?.setup_needed;

  const list = useMemo(() => {
    const videos = data?.videos || [];
    return videos.slice(0, 4).map(videoToVod);
  }, [data]);

  const featured = list[0];
  const rest = list.slice(1);

  return (
    <section id="vods" className="vods" data-screen-label="04 VODs">
      <SectionHeader
        index="04"
        eyebrow="Recent"
        title="VODs &amp; highlights"
        trailing={
          <div className="vods-trailing">
            <a className="link mono" href="https://twitch.tv/giptb/videos" target="_blank" rel="noreferrer">all archives ↗</a>
          </div>
        }
      />

      {featured ? (
        <div className="vods-layout">
          <a
            className={`vod-featured vod-tone-${featured.tone}`}
            href={featured.url}
            target="_blank"
            rel="noreferrer"
            aria-label={`Watch: ${featured.title}`}
          >
            <div className="vod-featured-art" style={featured.thumb ? { backgroundImage: `url("${featured.thumb}")`, backgroundSize: 'cover', backgroundPosition: 'center' } : undefined}>
              <div className="vod-featured-art-bg" />
              <div className="vod-featured-art-motif" aria-hidden="true">
                <WeaveMotif color="rgba(255,255,255,0.14)" />
                <WeaveMotif color="rgba(255,255,255,0.10)" />
                <WeaveMotif color="rgba(255,255,255,0.08)" />
              </div>
              <div className="vod-featured-cat-row">
                <span className="vod-chip mono"><span className="vod-chip-dot" />{featured.cat}</span>
                <span className="vod-chip vod-chip-soft mono">{featured.age}</span>
              </div>
              <span className="vod-featured-play">
                <svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
                <span>Play</span>
              </span>
              <div className="vod-featured-meta-row">
                <span className="vod-stat-v">{featured.views}</span>
                <span className="vod-stat-k mono">views</span>
                <span className="vod-meta-sep">·</span>
                <span className="vod-dur mono">{featured.dur}</span>
              </div>
            </div>
            <div className="vod-featured-body">
              <span className="mono dim">LATEST</span>
              <h3 className="vod-featured-title">{featured.title}</h3>
            </div>
          </a>

          <div className="vods-list">
            {rest.map((v) => (
              <a
                key={v.id}
                className={`vod-row vod-tone-${v.tone}`}
                href={v.url}
                target="_blank"
                rel="noreferrer"
                aria-label={`Watch: ${v.title}`}
              >
                <div className="vod-row-thumb" style={v.thumb ? { backgroundImage: `url("${v.thumb}")`, backgroundSize: 'cover', backgroundPosition: 'center' } : undefined}>
                  <div className="vod-row-thumb-bg" />
                  <div className="vod-row-thumb-motif" aria-hidden="true">
                    <WeaveMotif color="rgba(255,255,255,0.16)" />
                  </div>
                  <span className="vod-row-play" aria-hidden="true">
                    <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
                  </span>
                  <span className="vod-row-dur mono">{v.dur}</span>
                </div>
                <div className="vod-row-meta">
                  <div className="vod-row-tags">
                    <span className="vod-chip mono"><span className="vod-chip-dot" />{v.cat}</span>
                  </div>
                  <h3 className="vod-row-title">{v.title}</h3>
                  <div className="vod-row-sub mono">
                    <span className="vod-stat-v-sm">{v.views}</span>
                    <span className="vod-stat-k">views</span>
                    <span className="vod-meta-sep">·</span>
                    <span>{v.age}</span>
                  </div>
                </div>
              </a>
            ))}
          </div>
        </div>
      ) : (
        <div className="vods-empty">
          {loading && !data ? (
            <p className="vods-empty-msg">Loading VODs from Twitch…</p>
          ) : setupNeeded ? (
            <p className="vods-empty-msg">
              VODs will appear here once Twitch API credentials are set in Cloudflare Pages env vars.
            </p>
          ) : (
            <p className="vods-empty-msg">
              No recent VODs found on Twitch. <a className="link" href="https://twitch.tv/giptb" target="_blank" rel="noreferrer">Open channel ↗</a>
            </p>
          )}
        </div>
      )}
    </section>);
}

// ---------- EMOTES ----------
const EMOTES = [
  { file: 'Yippie.png',     name: 'yippie',     tag: 'HYPE',  cat: 'react' },
  { file: 'Smug.png',       name: 'smug',       tag: 'PVP',   cat: 'react' },
  { file: 'Crashout.png',   name: 'crashout',   tag: 'MOOD',  cat: 'react' },
  { file: 'Cry.png',        name: 'cry',        tag: 'SAD',   cat: 'react' },
  { file: 'Shy.png',        name: 'shy',        tag: 'UWU',   cat: 'react' },
  { file: 'Sleepy.png',     name: 'sleepy',     tag: 'LATE',  cat: 'mood' },
  { file: 'Sip.png',        name: 'sip',        tag: 'TEA',   cat: 'mood' },
  { file: 'Popcorn.png',    name: 'popcorn',    tag: 'DRAMA', cat: 'react' },
  { file: 'Peek.png',       name: 'peek',       tag: 'LURK',  cat: 'mood' },
  { file: 'Stonks.png',     name: 'stonks',     tag: 'WIN',   cat: 'meme' },
  { file: 'ThisIsFine.png', name: 'thisisfine', tag: 'PAIN',  cat: 'meme' },
  { file: 'Gifted.png',     name: 'gifted',     tag: 'SUB',   cat: 'event' },
  { file: 'Raid.png',       name: 'raid',       tag: 'INCOMING', cat: 'event' },
  { file: 'Dum.png',        name: 'dum',        tag: 'OOPS',  cat: 'meme' },
];

function Emotes() {
  const [filter, setFilter] = useState('all');
  const [hovered, setHovered] = useState(null);
  const filtered = filter === 'all' ? EMOTES : EMOTES.filter((e) => e.cat === filter);
  const cats = [
    { id: 'all',   label: 'All' },
    { id: 'react', label: 'Reactions' },
    { id: 'mood',  label: 'Moods' },
    { id: 'meme',  label: 'Memes' },
    { id: 'event', label: 'Events' },
  ];
  const focused = hovered != null ? EMOTES.find((e) => e.name === hovered) : null;
  return (
    <section id="emotes" className="emotes" data-screen-label="07 Emotes">
      <SectionHeader
        index="07"
        eyebrow="Drops"
        title="The emote pack"
        trailing={
          <div className="emotes-filters" role="tablist" aria-label="Emote category">
            {cats.map((c) => (
              <button
                key={c.id}
                type="button"
                role="tab"
                aria-selected={filter === c.id}
                className={`emote-filter mono ${filter === c.id ? 'is-active' : ''}`}
                onClick={() => setFilter(c.id)}
              >{c.label}</button>
            ))}
          </div>
        }
      />
      <div className="emotes-layout">
        <div className="emotes-grid" role="list">
          {filtered.map((e) => (
            <button
              key={e.name}
              type="button"
              role="listitem"
              className={`emote ${focused?.name === e.name ? 'is-focus' : ''}`}
              onMouseEnter={() => setHovered(e.name)}
              onMouseLeave={() => setHovered((cur) => (cur === e.name ? null : cur))}
              onFocus={() => setHovered(e.name)}
              onBlur={() => setHovered((cur) => (cur === e.name ? null : cur))}
              onClick={() => {
                try { navigator.clipboard.writeText(`:${e.name}:`); } catch (err) {}
              }}
              aria-label={`Copy :${e.name}: shortcode`}
              title={`:${e.name}:`}
            >
              <span className="emote-frame">
                <img src={`assets/${e.file}`} alt={e.name} loading="lazy" decoding="async" />
              </span>
              <span className="emote-tag mono">{e.tag}</span>
              <span className="emote-name mono">:{e.name}:</span>
            </button>
          ))}
        </div>
        <aside className="emotes-side">
          <div className="emotes-side-card">
            <div className="emotes-side-eyebrow mono">CHANNEL POINTS</div>
            <h3 className="emotes-side-title">
              {focused ? <>:{focused.name}:</> : <>Hover an emote</>}
            </h3>
            <p className="emotes-side-body">
              {focused
                ? <>Tagged <span className="mono accent">{focused.tag}</span>. Tap any emote to copy its shortcode. Tier 1 subs unlock the full pack across Twitch &amp; Discord.</>
                : <>Hover any emote to see its tag. Click to copy the shortcode. Tier&nbsp;1 subs unlock the full pack across Twitch &amp; Discord.</>
              }
            </p>
            <ul className="emotes-side-stats">
              <li><span className="mono dim">PACK</span><span>14 emotes · 1 animated</span></li>
              <li><span className="mono dim">DROPS</span><span>monthly · sub gift wave</span></li>
              <li><span className="mono dim">ARTIST</span><span>@InnaKerphc</span></li>
            </ul>
          </div>
          <a className="btn-primary emotes-cta" href="#socials">
            Sub for the pack →
          </a>
        </aside>
      </div>
    </section>
  );
}

// ---------- SOCIALS ----------
const SocialIcon = ({ name, size = 28 }) => {
  const c = "currentColor";
  switch (name) {
    case 'YouTube': return (
      <svg width={size} height={size} viewBox="0 0 24 24" fill={c} aria-hidden="true">
        <path d="M21.6 7.2a2.5 2.5 0 0 0-1.76-1.77C18.28 5 12 5 12 5s-6.28 0-7.84.43A2.5 2.5 0 0 0 2.4 7.2C2 8.77 2 12 2 12s0 3.23.4 4.8a2.5 2.5 0 0 0 1.76 1.77C5.72 19 12 19 12 19s6.28 0 7.84-.43a2.5 2.5 0 0 0 1.76-1.77C22 15.23 22 12 22 12s0-3.23-.4-4.8zM10 15V9l5.2 3L10 15z"/>
      </svg>
    );
    case 'Twitch': return (
      <svg width={size} height={size} viewBox="0 0 24 24" fill={c} aria-hidden="true">
        <path d="M4 3v15h5v3h3l3-3h4l5-5V3H4zm17 9-3 3h-5l-3 3v-3H7V5h14v7zm-4-6h-2v6h2V6zm-5 0h-2v6h2V6z"/>
      </svg>
    );
    case 'X / Twitter': return (
      <svg width={size} height={size} viewBox="0 0 24 24" fill={c} aria-hidden="true">
        <path d="M17.5 3h3.3l-7.2 8.2L22 21h-6.6l-5.2-6.8L4.2 21H1l7.7-8.8L1.4 3h6.8l4.7 6.2L17.5 3zm-1.2 16h1.8L7.8 5H5.9l10.4 14z"/>
      </svg>
    );
    case 'Discord': return (
      <svg width={size} height={size} viewBox="0 0 24 24" fill={c} aria-hidden="true">
        <path d="M19.3 5.3a17.5 17.5 0 0 0-4.4-1.4l-.2.4a16 16 0 0 0-5.3 0l-.2-.4a17.5 17.5 0 0 0-4.4 1.4A18.4 18.4 0 0 0 2 17.3a17.7 17.7 0 0 0 5.4 2.7l.4-.6a12 12 0 0 1-2-1l.4-.3a12.7 12.7 0 0 0 11.6 0l.4.3a12 12 0 0 1-2 1l.4.6A17.7 17.7 0 0 0 22 17.3a18.4 18.4 0 0 0-2.7-12zM8.6 14.7c-1.1 0-2-1-2-2.3s.9-2.3 2-2.3 2 1 2 2.3-.9 2.3-2 2.3zm6.8 0c-1.1 0-2-1-2-2.3s.9-2.3 2-2.3 2 1 2 2.3-.9 2.3-2 2.3z"/>
      </svg>
    );
    case 'TikTok': return (
      <svg width={size} height={size} viewBox="0 0 24 24" fill={c} aria-hidden="true">
        <path d="M19.6 6.7a5.6 5.6 0 0 1-3.4-1.2 5.6 5.6 0 0 1-2.2-3.5h-3.6v13.4a2.8 2.8 0 1 1-2-2.7v-3.6a6.4 6.4 0 1 0 5.6 6.3V9.6a9.2 9.2 0 0 0 5.6 1.9v-3.6c-.1 0-.1.1 0 .8z"/>
      </svg>
    );
    case 'Bluesky': return (
      <svg width={size} height={size} viewBox="0 0 24 24" fill={c} aria-hidden="true">
        <path d="M6 3c3 2 6 6 6 9 0-3 3-7 6-9 4 0 5 4 4 8s-3 5-5 5c-1 0-2 0-3 1 2 0 4 1 4 4-2 0-4-1-6-4-2 3-4 4-6 4 0-3 2-4 4-4-1-1-2-1-3-1-2 0-4-1-5-5s0-8 4-8z"/>
      </svg>
    );
    default: return null;
  }
};

const SOCIALS = [
  { name: 'Twitch',      handle: 'giptb',    sub: 'Live streams · Tue / Thu / Sat', tone: 'twitch',  followers: '38.2K', copy: 'https://twitch.tv/giptb',     copyLabel: 'channel url', featured: true },
  { name: 'YouTube',     handle: '@giptb',        sub: 'Main channel · VODs',            tone: 'youtube', followers: '42.8K', copy: 'https://youtube.com/@giptb',       copyLabel: 'channel url' },
  { name: 'X / Twitter', handle: '@giptb_vt',     sub: 'Daily nonsense',                 tone: 'x',       followers: '11.6K', copy: '@giptb_vt',                        copyLabel: 'handle' },
  { name: 'Discord',     handle: 'goldkeepers',   sub: 'Member club',                    tone: 'discord', followers: '4,210', copy: 'https://discord.gg/goldkeepers',   copyLabel: 'invite link' },
  { name: 'TikTok',      handle: '@giptb',        sub: 'Clips & shorts',                 tone: 'tiktok',  followers: '88.4K', copy: '@giptb',                           copyLabel: 'handle' },
  { name: 'Bluesky',     handle: '@giptb.bsky',   sub: 'Slower posting',                 tone: 'bluesky', followers: '2,840', copy: '@giptb.bsky.social',               copyLabel: 'handle' },
];


function Copy({ value, label = 'copy', size = 'sm' }) {
  const [copied, setCopied] = useState(false);
  const timeoutRef = useRef();
  const handleClick = (e) => {
    e.preventDefault();
    e.stopPropagation();
    try {
      navigator.clipboard.writeText(value);
    } catch (err) {
      // fallback
      const ta = document.createElement('textarea');
      ta.value = value;
      document.body.appendChild(ta);
      ta.select();
      try {document.execCommand('copy');} catch (e2) {}
      document.body.removeChild(ta);
    }
    setCopied(true);
    clearTimeout(timeoutRef.current);
    timeoutRef.current = setTimeout(() => setCopied(false), 1800);
  };
  return (
    <button
      type="button"
      className={`copy-btn copy-btn-${size} ${copied ? 'is-copied' : ''}`}
      onClick={handleClick}
      aria-label={copied ? 'Copied' : `Copy ${label}`}
      title={copied ? 'Copied' : `Copy ${label}`}>
      
      <span className="copy-btn-icon" aria-hidden="true">
        {copied ?
        <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 8.5 L7 12 L13 4" /></svg> :

        <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="4.5" y="4.5" width="8" height="9" rx="1.2" /><path d="M3.5 11 L3.5 3 a1 1 0 0 1 1 -1 L11 2" /></svg>
        }
      </span>
      <span className="copy-btn-label">{copied ? 'copied' : 'copy'}</span>
    </button>);

}

function Socials() {
  const featured = SOCIALS.find(s => s.featured);
  const rest = SOCIALS.filter(s => !s.featured);
  return (
    <section id="socials" className="socials" data-screen-label="05 Socials">
      <SectionHeader index="05" eyebrow="Find me" title="Stay in the caravan" />
      <div className="socials-layout">
        {featured && (
          <div className={`social social-featured social-${featured.tone}`}>
            <a className="social-link" href={featured.copy} target="_blank" rel="noreferrer" aria-label={`Open ${featured.name}`}>
              <div className="social-featured-bg" aria-hidden="true">
                <WeaveMotif color="rgba(255,255,255,0.06)" />
              </div>
              <div className="social-featured-top">
                <div className="social-featured-glyph">
                  <SocialIcon name={featured.name} size={48} />
                </div>
                <div className="social-featured-pill mono">
                  <span className="live-dot" />
                  primary
                </div>
              </div>
              <div className="social-featured-body">
                <div className="social-name">{featured.name}</div>
                <div className="social-handle mono">{featured.handle}</div>
                <div className="social-sub">{featured.sub}</div>
              </div>
              <div className="social-featured-foot">
                <div className="social-stat">
                  <div className="social-stat-v">{featured.followers}</div>
                  <div className="social-stat-k mono">followers</div>
                </div>
                <span className="social-cta mono">open channel ↗</span>
              </div>
            </a>
            <div className="social-foot">
              <span className="social-copy-value mono" title={featured.copy}>{featured.copy}</span>
              <Copy value={featured.copy} label={featured.copyLabel} />
            </div>
          </div>
        )}

        <div className="socials-grid">
          {rest.map((s, i) => (
            <div key={i} className={`social social-${s.tone}`}>
              <a className="social-link" href={s.copy.startsWith('http') ? s.copy : '#'} target="_blank" rel="noreferrer" aria-label={`Open ${s.name}`}>
                <div className="social-head">
                  <span className="social-glyph"><SocialIcon name={s.name} size={22} /></span>
                  <span className="social-arrow">↗</span>
                </div>
                <div className="social-meta">
                  <span className="social-name">{s.name}</span>
                  <span className="social-handle mono">{s.handle}</span>
                </div>
                <div className="social-bottom">
                  <div className="social-stat">
                    <span className="social-stat-v">{s.followers}</span>
                    <span className="social-stat-k mono">fans</span>
                  </div>
                  <div className="social-sub">{s.sub}</div>
                </div>
              </a>
              <div className="social-foot">
                <span className="social-copy-value mono" title={s.copy}>{s.copy}</span>
                <Copy value={s.copy} label={s.copyLabel} />
              </div>
            </div>
          ))}
        </div>
      </div>
    </section>);

}

// ---------- CREDITS / FOOTER ----------
const CONTACTS = [
  { k: 'Business',    v: 'hello@giptb.live',                copyLabel: 'email',        kind: 'mail' },
  { k: 'Discord',     v: 'https://discord.gg/goldkeepers',  copyLabel: 'invite link',  kind: 'invite' },
  { k: 'Discord ID',  v: 'giptb',                            copyLabel: 'discord id',   kind: 'id' },
];

const CREDITS_ROLE = [
  { role: 'OC DESIGN',   name: '@LionalStudio & @InnaKerphc' },
  { role: 'LOGO',        name: '@Azaleafox' },
  { role: 'CARD DESIGN', name: '@InnaKerphc' },
  { role: 'SITE BUILD',  name: 'The Goldkeepers' },
];

function Credits() {
  return (
    <footer id="credits" className="credits" data-screen-label="06 Credits">
      <div className="credits-inner">

        <div className="credits-mast">
          <div className="credits-mast-l">
            <span className="mono dim">06 / END</span>
            <h2 className="credits-wordmark">
              <span>Gip</span><span className="credits-wordmark-accent">TB</span>
              <span className="credits-wordmark-gem"><DiamondGem size={28} color="#4d2bd4" glow="#62c7e8" /></span>
            </h2>
            <p className="credits-tagline">
              Built with gold thread, bad WiFi, and a really stubborn pet rock named <em>Pebs</em>.
            </p>
          </div>
          <div className="credits-mast-r">
            <img className="credits-mast-logo" src="assets/Logo_GipTB_color.png" alt="GipTB logo" loading="lazy" />
            <span className="credits-stamp mono">est.<br/>20<br/>24</span>
          </div>
        </div>

        <div className="credits-body">
          <section className="credits-col credits-col-contact">
            <div className="credits-col-head">
              <span className="credits-col-num mono">A</span>
              <span className="credits-col-lbl mono">Contact &amp; inquiries</span>
            </div>
            <ul className="credits-contact-list">
              {CONTACTS.map((c, i) => (
                <li key={i} className={`credits-contact-row credits-contact-${c.kind}`}>
                  <div className="credits-contact-glyph" aria-hidden="true">
                    {c.kind === 'mail' && (
                      <svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.6"><rect x="2.5" y="4.5" width="15" height="11" rx="1.5"/><path d="M3 6 L10 11 L17 6"/></svg>
                    )}
                    {c.kind === 'invite' && (
                      <svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"><path d="M3 10h14M11 4l6 6-6 6"/></svg>
                    )}
                    {c.kind === 'id' && (
                      <svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.6"><circle cx="10" cy="7.5" r="3"/><path d="M3.5 16.5 C4.5 13.5 7 12 10 12 s5.5 1.5 6.5 4.5"/></svg>
                    )}
                  </div>
                  <div className="credits-contact-stack">
                    <span className="mono credits-contact-k">{c.k}</span>
                    <span className="credits-contact-v" title={c.v}>{c.v}</span>
                  </div>
                  <Copy value={c.v} label={c.copyLabel} size="md" />
                </li>
              ))}
            </ul>
          </section>

          <section className="credits-col credits-col-roles">
            <div className="credits-col-head">
              <span className="credits-col-num mono">B</span>
              <span className="credits-col-lbl mono">Thanks to</span>
            </div>
            <ul className="credits-roles">
              {CREDITS_ROLE.map((r, i) => (
                <li key={i} className="credits-role">
                  <div className="credits-role-bar" aria-hidden="true"></div>
                  <div className="credits-role-text">
                    <span className="credits-role-k mono">{r.role}</span>
                    <span className="credits-role-v">{r.name}</span>
                  </div>
                </li>
              ))}
            </ul>
            <p className="credits-roles-note">
              Don't repost or trace the art. Clip and tag freely — that's the deal.
            </p>
          </section>
        </div>

        <div className="credits-strip" aria-hidden="true"></div>

        <div className="credits-base">
          <span className="mono">© MMXXVI · GipTB · ALL RIGHTS SUNG</span>
          <span className="mono">stay cursed · stay kind</span>
        </div>
      </div>
    </footer>);

}

// ---------- SHARED ----------
function SectionHeader({ index, eyebrow, title, trailing }) {
  return (
    <header className="section-header">
      <div className="section-header-l">
        <span className="section-index mono">{index}</span>
        <span className="section-eyebrow mono">{eyebrow}</span>
      </div>
      <h2 className="section-title" dangerouslySetInnerHTML={{ __html: title }} />
      {trailing && <div className="section-header-r">{trailing}</div>}
    </header>);

}

// ---------- HORIZONTAL SCROLLER ----------
const SECTIONS = [
  { id: 'top',      label: 'Hero' },
  { id: 'watch',    label: 'Live' },
  { id: 'rcard',    label: 'Card' },
  { id: 'about',    label: 'About' },
  { id: 'schedule', label: 'Schedule' },
  { id: 'vods',     label: 'VODs' },
  { id: 'emotes',   label: 'Emotes' },
  { id: 'socials',  label: 'Socials' },
  { id: 'credits',  label: 'Credits' },
];

function HScrollChrome({ scrollerRef }) {
  const [progress, setProgress] = useState(0);
  const [activeIdx, setActiveIdx] = useState(0);
  const [dragging, setDragging] = useState(false);
  const trackRef = useRef(null);

  useEffect(() => {
    const el = scrollerRef.current;
    if (!el) return;
    const onScroll = () => {
      const max = el.scrollWidth - el.clientWidth;
      const p = max > 0 ? el.scrollLeft / max : 0;
      setProgress(p);
      const idx = Math.round(el.scrollLeft / el.clientWidth);
      setActiveIdx(Math.min(SECTIONS.length - 1, Math.max(0, idx)));
    };
    onScroll();
    el.addEventListener('scroll', onScroll, { passive: true });
    return () => el.removeEventListener('scroll', onScroll);
  }, [scrollerRef]);

  const jumpTo = (idx) => {
    const el = scrollerRef.current;
    if (!el) return;
    el.scrollTo({ left: idx * el.clientWidth, behavior: 'smooth' });
  };

  // Drag scrub: pointerdown on track, then move, then up.
  useEffect(() => {
    const track = trackRef.current;
    const scroller = scrollerRef.current;
    if (!track || !scroller) return;

    let dragging = false;

    const apply = (clientX) => {
      const rect = track.getBoundingClientRect();
      const t = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
      const max = scroller.scrollWidth - scroller.clientWidth;
      scroller.scrollTo({ left: t * max, behavior: dragging ? 'auto' : 'smooth' });
    };

    const onPointerDown = (e) => {
      dragging = true;
      setDragging(true);
      track.setPointerCapture && track.setPointerCapture(e.pointerId);
      apply(e.clientX);
      e.preventDefault();
    };
    const onPointerMove = (e) => {
      if (!dragging) return;
      apply(e.clientX);
    };
    const onPointerUp = (e) => {
      if (!dragging) return;
      dragging = false;
      setDragging(false);
      // Snap to the nearest panel
      const idx = Math.round(scroller.scrollLeft / scroller.clientWidth);
      scroller.scrollTo({ left: idx * scroller.clientWidth, behavior: 'smooth' });
    };

    track.addEventListener('pointerdown', onPointerDown);
    window.addEventListener('pointermove', onPointerMove);
    window.addEventListener('pointerup', onPointerUp);
    window.addEventListener('pointercancel', onPointerUp);
    return () => {
      track.removeEventListener('pointerdown', onPointerDown);
      window.removeEventListener('pointermove', onPointerMove);
      window.removeEventListener('pointerup', onPointerUp);
      window.removeEventListener('pointercancel', onPointerUp);
    };
  }, [scrollerRef]);

  return (
    <div className="hchrome" aria-hidden="false">
      <div
        ref={trackRef}
        className={`hscrub ${dragging ? 'is-dragging' : ''}`}
        role="slider"
        aria-valuemin={0}
        aria-valuemax={SECTIONS.length - 1}
        aria-valuenow={activeIdx}
        aria-label="Scroll between sections"
        tabIndex={0}
        onKeyDown={(e) => {
          if (e.key === 'ArrowRight') { e.preventDefault(); jumpTo(activeIdx + 1); }
          else if (e.key === 'ArrowLeft') { e.preventDefault(); jumpTo(activeIdx - 1); }
        }}
      >
        <div className="hscrub-track">
          <div className="hscrub-fill" style={{ transform: `scaleX(${progress})` }} />
          <div className="hscrub-ticks">
            {SECTIONS.map((_, i) => (
              <span
                key={i}
                className={`hscrub-tick ${i === activeIdx ? 'is-active' : ''}`}
                style={{ left: `${(i / (SECTIONS.length - 1)) * 100}%` }}
              />
            ))}
          </div>
          <div
            className="hscrub-thumb"
            style={{ left: `${progress * 100}%` }}
            aria-hidden="true"
          >
            <DiamondGem size={28} color="#4d2bd4" glow="#62c7e8" />
            <div className="hscrub-thumb-pulse" />
          </div>
        </div>
        <div className="hscrub-readout mono">
          <span className="hscrub-readout-num">{String(activeIdx + 1).padStart(2, '0')}</span>
          <span className="hscrub-readout-sep">/</span>
          <span className="hscrub-readout-tot">{String(SECTIONS.length).padStart(2, '0')}</span>
          <span className="hscrub-readout-lbl">{SECTIONS[activeIdx]?.label}</span>
          <span className="hscrub-readout-hint">drag to scrub</span>
        </div>
      </div>
      <div className="hchrome-dots">
        {SECTIONS.map((s, i) => (
          <button
            key={s.id}
            type="button"
            className={`hchrome-dot ${i === activeIdx ? 'is-active' : ''}`}
            onClick={() => jumpTo(i)}
            aria-label={`Go to ${s.label}`}
          >
            <span className="hchrome-dot-num mono">{String(i + 1).padStart(2, '0')}</span>
            <span className="hchrome-dot-lbl">{s.label}</span>
          </button>
        ))}
      </div>
    </div>
  );
}

function useHorizontalScroll(scrollerRef, enabled) {
  useEffect(() => {
    const el = scrollerRef.current;
    if (!el || !enabled) return;

    // Momentum / velocity model — wheel adds to velocity, friction decays it.
    // Gives buttery free-scrolling with no jitter.
    let velocity = 0;
    let rafId = null;
    let lastTime = 0;

    const tick = (time) => {
      const dt = lastTime ? Math.min(2, (time - lastTime) / 16.67) : 1;
      lastTime = time;
      const max = Math.max(0, el.scrollWidth - el.clientWidth);

      // Apply velocity
      let next = el.scrollLeft + velocity * dt;
      // Soft edge clamping (extra friction at boundaries)
      if (next < 0) { next = 0; velocity = 0; }
      else if (next > max) { next = max; velocity = 0; }
      el.scrollLeft = next;

      // Decay
      velocity *= Math.pow(0.94, dt);

      if (Math.abs(velocity) > 0.05) {
        rafId = requestAnimationFrame(tick);
      } else {
        velocity = 0;
        rafId = null;
        lastTime = 0;
      }
    };
    const kick = () => {
      if (!rafId) {
        lastTime = 0;
        rafId = requestAnimationFrame(tick);
      }
    };

    const onWheel = (e) => {
      // Let internal vertical scrollers take over if they actually overflow
      let node = e.target;
      while (node && node !== el) {
        const cs = node instanceof Element ? getComputedStyle(node) : null;
        if (cs && /(auto|scroll)/.test(cs.overflowY) && node.scrollHeight > node.clientHeight) return;
        node = node.parentNode;
      }
      e.preventDefault();
      const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
      // Direction reversal: damp existing velocity so it doesn't fight new input
      if ((delta > 0 && velocity < 0) || (delta < 0 && velocity > 0)) {
        velocity *= 0.3;
      }
      // Each wheel tick contributes a velocity impulse — repeated scrolls compound
      velocity += delta * 0.22;
      // Cap velocity so a mousewheel spam doesn't fly across 5 panels
      const maxV = el.clientWidth * 0.11;
      if (velocity > maxV) velocity = maxV;
      if (velocity < -maxV) velocity = -maxV;
      kick();
    };

    const jumpTo = (left) => {
      velocity = 0;
      el.scrollTo({ left, behavior: 'smooth' });
    };

    // Anchor links → jump to that panel
    const onAnchor = (e) => {
      const a = e.target.closest && e.target.closest('a[href^="#"]');
      if (!a) return;
      const id = a.getAttribute('href').slice(1);
      if (!id) return;
      const targetEl = document.getElementById(id);
      if (!targetEl) return;
      const panel = targetEl.closest('.h-panel');
      if (!panel) return;
      e.preventDefault();
      const idx = Array.prototype.indexOf.call(el.children, panel);
      if (idx >= 0) jumpTo(idx * el.clientWidth);
    };

    // Keyboard
    const onKey = (e) => {
      if (e.target.matches && e.target.matches('input, textarea, [contenteditable]')) return;
      const w = el.clientWidth;
      const curIdx = Math.round(el.scrollLeft / w);
      if (e.key === 'ArrowRight' || e.key === 'PageDown') { e.preventDefault(); jumpTo((curIdx + 1) * w); }
      else if (e.key === 'ArrowLeft' || e.key === 'PageUp') { e.preventDefault(); jumpTo((curIdx - 1) * w); }
      else if (e.key === 'Home') { e.preventDefault(); jumpTo(0); }
      else if (e.key === 'End') { e.preventDefault(); jumpTo((el.children.length - 1) * w); }
    };

    el.addEventListener('wheel', onWheel, { passive: false });
    document.addEventListener('click', onAnchor, true);
    window.addEventListener('keydown', onKey);
    return () => {
      if (rafId) cancelAnimationFrame(rafId);
      el.removeEventListener('wheel', onWheel);
      document.removeEventListener('click', onAnchor, true);
      window.removeEventListener('keydown', onKey);
    };
  }, [scrollerRef, enabled]);
}

// ---------- APP ----------
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "#62c7e8",
  "theme": "night",
  "layout": "horizontal"
} /*EDITMODE-END*/;

// Small screens (phones, narrow tablets) and pure-touch devices get vertical layout
// regardless of the saved tweak — horizontal scroll-snap panels don't survive on a
// 5" screen and feel wrong for thumbs. The Tweaks panel toggle is still honored on
// desktop / wide tablets.
function useResponsiveLayout(rawLayout) {
  const compute = () => {
    if (typeof window === 'undefined') return rawLayout;
    const narrow = window.matchMedia('(max-width: 900px)').matches;
    const coarse = window.matchMedia('(hover: none) and (pointer: coarse)').matches;
    if (narrow || coarse) return 'vertical';
    return rawLayout;
  };
  const [layout, setLayout] = useState(compute);
  useEffect(() => {
    const mqN = window.matchMedia('(max-width: 900px)');
    const mqC = window.matchMedia('(hover: none) and (pointer: coarse)');
    const update = () => setLayout(compute());
    update();
    mqN.addEventListener('change', update);
    mqC.addEventListener('change', update);
    return () => {
      mqN.removeEventListener('change', update);
      mqC.removeEventListener('change', update);
    };
  }, [rawLayout]);
  return layout;
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const scrollerRef = useRef(null);
  const effectiveLayout = useResponsiveLayout(t.layout);

  useEffect(() => {
    document.documentElement.style.setProperty('--accent', t.accent);
    document.documentElement.dataset.theme = t.theme;
    document.documentElement.dataset.layout = effectiveLayout;
  }, [t.accent, t.theme, effectiveLayout]);

  const horizontal = effectiveLayout === 'horizontal';
  useHorizontalScroll(scrollerRef, horizontal);

  const sections = (
    <>
      <div className="h-panel h-panel-hero" id="top"><Hero accent={t.accent} /></div>
      <div className="h-panel" id="watch-panel"><Live channel="giptb" /></div>
      <div className="h-panel" id="rcard"><RotatingCard /></div>
      <div className="h-panel" id="about-panel"><About /></div>
      <div className="h-panel" id="schedule-panel"><Schedule accent={t.accent} /></div>
      <div className="h-panel" id="vods-panel"><VODs /></div>
      <div className="h-panel" id="emotes-panel"><Emotes /></div>
      <div className="h-panel" id="socials-panel"><Socials /></div>
      <div className="h-panel h-panel-credits" id="credits-panel"><Credits /></div>
    </>
  );

  return (
    <>
      <TopNav accent={t.accent} />
      <div ref={scrollerRef} className={`page ${horizontal ? 'page-h' : 'page-v'}`}>
        {sections}
      </div>
      {horizontal && <HScrollChrome scrollerRef={scrollerRef} />}

      <TweaksPanel title="Tweaks">
        <TweakSection title="Layout">
          <TweakRadio
            label="Direction"
            value={t.layout}
            onChange={(v) => setTweak('layout', v)}
            options={[{ value: 'horizontal', label: '← →' }, { value: 'vertical', label: '↑ ↓' }]} />
        </TweakSection>
        <TweakSection title="Accent">
          <TweakColor
            label="Highlight"
            value={t.accent}
            onChange={(v) => setTweak('accent', v)}
            options={['#62c7e8', '#f5d63a', '#d4a629', '#a886ff', '#ff7a59']} />
        </TweakSection>
        <TweakSection title="Theme">
          <TweakRadio
            label="Background"
            value={t.theme}
            onChange={(v) => setTweak('theme', v)}
            options={[{ value: 'night', label: 'Night' }, { value: 'dusk', label: 'Dusk' }, { value: 'paper', label: 'Paper' }]} />
        </TweakSection>
      </TweaksPanel>
    </>);
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);