// SPOT pre-launch landing page (design variant 1A "Centered").
//
// One screen: say what SPOT is, capture an email for early access, show that
// other people signed up. Everything below the fold is supporting cast.
//
// Two deliberate departures from the design hand-off, both because the page
// collects personal data from strangers:
//
//   * a consent checkbox, unticked, next to a link to the privacy policy.
//     "No spam, unsubscribe anytime" in fine print is a promise, not consent,
//     and an EU mailing list needs the second one.
//   * a name field. The hand-off had email + city; the list is more useful
//     with a name, and asking for it up front beats guessing later.
//
// The subscriber counter is null until the backend decides there are enough
// sign-ups to be worth showing (WAITLIST_COUNT_MIN). "3 people already
// subscribed" is weaker than saying nothing.

const NEON = '#d8ff00';
const INK = '#0a0a09';

const USE_CASES = [
  { id: 'discover', title: 'Discover events',          desc: 'Find festivals, club nights and house parties near you.' },
  { id: 'create',   title: 'Host and invite',          desc: 'Create your own events and invite your friends.' },
  { id: 'friends',  title: 'See where friends go',     desc: 'Spot which events your friends are attending.' },
  { id: 'organize', title: 'Publish as an organiser',  desc: 'Put your events in front of the whole scene.' },
];

const FEATURES = [
  { n: '01', title: 'Discover events', desc: 'Festivals, club nights, house parties - all in one feed.' },
  { n: '02', title: 'Host your own',   desc: 'Public or private, free or invite-only. Your scene, your rules.' },
  { n: '03', title: "See who's going", desc: 'Spot the crowd before you commit. Bring your people.' },
];

// The API lives on another origin, so this cannot be a relative path. CORS
// already allows spotevent.be - see the allowlist in the backend.
const API = 'https://api.spotevent.be/api';

// The privacy policy is bilingual and defaults to the browser's language.
// This page is English, so link to the English side explicitly - the whole
// point of the link is to show the wording being agreed to.
const PRIVACY_URL = 'privacy/?lang=en';
const SUPPORT_URL = 'support/?lang=en';
// The public event feed. Served by the backend through a rewrite in
// vercel.json, so it is same-origin and an absolute path is correct here.
//
// Footer only, deliberately: this page exists to convert to sign-ups
// while the app is in closed testing, and a prominent "browse events"
// button hands visitors what they came for without asking anything back.
// It earns a real placement once the app is publicly downloadable.
//
// Labelled "Browse events", not "Discover events": the feature grid above
// already has a card by that name, and two identical labels pointing at
// different things read as an anchor link to the one you can see.
const DISCOVER_URL = '/discover';

// ── Background ───────────────────────────────────────────────────────────
// The spotlight the product is named after. On a pointer device it follows
// the cursor; on a touch device there is no cursor to follow, so it drifts
// in a slow circle instead - without that it reads as a static gradient on
// exactly the devices most visitors will use.
//
// Position is written straight to the DOM inside a rAF rather than held in
// state, so neither mode re-renders the tree.
function useSpotlight(enabled) {
  const frameRef = React.useRef(null);
  const layerRef = React.useRef(null);
  const darkRef = React.useRef(null);
  const raf = React.useRef(0);

  React.useEffect(() => {
    const frame = frameRef.current;
    if (!frame) return;

    const paint = (mx, my) => {
      if (layerRef.current) {
        // Muted olive rather than full NEON at the core. The copy on this
        // page is white, and the spotlight tracks the cursor straight over
        // it - white on #d8ff00 is ~1.1:1 contrast, so the hero text used to
        // disappear whenever the beam crossed it. These stops keep the same
        // hue and falloff but cap the brightest point around 4.6:1, and the
        // text spends most of its time over the 6:1+ mid-stops.
        layerRef.current.style.background =
          `radial-gradient(circle at ${mx}% ${my}%, #6e7d00 0%, #5a6600 10%, #2e3400 28%, ${INK} 60%)`;
      }
      if (darkRef.current) {
        darkRef.current.style.background =
          `radial-gradient(circle at ${100 - mx}% ${100 - my}%, rgba(0,0,0,0.55) 0%, transparent 50%)`;
      }
    };

    paint(50, 50);
    if (!enabled) return;

    // No hover means no cursor to track: orbit instead.
    if (window.matchMedia('(hover: none)').matches) {
      // The 0.4s ease that smooths cursor jumps has to go here. This path
      // repaints every frame, so each change would start a transition the
      // next frame immediately supersedes - the browser ends up animating
      // toward a target that never arrives, which on a phone is both
      // stuttery and needless work.
      if (layerRef.current) layerRef.current.style.transition = 'none';
      const start = performance.now();
      const tick = (now) => {
        const angle = ((now - start) / 6000) * Math.PI * 2;
        paint(50 + Math.cos(angle) * 18, 50 + Math.sin(angle) * 14);
        raf.current = requestAnimationFrame(tick);
      };
      raf.current = requestAnimationFrame(tick);
      return () => cancelAnimationFrame(raf.current);
    }

    const onMove = (e) => {
      const r = frame.getBoundingClientRect();
      const mx = ((e.clientX - r.left) / r.width) * 100;
      const my = ((e.clientY - r.top) / r.height) * 100;
      cancelAnimationFrame(raf.current);
      raf.current = requestAnimationFrame(() => paint(mx, my));
    };
    const onLeave = () => {
      cancelAnimationFrame(raf.current);
      raf.current = requestAnimationFrame(() => paint(50, 50));
    };

    frame.addEventListener('pointermove', onMove);
    frame.addEventListener('pointerleave', onLeave);
    return () => {
      cancelAnimationFrame(raf.current);
      frame.removeEventListener('pointermove', onMove);
      frame.removeEventListener('pointerleave', onLeave);
    };
  }, [enabled]);

  return { frameRef, layerRef, darkRef };
}

// Grain, as an inline SVG data URI so it costs no request.
const GRAIN =
  "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E\")";

// ── Small pieces ─────────────────────────────────────────────────────────
function Dot({ size = 8, animate }) {
  return (
    <span style={{
      width: size, height: size, borderRadius: '50%', background: NEON,
      display: 'inline-block', flexShrink: 0,
      boxShadow: animate ? `0 0 ${size * 1.6}px ${NEON}` : 'none',
      animation: animate ? 'spotPulse 2s ease-in-out infinite' : 'none',
    }} />
  );
}

function Field({ label, ...props }) {
  return (
    <input
      aria-label={label}
      {...props}
      style={{
        padding: '13px 15px', borderRadius: 10,
        border: '1.5px solid rgba(255,255,255,0.16)',
        background: 'rgba(255,255,255,0.05)', color: '#fff',
        fontSize: 14.5, fontFamily: 'inherit', width: '100%',
        transition: 'border-color 0.15s ease',
      }}
    />
  );
}

// A personal invite link is /i/<user_id>, rewritten to this page by
// vercel.json so the id is still in the path when we read it here. It is only
// ever attribution - the backend drops it unless it names a real account.
function inviterFromPath() {
  const m = window.location.pathname.match(/^\/i\/([A-Za-z0-9_-]{1,64})\/?$/);
  return m ? m[1] : null;
}

// ── Subscribe sheet ──────────────────────────────────────────────────────
function SubscribeSheet({ onClose, animate }) {
  const [picks, setPicks] = React.useState([]);
  const [name, setName] = React.useState('');
  const [email, setEmail] = React.useState('');
  const [city, setCity] = React.useState('');
  const [consent, setConsent] = React.useState(false);
  const [website, setWebsite] = React.useState('');   // honeypot
  const [platform, setPlatform] = React.useState('');
  const [playAccount, setPlayAccount] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');
  const [position, setPosition] = React.useState(null);

  const toggle = (id) =>
    setPicks((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id]));

  const submit = async (e) => {
    e.preventDefault();
    if (busy) return;
    // Asked before consent because it is the cheaper thing to fix: someone who
    // has to re-tick a checkbox they already read is more annoyed than someone
    // told they missed a two-button question.
    if (!platform) { setError('Let us know which phone you have.'); return; }
    if (!consent) { setError('Tick the box to sign up.'); return; }
    setBusy(true); setError('');
    try {
      const res = await fetch(`${API}/subscribe`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          email: email.trim(), name: name.trim(), city: city.trim(),
          use_cases: picks, accepted_privacy: true, website, platform,
          play_account: platform === 'android' ? playAccount.trim() : '',
          invited_by: inviterFromPath(),
        }),
      });
      if (!res.ok) {
        const d = await res.json().catch(() => ({}));
        setError(d.detail || 'Sign-up failed. Please try again in a moment.');
        return;
      }
      const d = await res.json();
      setPosition(d.position || null);
    } catch {
      setError('No connection. Please try again in a moment.');
    } finally {
      setBusy(false);
    }
  };


  return (
    // Fixed, not absolute, so the sheet is bound to the visible viewport.
    //
    // The page frame is `min-height: 100dvh`, and dvh is the viewport with
    // the browser UI retracted. With the URL bar on screen the frame is
    // therefore ~100px taller than what the visitor can actually see (848
    // vs 752 on a 390x844 phone). An absolute `inset: 0` inherits that
    // taller box, so `bottom: 0` put the sheet's lower edge behind the URL
    // bar, and `maxHeight: 92%` resolved to 92% of 848 - just enough to fit
    // the form's 772px without ever triggering `overflow: auto`. No inner
    // scroll, body scroll locked, and the Sign up button stranded 29px past
    // the bottom of the screen.
    //
    // Fixed makes both numbers refer to the real viewport: the 92% becomes
    // 692px against 772px of content, so the sheet scrolls internally and
    // the button is reachable again.
    <div style={{ position: 'fixed', inset: 0, zIndex: 20 }}>
      <div
        onClick={onClose}
        style={{
          position: 'absolute', inset: 0, background: 'rgba(5,5,4,0.7)',
          backdropFilter: 'blur(6px)', animation: 'fadeIn 0.45s ease',
        }}
      />
      <div className="sheet" style={{
        position: 'absolute', left: 0, right: 0, bottom: 0,
        background: '#111110', borderRadius: '26px 26px 0 0',
        borderTop: `1px solid ${NEON}40`, boxShadow: '0 -30px 80px rgba(0,0,0,0.6)',
        maxHeight: '92%', overflow: 'auto', padding: '22px 44px 40px',
        animation: animate ? 'sheetUp 0.5s cubic-bezier(.16,1,.3,1)' : 'none',
      }}>
        <div style={{
          width: 44, height: 5, borderRadius: 99, background: 'rgba(255,255,255,0.22)',
          margin: '0 auto 20px',
        }} />
        <button
          onClick={onClose}
          aria-label="Close"
          style={{
            position: 'absolute', top: 18, right: 22, width: 34, height: 34,
            borderRadius: '50%', border: '1px solid rgba(255,255,255,0.2)',
            background: 'transparent', color: '#fff', fontSize: 17, cursor: 'pointer',
          }}
        >×</button>

        {position !== null ? (
          <Success position={position} />
        ) : (
          <div className="sheetBody">
            <div>
              <div style={{
                fontFamily: "'JetBrains Mono', monospace", fontSize: 11,
                letterSpacing: '0.16em', textTransform: 'uppercase', color: NEON,
              }}>Pick what you'd do</div>
              <h2 style={{
                fontFamily: "'Bricolage Grotesque', sans-serif", fontSize: 34, fontWeight: 600,
                lineHeight: 1.05, letterSpacing: '-0.03em', margin: '10px 0 20px',
              }}>Tell us how you'll use <span style={{ color: NEON }}>SPOT</span>.</h2>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
                {USE_CASES.map((u) => {
                  const on = picks.includes(u.id);
                  return (
                    <button
                      key={u.id}
                      type="button"
                      onClick={() => toggle(u.id)}
                      aria-pressed={on}
                      style={{
                        display: 'flex', alignItems: 'center', gap: 12, textAlign: 'left',
                        padding: '14px 16px', borderRadius: 12, cursor: 'pointer',
                        border: `1.5px solid ${on ? NEON : 'rgba(255,255,255,0.14)'}`,
                        background: on ? 'rgba(216,255,0,0.10)' : 'rgba(255,255,255,0.03)',
                        color: '#fff', font: 'inherit', transition: 'all 0.15s ease',
                      }}
                    >
                      <span style={{
                        width: 26, height: 26, borderRadius: 7, flexShrink: 0,
                        background: on ? NEON : 'rgba(255,255,255,0.1)',
                        color: INK, display: 'flex', alignItems: 'center',
                        justifyContent: 'center', fontWeight: 700, fontSize: 15,
                      }}>{on ? '✓' : ''}</span>
                      <span>
                        <span style={{ display: 'block', fontSize: 15, fontWeight: 600 }}>{u.title}</span>
                        <span style={{ display: 'block', fontSize: 13, opacity: 0.6 }}>{u.desc}</span>
                      </span>
                    </button>
                  );
                })}
              </div>
            </div>

            <form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 12, alignSelf: 'start' }}>
              <Field label="Name" type="text" required placeholder="Your name"
                     value={name} onChange={(e) => setName(e.target.value)} />
              <Field label="Email address" type="email" required placeholder="you@email.com"
                     value={email} onChange={(e) => setEmail(e.target.value)} />
              <Field label="City or region" type="text" placeholder="City / region"
                     value={city} onChange={(e) => setCity(e.target.value)} />

              {/* Which phone, because the two beta invites are not the same
                  thing: iOS testers get a TestFlight link and need Apple's
                  TestFlight app first, Android testers install normally. Asking
                  now is the only way to send each group something that works. */}
              <div role="radiogroup" aria-label="Which phone do you have?">
                <div style={{ fontSize: 12.5, opacity: 0.75, marginBottom: 7 }}>
                  Which phone do you have?
                </div>
                <div style={{ display: 'flex', gap: 8 }}>
                  {[{ id: 'ios', label: 'iPhone' }, { id: 'android', label: 'Android' }].map((p) => {
                    const on = platform === p.id;
                    return (
                      <button
                        key={p.id}
                        type="button"
                        role="radio"
                        aria-checked={on}
                        onClick={() => { setPlatform(p.id); setError(''); }}
                        style={{
                          flex: 1, padding: '12px 14px', borderRadius: 10, cursor: 'pointer',
                          border: `1.5px solid ${on ? NEON : 'rgba(255,255,255,0.16)'}`,
                          background: on ? 'rgba(216,255,0,0.10)' : 'rgba(255,255,255,0.05)',
                          color: '#fff', font: 'inherit', fontSize: 14.5,
                          fontWeight: on ? 600 : 400, transition: 'all 0.15s ease',
                        }}
                      >{p.label}</button>
                    );
                  })}
                </div>
              </div>

              {/* Android access is granted per Google account, and a signup
                  address often is not one - Play then answers "app not
                  available", which reads as a broken link rather than as the
                  wrong account. Asking here beats chasing people by email
                  later. Optional on purpose: not knowing your Play account
                  must not cost you the signup, and an empty value falls back
                  to the address above. */}
              {platform === 'android' && (
                <div>
                  <div style={{ fontSize: 12.5, opacity: 0.75, marginBottom: 7 }}>
                    Google account on your phone
                  </div>
                  <Field
                    label="Google account on your phone"
                    type="email"
                    placeholder="you@gmail.com"
                    value={playAccount}
                    onChange={(e) => setPlayAccount(e.target.value)}
                  />
                  <div style={{ fontSize: 11.5, opacity: 0.55, marginTop: 6, lineHeight: 1.45 }}>
                    Android testing runs through this account. Open the Play Store,
                    tap your picture, and it is at the top. Leave empty if it is the
                    same as your email above.
                  </div>
                </div>
              )}

              {/* Honeypot: off-screen rather than display:none, which some
                  bots skip. A human never reaches it - no tab stop, no label. */}
              <input
                type="text" tabIndex={-1} autoComplete="off" aria-hidden="true"
                value={website} onChange={(e) => setWebsite(e.target.value)}
                style={{ position: 'absolute', left: '-9999px', width: 1, height: 1, opacity: 0 }}
              />

              <label style={{
                display: 'flex', gap: 10, alignItems: 'flex-start',
                fontSize: 12.5, lineHeight: 1.45, opacity: 0.75, cursor: 'pointer',
              }}>
                <input
                  type="checkbox" checked={consent}
                  onChange={(e) => { setConsent(e.target.checked); setError(''); }}
                  style={{ marginTop: 2, width: 16, height: 16, accentColor: NEON, flexShrink: 0 }}
                />
                {/* Must stay word-for-word identical to WAITLIST_CONSENT_TEXT
                    in the backend, which stores it as consent evidence. */}
                <span>
                  I agree to the{' '}
                  <a href={PRIVACY_URL} style={{ color: NEON }}>privacy policy</a>{' '}
                  and want an email when Spot is available.
                </span>
              </label>

              {error ? <div style={{ fontSize: 13, color: '#ff6b6b' }}>{error}</div> : null}

              <button
                type="submit"
                disabled={busy}
                style={{
                  padding: '14px 16px', borderRadius: 10, border: 'none',
                  background: NEON, color: INK, fontSize: 15, fontWeight: 700,
                  fontFamily: 'inherit', cursor: busy ? 'default' : 'pointer',
                  opacity: busy ? 0.6 : 1,
                }}
              >{busy ? 'Working…' : 'Sign up'}</button>

              <div style={{ fontSize: 12, opacity: 0.5, textAlign: 'center' }}>
                No spam. One email at launch. Unsubscribe anytime.
              </div>
            </form>
          </div>
        )}
      </div>
    </div>
  );
}

function Success({ position }) {
  return (
    <div style={{
      display: 'flex', flexDirection: 'column', alignItems: 'center',
      gap: 14, padding: '30px 0 18px', textAlign: 'center',
    }}>
      <div style={{
        width: 46, height: 46, borderRadius: '50%', background: NEON, color: INK,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        fontSize: 24, fontWeight: 700,
      }}>✓</div>
      <div style={{
        fontFamily: "'Bricolage Grotesque', sans-serif", fontSize: 26, fontWeight: 600,
      }}>You're on the list.</div>
      <div style={{ fontSize: 14.5, opacity: 0.7, maxWidth: 420, lineHeight: 1.5 }}>
        We'll email you as soon as Spot is available.
        {position ? <> You're <b style={{ color: NEON }}>#{position.toLocaleString('en-GB')}</b> in line.</> : null}
      </div>
    </div>
  );
}

// ── Page ─────────────────────────────────────────────────────────────────
function Landing() {
  // One source of truth for motion: the OS setting. No manual toggle - a
  // preference the visitor already expressed beats one we invent.
  const [animate] = React.useState(
    () => !window.matchMedia?.('(prefers-reduced-motion: reduce)').matches,
  );
  const { frameRef, layerRef, darkRef } = useSpotlight(animate);
  // spotevent.be/#signup opens straight into the form, so a link in a social
  // bio or a story can skip the landing screen.
  const [sheet, setSheet] = React.useState(() => window.location.hash === '#signup');
  const [count, setCount] = React.useState(null);
  const [hover, setHover] = React.useState(false);
  const [invitedBy] = React.useState(inviterFromPath);

  React.useEffect(() => {
    let alive = true;
    fetch(`${API}/subscribe/count`)
      .then((r) => (r.ok ? r.json() : null))
      .then((d) => { if (alive && d && typeof d.count === 'number') setCount(d.count); })
      .catch(() => {});   // no counter is fine; a broken page is not
    return () => { alive = false; };
  }, []);

  React.useEffect(() => {
    document.body.style.overflow = sheet ? 'hidden' : '';
    return () => { document.body.style.overflow = ''; };
  }, [sheet]);

  return (
    <div ref={frameRef} style={{
      position: 'relative', width: '100%', minHeight: '100dvh',
      display: 'flex', flexDirection: 'column', overflow: 'hidden', background: INK,
    }}>
      <div ref={layerRef} style={{
        position: 'absolute', inset: 0,
        transition: animate ? 'background 0.4s cubic-bezier(.2,.8,.3,1)' : 'none',
      }} />
      <div ref={darkRef} style={{ position: 'absolute', inset: 0 }} />
      <div style={{
        position: 'absolute', inset: 0, backgroundImage: GRAIN,
        opacity: 0.35, mixBlendMode: 'overlay', pointerEvents: 'none',
      }} />

      <div style={{ position: 'relative', display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
        <header className="gutter topbar" style={{
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          padding: '28px 48px', fontFamily: "'JetBrains Mono', monospace",
          fontSize: 13, letterSpacing: '0.08em', textTransform: 'uppercase',
        }}>
          <span style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
            <Dot size={7} animate={animate} />spotevent.be
          </span>
          <span style={{ opacity: 0.55 }}>Release date: 15th of September</span>
        </header>

        <main className="gutter" style={{
          flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center',
          justifyContent: 'center', gap: 28, padding: '8px 48px 24px', textAlign: 'center',
        }}>
          <SpotMark size={80} glow={animate} accent={NEON} style={{ margin: '0 auto 18px' }} />

          {invitedBy && (
            // Someone arriving on a personal link should see that the link
            // worked. We deliberately do not name the inviter: their name is
            // in the message they sent, and looking it up here would mean a
            // public endpoint that turns any user_id into a name.
            <p style={{
              margin: 0, fontSize: 14, fontWeight: 600, letterSpacing: '-0.01em',
              color: NEON, background: 'rgba(216,255,0,0.08)',
              border: '1px solid rgba(216,255,0,0.25)',
              borderRadius: 99, padding: '7px 16px',
            }}>
              You have been invited to Spot
            </p>
          )}

          <h1 className="hero" style={{
            fontFamily: "'Bricolage Grotesque', sans-serif", fontWeight: 700,
            fontSize: 76, lineHeight: 1, letterSpacing: '-0.045em',
            fontVariationSettings: '"opsz" 96', margin: 0,
          }}>
            <span style={{
              color: NEON, letterSpacing: '-0.05em',
              textShadow: animate ? `0 0 36px ${NEON}8c` : 'none',
            }}>SPOT</span>. On.
          </h1>

          <p style={{ fontSize: 16, opacity: 0.78, maxWidth: 560, marginTop: 16, lineHeight: 1.5 }}>
            Find your next night out. A social app for discovering, hosting and joining
            the next events - from festivals to the smallest house party.
          </p>

          <div style={{ display: 'flex', flexDirection: 'column', gap: 14, alignItems: 'center' }}>
            <button
              onClick={() => setSheet(true)}
              onMouseEnter={() => setHover(true)}
              onMouseLeave={() => setHover(false)}
              style={{
                border: 'none', borderRadius: 99, padding: '15px 30px',
                fontFamily: 'inherit', fontSize: 16, fontWeight: 700,
                letterSpacing: '-0.01em', color: INK, cursor: 'pointer',
                background: hover
                  ? `radial-gradient(circle at 80% 60%, #eaff4d 0%, ${NEON} 45%, #9fd400 100%)`
                  : NEON,
                backgroundSize: '160% 160%',
                boxShadow: !animate ? 'none'
                  : hover ? `0 0 46px ${NEON}8c` : `0 0 30px ${NEON}4d`,
                transform: hover ? 'translateY(-2px)' : 'none',
                transition: 'all 0.5s cubic-bezier(.2,.8,.3,1)',
              }}
            >Sign up for early access</button>

            {/* Hidden until the backend says the number is worth showing. */}
            {count !== null ? (
              <div style={{ display: 'flex', alignItems: 'center', gap: 9, fontSize: 13.5, opacity: 0.72 }}>
                <Dot animate={animate} />
                <span><b style={{ color: NEON }}>{count.toLocaleString('en-GB')}</b> people already signed up</span>
              </div>
            ) : null}
          </div>
        </main>

        <section className="features gutter" style={{
          display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 28,
          padding: '24px 48px 28px', borderTop: '1px solid rgba(255,255,255,0.12)',
        }}>
          {FEATURES.map((f) => (
            <div key={f.n} style={{ display: 'flex', gap: 12 }}>
              <span style={{
                fontFamily: "'JetBrains Mono', monospace", fontSize: 11, color: NEON,
                letterSpacing: '0.1em', minWidth: 22,
              }}>{f.n}</span>
              <span>
                <span style={{ display: 'block', fontSize: 14, fontWeight: 600 }}>{f.title}</span>
                <span style={{ display: 'block', fontSize: 12.5, opacity: 0.62, lineHeight: 1.4 }}>{f.desc}</span>
              </span>
            </div>
          ))}
        </section>

        <footer className="footer gutter" style={{
          display: 'grid', gridTemplateColumns: '1fr auto 1fr', alignItems: 'center', gap: 20,
          padding: '14px 48px', borderTop: '1px solid rgba(255,255,255,0.1)',
          fontFamily: "'JetBrains Mono', monospace", fontSize: 11.5,
          letterSpacing: '0.04em', color: 'rgba(255,255,255,0.55)',
        }}>
          <span style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <span style={{ opacity: 0.55 }}>Follow us</span>
            <a className="social" href="https://www.instagram.com/spotevent.be/" target="_blank" rel="noopener"
               aria-label="Instagram">IG</a>
            {/* Points at Instagram until the TikTok account exists - a live
                link to the wrong network beats a 404 on a launch page. */}
            <a className="social" href="https://www.instagram.com/spotevent.be/" target="_blank" rel="noopener"
               aria-label="TikTok">TT</a>
          </span>
          <span style={{ display: 'flex', alignItems: 'center', gap: 8, justifyContent: 'center' }}>
            <SpotMark size={14} accent={NEON} />
            © {new Date().getFullYear()} SPOT
          </span>
          <span style={{ textAlign: 'right', display: 'flex', gap: 16, justifyContent: 'flex-end' }}>
            {/* Consent has to be as easy to withdraw as it was to give, so the
                banner needs a way back once it has been dismissed. The handler
                is delegated in assets/consent.js - hence the data attribute
                rather than an onClick. */}
            <a href="#" data-spot-consent-open="" style={{
              color: 'rgba(255,255,255,0.7)', textDecoration: 'none',
              borderBottom: `1px solid ${NEON}80`, paddingBottom: 1,
            }}>Cookie settings</a>
            <a href={DISCOVER_URL} style={{
              color: 'rgba(255,255,255,0.7)', textDecoration: 'none',
              borderBottom: `1px solid ${NEON}80`, paddingBottom: 1,
            }}>Browse events</a>
            <a href={SUPPORT_URL} style={{
              color: 'rgba(255,255,255,0.7)', textDecoration: 'none',
              borderBottom: `1px solid ${NEON}80`, paddingBottom: 1,
            }}>Support</a>
            <a href={PRIVACY_URL} style={{
              color: 'rgba(255,255,255,0.7)', textDecoration: 'none',
              borderBottom: `1px solid ${NEON}80`, paddingBottom: 1,
            }}>Privacy policy</a>
          </span>
        </footer>
      </div>

      {sheet ? (
        <SubscribeSheet animate={animate} onClose={() => setSheet(false)} />
      ) : null}
    </div>
  );
}

window.Landing = Landing;
