// Mixes, Events, About, Contact pages.

const { TopBar, Footer, Mark, Atmo, Cover, ChapterCover, Embed, Breadcrumb } = window.MOTD_ATOMS;
const { RELEASES, CHAPTERS, EVENTS } = window.MOTD_DATA;

// =============================================================
// PODCAST — Chapters list
//
// The chapter list is populated by FETCHING the live Soundcloud RSS feed
// for `mysteriesofthedeep` at load time, parsing the XML, and rendering
// the items in the existing layout. If the fetch fails (network, CORS,
// proxy down, parse error) we silently fall back to the static `CHAPTERS`
// table so the page still renders something credible.
//
// Why a proxy. feeds.soundcloud.com responds without CORS headers, so a
// direct browser fetch is blocked. We try the direct call first (in case
// SC ever flips the header), then fall back to allorigins.win, a public
// CORS proxy that returns the upstream body verbatim.
// =============================================================
const SOUNDCLOUD_FEED_URL =
  'https://feeds.soundcloud.com/users/soundcloud:users:15876127/sounds.rss';

// Tiny roman-numeral encoder used to render chapter numbers in the existing
// style (CLII, CLI, CL, CXLIX…). Feed items don't include the chapter index,
// so we compute it: newest item = total count, descending. (Approximate, but
// the existing site shows the same numbering scheme.)
function toRoman(n) {
  const map = [
    [1000,'M'],[900,'CM'],[500,'D'],[400,'CD'],
    [100,'C'],[90,'XC'],[50,'L'],[40,'XL'],
    [10,'X'],[9,'IX'],[5,'V'],[4,'IV'],[1,'I'],
  ];
  let out = '';
  for (const [v, sym] of map) { while (n >= v) { out += sym; n -= v; } }
  return out || '—';
}

// Format ISO-ish duration into mm:ss. SC iTunes <itunes:duration> can be
// "HH:MM:SS" or just seconds depending on the item. Handle both.
function formatDur(raw) {
  if (!raw) return '—';
  const s = String(raw).trim();
  if (s.includes(':')) {
    const parts = s.split(':').map(n => parseInt(n, 10));
    if (parts.length === 3) {
      const [h, m, sec] = parts;
      return (h * 60 + m) + ':' + String(sec).padStart(2, '0');
    }
    return s;
  }
  const n = parseInt(s, 10);
  if (!isFinite(n)) return s;
  const m = Math.floor(n / 60);
  const sec = n % 60;
  return m + ':' + String(sec).padStart(2, '0');
}

function formatDate(rawDate) {
  if (!rawDate) return '';
  const d = new Date(rawDate);
  if (isNaN(d.getTime())) return '';
  return d.toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })
          .replace(',', ',');
}

// Parse Soundcloud title strings into (artist, title, where) where possible.
// Common patterns observed on the SC feed:
//   "Mysteries of the Deep CLII | Refracted at Springkell"
//   "MotD CLI — CMD"
//   "Mysteries of the Deep CL — Lynne at Variations"
// Channel prefix + chapter numeral + various separators (|, —, -, :, ·)
// before the real episode title. We strip those and then split " at " /
// " live at " for the venue.
// Roman → decimal converter for chapter numbers parsed out of feed titles.
// Subtractive (IV, IX, XL, XC, CD, CM) handled; case-insensitive input.
function romanToInt(roman) {
  const map = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000 };
  const s = String(roman || '').toUpperCase();
  let total = 0;
  for (let i = 0; i < s.length; i++) {
    const cur = map[s[i]] || 0;
    const next = map[s[i + 1]] || 0;
    total += cur < next ? -cur : cur;
  }
  return total;
}

function splitTitle(t) {
  let s = String(t || '').trim();
  const SEP_CLASS = '[\\s\\-\u2010\u2011\u2012\u2013\u2014\u2015:·|]';
  const SEP = new RegExp('^' + SEP_CLASS + '+');
  s = s.replace(/^(motd|mysteries of the deep|mysteries)\s*/i, '');
  s = s.replace(SEP, '');

  // Try to extract a chapter roman numeral, but be VERY conservative.
  // SoundCloud titles used to look like "Mysteries of the Deep CLII | Refracted at Springkell"
  // — clean numeral followed by a separator. The current feed has many titles
  // with NO numeral, e.g. "Mysteries of the Deep - CMD" or
  // "Mysteries of the Deep - Lynne at Variations". The earlier parser was
  // happily matching "CMD" (= 1400 in roman) as a chapter number, and
  // stripping the leading "L" from "Lynne" (= 50). We now require:
  //   1) The candidate must be UPPERCASE roman chars only.
  //   2) It must be followed by a separator character (whitespace, dash,
  //      pipe, colon, etc.) — NOT followed by another letter. This prevents
  //      "Lynne" → "L" + "ynne".
  //   3) After stripping the candidate, there must still be meaningful
  //      content left in the string. If the candidate consumes the entire
  //      remaining title (like "CMD" being the whole artist name), it's
  //      clearly an artist, not a chapter number.
  //   4) Length must be ≥ 1 and ≤ 7 (chapter numbers don't realistically
  //      go above ~CCC = 300, which fits in 7 chars).
  let chapterNumber = null;
  const romanMatch = s.match(/^([CMDILVX]+)(?=[\s\-\u2010-\u2015:·|]|$)/);
  if (romanMatch) {
    const candidate = romanMatch[1];
    const remainder = s.slice(candidate.length).replace(SEP, '').trim();
    if (candidate.length <= 7 && remainder.length > 0) {
      chapterNumber = romanToInt(candidate);
      s = remainder;
    }
  }

  const where = (s.match(/\b(at|live at)\s+(.+)$/i) || [])[0] || '';
  if (where) s = s.slice(0, s.length - where.length);
  s = s.replace(new RegExp(SEP_CLASS + '+$'), '').trim();
  return { title: s, where, chapterNumber };
}

function parseSoundcloudFeed(xmlText) {
  const doc = new DOMParser().parseFromString(xmlText, 'text/xml');
  const err = doc.querySelector('parsererror');
  if (err) throw new Error('feed parse error');
  const items = [...doc.querySelectorAll('item')];
  if (!items.length) throw new Error('no items in feed');

  const ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd';

  // Channel-level artwork as a fallback when an individual <item> doesn't
  // include its own <itunes:image>.
  const channel = doc.querySelector('channel');
  const channelArt =
    (channel && channel.getElementsByTagNameNS(ITUNES_NS, 'image')[0]?.getAttribute('href'))
    || (channel && channel.querySelector('image > url')?.textContent)
    || '';

  // First pass — collect parsed metadata (with possibly-null chapter number).
  const total = items.length;
  const raw = items.map((node) => {
    const rawTitle = node.querySelector('title')?.textContent || '';
    const link     = node.querySelector('link')?.textContent || '';
    const pubDate  = node.querySelector('pubDate')?.textContent || '';
    const author   = node.getElementsByTagNameNS(ITUNES_NS, 'author')[0]?.textContent
                  || node.querySelector('author')?.textContent
                  || '';
    const dur      = node.getElementsByTagNameNS(ITUNES_NS, 'duration')[0]?.textContent || '';
    const image    = node.getElementsByTagNameNS(ITUNES_NS, 'image')[0]?.getAttribute('href')
                  || channelArt
                  || '';
    return { rawTitle, link, pubDate, author, dur, image, split: splitTitle(rawTitle) };
  });

  // Second pass — fill in chapter numbers for items whose titles didn't
  // include a roman numeral (the most recent item often omits it). The feed
  // is newest→oldest, so an item with no number gets `nextKnown + offset`
  // where offset is the index distance to the next item that DID parse
  // cleanly. Falls back to a backwards scan if no later item has one
  // (rare — happens only if the entire tail is unparseable).
  const numbers = raw.map(r => r.split.chapterNumber);
  for (let i = 0; i < numbers.length; i++) {
    if (numbers[i] != null) continue;
    for (let j = i + 1; j < numbers.length; j++) {
      if (numbers[j] != null) { numbers[i] = numbers[j] + (j - i); break; }
    }
    if (numbers[i] != null) continue;
    for (let j = i - 1; j >= 0; j--) {
      if (numbers[j] != null) { numbers[i] = numbers[j] - (i - j); break; }
    }
    // If STILL null (no numerals anywhere in the feed), fall back to the
    // positional count so the page always renders a number.
    if (numbers[i] == null) numbers[i] = total - i;
  }

  return raw.map((r, i) => {
    const parsedArtist = r.split.title;
    return {
      num:    numbers[i],
      title:  parsedArtist || r.rawTitle,
      artist: parsedArtist || r.author || 'Mysteries of the Deep',
      where:  r.split.where,
      date:   formatDate(r.pubDate),
      dur:    formatDur(r.dur),
      url:    r.link,
      image:  r.image,
    };
  });
}

// Module-level cache + promise. Both HomePage and MixesPage want the live
// Soundcloud feed; only fetch it once per page load. The promise resolves
// to the parsed chapter list (newest first), or rejects if every source
// fails (HTTP/parse/CORS).
//
// IMPORTANT: only resolved promises are cached. A rejection clears the
// cache so a later mount can try again (proxies are flaky — allorigins.win
// will sometimes 408 on a cold cache for a minute, then come back).
window.MOTD_SC_FEED = window.MOTD_SC_FEED || null;

// Public CORS proxies we'll try in order if the direct CORS fetch is
// blocked. Add a new one to the front of the list to prefer it. Each entry
// returns the upstream body verbatim — pick proxies that DON'T wrap the
// payload in JSON, so the XML parser handles it directly.
const CORS_PROXIES = [
  (u) => 'https://corsproxy.io/?' + encodeURIComponent(u),
  (u) => 'https://api.allorigins.win/raw?url=' + encodeURIComponent(u),
  (u) => 'https://api.codetabs.com/v1/proxy/?quest=' + encodeURIComponent(u),
  (u) => 'https://thingproxy.freeboard.io/fetch/' + u,
];

async function tryFetchFeed(url, signal) {
  const r = await fetch(url, { signal });
  if (!r.ok) throw new Error('http ' + r.status);
  const txt = await r.text();
  const parsed = parseSoundcloudFeed(txt);
  if (!parsed.length) throw new Error('empty');
  return parsed;
}

window.fetchMotdChapters = function () {
  if (window.MOTD_SC_FEED) return window.MOTD_SC_FEED;
  const promise = (async () => {
    // 1. Netlify Function — same-origin, no CORS, edge-cached. Fast.
    //    Use the direct .netlify/functions/ path; the /api/sc-feed pretty
    //    redirect didn't pick up reliably on this site.
    try { return await tryFetchFeed('/.netlify/functions/sc-feed'); }
    catch (e) { /* fall through to public proxies (local dev) */ }

    // 2. Direct SC fetch (works only if SC ever flips on CORS headers).
    try { return await tryFetchFeed(SOUNDCLOUD_FEED_URL); }
    catch (e) { /* fall through */ }

    // 3. Public CORS proxies as a final fallback chain.
    for (const makeUrl of CORS_PROXIES) {
      try {
        const controller = new AbortController();
        const timer = setTimeout(() => controller.abort(), 8000);
        try {
          const result = await tryFetchFeed(makeUrl(SOUNDCLOUD_FEED_URL), controller.signal);
          clearTimeout(timer);
          return result;
        } finally {
          clearTimeout(timer);
        }
      } catch (e) { /* try next proxy */ }
    }
    throw new Error('all sources failed');
  })();

  // Cache the resolved promise; clear cache on rejection so subsequent
  // mounts can retry (a flaky proxy might come back).
  window.MOTD_SC_FEED = promise;
  promise.catch(() => { if (window.MOTD_SC_FEED === promise) window.MOTD_SC_FEED = null; });
  return promise;
};

const MixesPage = ({ go, t }) => {
  const { SigilDivider } = window.MOTD_SIGILS;
  // Start with the static CHAPTERS table as a synchronous fallback so the
  // page never flashes empty. Replace once the live feed resolves.
  const [chapters, setChapters] = React.useState(CHAPTERS);
  const [status, setStatus] = React.useState('loading'); // 'loading' | 'live' | 'fallback'

  React.useEffect(() => {
    let cancelled = false;
    window.fetchMotdChapters().then(
      (live) => { if (!cancelled) { setChapters(live); setStatus('live'); } },
      ()     => { if (!cancelled) { setStatus('fallback'); } }
    );
    return () => { cancelled = true; };
  }, []);

  const featured = chapters[0];
  return (
    <div className="page-fade">
      <div data-graphics-only>
        <SigilDivider
          ns={[36, 42, 53]}
          size={64}
          stroke={3}
          faint={true}
          label={`152 chapters \u00b7 2011 \u2014 present`}
          margin="50px 0 50px"
        />
      </div>

      {/* Featured chapter — artwork is the external link to Soundcloud.
          Sits next to a short text block so the page still reads as podcast,
          not just a wall of artwork. The cover is capped at min(58vh, 32vw)
          so it scales with both viewport height (won't push past the fold
          on short laptops) and width (grows proportionally on wide
          monitors), mirroring the Latest Release hero on the home page. */}
      <section style={{
        paddingBottom: 60,
        display: 'grid',
        gridTemplateColumns: 'min(58vh, 32vw) minmax(0, 1fr)',
        columnGap: 'clamp(28px, 3.5vw, 56px)',
        alignItems: 'end',
      }}>
        <ChapterCover
          c={featured}
          style={{ width: '100%', maxHeight: 'min(58vh, 32vw)', maxWidth: 'min(58vh, 32vw)' }}
        />
        <div style={{ paddingBottom: 8 }}>
          <div className="h3">Featured chapter</div>
          <h2 className="h2" style={{ marginTop: 18, fontSize: 'clamp(36px, 4vw, 56px)', lineHeight: '1.04em' }}>
            {featured.title}
            {featured.where ? <span className="italic" style={{ color: 'var(--fg-soft)' }}>{` · ${featured.where}`}</span> : null}
          </h2>
          <div className="h4 italic" style={{ marginTop: 12, color: 'var(--fg-soft)' }}>{featured.artist}</div>
          <div style={{ marginTop: 18, display: 'flex', gap: 22, alignItems: 'baseline' }}>
            <span className="mono">{featured.dur}</span>
            <span className="mono" style={{ color: 'var(--fg-soft)' }}>{featured.date}</span>
          </div>
          <div style={{ marginTop: 28 }}>
            <a href={featured.url} target="_blank" rel="noreferrer" className="btn-primary">
              Listen on Soundcloud ↗︎
            </a>
          </div>
        </div>
      </section>

      {/* All chapters — each row is an external link to Soundcloud.
          A small chapter-cover thumb sits at the left so the artwork is
          visible (and itself clickable) in the list view. */}
      <section style={{ paddingBottom: 40 }}>
        <div>
          {chapters.map((c) => (
            <div
              key={c.num + ':' + c.url}
              onClick={() => window.open(c.url, '_blank', 'noopener,noreferrer')}
              role="link"
              tabIndex={0}
              onKeyDown={(e) => { if (e.key === 'Enter') window.open(c.url, '_blank', 'noopener,noreferrer'); }}
              aria-label={`Listen to Chapter ${c.num} · ${c.title} on Soundcloud`}
              style={{
                display: 'grid',
                gridTemplateColumns: '88px 110px 1fr 80px 40px',
                gap: 28, alignItems: 'center',
                padding: '20px 0',
                borderTop: '1px solid var(--rule)',
                color: 'inherit',
                textDecoration: 'none',
                cursor: 'pointer',
              }}
              onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bg-soft)'}
              onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
            >
              <ChapterCover c={c} style={{ width: 88, height: 88 }} />
              <div className="mono" style={{ color: 'var(--fg-soft)' }}>{c.num}</div>
              <div>
                <div className="h4">{c.title}{c.where ? ` — ${c.where}` : ''}</div>
                <div className="p3" style={{ marginTop: 4 }}>{c.artist}</div>
              </div>
              <div className="mono" style={{ textAlign: 'right' }}>{c.dur}</div>
              <div className="mono" style={{ textAlign: 'right' }}>↗︎</div>
            </div>
          ))}
        </div>
      </section>
    </div>
  );
};

// =============================================================
// EVENTS
// =============================================================
// EVENTS
// Card grid grouped by year. EVENTS is newest-first in proto-data.jsx;
// the grouping preserves that order (newest year first, newest event
// within each year first).
// =============================================================
const EventsPage = ({ go, t }) => {
  const { SigilDivider } = window.MOTD_SIGILS;
  // Bucket by year. Use a Map so we preserve insertion order = chronological.
  const byYear = React.useMemo(() => {
    const m = new Map();
    for (const e of EVENTS) {
      const y = e.year || (e.date.match(/\d{4}/) || ['—'])[0];
      if (!m.has(y)) m.set(y, []);
      m.get(y).push(e);
    }
    return m;
  }, []);

  // Single event card. Larger than the previous list row — flyer dominates,
  // metadata sits underneath in the negative space below. Always renders an
  // anchor when a link exists; renders a plain div otherwise so dead Facebook
  // events don't navigate users into a 404.
  const Card = ({ e }) => {
    const interactive = !!e.link;
    const inner = (
      <>
        <div className="event-card-flyer">
          {e.flyer ? (
            <img
              src={window.assetUrl(e.flyer)}
              alt={e.name}
              loading="lazy"
            />
          ) : (
            // No flyer captured for this event — render a typographic stand-in
            // using the event name. Keeps the grid rhythm intact.
            <div className="event-card-placeholder">
              <div className="mono" style={{ fontSize: 11, color: 'var(--fg-faint)' }}>
                {e.date}
              </div>
              <div className="italic" style={{
                marginTop: 14,
                fontFamily: 'var(--motd-head)',
                fontWeight: 300,
                fontSize: 22,
                lineHeight: 1.15,
                color: 'var(--fg-soft)',
              }}>
                {e.name}
              </div>
            </div>
          )}
          {interactive && <span className="event-card-arrow">↗︎</span>}
        </div>
        <div className="event-card-meta">
          <div className="mono">{e.date}</div>
          <div className="event-card-name">{e.name}</div>
          {e.lineup && (
            <div className="p3 italic event-card-lineup">with {e.lineup}</div>
          )}
          {(e.venue || e.city) && (
            <div className="p3 event-card-where">
              {e.venue && e.city ? `${e.venue} · ${e.city}` : (e.venue || e.city)}
            </div>
          )}
        </div>
      </>
    );
    const className = 'event-card' + (interactive ? '' : ' event-card-static');
    if (interactive) {
      return (
        <a href={e.link} target="_blank" rel="noreferrer" className={className}>
          {inner}
        </a>
      );
    }
    return <div className={className}>{inner}</div>;
  };

  return (
    <div className="page-fade">
      <div data-graphics-only>
        <SigilDivider
          ns={[31, 48, 58]}
          size={64}
          stroke={3}
          faint={true}
          label={`${EVENTS.length} events \u00b7 2014 \u2014 present`}
          margin="50px 0 30px"
        />
      </div>

      {[...byYear.entries()].map(([year, events], yi) => (
        <section
          key={year}
          style={{
            // Skip the top rule on the FIRST year section — the SigilDivider
            // above already provides a hairline, so a borderTop here would
            // read as a duplicate divider.
            borderTop: yi === 0 ? 'none' : '1px solid var(--rule)',
            padding: yi === 0 ? '20px 0 56px' : '36px 0 56px',
          }}
        >
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 18, marginBottom: 26 }}>
            <div className="h2" style={{
              fontFamily: 'var(--motd-mono)',
              fontWeight: 400,
              fontSize: 'clamp(36px, 4.4vw, 64px)',
              lineHeight: 1,
              letterSpacing: '-0.01em',
            }}>{year}</div>
            <div className="mono" style={{ color: 'var(--fg-faint)' }}>
              {events.length} event{events.length === 1 ? '' : 's'}
            </div>
          </div>
          <div className="event-grid">
            {events.map((e, i) => <Card key={i} e={e} />)}
          </div>
        </section>
      ))}
    </div>
  );
};

// =============================================================
// NEWSLETTER FORM
//
// Wired to Netlify Forms. For Netlify to recognize a JS-rendered form,
// two things must be true:
//   1. index.html includes a static "shadow" copy of the form (with
//      matching name + field names) so Netlify's deploy-time scanner
//      can find it. See <form name="newsletter" netlify hidden> there.
//   2. This React form posts URL-encoded data to "/" with `form-name`
//      set to "newsletter" — that's the Netlify magic field.
//
// Submissions then appear in Netlify dashboard → Forms → "newsletter".
// =============================================================
const NewsletterForm = () => {
  const [status, setStatus] = React.useState('idle'); // idle | submitting | ok | error

  const encode = (data) =>
    Object.keys(data)
      .map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(data[k]))
      .join('&');

  const onSubmit = async (e) => {
    e.preventDefault();
    setStatus('submitting');
    const form = e.currentTarget;
    const data = {
      'form-name': 'newsletter',
      name:  form.elements.name.value.trim(),
      email: form.elements.email.value.trim(),
    };
    try {
      const res = await fetch('/', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: encode(data),
      });
      if (res.ok) {
        setStatus('ok');
        form.reset();
      } else {
        setStatus('error');
      }
    } catch (err) {
      setStatus('error');
    }
  };

  if (status === 'ok') {
    return (
      <p className="p2 italic" style={{ marginTop: 28, color: 'var(--fg)' }}>
        Thanks — you're on the list.
      </p>
    );
  }

  return (
    <form
      name="newsletter"
      method="POST"
      data-netlify="true"
      onSubmit={onSubmit}
      style={{ marginTop: 28, display: 'grid', gap: 18, maxWidth: 460 }}
    >
      {/* Required by Netlify Forms — names the form on submission. */}
      <input type="hidden" name="form-name" value="newsletter" />
      <input className="field" type="text"  name="name"  placeholder="Name"          required />
      <input className="field" type="email" name="email" placeholder="Email address" required />
      <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
        <button className="btn-primary" type="submit" disabled={status === 'submitting'}>
          {status === 'submitting' ? 'Subscribing…' : 'Subscribe'}
        </button>
        {status === 'error' && (
          <span className="p3" style={{ color: '#c66' }}>
            Something went wrong. Try again.
          </span>
        )}
      </div>
    </form>
  );
};

// =============================================================
// CONTACT FORM
//
// Same Netlify Forms pattern as the newsletter signup. Posts URL-
// encoded data to "/" with `form-name=contact`. Submissions appear in
// Netlify dashboard → Forms → "contact". Configure an email
// notification under Site configuration → Forms → Form notifications
// so each contact gets piped to your inbox.
//
// Pairs with the obfuscated `info@` mailto: link below — the form is
// the primary contact path (better signal, no spam, structured fields);
// the mailto: is a fallback for people who prefer their own client.
// =============================================================
const ContactForm = () => {
  const [status, setStatus] = React.useState('idle');

  const encode = (data) =>
    Object.keys(data)
      .map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(data[k]))
      .join('&');

  const onSubmit = async (e) => {
    e.preventDefault();
    setStatus('submitting');
    const form = e.currentTarget;
    const data = {
      'form-name': 'contact',
      name:    form.elements.name.value.trim(),
      email:   form.elements.email.value.trim(),
      subject: form.elements.subject.value.trim(),
      message: form.elements.message.value.trim(),
    };
    try {
      const res = await fetch('/', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: encode(data),
      });
      if (res.ok) {
        setStatus('ok');
        form.reset();
      } else {
        setStatus('error');
      }
    } catch (err) {
      setStatus('error');
    }
  };

  if (status === 'ok') {
    return (
      <p className="p2 italic" style={{ marginTop: 28, color: 'var(--fg)' }}>
        Thanks — we'll be in touch.
      </p>
    );
  }

  return (
    <form
      name="contact"
      method="POST"
      data-netlify="true"
      onSubmit={onSubmit}
      style={{ marginTop: 28, display: 'grid', gap: 18, maxWidth: 680 }}
    >
      <input type="hidden" name="form-name" value="contact" />
      <input className="field" type="text"  name="name"    placeholder="Name"    required />
      <input className="field" type="email" name="email"   placeholder="Email"   required />
      <input className="field" type="text"  name="subject" placeholder="Subject" />
      <textarea
        className="field"
        name="message"
        placeholder="Message"
        rows={6}
        required
        style={{ resize: 'vertical', fontFamily: 'var(--motd-head)' }}
      />
      <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
        <button className="btn-primary" type="submit" disabled={status === 'submitting'}>
          {status === 'submitting' ? 'Sending…' : 'Send message'}
        </button>
        {status === 'error' && (
          <span className="p3" style={{ color: '#c66' }}>
            Something went wrong. Try again.
          </span>
        )}
      </div>
    </form>
  );
};

// =============================================================
// OBFUSCATED EMAIL
//
// Renders an email address as a clickable link, but assembles both the
// visible text AND the href at render time from separate user/domain
// props. The joined string ("info@mysteriesofthedeep.net") therefore
// never appears in static HTML — bots scraping the rendered source for
// `mailto:` or for an `@`-pattern get nothing useful.
//
// Not bulletproof (a bot that runs JS could still grab it), but defeats
// the simple regex-based scrapers that account for the vast majority
// of address-harvesting spam.
// =============================================================
const ObfuscatedEmail = ({ user, domain, className = 'mono', style }) => {
  const addr = user + '\u0040' + domain;       // hidden in code, not literal "@"
  return (
    <a
      href={'mailto:' + addr}
      className={className}
      style={{ color: 'var(--fg)', textDecoration: 'none', ...style }}
    >
      {/* Insert a zero-width joiner around the @ so even rendered DOM
          text won't match a naive `\w+@\w+\.\w+` regex if scraped post-hydration. */}
      {user}<span aria-hidden="true">{'\u200d'}</span>@<span aria-hidden="true">{'\u200d'}</span>{domain}
    </a>
  );
};

// =============================================================
// ABOUT (incl. contact + demo submission)
// =============================================================
const AboutPage = ({ go, t }) => {
  const { SigilWatermark } = window.MOTD_SIGILS;
  return (
  <div className="page-fade">
    {/* Manifesto block — sits above the contact/newsletter row. A large
        faint sigil watermark bleeds off the right edge as quiet texture.
        The wrapper's `right: 0` (not `var(--motd-margin)`) is intentional:
        the about section's right edge is already inset by --motd-margin
        via the .site container, whereas the home hero extends to the
        viewport edge with a negative inline margin and then uses
        `right: var(--motd-margin)`. Both anchors end up at the SAME
        viewport-x as a result, so the two watermarks visually line up. */}
    <section style={{
      position: 'relative',
      marginTop: 12,
      padding: '32px 0 50px',
    }}>
      <div data-graphics-only style={{
        position: 'absolute', top: 0, right: 0,
        pointerEvents: 'none', zIndex: 0,
      }}>
        <SigilWatermark
          n={31}
          size={520}
          opacity={0.06}
          stroke={1.4}
          style={{ position: 'static', transform: 'translate(45%, -22%)' }}
        />
      </div>
      <div style={{ position: 'relative', zIndex: 1, maxWidth: 780 }}>
        <div className="h3">About</div>
        <h1 className="h1" style={{
          marginTop: 22,
          fontSize: 'clamp(40px, 5vw, 80px)',
          lineHeight: '1.02em',
        }}>
          New York-based record label,<br />podcast, and event series.
        </h1>
        <p className="p1" style={{
          marginTop: 36, color: 'var(--fg)', maxWidth: 680,
        }}>
          Mysteries, as fans lovingly call it, is concerned with the
          exploration of sound, and thoughtfully engaging our listeners.
          Originally conceptualized as a podcast focused on ambient and
          experimental music, the project expanded further into a
          live event series and record label.
        </p>
      </div>
    </section>

    <section style={{ padding: '20px 0 60px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 80, alignItems: 'start', borderTop: '1px solid var(--rule)' }}>
      {/* Contact column — split into two paths:
          1) Primary: a dedicated contact form (Netlify Forms again).
             Most visitors should use this — it gives us name/email/
             subject/message in a structured row, with built-in spam
             filtering, and arrives in the Netlify dashboard / email
             notification.
          2) Secondary fallback: an obfuscated `info@` mailto: link, for
             people who'd rather use their own mail client. Address is
             assembled at runtime so it doesn't appear in static markup. */}
      <div style={{ paddingTop: 40 }}>
        <div className="h3">Contact</div>
        <ContactForm />
        {/* No raw email fallback — the form is the only contact path.
            Keeps the page text completely free of harvestable addresses
            so spam bots can't scrape anything useful from the source. */}
      </div>

      {/* Newsletter signup — wired to Netlify Forms. Submissions appear
          in Netlify dashboard → Forms → "newsletter". Export as CSV
          for import into a newsletter platform later. Includes both a
          name and email field, per request. */}
      <div style={{ paddingTop: 40 }}>
        <div className="h3">Newsletter</div>
        <p className="p2" style={{ marginTop: 22, maxWidth: 420, color: 'var(--fg-soft)' }}>
          Occasional dispatches — new releases, podcast chapters, and event
          announcements.
        </p>
        <NewsletterForm />
      </div>
    </section>
  </div>
  );
};

window.MOTD_PAGES_B = { MixesPage, EventsPage, AboutPage };
