// mythology.jsx — gallery grid + song detail drill-in
// Each song = a myth. Reads from `diaval_myths_public` view (lyric/body masked
// for non-published rows; presave-only myths are filtered out of the gallery
// anyway since the gallery is the curated catalog).
// Pre-release helpers (isPrerelease, useCountdown) live in shared.jsx.

// Auto-fitting title — starts at the requested font size, then measures the
// rendered element vs its parent container and shrinks (in 2px steps) until
// it fits or hits the floor. ONLY shrinks single-word titles (e.g. "Prometheus")
// that can't wrap. Multi-word titles are returned at maxSize and allowed to wrap
// naturally — shrinking them just makes them smaller for no reason.
function AutoFitTitle({ text, maxSize, minSize, style }) {
  const isSingleWord = !/\s/.test(String(text || ""));
  const ref = React.useRef(null);
  const [size, setSize] = React.useState(maxSize);
  React.useLayoutEffect(() => {
    setSize(maxSize); // reset to the max for new text/viewport
  }, [text, maxSize, minSize]);
  React.useLayoutEffect(() => {
    if (!isSingleWord) return; // multi-word: let it wrap, never shrink
    const el = ref.current;
    if (!el || !el.parentElement) return;
    const parent = el.parentElement;
    let s = size;
    el.style.fontSize = s + "px";
    let guard = 60;
    while (el.scrollWidth > parent.clientWidth - 4 && s > minSize && guard-- > 0) {
      s -= 2;
      el.style.fontSize = s + "px";
    }
    if (s !== size) setSize(s);
  }, [size, text, minSize, isSingleWord]);
  return (
    <div ref={ref} style={{ ...style, fontSize: isSingleWord ? size : maxSize }}>
      {text}
    </div>
  );
}

// VideoLightbox — fullscreen modal that opens when the user taps a video slide
// in the mythology detail carousel. Renders a 16:9 player centered on a dark
// backdrop. Supports close via X button, clicking the backdrop, or pressing Esc.
// The iframe/video only mounts when `url` is set, so videos don't autoload in
// the background until the user actually wants to watch.
function VideoLightbox({ url, onClose, S }) {
  React.useEffect(() => {
    if (!url) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    // Lock body scroll while open so the page beneath doesn't move.
    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", onKey);
      document.body.style.overflow = prevOverflow;
    };
  }, [url, onClose]);

  if (!url) return null;
  const embed = window.diavalVideoEmbedFor ? window.diavalVideoEmbedFor(url) : null;

  return (
    <div
      onClick={onClose}
      role="dialog"
      aria-modal="true"
      style={{
        position: "fixed", inset: 0, zIndex: 100,
        background: "rgba(0,0,0,0.94)",
        display: "flex", alignItems: "center", justifyContent: "center",
        padding: "5vh 4vw",
      }}>
      <button
        onClick={(e) => { e.stopPropagation(); onClose(); }}
        aria-label="Close video"
        style={{
          position: "fixed", top: 16, right: 16, zIndex: 101,
          width: 44, height: 44, padding: 0,
          background: "rgba(0,0,0,0.6)", color: S.bone,
          border: `1px solid rgba(255,255,255,0.25)`, borderRadius: "50%",
          fontSize: 22, fontFamily: S.meta, lineHeight: 1, cursor: "pointer",
          display: "flex", alignItems: "center", justifyContent: "center",
        }}>×</button>
      <div
        onClick={(e) => e.stopPropagation()}
        style={{
          position: "relative", width: "100%", maxWidth: 1280,
          aspectRatio: "16 / 9", background: "#000",
          border: `1px solid rgba(255,255,255,0.1)`,
        }}>
        {embed?.kind === "iframe" && (
          <iframe
            src={`${embed.src}${embed.src.includes("?") ? "&" : "?"}autoplay=1`}
            title="video"
            allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
            allowFullScreen
            style={{ position: "absolute", inset: 0, width: "100%", height: "100%", border: "none" }}
          />
        )}
        {embed?.kind === "video" && (
          <video src={embed.src} controls autoPlay preload="metadata" controlsList="nodownload" playsInline
            style={{ position: "absolute", inset: 0, width: "100%", height: "100%", background: "#000" }} />
        )}
        {embed?.kind === "link" && (
          <a href={embed.src} target="_blank" rel="noreferrer" style={{
            position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center",
            color: S.bone, textDecoration: "none", fontFamily: S.meta, fontSize: 16, letterSpacing: "0.3em",
          }}>OPEN VIDEO ↗</a>
        )}
      </div>
    </div>
  );
}

// Carousel arrow button style — semi-transparent disc, vertically centered on
// the cover area. Higher z-index than the title overlay so users can hit it
// even when text crowds the same vertical band.
function carouselArrowStyle(S, side, isMobile) {
  return {
    position: "absolute",
    [side]: isMobile ? 8 : 14,
    top: "50%", transform: "translateY(-50%)",
    width: isMobile ? 36 : 44, height: isMobile ? 36 : 44,
    borderRadius: "50%",
    background: "rgba(0,0,0,0.55)", color: S.bone,
    border: `1px solid rgba(255,255,255,0.2)`,
    fontSize: isMobile ? 22 : 28, fontFamily: S.meta, lineHeight: 1,
    cursor: "pointer", padding: 0, zIndex: 6,
    display: "flex", alignItems: "center", justifyContent: "center",
    backdropFilter: "blur(4px)", WebkitBackdropFilter: "blur(4px)",
  };
}

// Collapsible: clamps content to a max height (~3 lines by default). When the
// content overflows:
//   - When collapsed, the entire truncated body is clickable → expands.
//     (so users can tap the faded preview itself, not just hunt for the header)
//   - When expanded, body click does nothing (so text is freely selectable);
//     the header remains the click target to recollapse.
// `prefix` renders between the header and the collapsible body (used for the
// VoicePlayer which is always visible above the myth body).
function Collapsible({ header, prefix, children, S, maxHeight = 84 }) {
  const [expanded, setExpanded] = React.useState(false);
  const [overflows, setOverflows] = React.useState(false);
  const innerRef = React.useRef(null);

  React.useLayoutEffect(() => {
    const el = innerRef.current;
    if (!el) return;
    setOverflows(el.scrollHeight > maxHeight + 4);
  }, [children, maxHeight]);

  const clickable = overflows;
  const onToggle = () => { if (clickable) setExpanded(e => !e); };
  const onBodyClick = () => { if (clickable && !expanded) setExpanded(true); };

  return (
    <div>
      {header && (
        <div
          onClick={onToggle}
          role={clickable ? "button" : undefined}
          aria-expanded={clickable ? expanded : undefined}
          style={{
            fontSize: 12, color: S.bloodHot, letterSpacing: "0.3em", marginBottom: 14,
            cursor: clickable ? "pointer" : "default",
            display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12,
            userSelect: "none",
          }}>
          <span>{header}</span>
          {clickable && (
            <span style={{
              fontSize: 11, color: S.ashDim, fontFamily: S.meta, letterSpacing: "0.2em",
              display: "inline-flex", alignItems: "center", gap: 6,
            }}>
              {expanded ? "COLLAPSE" : "EXPAND"}
              <span style={{
                display: "inline-block",
                transition: "transform .2s",
                transform: expanded ? "rotate(180deg)" : "rotate(0deg)",
              }}>▾</span>
            </span>
          )}
        </div>
      )}
      {prefix}
      <div
        onClick={onBodyClick}
        style={{
          position: "relative",
          maxHeight: expanded || !overflows ? "none" : maxHeight,
          overflow: "hidden",
          transition: "max-height .25s ease",
          cursor: clickable && !expanded ? "pointer" : "default",
        }}>
        <div ref={innerRef}>{children}</div>
        {!expanded && overflows && (
          <div aria-hidden="true" style={{
            position: "absolute", left: 0, right: 0, bottom: 0, height: 36,
            background: "linear-gradient(180deg, rgba(2,3,10,0) 0%, rgba(2,3,10,0.92) 100%)",
            pointerEvents: "none",
          }} />
        )}
      </div>
    </div>
  );
}

function MythologyPage({ tab, onTabChange, initialOpen, onOpenChange }) {
  const S = window.shellStyles;
  const [open, setOpenInternal] = React.useState(initialOpen || null); // myth id
  // Read cached gallery for synchronous initial render — tab switches no longer
  // flash through a loading state on revisit.
  const cachedMyths = window.diavalCacheGet ? window.diavalCacheGet("mythology-gallery") : null;
  const [myths, setMyths] = React.useState(cachedMyths || []);
  const [loading, setLoading] = React.useState(!cachedMyths);
  const [loadError, setLoadError] = React.useState(null);

  // Wrapper around setOpen that ALSO notifies the parent so it can update the URL.
  // Path becomes /mythology/<slug> when open, /mythology when closed — shareable
  // press URLs that the Cloudflare Worker pre-renders with myth-specific meta.
  const setOpen = React.useCallback((slug) => {
    setOpenInternal(slug);
    if (onOpenChange) onOpenChange(slug || null);
  }, [onOpenChange]);

  // Sync internal state when the parent's URL-driven prop changes (popstate /
  // direct path load like /mythology/<slug>). Uses null-safe equality so the
  // notify path doesn't ping-pong with the popstate path.
  React.useEffect(() => {
    if ((initialOpen || null) !== (open || null)) setOpenInternal(initialOpen || null);
  }, [initialOpen]);

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const fetcher = async () => {
          const { data, error } = await window.diavalSupabase
            .from("diaval_myths_public")
            .select("id, title, file, myth, duration, color, year, bpm, blurb, body, lyric, release_date, hyperfollow_url, cover_url, spotify_preview_url, spotify_track_id, spotify_track_url, spotify_album_art_url, voice_memo_url, voice_memo_generated_at, like_count, voice_play_count")
            .eq("is_published", true)
            .order("release_date", { ascending: false, nullsLast: true })
            .order("position", { ascending: true });
          if (error) throw error;
          return data ?? [];
        };
        const data = window.diavalCachedFetch
          ? await window.diavalCachedFetch("mythology-gallery", fetcher)
          : await fetcher();
        if (!cancelled) { setMyths(data); setLoading(false); }
      } catch (e) {
        if (!cancelled) { setLoadError(e.message); setLoading(false); }
      }
    })();
    return () => { cancelled = true; };
  }, []);

  const myth = open ? myths.find((m) => m.id === open) : null;
  const broadcastLabel = `MYTHOLOGY · ${String(myths.length).padStart(3, "0")} FILES`;

  return (
    <Shell tab={tab} onTabChange={onTabChange} page="003" broadcast={broadcastLabel}>
      {loading ? (
        <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 13, color: S.ashDim, letterSpacing: "0.3em" }}>
          ⸺ TUNING SIGNAL ⸺
        </div>
      ) : loadError ? (
        <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", gap: 12, padding: 40 }}>
          <div style={{ fontSize: 13, color: S.bloodHot, letterSpacing: "0.3em" }}>◢ MYTHOLOGY FEED CORRUPTED</div>
          <div style={{ fontSize: 13, color: S.ash, fontStyle: "italic", maxWidth: 600, textAlign: "center" }}>{loadError}</div>
        </div>
      ) : !open ? (
        <MythologyGrid myths={myths} onOpen={setOpen} S={S} />
      ) : myth ? (
        <MythDetail myth={myth} myths={myths} S={S} onBack={() => setOpen(null)} onOpen={setOpen} />
      ) : (
        <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 13, color: S.ashDim, letterSpacing: "0.3em" }}>
          ⸺ FILE NOT FOUND ⸺
        </div>
      )}
    </Shell>
  );
}

function MythologyGrid({ myths, onOpen, S }) {
  const { isMobile } = (window.useViewport ? window.useViewport() : { isMobile: false });
  const [search, setSearch] = React.useState("");
  const [yearFilter, setYearFilter] = React.useState(null); // null = ALL

  // Year options derived from data, newest first. release_date wins, year falls back.
  const yearOf = (m) => (m.release_date ? String(m.release_date).slice(0, 4) : (m.year ? String(m.year) : null));
  const availableYears = React.useMemo(() => {
    const set = new Set();
    for (const m of myths) { const y = yearOf(m); if (y) set.add(y); }
    return Array.from(set).sort((a, b) => b.localeCompare(a));
  }, [myths]);

  const q = search.trim().toLowerCase();
  const filteredMyths = myths.filter((m) => {
    if (yearFilter && yearOf(m) !== yearFilter) return false;
    if (q) {
      const hay = `${m.title || ""} ${m.myth || ""} ${m.blurb || ""}`.toLowerCase();
      if (!hay.includes(q)) return false;
    }
    return true;
  });

  const totalAll = String(myths.length).padStart(3, "0");
  const totalShown = String(filteredMyths.length).padStart(3, "0");
  const filtersActive = !!q || !!yearFilter;

  const pillStyle = (active) => ({
    padding: "6px 12px", fontFamily: S.meta, fontSize: 11, letterSpacing: "0.25em",
    background: active ? S.bloodHot : "transparent",
    color: active ? S.bone : S.ash,
    border: `1px solid ${active ? S.bloodHot : S.concreteHi}`,
    cursor: "pointer",
  });

  return (
    <div style={{
      position: isMobile ? "relative" : "absolute",
      inset: isMobile ? "auto" : 0,
      overflow: isMobile ? "visible" : "auto",
      padding: isMobile ? "20px 16px 24px" : "32px 56px 24px",
    }}>
      {/* header strip */}
      <div style={{ display: "flex", flexDirection: isMobile ? "column" : "row", justifyContent: "space-between", alignItems: isMobile ? "flex-start" : "flex-end", marginBottom: isMobile ? 18 : 24, gap: isMobile ? 12 : 32 }}>
        <div>
          <div style={{ fontSize: 12, color: S.bloodHot, letterSpacing: "0.3em", marginBottom: 8 }}>
            // III. MYTHOLOGY / GALLERY
          </div>
          <h2 style={{
            margin: 0, fontFamily: S.display, fontSize: isMobile ? 28 : 64, lineHeight: 0.92,
            letterSpacing: "-0.02em", color: S.bone,
          }}>
            EVERY SONG <span style={{ color: S.bloodHot }}>IS A MYTH</span>
          </h2>
        </div>
        <div style={{ fontSize: 12, color: S.ash, letterSpacing: "0.3em", textAlign: isMobile ? "left" : "right" }}>
          INDEX · {filtersActive ? totalShown : totalAll} / {totalAll}<br/>
          <span style={{ color: S.bloodHot }}>SORT · LATEST FIRST</span>
        </div>
      </div>

      {/* filter bar — search + year pills */}
      <div style={{
        display: "flex", flexDirection: isMobile ? "column" : "row", alignItems: isMobile ? "stretch" : "center",
        gap: isMobile ? 10 : 14, marginBottom: isMobile ? 18 : 22,
      }}>
        <input
          value={search} onChange={(e) => setSearch(e.target.value)}
          placeholder="search myths..."
          style={{
            flex: isMobile ? "0 0 auto" : "1 1 auto", maxWidth: isMobile ? "100%" : 320,
            background: S.concrete, border: `1px solid ${S.concreteHi}`, color: S.bone,
            padding: "10px 14px", fontFamily: S.meta, fontSize: 13, letterSpacing: "0.04em",
          }}
        />
        {availableYears.length > 1 && (
          <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
            <button onClick={() => setYearFilter(null)} style={pillStyle(yearFilter === null)}>ALL</button>
            {availableYears.map(y => (
              <button key={y} onClick={() => setYearFilter(y)} style={pillStyle(yearFilter === y)}>{y}</button>
            ))}
          </div>
        )}
        {filtersActive && (
          <button onClick={() => { setSearch(""); setYearFilter(null); }} style={{
            padding: "6px 10px", fontFamily: S.meta, fontSize: 10, letterSpacing: "0.25em",
            background: "transparent", border: `1px solid ${S.bloodHot}`, color: S.bloodHot, cursor: "pointer",
          }}>CLEAR ×</button>
        )}
      </div>

      {/* grid — 4 cols desktop, 2 cols mobile */}
      {filteredMyths.length === 0 ? (
        <div style={{ padding: "60px 0", textAlign: "center", fontSize: 13, color: S.ashDim, letterSpacing: "0.3em" }}>
          ⸺ NO MATCHES — TRY A WIDER FILTER ⸺
        </div>
      ) : (
        <div style={{
          display: "grid",
          gridTemplateColumns: isMobile ? "repeat(2, 1fr)" : "repeat(4, 1fr)",
          gap: isMobile ? 10 : 14,
        }}>
          {filteredMyths.map((m) => (
            <MythCard key={m.id} myth={m} onClick={() => onOpen(m.id)} S={S} />
          ))}
        </div>
      )}
    </div>
  );
}

function MythCard({ myth, onClick, S }) {
  const [hover, setHover] = React.useState(false);
  const { isMobile } = (window.useViewport ? window.useViewport() : { isMobile: false });
  const coverSrc = myth.cover_url || myth.spotify_album_art_url || null;
  return (
    <div
      onClick={onClick}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      style={{
        position: "relative", background: S.raven, border: `1px solid ${hover ? S.bloodHot : S.concreteHi}`,
        cursor: "pointer", overflow: "hidden", display: "flex", flexDirection: "column",
        transition: "border .15s, transform .15s", transform: hover ? "translateY(-2px)" : "none",
      }}>
      {/* artwork */}
      <div style={{
        position: "relative", aspectRatio: "1 / 1",
        background: `
          radial-gradient(ellipse at 30% 30%, ${myth.color}22, transparent 50%),
          radial-gradient(ellipse at 70% 80%, ${myth.color}33, transparent 60%),
          ${S.raven}
        `,
        borderBottom: `1px solid ${S.concreteHi}`, overflow: "hidden",
      }}>
        {/* Cover image (Spotify album art / uploaded file). Falls back to marble figure if absent. */}
        {coverSrc && (
          <>
            <img src={coverSrc} alt="" loading="lazy"
              style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
            {/* Dark scrim along the bottom so the title + eyebrow stay legible over bright covers */}
            <div style={{
              position: "absolute", inset: 0, pointerEvents: "none",
              background: "linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0) 35%, rgba(0,0,0,0.75) 100%)",
            }} />
          </>
        )}
        {!coverSrc && <MarbleVeins color={`${myth.color}40`} />}
        <div style={{
          position: "absolute", inset: 0,
          background: `repeating-linear-gradient(45deg, transparent 0 4px, rgba(255,255,255,0.018) 4px 5px)`,
          pointerEvents: "none",
        }} />
        <div style={{ position: "absolute", inset: 0, filter: "url(#ind-noise)", opacity: coverSrc ? 0.25 : 0.45, mixBlendMode: "overlay", pointerEvents: "none" }} />

        {/* file label top — white + text shadow so it reads against any cover */}
        <div style={{
          position: "absolute", top: 10, left: 12, right: 12,
          display: "flex", justifyContent: "space-between",
          fontSize: 12, color: S.bone, letterSpacing: "0.3em",
          textShadow: "0 1px 4px rgba(0,0,0,0.7)",
        }}>
          <span>FILE {myth.file}</span>
          <span>● {myth.duration}</span>
        </div>

        {/* pre-release ribbon — only when release_date is in the future */}
        {isPrerelease(myth.release_date) && (
          <div style={{
            position: "absolute", top: 36, left: 12,
            padding: "4px 8px", background: S.bloodHot, color: S.bone,
            fontSize: 10, letterSpacing: "0.3em", fontFamily: S.meta,
          }}>
            ◢ PRE-RELEASE · {myth.release_date}
          </div>
        )}

        {/* title — white + text shadow site-wide for legibility over any cover.
            Subtitle truncates at the first comma to keep "Paris, Prince of Troy" → "Paris" — long
            subtitles look cluttered next to a 12-char title in a small card. */}
        <div style={{ position: "absolute", left: 12, right: 12, bottom: 12 }}>
          <div style={{
            fontSize: 12, color: S.bone, letterSpacing: "0.3em", marginBottom: 4,
            textShadow: "0 1px 4px rgba(0,0,0,0.7)",
            whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
          }}>{myth.myth ? String(myth.myth).split(",")[0] : ""}</div>
          <AutoFitTitle
            text={myth.title}
            maxSize={isMobile ? 26 : 42}
            minSize={isMobile ? 26 : 42}
            style={{
              fontFamily: S.display, lineHeight: 0.92,
              letterSpacing: "-0.04em", color: S.bone, textTransform: "uppercase",
              textShadow: "0 1px 4px rgba(0,0,0,0.7)",
              wordBreak: "normal",
            }}
          />
        </div>

        {/* hover overlay */}
        {hover && (
          <div style={{
            position: "absolute", inset: 0,
            background: `linear-gradient(180deg, transparent 40%, rgba(196,30,30,0.15) 100%)`,
            pointerEvents: "none",
          }} />
        )}
        {hover && (
          <div style={{
            position: "absolute", top: 36, right: 12,
            width: 28, height: 28, background: S.bloodHot, color: S.bone,
            display: "flex", alignItems: "center", justifyContent: "center",
            fontSize: 13,
          }}>
            <Glyph name="arrow" size={14} color={S.bone} />
          </div>
        )}
      </div>

      {/* meta strip — BPM intentionally stacked (number on top, BPM label below)
          so it claims less horizontal room. Date downsized to fit cleanly. */}
      <div style={{ padding: "10px 12px", display: "flex", justifyContent: "space-between", alignItems: "center", color: S.ash, gap: 10 }}>
        <span style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", lineHeight: 1, fontFamily: S.cond, color: S.bone }}>
          <span style={{ fontSize: 13, letterSpacing: "0.04em" }}>{myth.bpm}</span>
          <span style={{ fontSize: 9, letterSpacing: "0.2em", color: S.ashDim, marginTop: 2 }}>BPM</span>
        </span>
        <Heart kind="myth" rowId={myth.id} initialCount={myth.like_count || 0} S={S} size={14} />
        <span style={{ fontSize: 10, letterSpacing: "0.18em", whiteSpace: "nowrap" }}>{myth.release_date || myth.year}</span>
      </div>
    </div>
  );
}

function MythDetail({ myth, myths, S, onBack, onOpen }) {
  const { isMobile } = (window.useViewport ? window.useViewport() : { isMobile: false });
  const [playing, setPlaying] = React.useState(false);
  const [progress, setProgress] = React.useState(0);
  const [streamLinks, setStreamLinks] = React.useState([]);
  const [tagGenres, setTagGenres] = React.useState([]); // [{id, name}]
  const [tagMoods, setTagMoods] = React.useState([]);   // [{id, name}]
  const [gallery, setGallery] = React.useState([]);     // [{kind:'image'|'video', url, caption, object_position}]
  const [slideIdx, setSlideIdx] = React.useState(0);
  const [lightboxVideoUrl, setLightboxVideoUrl] = React.useState(null);
  const touchStartX = React.useRef(null);

  // Fetch the per-myth gallery (extra images + video links). Cover is implicit
  // slide 0; gallery items follow in `position` order.
  React.useEffect(() => {
    let cancelled = false;
    setSlideIdx(0);
    setGallery([]);
    window.diavalSupabase
      .from("diaval_myth_gallery")
      .select("id, kind, url, caption, object_position, position")
      .eq("myth_id", myth.id)
      .eq("is_visible", true)
      .order("position", { ascending: true })
      .then(({ data }) => { if (!cancelled) setGallery(data || []); });
    return () => { cancelled = true; };
  }, [myth.id]);
  // Load this myth's genre + mood tags (max 3 of each)
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      const sb = window.diavalSupabase;
      const [{ data: g }, { data: m }] = await Promise.all([
        sb.from("diaval_myth_genres").select("position, diaval_genres(id, name, is_visible)").eq("myth_id", myth.id).order("position"),
        sb.from("diaval_myth_moods").select("position, diaval_moods(id, name, is_visible)").eq("myth_id", myth.id).order("position"),
      ]);
      if (cancelled) return;
      setTagGenres((g || []).map(r => r.diaval_genres).filter(x => x && x.is_visible));
      setTagMoods((m || []).map(r => r.diaval_moods).filter(x => x && x.is_visible));
    })();
    return () => { cancelled = true; };
  }, [myth.id]);
  // Pre-release: re-renders every minute via the countdown hook so D:H:M stays current
  const prerelease = isPrerelease(myth.release_date);
  const countdown = useCountdown(myth.release_date);
  const audioRef = React.useRef(null);
  const hasPreview = !!myth.spotify_preview_url;
  // Spotify previews are always 30 seconds.
  const PREVIEW_LEN = 30;

  // Reset + refetch when myth changes
  React.useEffect(() => {
    setPlaying(false);
    setProgress(0);
    setStreamLinks([]);
    let cancelled = false;
    window.diavalSupabase
      .from("diaval_myth_links")
      .select("platform, label, url, position")
      .eq("myth_id", myth.id)
      .order("position", { ascending: true })
      .then(({ data }) => { if (!cancelled && data) setStreamLinks(data); });
    return () => { cancelled = true; };
  }, [myth.id]);

  // Real audio: drive `progress` from the audio element's timeupdate.
  React.useEffect(() => {
    if (!hasPreview) return;
    const a = audioRef.current;
    if (!a) return;
    if (playing) a.play().catch(() => setPlaying(false));
    else a.pause();
  }, [playing, hasPreview]);

  // Fallback simulated progress when no preview URL exists.
  React.useEffect(() => {
    if (hasPreview) return;
    if (!playing) return;
    const t = setInterval(() => setProgress((p) => (p >= 1 ? 0 : p + 0.003)), 100);
    return () => clearInterval(t);
  }, [playing, hasPreview]);

  const idx = myths.findIndex((m) => m.id === myth.id);
  const prev = myths[(idx - 1 + myths.length) % myths.length];
  const next = myths[(idx + 1) % myths.length];
  const fullDurSec = parseInt(myth.duration.split(":")[0]) * 60 + parseInt(myth.duration.split(":")[1]);
  // Display duration: 30s preview if available, otherwise the full track length.
  const displayDur = hasPreview ? PREVIEW_LEN : fullDurSec;

  return (
    <div style={{
      position: isMobile ? "relative" : "absolute",
      inset: isMobile ? "auto" : 0,
      display: isMobile ? "block" : "grid",
      gridTemplateColumns: isMobile ? undefined : "560px 1fr",
      gap: 0,
      overflow: isMobile ? "visible" : "hidden",
    }}>
      <VideoLightbox url={lightboxVideoUrl} onClose={() => setLightboxVideoUrl(null)} S={S} />
      {/* LEFT — full bleed art */}
      <div style={{
        position: "relative", overflow: "hidden",
        height: isMobile ? 360 : "auto",
        background: `
          radial-gradient(ellipse at 30% 30%, ${myth.color}33, transparent 55%),
          radial-gradient(ellipse at 70% 80%, ${myth.color}55, transparent 60%),
          ${S.raven}
        `,
        borderRight: isMobile ? "none" : `1px solid ${S.concreteHi}`,
        borderBottom: isMobile ? `1px solid ${S.concreteHi}` : "none",
      }}>
        {/* Carousel: slide 0 is the cover (or marble fallback), slides 1+ are
            gallery items (image or video). Manual nav only — < / > arrows when
            more than 1 slide, plus swipe on mobile. */}
        {(() => {
          const coverSrc = myth.cover_url || myth.spotify_album_art_url || null;
          const slides = [
            ...(coverSrc ? [{ kind: "image", url: coverSrc, object_position: "50% 50%" }] : [{ kind: "fallback" }]),
            ...gallery.map(g => ({ kind: g.kind, url: g.url, object_position: g.object_position || "50% 50%", caption: g.caption })),
          ];
          const total = slides.length;
          const idxClamped = ((slideIdx % total) + total) % total;
          const slide = slides[idxClamped];
          const showArrows = total > 1;
          const goPrev = (e) => { if (e) e.stopPropagation(); setSlideIdx(i => (i - 1 + total) % total); };
          const goNext = (e) => { if (e) e.stopPropagation(); setSlideIdx(i => (i + 1) % total); };
          const onTouchStart = (e) => { touchStartX.current = e.touches?.[0]?.clientX ?? null; };
          const onTouchEnd = (e) => {
            if (touchStartX.current == null) return;
            const dx = (e.changedTouches?.[0]?.clientX ?? 0) - touchStartX.current;
            if (Math.abs(dx) > 40) { dx > 0 ? goPrev() : goNext(); }
            touchStartX.current = null;
          };
          const renderSlide = () => {
            if (slide.kind === "fallback") {
              return (
                <>
                  <MarbleVeins color={`${myth.color}55`} />
                  <div style={{ position: "absolute", inset: 0,
                    background: `repeating-linear-gradient(45deg, transparent 0 6px, rgba(255,255,255,0.02) 6px 7px)` }} />
                  <div style={{
                    position: "absolute", left: "18%", top: "10%", width: "64%", height: "78%",
                    background: `linear-gradient(170deg, rgba(232,228,220,0.45) 0%, rgba(180,170,156,0.25) 45%, rgba(40,30,28,0.15) 85%, transparent 100%)`,
                    clipPath: "polygon(40% 0%, 60% 0%, 70% 10%, 72% 28%, 65% 42%, 78% 58%, 88% 100%, 12% 100%, 22% 58%, 35% 42%, 28% 28%, 30% 10%)",
                    filter: "blur(1.2px)",
                  }} />
                </>
              );
            }
            if (slide.kind === "image") {
              return (
                <img src={slide.url} alt="" loading={idxClamped === 0 ? "eager" : "lazy"}
                  fetchpriority={idxClamped === 0 ? "high" : "auto"}
                  style={{ position: "absolute", inset: 0, width: "100%", height: "100%",
                           objectFit: "cover", objectPosition: slide.object_position || "50% 50%", display: "block" }} />
              );
            }
            if (slide.kind === "video") {
              // Video slides render as a tap-to-play placeholder rather than
              // an inline player — the title text + bottom player overlay would
              // otherwise cover the video. Tapping opens VideoLightbox where
              // the user can watch unobstructed.
              const openLightbox = (ev) => { ev.stopPropagation(); setLightboxVideoUrl(slide.url); };
              return (
                <button
                  onClick={openLightbox}
                  aria-label={slide.caption ? `Play video: ${slide.caption}` : "Play video"}
                  style={{
                    position: "absolute", inset: 0, padding: 0, border: "none",
                    background: "linear-gradient(180deg, #1a1414 0%, #050505 100%)",
                    color: S.bone, cursor: "pointer",
                    display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", gap: 14,
                  }}>
                  <span style={{
                    width: 84, height: 84, borderRadius: "50%",
                    background: "rgba(0,0,0,0.55)", border: `1px solid rgba(255,255,255,0.4)`,
                    display: "flex", alignItems: "center", justifyContent: "center",
                    boxShadow: "0 4px 24px rgba(0,0,0,0.55)",
                  }}>
                    <svg width="30" height="30" viewBox="0 0 24 24" fill="currentColor"><path d="M7 4 L20 12 L7 20 Z" /></svg>
                  </span>
                  <span style={{ fontFamily: S.meta, fontSize: 11, letterSpacing: "0.3em", color: "rgba(255,255,255,0.7)" }}>
                    {slide.caption ? slide.caption.toUpperCase() : "TAP TO WATCH"}
                  </span>
                </button>
              );
            }
            return null;
          };
          const isVideoSlide = slide.kind === "video";

          return (
            <div onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}
              style={{ position: "absolute", inset: 0 }}>
              {renderSlide()}
              {/* Bottom-up dark gradient — only on image slides; videos get a tiny
                  bottom darken so the player area stays readable but the video stays clear. */}
              <div style={{
                position: "absolute", inset: 0, pointerEvents: "none",
                background: isVideoSlide
                  ? "linear-gradient(180deg, transparent 0%, transparent 60%, rgba(0,0,0,0.65) 100%)"
                  : "linear-gradient(180deg, rgba(0,0,0,0.25) 0%, rgba(0,0,0,0.05) 30%, rgba(0,0,0,0.85) 100%)",
              }} />
              {showArrows && (
                <>
                  <button onClick={goPrev} aria-label="Previous slide" style={carouselArrowStyle(S, "left", isMobile)}>‹</button>
                  <button onClick={goNext} aria-label="Next slide" style={carouselArrowStyle(S, "right", isMobile)}>›</button>
                  {/* dot indicator — top center, small, only visible on multi-slide */}
                  <div style={{
                    position: "absolute", top: isMobile ? 44 : 56, left: "50%", transform: "translateX(-50%)",
                    display: "flex", gap: 6, pointerEvents: "none", zIndex: 5,
                  }}>
                    {slides.map((_, i) => (
                      <span key={i} style={{
                        width: 6, height: 6, borderRadius: "50%",
                        background: i === idxClamped ? S.bone : "rgba(255,255,255,0.35)",
                      }} />
                    ))}
                  </div>
                </>
              )}
            </div>
          );
        })()}
        <div style={{ position: "absolute", inset: 0, filter: "url(#ind-noise)", opacity: (myth.cover_url || myth.spotify_album_art_url) ? 0.25 : 0.5, mixBlendMode: "overlay", pointerEvents: "none" }} />

        {/* corners — white + text shadow so they read against any cover image.
            Mobile: tighter letter-spacing + smaller font so FILE/date and heart+subtitle
            don't collide on narrow viewports. */}
        <div style={{
          position: "absolute", top: isMobile ? 14 : 20, left: isMobile ? 14 : 20,
          fontSize: isMobile ? 10 : 12, color: S.bone,
          letterSpacing: isMobile ? "0.16em" : "0.3em",
          textShadow: "0 1px 4px rgba(0,0,0,0.7)",
          whiteSpace: "nowrap",
        }}>
          ◢ FILE {myth.file} · {myth.release_date || myth.year}
        </div>
        <div style={{
          position: "absolute", top: isMobile ? 14 : 20, right: isMobile ? 14 : 20,
          fontSize: isMobile ? 10 : 12, color: S.bone,
          letterSpacing: isMobile ? "0.16em" : "0.3em",
          textShadow: "0 1px 4px rgba(0,0,0,0.7)",
          display: "flex", alignItems: "center", gap: isMobile ? 8 : 14,
          maxWidth: isMobile ? "42%" : "55%",
        }}>
          <Heart kind="myth" rowId={myth.id} initialCount={myth.like_count || 0} S={S} size={isMobile ? 13 : 16} />
          {/* Truncate at first comma so long subtitles like "Paris, Prince of Troy" don't crash into the FILE label */}
          <span title={myth.myth} style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
            {myth.myth ? String(myth.myth).split(",")[0] : ""}
          </span>
        </div>

        {/* big title */}
        {/* Title/blurb block sits above the bottom player. The bottom offset must
            clear the player + a 24px breathing gap. Iframe player is 80 tall vs
            play-button 48-56, so the offset adapts to whichever is shown. */}
        <div style={{
          position: "absolute",
          left: isMobile ? 16 : 28,
          right: isMobile ? 16 : 28,
          bottom: hasPreview
            ? (isMobile ? 84 : 96)
            : myth.spotify_track_id
              ? (isMobile ? 120 : 132)
              : (isMobile ? 56 : 64),
        }}>
          <AutoFitTitle
            text={myth.title}
            maxSize={isMobile ? 44 : 85}
            minSize={isMobile ? 44 : 85}
            style={{
              fontFamily: S.display,
              lineHeight: 0.86, letterSpacing: "-0.04em", color: S.bone,
              wordBreak: "normal",
              overflowWrap: "normal",
            }}
          />
          {/* Genre + mood badges (admin-managed presets, max 3 of each) */}
          {(tagGenres.length > 0 || tagMoods.length > 0) && (
            <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 12 }}>
              {tagGenres.map(g => (
                <span key={`g-${g.id}`} style={{
                  padding: "3px 8px", fontSize: 10, letterSpacing: "0.25em", fontFamily: S.meta,
                  color: S.bone, background: `${S.bloodHot}cc`, border: `1px solid ${S.bloodHot}`,
                }}>{g.name}</span>
              ))}
              {tagMoods.map(m => (
                <span key={`m-${m.id}`} style={{
                  padding: "3px 8px", fontSize: 10, letterSpacing: "0.25em", fontFamily: S.meta,
                  color: S.bone, background: "rgba(220,20,60,0.78)", border: "1px solid #dc143c",
                }}>{m.name}</span>
              ))}
            </div>
          )}
          <div style={{ fontSize: 12, color: S.ash, marginTop: 12, letterSpacing: "0.06em", fontStyle: "italic" }}>
            "{myth.blurb}"
          </div>
        </div>

        {/* Bottom player.
            • If spotify_preview_url available: custom play button + waveform (works inline).
            • Else if spotify_track_id available: Spotify's official embed iframe — works
              even when the Web API returns no preview_url (Spotify removed preview_url for
              many tracks late 2024 but their embed player still streams previews).
            • Else: just an OPEN IN SPOTIFY link. */}
        {hasPreview ? (
          <>
            <button onClick={() => setPlaying(!playing)} style={{
              position: "absolute", left: isMobile ? 16 : 28, bottom: isMobile ? 16 : 28,
              width: isMobile ? 48 : 56, height: isMobile ? 48 : 56,
              background: S.bloodHot, border: "none", color: S.bone, cursor: "pointer", padding: 0,
              display: "flex", alignItems: "center", justifyContent: "center",
            }}>
              {playing
                ? <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14"/><rect x="14" y="5" width="4" height="14"/></svg>
                : <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M7 4 L20 12 L7 20 Z"/></svg>}
            </button>
            <div style={{
              position: "absolute",
              left: isMobile ? 76 : 96,
              right: isMobile ? 16 : 28,
              bottom: isMobile ? 16 : 28,
              display: "flex", flexDirection: "column", justifyContent: "center",
              height: isMobile ? 48 : 56,
            }}>
              <div style={{ display: "flex", justifyContent: "space-between", fontSize: 12, color: S.ash, letterSpacing: "0.25em", marginBottom: 8 }}>
                <span>30-SEC PREVIEW · {myth.title}</span>
                <span>
                  {Math.floor(progress * displayDur / 60)}:{String(Math.floor(progress * displayDur) % 60).padStart(2,"0")}
                  {" / 0:30"}
                </span>
              </div>
              <Waveform bars={48} color={S.ashDim} playedColor={myth.color} progress={progress} height={20} />
              <audio
                ref={audioRef}
                src={myth.spotify_preview_url}
                preload="auto"
                onTimeUpdate={(e) => setProgress(e.currentTarget.currentTime / PREVIEW_LEN)}
                onEnded={() => { setPlaying(false); setProgress(0); }}
                style={{ display: "none" }}
              />
            </div>
          </>
        ) : myth.spotify_track_id ? (
          <div style={{
            position: "absolute",
            left: isMobile ? 16 : 28, right: isMobile ? 16 : 28, bottom: isMobile ? 16 : 28,
          }}>
            <iframe
              src={`https://open.spotify.com/embed/track/${myth.spotify_track_id}?theme=0`}
              width="100%" height="80"
              frameBorder="0" loading="lazy"
              allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
              style={{ display: "block", borderRadius: 0, border: `1px solid ${S.concreteHi}` }}
            />
          </div>
        ) : (
          <div style={{
            position: "absolute",
            left: isMobile ? 16 : 28, right: isMobile ? 16 : 28, bottom: isMobile ? 16 : 28,
            fontSize: 12, color: S.ashDim, letterSpacing: "0.3em", fontStyle: "italic",
          }}>
            ◢ NO PREVIEW
            {myth.spotify_track_url && (
              <>
                <span style={{ margin: "0 10px", color: S.ashDim }}>·</span>
                <a href={myth.spotify_track_url} target="_blank" rel="noreferrer"
                   style={{ color: S.bloodHot, textDecoration: "none" }}>OPEN IN SPOTIFY ↗</a>
              </>
            )}
          </div>
        )}
      </div>

      {/* RIGHT — copy panel, scrollable */}
      <div style={{ position: "relative", overflow: isMobile ? "visible" : "hidden", display: "flex", flexDirection: "column" }}>
        {/* top bar with back + nav */}
        <div style={{
          minHeight: 48, borderBottom: `1px solid ${S.concreteHi}`,
          display: "flex", flexWrap: isMobile ? "wrap" : "nowrap",
          alignItems: "center", justifyContent: "space-between",
          padding: isMobile ? "10px 16px" : "0 24px", fontSize: 12, letterSpacing: "0.25em", color: S.ash,
          flexShrink: 0, gap: isMobile ? 8 : 0,
        }}>
          <a onClick={onBack} style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", color: S.bone }}>
            <span style={{ transform: "scaleX(-1)", display: "inline-flex" }}><Glyph name="arrow" size={12} color={S.bone} /></span>
            BACK
          </a>
          <span>FILE {myth.file} / {String(myths.length).padStart(3, "0")}</span>
          {myths.length > 1 && (
            <div style={{ display: "flex", gap: isMobile ? 12 : 18, width: isMobile ? "auto" : "auto", justifyContent: "flex-end" }}>
              {/* On mobile, drop the title text to avoid awkward truncation — arrows alone are enough,
                  the count above tells you where you are in the catalogue. */}
              <a onClick={() => onOpen(prev.id)} title={`← ${prev.title}`} style={{ cursor: "pointer", color: S.ash, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: isMobile ? 60 : "none" }}>
                {isMobile ? "←" : `← ${prev.title}`}
              </a>
              <a onClick={() => onOpen(next.id)} title={`${next.title} →`} style={{ cursor: "pointer", color: S.bone, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: isMobile ? 60 : "none" }}>
                {isMobile ? "→" : `${next.title} →`}
              </a>
            </div>
          )}
        </div>

        {/* scroll area */}
        <div style={{ flex: 1, overflow: isMobile ? "visible" : "auto", padding: isMobile ? "20px 16px" : "32px 48px" }}>
          {/* blurb — header itself is the collapse toggle */}
          {myth.blurb && (
            <div style={{ marginBottom: 32 }}>
              <Collapsible S={S} maxHeight={72} header="◢ BLURB">
                <div style={{ fontFamily: S.meta, fontSize: 16, lineHeight: 1.6, color: S.bone, fontStyle: "italic", borderLeft: `2px solid ${myth.color}`, paddingLeft: 16 }}>
                  "{myth.blurb}"
                </div>
              </Collapsible>
            </div>
          )}

          {/* myth retold — VoicePlayer renders between header and body via `prefix`
              so the player stays visible even when the body is collapsed.
              On mythology pages we label the player "LISTEN" (not "VOICE MEMO")
              to differentiate from the transmission voice memo. */}
          <div style={{ marginBottom: 32 }}>
            <Collapsible
              S={S} maxHeight={84} header="◢ THE MYTH · RETOLD"
              prefix={
                <VoicePlayer
                  voiceUrl={myth.voice_memo_url}
                  eyebrow={`◢ LISTEN · ${myth.title}`}
                  engageKind="myth"
                  engageRowId={myth.id}
                  S={S}
                />
              }>
              <p style={{
                margin: 0, fontFamily: S.meta, fontSize: 14, lineHeight: 1.75, color: S.bone,
                maxWidth: 620, whiteSpace: "pre-wrap",
              }}>
                {stripVoiceCues(myth.body)}
              </p>
              <div style={{ marginTop: 14, fontSize: 13, color: S.ash, letterSpacing: "0.2em" }}>
                — D//V
              </div>
            </Collapsible>
          </div>

          {/* lyric */}
          <div style={{ marginBottom: 32 }}>
            <Collapsible S={S} maxHeight={108} header="◢ LYRIC · FRAGMENT">
              <pre style={{
                margin: 0, fontFamily: S.meta,
                fontSize: 12, fontWeight: 300, fontStyle: "italic", lineHeight: 1.65,
                color: S.bone, letterSpacing: "0.02em", whiteSpace: "pre-wrap",
                borderLeft: `2px solid ${S.bloodHot}`, paddingLeft: 20,
              }}>{myth.lyric}</pre>
            </Collapsible>
          </div>

          {/* streaming OR pre-save (when release_date is in the future) */}
          {prerelease ? (
            <div style={{ border: `1px solid ${S.bloodHot}`, padding: isMobile ? 18 : 24, background: S.concrete }}>
              <div style={{ fontSize: 12, color: S.bloodHot, letterSpacing: "0.3em", marginBottom: 12 }}>
                ◢ PRE-RELEASE · DROPS {myth.release_date}
              </div>
              <div style={{ display: "flex", gap: isMobile ? 14 : 24, marginBottom: 18, flexWrap: "wrap" }}>
                {[
                  ["DAYS", countdown?.days],
                  ["HOURS", countdown?.hours],
                  ["MINUTES", countdown?.minutes],
                ].map(([label, v]) => (
                  <div key={label}>
                    <div style={{ fontFamily: S.display, fontSize: isMobile ? 36 : 48, lineHeight: 1, color: S.bone, letterSpacing: "-0.01em" }}>
                      {v == null ? "–" : String(v).padStart(2, "0")}
                    </div>
                    <div style={{ fontSize: 10, color: S.ash, letterSpacing: "0.3em", marginTop: 4 }}>{label}</div>
                  </div>
                ))}
              </div>
              {myth.hyperfollow_url ? (
                <>
                  <a href={myth.hyperfollow_url} target="_blank" rel="noreferrer" style={{
                    display: "inline-flex", alignItems: "center", gap: 10,
                    padding: "14px 22px", background: S.bloodHot, color: S.bone,
                    fontFamily: S.meta, fontSize: 13, letterSpacing: "0.3em",
                    textDecoration: "none", border: "none", cursor: "pointer",
                  }}>
                    ↓ PRE-SAVE ON ALL PLATFORMS <Glyph name="arrow" size={12} color={S.bone} />
                  </a>
                  <div style={{ marginTop: 12, fontSize: 11, color: S.ash, letterSpacing: "0.18em", lineHeight: 1.6 }}>
                    spotify · apple music · iheart · everywhere you listen
                  </div>
                </>
              ) : (
                <div style={{ fontSize: 12, color: S.ash, letterSpacing: "0.18em" }}>
                  PRE-SAVE LINK PENDING ⸺ ADMIN SYNC.
                </div>
              )}
            </div>
          ) : (
            <div>
              <div style={{ fontSize: 12, color: S.bloodHot, letterSpacing: "0.3em", marginBottom: 14, display: "flex", justifyContent: "space-between", alignItems: "baseline", flexWrap: "wrap", gap: 8 }}>
                <span>◢ LISTEN · ALL PLATFORMS</span>
                <a href={`/listen/${myth.id}`} style={{ color: S.bone, textDecoration: "none", borderBottom: `1px solid ${myth.color}`, paddingBottom: 1 }}>
                  OPEN SMART LINK ↗
                </a>
              </div>
              {streamLinks.length > 0 ? (
                <div style={{ display: "grid", gridTemplateColumns: isMobile ? "1fr" : "1fr 1fr", gap: 8 }}>
                  {streamLinks.map((p) => (
                    <a key={p.platform} href={p.url} target="_blank" rel="noreferrer" style={{
                      display: "flex", alignItems: "center", gap: 12, padding: "12px 14px",
                      background: S.concrete, borderLeft: `3px solid ${myth.color}`,
                      fontSize: 13, letterSpacing: "0.18em", color: S.bone, cursor: "pointer",
                      transition: "background .15s", textDecoration: "none",
                    }}
                    onMouseEnter={(e) => (e.currentTarget.style.background = S.concreteHi)}
                    onMouseLeave={(e) => (e.currentTarget.style.background = S.concrete)}>
                      <Glyph name={p.platform} size={14} color={S.bone} />
                      <span style={{ flex: 1 }}>{p.label}</span>
                      <Glyph name="arrow" size={12} color={S.ash} />
                    </a>
                  ))}
                </div>
              ) : (
                <div style={{
                  padding: "16px 18px", background: S.concrete, borderLeft: `3px solid ${S.ashDim}`,
                  fontSize: 12, color: S.ash, letterSpacing: "0.18em", lineHeight: 1.7,
                }}>
                  {myth.hyperfollow_url ? (
                    <>STREAMING LINKS PENDING ⸺ <a href={myth.hyperfollow_url} target="_blank" rel="noreferrer" style={{ color: S.bloodHot, textDecoration: "none", borderBottom: `1px solid ${S.bloodHot}` }}>HYPERFOLLOW ↗</a></>
                  ) : (
                    "NO STREAMING LINKS YET ⸺ SYNC FROM ADMIN."
                  )}
                </div>
              )}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

window.MythologyPage = MythologyPage;
