// SCN2A Australia — "Add your family" (Join) page.
//
// FIVE steps, each one saved the moment it is completed:
//   1 About you            4 The Family Map (optional)
//   2 The person with SCN2A   — or, if bereaved, a single In Memory question
//   3 Address and their doctor 5 Staying connected
// ...then a Done screen. See docs/intake-forms-redesign.md for why the form
// asks what it asks, and docs/intake-payload-contract.md for the exact wire
// format below — that contract is binding; this file must not invent fields.
//
// SAVE-AS-YOU-GO, AND WHY CONSENT MOVED TO STEP 1
// Earlier builds of this form had one submit button at the end: a family who
// stopped partway gave the org nothing at all, and consent was asked for on
// the very last screen after three screens of a child's details had already
// been typed in. Both of those are gone. Every completed step is POSTed to
// /api/join on its own, so an abandoned form still leaves a contactable
// record. That only works honestly if consent is asked for before anything is
// stored — so Q8, the storage consent checkbox, sits on step 1 and step 1
// cannot be left without it. api/join.js re-checks this server-side; it never
// trusts the client alone.
//
// ONE submissionId FOR THE WHOLE FORM
// A UUID v4 is minted the moment step 1's POST succeeds (or reused if one
// already exists — see ensureSubmissionId) and sent with every subsequent
// POST. n8n upserts on submissionId, so five POSTs from one family produce
// one Notion record, not five. It lives in sessionStorage ONLY as that one
// opaque id — never the payload itself. See JOIN_PENDING_KEY below for why
// that boundary is absolute.
//
// A FAILED POST NEVER SILENTLY ADVANCES
// If a step's POST fails, the family stays on that step and sees an inline
// "try again" banner. Nothing is assumed saved that was not confirmed saved.
//
// WHY NOTHING IS EVER WRITTEN TO localStorage
// Earlier builds parked the whole payload in localStorage under
// JOIN_PENDING_KEY after a failed submit — a child's full name, exact date of
// birth, home address, phone, variant and, in the old design, seizure
// history — on a key nothing ever read back. It rescued no submissions and
// simply left that record sitting in whatever browser the family happened to
// use, including a shared or library machine, until they manually cleared
// site data. That key is still purged on mount for anyone who still has one.
// This rewrite does not repeat the mistake in a new shape: sessionStorage
// holds the submissionId and nothing else, ever.
//
// BEREAVEMENT
// A quiet, optional tick under step 2, never a question put to every family.
// Ticking it swaps step 4's map for a single "would you like them remembered"
// question, moves Bereavement support to the top of step 5's needs list, and
// flags the record for a person to read. Nothing about it is ever published
// automatically, on this screen or afterwards — see the Done screen below.

const JOIN_ENDPOINT = '/api/join';
// Legacy only. Previously held a full submission after a failed POST, on a
// key no code ever read. Retained solely so JoinPage can delete it on mount.
const JOIN_PENDING_KEY = 'scn2a-join-pending';
// Holds ONLY the submissionId (a UUID v4) for the life of this browser tab —
// never the form's answers. See the file header for why that line is firm.
const JOIN_SESSION_ID_KEY = 'scn2a-join-submission-id';
const JOIN_SUPPORT_EMAIL = 'info@scn2aaustralia.org';

const JOIN_STATES = ['NSW', 'VIC', 'QLD', 'SA', 'WA', 'TAS', 'ACT', 'NT', 'Outside Australia'];
const JOIN_RELATIONSHIPS = ['Parent or guardian', 'Self (adult with SCN2A)', 'Other family member', 'Other'];
const JOIN_VARIANT_TYPES = ['Gain of function', 'Loss of function', 'Mixed', 'Other or unsure'];
const JOIN_DOCTOR_SPECIALTIES = ['Neurologist', 'Paediatrician', 'Geneticist', 'GP', 'Other'];
const JOIN_MAP_TOPICS = ['Diagnosis and the early days', 'Seizures and medication', 'Therapies and interventions', 'School and NDIS', 'Daily life and routines', 'Being an adult with SCN2A'];
const JOIN_NEEDS_BASE = ['Someone to talk to', 'Information about SCN2A', 'Help with NDIS or funding', 'Connecting with other families', 'Research and studies', 'Just keeping in touch for now'];
const JOIN_NATURAL_HISTORY = ['Yes', 'No', 'Tell me more'];
const JOIN_RESEARCH_CONTACT = ['Yes', 'No', 'Maybe, tell me more'];
const JOIN_PEER_SUPPORT = [
  'I would like to meet families near me or with a similar presentation',
  'I would like to be a peer support for another family',
  'I would like to receive a peer support partner',
];
const JOIN_VOLUNTEER = ['Yes', 'Not at this stage', 'Tell me more'];
const JOIN_MEMORIAL = ['Yes', 'No', 'Not right now'];

const JOIN_EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;

// A UUID v4, minted client-side. Prefer the browser's own generator; fall
// back to a manual one for older browsers so the form never depends on it.
function joinUuidV4() {
  if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') return crypto.randomUUID();
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
    const r = (Math.random() * 16) | 0;
    const v = c === 'x' ? r : (r & 0x3) | 0x8;
    return v.toString(16);
  });
}

// ---- Shared field styling (mirrors PinPreviewForm on the Family Map page) ----
const joinFieldStyle = (error) => ({
  width: '100%', boxSizing: 'border-box', padding: '10px 12px',
  border: `1px solid ${error ? '#8E3B5E' : '#D5CFBF'}`, borderRadius: 5,
  fontFamily: 'inherit', fontSize: 14, color: '#0D1B2A', background: '#fff',
});
const JOIN_LABEL_STYLE = { display: 'block', fontSize: 11, fontWeight: 600, color: '#0D1B2A', marginBottom: 6 };
const JOIN_HINT_STYLE = { fontSize: 11.5, color: '#6B6659', lineHeight: 1.55, margin: '6px 0 0' };
const JOIN_ERROR_STYLE = { fontSize: 12.5, color: '#8E3B5E', marginTop: 6, lineHeight: 1.4 };

// One labelled field: label (+ optional tag), the control, then hint and error.
function JoinField({ id, label, optional, hint, error, children, style }) {
  return (
    <div style={{ marginBottom: 18, ...style }}>
      <label htmlFor={id} style={JOIN_LABEL_STYLE}>
        {label}{optional && <span style={{ fontWeight: 500, color: '#6B6659' }}> (optional)</span>}
      </label>
      {children}
      {hint && <p style={JOIN_HINT_STYLE}>{hint}</p>}
      {error && <div role="alert" style={JOIN_ERROR_STYLE}>{error}</div>}
    </div>
  );
}

// A small eyebrow divider used to group fields inside a step.
function JoinGroupLabel({ children }) {
  return (
    <div className="eyebrow" style={{ color: '#6B6659', margin: '26px 0 14px', paddingTop: 18, borderTop: '1px solid #E8E3DA' }}>
      {children}
    </div>
  );
}

// A framed consent/opt-in checkbox. `tone` shifts the frame: 'map' (soft
// blue) or 'registry' (sand). When `error` is set the frame turns mulberry.
function JoinConsent({ id, checked, onChange, tone, error, children }) {
  const bg = tone === 'map' ? '#E5EDF8' : '#EFEAE1';
  return (
    <div style={{ background: bg, border: `1px solid ${error ? '#8E3B5E' : '#D5CFBF'}`, borderRadius: 5, padding: '14px 16px', marginTop: 6 }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', gap: 10 }}>
        <input id={id} type="checkbox" checked={checked} onChange={onChange}
          style={{ marginTop: 2, width: 17, height: 17, flexShrink: 0, accentColor: '#1E3A6E', cursor: 'pointer' }} />
        <label htmlFor={id} style={{ fontSize: 13, lineHeight: 1.55, color: '#0D1B2A', cursor: 'pointer' }}>
          {children}
        </label>
      </div>
      {error && <div role="alert" style={{ ...JOIN_ERROR_STYLE, marginLeft: 27 }}>{error}</div>}
    </div>
  );
}

// Inline retry banner shown on a step whose POST just failed. Reuses the same
// mulberry-accented banner language as the rest of the form's error states —
// this is new behaviour (per-step saving needs a way to fail without silently
// advancing) but it should not look like a new design.
function JoinRetryBanner({ message, onRetry, busy }) {
  return (
    <div role="alert" style={{ display: 'flex', flexWrap: 'wrap', gap: 12, alignItems: 'center', justifyContent: 'space-between', background: '#EFEAE1', border: '1px solid #D5CFBF', borderLeft: '3px solid #8E3B5E', borderRadius: 5, padding: '14px 16px', marginTop: 16 }}>
      <p style={{ fontSize: 13, lineHeight: 1.55, color: '#0D1B2A', margin: 0, flex: '1 1 260px' }}>{message}</p>
      <Button variant="mulberry" onClick={onRetry} disabled={busy}>{busy ? 'Trying again…' : 'Try again →'}</Button>
    </div>
  );
}

// ---- Progress indicator: five steps, plus Done ----
function JoinProgress({ step }) {
  const labels = ['About you', 'The person with SCN2A', 'Address & doctor', 'Family Map', 'Staying connected', 'Done'];
  return (
    <ol aria-label={`Step ${Math.min(step, 4) + 1} of 5`} style={{ listStyle: 'none', display: 'flex', alignItems: 'flex-start', margin: '0 0 28px', padding: 0 }}>
      {labels.map((label, i) => {
        const done = i < step;
        const current = i === step;
        return (
          <li key={label} aria-current={current ? 'step' : undefined}
            style={{ flex: i < labels.length - 1 ? 1 : '0 0 auto', display: 'flex', alignItems: 'flex-start' }}>
            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: 62 }}>
              <span aria-hidden="true" style={{
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                width: 28, height: 28, borderRadius: 999, fontSize: 12.5, fontWeight: 800,
                background: done || current ? '#1E3A6E' : '#fff',
                color: done || current ? '#fff' : '#6B6659',
                border: `2px solid ${done || current ? '#1E3A6E' : '#D5CFBF'}`,
                transition: 'background 200ms, border-color 200ms',
              }}>{done ? '✓' : i + 1}</span>
              <span style={{ marginTop: 7, fontSize: 10.5, fontWeight: current ? 700 : 600, color: current ? '#0D1B2A' : '#6B6659', letterSpacing: '0.01em', textAlign: 'center', lineHeight: 1.3 }}>
                {label}{i === 3 && <span style={{ display: 'block', fontWeight: 500, color: '#6B6659' }}>optional</span>}
              </span>
            </div>
            {i < labels.length - 1 && (
              <span aria-hidden="true" style={{ flex: 1, height: 2, background: i < step ? '#1E3A6E' : '#D5CFBF', margin: '13px -10px 0', borderRadius: 2, transition: 'background 200ms' }} />
            )}
          </li>
        );
      })}
    </ol>
  );
}

// ---- The page ----
function JoinPage({ navigate }) {
  // 0-4 are the five steps (matching the contract's step 1-5); 5 is Done.
  const [step, setStep] = React.useState(0);

  // Step 1 (index 0) — about you. `consent` is the storage consent required
  // to leave this step, and is re-sent with every later POST per the contract.
  const [about, setAboutState] = React.useState({
    firstName: '', lastName: '', email: '', phone: '', relationship: '', state: '', suburb: '',
    consent: false, newsletter: false,
  });
  // Step 2 (index 1) — the person with SCN2A, plus the quiet bereavement opt-in.
  const [person, setPersonState] = React.useState({
    personFirstName: '', personLastName: '', personDob: '', variantType: '', variant: '',
    deceased: false, deceasedDate: '',
  });
  // Step 3 (index 2) — address and their doctor.
  const [address, setAddressState] = React.useState({
    streetAddress: '', addressSuburb: '', addressState: '', postcode: '', country: 'Australia',
    doctorName: '', doctorClinic: '', doctorSpecialty: '', doctorState: '',
    secondContactName: '', secondContactDetail: '',
  });
  // Step 4 (index 3) — the Family Map, or (if deceased) the In Memory question.
  const [mapStep, setMapStepState] = React.useState({
    mapConsent: false, mapTopics: [], mapBlurb: '', openToMessages: false, memorialConsent: '',
  });
  // Step 5 (index 4) — staying connected.
  const [connect, setConnectState] = React.useState({
    needs: [], naturalHistoryStudy: '', researchContact: '', peerSupport: [], volunteer: '', anythingElse: '',
  });

  const [errors, setErrors] = React.useState({});
  const [submitting, setSubmitting] = React.useState(false);
  const [submitError, setSubmitError] = React.useState(null);
  const [website, setWebsite] = React.useState(''); // honeypot — humans never see it
  // Whether step 1's POST has ever succeeded. Every later step blocks on its
  // own POST succeeding before it will advance, so in normal use this is
  // already guaranteed true by the time anyone reaches the Done screen — it
  // is kept as a defensive check so that screen never has to lie.
  const [step1Saved, setStep1Saved] = React.useState(false);

  // In-memory fallback if sessionStorage throws (private browsing, storage
  // disabled). The id still never touches localStorage or the payload.
  const fallbackIdRef = React.useRef(null);

  // Purge the legacy pending-submission key. Anyone whose POST failed under an
  // earlier build still has their child's full clinical and address record in
  // this browser, unread and unexpiring. Nothing recovers it, so remove it.
  React.useEffect(() => {
    try { localStorage.removeItem(JOIN_PENDING_KEY); } catch (e) { /* ignore storage errors */ }
  }, []);

  const setAbout = (patch) => { setAboutState(a => ({ ...a, ...patch })); setErrors({}); setSubmitError(null); };
  const setPerson = (patch) => { setPersonState(p => ({ ...p, ...patch })); setErrors({}); setSubmitError(null); };
  const setAddress = (patch) => { setAddressState(a => ({ ...a, ...patch })); setErrors({}); setSubmitError(null); };
  const setMap = (patch) => { setMapStepState(m => ({ ...m, ...patch })); setErrors({}); setSubmitError(null); };
  const setConnect = (patch) => { setConnectState(c => ({ ...c, ...patch })); setErrors({}); setSubmitError(null); };
  const toggleIn = (list, value) => (list.includes(value) ? list.filter(v => v !== value) : [...list, value]);

  const scrollToFlow = () => {
    const el = document.getElementById('join-flow');
    if (!el) return;
    const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    el.scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block: 'start' });
  };
  const goTo = (n) => { setStep(n); setErrors({}); setSubmitError(null); scrollToFlow(); };

  // ---- submissionId: minted once, kept in sessionStorage as an id only ----
  const ensureSubmissionId = () => {
    try {
      let id = sessionStorage.getItem(JOIN_SESSION_ID_KEY);
      if (!id) { id = joinUuidV4(); sessionStorage.setItem(JOIN_SESSION_ID_KEY, id); }
      return id;
    } catch (e) {
      if (!fallbackIdRef.current) fallbackIdRef.current = joinUuidV4();
      return fallbackIdRef.current;
    }
  };

  // ---- POST one step's fields. Never advances the step itself — callers do
  // that only once this resolves true. ----
  const postStep = async (stepNumber, complete, fields) => {
    const payload = {
      submissionId: ensureSubmissionId(),
      step: stepNumber,
      complete: !!complete,
      consent: !!about.consent,
      source: 'website-join-form',
      website, // honeypot — always '' for humans
      ...fields,
    };
    setSubmitting(true);
    setSubmitError(null);
    try {
      const res = await fetch(JOIN_ENDPOINT, {
        method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload),
      });
      if (!res.ok) {
        // The handler explains every rejection in JSON. Throwing that away and
        // blaming the network is how a 400 that fired on every single
        // submission came to look like a flaky connection: the message said
        // "try again", trying again did the same thing, and the real reason
        // was sitting unread in the response body.
        let reason = '';
        try { const b = await res.json(); reason = (b && b.error) || ''; } catch (e) { /* no JSON body */ }
        const rejected = new Error(reason || `Save failed (HTTP ${res.status}).`);
        rejected.serverRejected = true;
        throw rejected;
      }
      setSubmitting(false);
      return true;
    } catch (err) {
      // Always leave a trace. Nothing here reaches the family, and without it
      // the only symptom is a banner that names the wrong cause.
      console.error(`[join] step ${stepNumber} did not save:`, err);
      setSubmitting(false);
      setSubmitError(err && err.serverRejected
        ? `That did not save: ${err.message} Your answers on this screen are still here, nothing is lost. If it keeps happening, please email ${JOIN_SUPPORT_EMAIL} and we will add your family by hand.`
        : 'That did not save, sorry, likely just a dropped connection. Your answers on this screen are still here, nothing is lost. Please try again.');
      return false;
    }
  };

  // ---- Step 1 — About you ----
  const validateAbout = () => {
    const e = {};
    if (!about.firstName.trim()) e.firstName = 'Please tell us your first name.';
    if (!about.lastName.trim()) e.lastName = 'Please tell us your last name.';
    if (!about.email.trim()) e.email = 'Please enter your email address.';
    else if (!JOIN_EMAIL_RE.test(about.email.trim())) e.email = 'That email address does not look right, please check it.';
    if (!about.phone.trim()) e.phone = 'Please enter a phone number.';
    if (!about.relationship) e.relationship = 'Please choose the option closest to you.';
    if (!about.state) e.state = 'Please choose your state or territory.';
    if (!about.suburb.trim()) e.suburb = 'Please tell us your suburb or town.';
    if (!about.consent) e.consent = 'We need this to save your answers as you go. Please tick the box to continue.';
    return e;
  };
  const continueFromAbout = async () => {
    const e = validateAbout();
    if (Object.keys(e).length) { setErrors(e); return; }
    setErrors({});
    const ok = await postStep(1, false, {
      firstName: about.firstName.trim().slice(0, 80),
      lastName: about.lastName.trim().slice(0, 80),
      email: about.email.trim().slice(0, 160),
      phone: about.phone.trim().slice(0, 40),
      relationship: about.relationship,
      state: about.state,
      suburb: about.suburb.trim().slice(0, 80),
      newsletter: !!about.newsletter,
    });
    if (ok) { setStep1Saved(true); goTo(1); }
  };

  // ---- Step 2 — The person with SCN2A ----
  const validatePerson = () => {
    const e = {};
    if (!person.personFirstName.trim()) e.personFirstName = 'Please tell us their first name.';
    if (!person.personLastName.trim()) e.personLastName = 'Please tell us their last name.';
    if (!person.personDob) e.personDob = 'Please enter their date of birth.';
    else if (new Date(person.personDob) > new Date()) e.personDob = 'Please check the date of birth.';
    if (!person.variantType) e.variantType = 'Please choose an option, "Other or unsure" is fine.';
    if (person.deceased && person.deceasedDate && new Date(person.deceasedDate) > new Date()) e.deceasedDate = 'Please check this date.';
    return e;
  };
  const continueFromPerson = async () => {
    const e = validatePerson();
    if (Object.keys(e).length) { setErrors(e); return; }
    setErrors({});
    // deceasedDate is left out entirely unless the family ticked the
    // bereavement box and gave a date. Sending '' for everyone else is what
    // made the server's date check reject every ordinary step 2 — absent and
    // empty are not the same thing (docs/intake-payload-contract.md).
    const personFields = {
      personFirstName: person.personFirstName.trim().slice(0, 80),
      personLastName: person.personLastName.trim().slice(0, 80),
      personDob: person.personDob,
      variantType: person.variantType,
      variant: person.variant.trim().slice(0, 120),
      deceased: !!person.deceased,
    };
    if (person.deceased && person.deceasedDate) personFields.deceasedDate = person.deceasedDate;
    const ok = await postStep(2, false, personFields);
    if (ok) {
      // Address & state are pre-filled from step 1, once, without overwriting
      // anything the family has already typed here themselves.
      setAddressState(a => ({
        ...a,
        addressSuburb: a.addressSuburb || about.suburb,
        addressState: a.addressState || about.state,
      }));
      goTo(2);
    }
  };

  // ---- Step 3 — Address and their doctor ----
  const validateAddress = () => {
    const e = {};
    if (!address.streetAddress.trim()) e.streetAddress = 'Please enter a street address.';
    if (!address.addressSuburb.trim()) e.addressSuburb = 'Please enter a suburb or town.';
    if (!address.addressState) e.addressState = 'Please choose a state or territory.';
    if (!address.postcode.trim()) e.postcode = 'Please enter a postcode.';
    if (!address.country.trim()) e.country = 'Please enter a country.';
    if (!address.doctorName.trim()) e.doctorName = "Please enter their doctor's name.";
    return e;
  };
  const continueFromAddress = async () => {
    const e = validateAddress();
    if (Object.keys(e).length) { setErrors(e); return; }
    setErrors({});
    const ok = await postStep(3, false, {
      streetAddress: address.streetAddress.trim().slice(0, 200),
      addressSuburb: address.addressSuburb.trim().slice(0, 80),
      addressState: address.addressState,
      postcode: address.postcode.trim().slice(0, 12),
      country: address.country.trim().slice(0, 80),
      doctorName: address.doctorName.trim().slice(0, 120),
      doctorClinic: address.doctorClinic.trim().slice(0, 160),
      doctorSpecialty: address.doctorSpecialty,
      doctorState: address.doctorState,
      secondContactName: address.secondContactName.trim().slice(0, 120),
      secondContactDetail: address.secondContactDetail.trim().slice(0, 160),
    });
    if (ok) goTo(3);
  };

  // ---- Step 4 — The Family Map (optional), or the In Memory question ----
  // Nothing here is required: mapTopics and mapBlurb stay optional even once
  // mapConsent is ticked (a pin with just a first name and a general area is
  // worth having; today's mandatory wall in front of a pin is the thing this
  // change removes). Unavailable outside Australia, said where the box would
  // be. If the person has died, this step is replaced entirely.
  const mapUnavailable = !person.deceased && about.state === 'Outside Australia';
  const buildStep4Fields = () => {
    if (person.deceased) return { memorialConsent: mapStep.memorialConsent || '' };
    if (mapUnavailable) return { mapConsent: false, mapTopics: [], mapBlurb: '', openToMessages: false };
    return {
      mapConsent: !!mapStep.mapConsent,
      mapTopics: mapStep.mapConsent ? mapStep.mapTopics : [],
      mapBlurb: mapStep.mapConsent ? mapStep.mapBlurb.trim().slice(0, 200) : '',
      openToMessages: mapStep.mapConsent ? !!mapStep.openToMessages : false,
    };
  };
  const continueFromMap = async () => {
    setErrors({});
    const ok = await postStep(4, false, buildStep4Fields());
    if (ok) goTo(4);
  };

  // ---- Step 5 — Staying connected ----
  const validateConnected = () => {
    const e = {};
    if (!connect.naturalHistoryStudy) e.naturalHistoryStudy = 'Please choose an option, "Tell me more" is fine.';
    return e;
  };
  const continueFromConnected = async () => {
    const e = validateConnected();
    if (Object.keys(e).length) { setErrors(e); return; }
    setErrors({});
    const ok = await postStep(5, true, {
      needs: connect.needs,
      naturalHistoryStudy: connect.naturalHistoryStudy,
      researchContact: connect.researchContact,
      peerSupport: connect.peerSupport,
      volunteer: connect.volunteer,
      anythingElse: connect.anythingElse.trim().slice(0, 2000),
    });
    if (ok) goTo(5);
  };

  // ---- Shared step chrome ----
  const stepHeader = (stepNo, title, intro) => (
    <div style={{ marginBottom: 22 }}>
      <div className="eyebrow" style={{ color: '#4A7EC7', marginBottom: 8 }}>{stepNo}</div>
      <h2 className="h2-mobile" style={{ fontSize: 24, fontWeight: 700, letterSpacing: '-0.01em', margin: '0 0 8px', color: '#0D1B2A' }}>{title}</h2>
      {intro && <p style={{ fontSize: 14.5, lineHeight: 1.6, color: '#6B6659', margin: 0 }}>{intro}</p>}
    </div>
  );

  const navRow = (children) => (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, justifyContent: 'space-between', alignItems: 'center', marginTop: 26, paddingTop: 20, borderTop: '1px solid #E8E3DA' }}>
      {children}
    </div>
  );

  // ---- Step bodies ----
  const renderAbout = () => (
    <div className="fmap-rise" key="s0">
      {stepHeader('Step 1 of 5 · About you', "Let's start with you.", 'A few quick things so we know who we are talking to, and can save your answers as you go.')}

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }} className="stack-mobile">
        <JoinField id="join-first-name" label="Your first name" error={errors.firstName}>
          <input id="join-first-name" type="text" value={about.firstName} maxLength={80} autoComplete="given-name"
            onChange={(e) => setAbout({ firstName: e.target.value })} placeholder="e.g. Sam" style={joinFieldStyle(errors.firstName)} />
        </JoinField>
        <JoinField id="join-last-name" label="Your last name" error={errors.lastName}>
          <input id="join-last-name" type="text" value={about.lastName} maxLength={80} autoComplete="family-name"
            onChange={(e) => setAbout({ lastName: e.target.value })} placeholder="e.g. Nguyen" style={joinFieldStyle(errors.lastName)} />
        </JoinField>
      </div>

      <JoinField id="join-email" label="Your email" error={errors.email} hint="Where your welcome email and anything you opt into will arrive.">
        <input id="join-email" type="email" value={about.email} maxLength={160} autoComplete="email"
          onChange={(e) => setAbout({ email: e.target.value })} placeholder="you@example.com" style={joinFieldStyle(errors.email)} />
      </JoinField>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }} className="stack-mobile">
        <JoinField id="join-phone" label="Your phone" error={errors.phone}>
          <input id="join-phone" type="tel" value={about.phone} maxLength={40} autoComplete="tel"
            onChange={(e) => setAbout({ phone: e.target.value })} placeholder="e.g. 0400 000 000" style={joinFieldStyle(errors.phone)} />
        </JoinField>
        <JoinField id="join-relationship" label="Your relationship to the person with SCN2A" error={errors.relationship}>
          <select id="join-relationship" value={about.relationship} onChange={(e) => setAbout({ relationship: e.target.value })} style={joinFieldStyle(errors.relationship)}>
            <option value="">Choose…</option>
            {JOIN_RELATIONSHIPS.map(r => <option key={r} value={r}>{r}</option>)}
          </select>
        </JoinField>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }} className="stack-mobile">
        <JoinField id="join-state" label="State or territory" error={errors.state}>
          <select id="join-state" value={about.state} onChange={(e) => setAbout({ state: e.target.value })} style={joinFieldStyle(errors.state)}>
            <option value="">Choose…</option>
            {JOIN_STATES.map(s => <option key={s} value={s}>{s}</option>)}
          </select>
        </JoinField>
        <JoinField id="join-suburb" label="Suburb or town" error={errors.suburb}>
          <input id="join-suburb" type="text" value={about.suburb} maxLength={80}
            onChange={(e) => setAbout({ suburb: e.target.value })} placeholder="e.g. Fremantle" style={joinFieldStyle(errors.suburb)} />
        </JoinField>
      </div>

      <p style={{ fontSize: 13, lineHeight: 1.6, color: '#0D1B2A', margin: '22px 0 8px' }}>
        We save your answers as you go, so you can stop and come back. That means we need your permission before we start.
      </p>
      <JoinConsent id="join-consent" tone="registry" checked={about.consent} error={errors.consent}
        onChange={(e) => setAbout({ consent: e.target.checked })}>
        <strong>I consent to SCN2A Australia storing and using my information as described in the{' '}
          <a href="/privacy" onClick={(e) => { e.preventDefault(); e.stopPropagation(); navigate('privacy'); }}
            style={{ color: '#1E3A6E', textDecoration: 'underline', textUnderlineOffset: 2 }}>Privacy Policy</a>.</strong>
      </JoinConsent>

      <label style={{ display: 'flex', alignItems: 'flex-start', gap: 9, margin: '16px 0 0', cursor: 'pointer' }}>
        <input type="checkbox" checked={about.newsletter} onChange={(e) => setAbout({ newsletter: e.target.checked })}
          style={{ marginTop: 2, width: 16, height: 16, flexShrink: 0, accentColor: '#1E3A6E' }} />
        <span style={{ fontSize: 13, lineHeight: 1.55, color: '#0D1B2A' }}>Send me the SCN2A Australia newsletter.</span>
      </label>

      {submitError && <JoinRetryBanner message={submitError} onRetry={continueFromAbout} busy={submitting} />}

      {navRow(
        <>
          <span style={{ fontSize: 12, color: '#6B6659' }}>Saved as soon as you continue.</span>
          <Button variant="navy" onClick={continueFromAbout} disabled={submitting}>{submitting ? 'Saving…' : 'Continue →'}</Button>
        </>
      )}
    </div>
  );

  const renderPerson = () => (
    <div className="fmap-rise" key="s1">
      {stepHeader('Step 2 of 5 · The person with SCN2A', 'Tell us about them.', "Just their name, date of birth and variant type. Anything clinical beyond this belongs to the natural history study, not this form.")}

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }} className="stack-mobile">
        <JoinField id="join-person-first-name" label="Their first name" error={errors.personFirstName}>
          <input id="join-person-first-name" type="text" value={person.personFirstName} maxLength={80}
            onChange={(e) => setPerson({ personFirstName: e.target.value })} placeholder="e.g. Alex" style={joinFieldStyle(errors.personFirstName)} />
        </JoinField>
        <JoinField id="join-person-last-name" label="Their last name" error={errors.personLastName}>
          <input id="join-person-last-name" type="text" value={person.personLastName} maxLength={80}
            onChange={(e) => setPerson({ personLastName: e.target.value })} style={joinFieldStyle(errors.personLastName)} />
        </JoinField>
      </div>

      <JoinField id="join-person-dob" label="Their date of birth" error={errors.personDob}
        hint="Kept private, never published. If you join the Family Map, the card shows a calculated age only, so it stays correct without a birthday ever being shown.">
        <input id="join-person-dob" type="date" value={person.personDob} max={new Date().toISOString().slice(0, 10)}
          onChange={(e) => setPerson({ personDob: e.target.value })} style={joinFieldStyle(errors.personDob)} />
      </JoinField>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }} className="stack-mobile">
        <JoinField id="join-variant-type" label="Variant type" error={errors.variantType}>
          <select id="join-variant-type" value={person.variantType} onChange={(e) => setPerson({ variantType: e.target.value })} style={joinFieldStyle(errors.variantType)}>
            <option value="">Choose…</option>
            {JOIN_VARIANT_TYPES.map(v => <option key={v} value={v}>{v}</option>)}
          </select>
        </JoinField>
        <JoinField id="join-variant" label="SCN2A variant, if you know it" optional error={errors.variant} hint="e.g. p.Arg853Gln. Unsure is fine.">
          <input id="join-variant" type="text" value={person.variant} maxLength={120}
            onChange={(e) => setPerson({ variant: e.target.value })} placeholder="e.g. p.Arg853Gln" style={joinFieldStyle(errors.variant)} />
        </JoinField>
      </div>

      <JoinGroupLabel>If it applies to you</JoinGroupLabel>
      <p style={{ fontSize: 13, lineHeight: 1.6, color: '#0D1B2A', margin: '0 0 8px' }}>
        If the person you are registering has died, you can tell us here. We will take care with what happens next, and we will not ask you anything else about them today.
      </p>
      <JoinConsent id="join-deceased" tone="registry" checked={person.deceased}
        onChange={(e) => setPerson({ deceased: e.target.checked, deceasedDate: e.target.checked ? person.deceasedDate : '' })}>
        Sadly, they have passed away.
      </JoinConsent>
      {person.deceased && (
        <JoinField id="join-deceased-date" label="The date, if you would like us to have it" optional error={errors.deceasedDate} style={{ marginTop: 14 }}>
          <input id="join-deceased-date" type="date" value={person.deceasedDate} max={new Date().toISOString().slice(0, 10)}
            onChange={(e) => setPerson({ deceasedDate: e.target.value })} style={joinFieldStyle(errors.deceasedDate)} />
        </JoinField>
      )}

      {submitError && <JoinRetryBanner message={submitError} onRetry={continueFromPerson} busy={submitting} />}

      {navRow(
        <>
          <Button variant="outline" onClick={() => goTo(0)}>← Back</Button>
          <Button variant="navy" onClick={continueFromPerson} disabled={submitting}>{submitting ? 'Saving…' : 'Continue →'}</Button>
        </>
      )}
    </div>
  );

  const renderAddress = () => (
    <div className="fmap-rise" key="s2">
      {stepHeader('Step 3 of 5 · Address and their doctor', 'Where you are, and who looks after them.',
        'This is private and never shown anywhere, including the map. It lets us post things to you, and helps us see where clinicians who know SCN2A are around the country.')}

      <JoinField id="join-street" label="Street address" error={errors.streetAddress}
        hint="Never shown anywhere and never appears on the map. It lets us post things to you and tell you what is happening near you.">
        <input id="join-street" type="text" value={address.streetAddress} maxLength={200} autoComplete="street-address"
          onChange={(e) => setAddress({ streetAddress: e.target.value })} style={joinFieldStyle(errors.streetAddress)} />
      </JoinField>
      <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 14 }} className="stack-mobile">
        <JoinField id="join-address-suburb" label="Suburb" error={errors.addressSuburb}>
          <input id="join-address-suburb" type="text" value={address.addressSuburb} maxLength={80} autoComplete="address-level2"
            onChange={(e) => setAddress({ addressSuburb: e.target.value })} style={joinFieldStyle(errors.addressSuburb)} />
        </JoinField>
        <JoinField id="join-address-state" label="State or territory" error={errors.addressState}>
          <select id="join-address-state" value={address.addressState} onChange={(e) => setAddress({ addressState: e.target.value })} style={joinFieldStyle(errors.addressState)}>
            <option value="">Choose…</option>
            {JOIN_STATES.map(s => <option key={s} value={s}>{s}</option>)}
          </select>
        </JoinField>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: 14 }} className="stack-mobile">
        <JoinField id="join-postcode" label="Postcode" error={errors.postcode}>
          <input id="join-postcode" type="text" inputMode="numeric" value={address.postcode} maxLength={12} autoComplete="postal-code"
            onChange={(e) => setAddress({ postcode: e.target.value })} style={joinFieldStyle(errors.postcode)} />
        </JoinField>
        <JoinField id="join-country" label="Country" error={errors.country}>
          <input id="join-country" type="text" value={address.country} maxLength={80} autoComplete="country-name"
            onChange={(e) => setAddress({ country: e.target.value })} style={joinFieldStyle(errors.country)} />
        </JoinField>
      </div>

      <JoinGroupLabel>Their doctor</JoinGroupLabel>
      <JoinField id="join-doctor-name" label="Primary doctor's name" error={errors.doctorName}
        hint="We ask so we can build a picture of where clinicians who know SCN2A are around the country, and point families towards care near them. We are not asking in order to contact your doctor.">
        <input id="join-doctor-name" type="text" value={address.doctorName} maxLength={120}
          onChange={(e) => setAddress({ doctorName: e.target.value })} style={joinFieldStyle(errors.doctorName)} />
      </JoinField>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }} className="stack-mobile">
        <JoinField id="join-doctor-clinic" label="Their hospital or clinic" optional error={errors.doctorClinic}
          hint="Leave this blank if you don't have a specialist yet, or your doctor is in general practice.">
          <input id="join-doctor-clinic" type="text" value={address.doctorClinic} maxLength={160}
            onChange={(e) => setAddress({ doctorClinic: e.target.value })} style={joinFieldStyle(errors.doctorClinic)} />
        </JoinField>
        <JoinField id="join-doctor-specialty" label="Their specialty" optional error={errors.doctorSpecialty}>
          <select id="join-doctor-specialty" value={address.doctorSpecialty} onChange={(e) => setAddress({ doctorSpecialty: e.target.value })} style={joinFieldStyle(errors.doctorSpecialty)}>
            <option value="">Choose…</option>
            {JOIN_DOCTOR_SPECIALTIES.map(v => <option key={v} value={v}>{v}</option>)}
          </select>
        </JoinField>
      </div>
      <JoinField id="join-doctor-state" label="State or territory of that clinic" optional error={errors.doctorState}>
        <select id="join-doctor-state" value={address.doctorState} onChange={(e) => setAddress({ doctorState: e.target.value })} style={{ ...joinFieldStyle(errors.doctorState), maxWidth: 260 }}>
          <option value="">Choose…</option>
          {JOIN_STATES.map(s => <option key={s} value={s}>{s}</option>)}
        </select>
      </JoinField>

      <JoinGroupLabel>A second contact</JoinGroupLabel>
      <p style={{ fontSize: 12.5, lineHeight: 1.55, color: '#6B6659', margin: '0 0 14px' }}>
        Optional. A phone changes, a relationship changes, one parent carries the load — a second contact is the cheapest insurance against losing a family entirely.
      </p>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }} className="stack-mobile">
        <JoinField id="join-second-contact-name" label="Second contact name" optional error={errors.secondContactName}>
          <input id="join-second-contact-name" type="text" value={address.secondContactName} maxLength={120}
            onChange={(e) => setAddress({ secondContactName: e.target.value })} style={joinFieldStyle(errors.secondContactName)} />
        </JoinField>
        <JoinField id="join-second-contact-detail" label="Their phone or email" optional error={errors.secondContactDetail}>
          <input id="join-second-contact-detail" type="text" value={address.secondContactDetail} maxLength={160}
            onChange={(e) => setAddress({ secondContactDetail: e.target.value })} style={joinFieldStyle(errors.secondContactDetail)} />
        </JoinField>
      </div>

      {submitError && <JoinRetryBanner message={submitError} onRetry={continueFromAddress} busy={submitting} />}

      {navRow(
        <>
          <Button variant="outline" onClick={() => goTo(1)}>← Back</Button>
          <Button variant="navy" onClick={continueFromAddress} disabled={submitting}>{submitting ? 'Saving…' : 'Continue →'}</Button>
        </>
      )}
    </div>
  );

  const renderMap = () => {
    if (person.deceased) {
      return (
        <div className="fmap-rise" key="s3-memorial">
          {stepHeader('Step 4 of 5 · In Memory', 'Would you like them remembered?',
            'This step usually asks about the Family Map. Because you told us they have passed away, we ask something gentler instead.')}
          <div style={{ display: 'flex', gap: 10, padding: '12px 14px', background: '#EFEAE1', borderRadius: 5, marginBottom: 20 }}>
            <p style={{ fontSize: 12.5, lineHeight: 1.55, color: '#0D1B2A', margin: 0 }}>
              Nothing is ever published automatically. A member of our team will always be in touch personally first, and any In Memory page only ever goes up with your yes.
            </p>
          </div>
          <JoinField id="join-memorial-consent" label={`Would you like ${person.personFirstName.trim() || 'them'} remembered on our In Memory page?`} optional error={errors.memorialConsent}>
            <select id="join-memorial-consent" value={mapStep.memorialConsent} onChange={(e) => setMap({ memorialConsent: e.target.value })} style={{ ...joinFieldStyle(errors.memorialConsent), maxWidth: 260 }}>
              <option value="">Choose…</option>
              {JOIN_MEMORIAL.map(v => <option key={v} value={v}>{v}</option>)}
            </select>
          </JoinField>
          {submitError && <JoinRetryBanner message={submitError} onRetry={continueFromMap} busy={submitting} />}
          {navRow(
            <>
              <Button variant="outline" onClick={() => goTo(2)}>← Back</Button>
              <Button variant="navy" onClick={continueFromMap} disabled={submitting}>{submitting ? 'Saving…' : 'Continue →'}</Button>
            </>
          )}
        </div>
      );
    }

    return (
      <div className="fmap-rise" key="s3">
        {stepHeader('Step 4 of 5 · The Family Map · Optional', 'Add your pin to the Family Map.',
          'Completely optional. If the map is not for you, leave the box below unticked and carry on, the rest of the form works exactly the same.')}

        {mapUnavailable ? (
          <div style={{ display: 'flex', gap: 10, padding: '14px 16px', background: '#EFEAE1', border: '1px solid #D5CFBF', borderRadius: 5 }}>
            <p style={{ fontSize: 13, lineHeight: 1.6, color: '#0D1B2A', margin: 0 }}>
              The Family Map covers Australia only, so we cannot place a pin from outside it. No problem at all, everything else in this form still applies, and we would love to stay in touch.
            </p>
          </div>
        ) : (
          <>
            <div style={{ display: 'flex', gap: 10, padding: '12px 14px', background: '#E5EDF8', borderRadius: 5, marginBottom: 20 }}>
              <span aria-hidden="true" style={{ color: '#4A7EC7', fontWeight: 700, fontSize: 14, flexShrink: 0 }}>✓</span>
              <p style={{ fontSize: 12.5, lineHeight: 1.55, color: '#0D1B2A', margin: 0 }}>
                The map only ever shows a first name, a general area and an age. Pins are placed approximately by our team, never at an address, and every submission is reviewed before a pin appears.
              </p>
            </div>

            <JoinConsent id="join-map-consent" tone="map" checked={mapStep.mapConsent}
              onChange={(e) => setMap({ mapConsent: e.target.checked })}>
              <strong>Yes, show our family on the SCN2A Family Map</strong> — first name, general area and an age only.
            </JoinConsent>

            {mapStep.mapConsent && (
              <div style={{ marginTop: 18 }}>
                <fieldset style={{ border: 'none', padding: 0, margin: '0 0 18px' }}>
                  <legend style={{ ...JOIN_LABEL_STYLE, padding: 0 }}>Topics you are happy to talk about <span style={{ fontWeight: 500, color: '#6B6659' }}>(optional, choose any)</span></legend>
                  <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 4 }}>
                    {JOIN_MAP_TOPICS.map(t => (
                      <label key={t} style={{ display: 'flex', alignItems: 'flex-start', gap: 9, fontSize: 13, lineHeight: 1.5, color: '#0D1B2A', cursor: 'pointer' }}>
                        <input type="checkbox" checked={mapStep.mapTopics.includes(t)} onChange={() => setMap({ mapTopics: toggleIn(mapStep.mapTopics, t) })}
                          style={{ marginTop: 2, width: 15, height: 15, flexShrink: 0, accentColor: '#1E3A6E' }} />
                        {t}
                      </label>
                    ))}
                  </div>
                </fieldset>

                <JoinField id="join-blurb" label="A short note for your card" optional
                  hint={`A sentence or two other families will see on your card, the blurb can always be added later. ${200 - mapStep.mapBlurb.length} characters left.`}>
                  <textarea id="join-blurb" value={mapStep.mapBlurb} maxLength={200} rows="3"
                    onChange={(e) => setMap({ mapBlurb: e.target.value })}
                    placeholder="e.g. Diagnosed at 14 months. Happy to share what we've learned about the NDIS…"
                    style={{ ...joinFieldStyle(false), lineHeight: 1.5, resize: 'vertical' }} />
                </JoinField>

                <label style={{ display: 'flex', alignItems: 'flex-start', gap: 9, cursor: 'pointer' }}>
                  <input type="checkbox" checked={mapStep.openToMessages} onChange={(e) => setMap({ openToMessages: e.target.checked })}
                    style={{ marginTop: 2, width: 16, height: 16, flexShrink: 0, accentColor: '#1E3A6E' }} />
                  <span style={{ fontSize: 13, lineHeight: 1.55, color: '#0D1B2A' }}>
                    <strong>I am open to introductions to other families.</strong>
                    <span style={{ display: 'block', fontSize: 12, color: '#6B6659', marginTop: 3 }}>
                      Introductions are made by our team. If another family asks to meet you, we speak to you first, and connect you only if you are both happy to go ahead. Your contact details are never shared without your say-so.
                    </span>
                  </span>
                </label>
              </div>
            )}
          </>
        )}

        {submitError && <JoinRetryBanner message={submitError} onRetry={continueFromMap} busy={submitting} />}

        {navRow(
          <>
            <Button variant="outline" onClick={() => goTo(2)}>← Back</Button>
            <Button variant="navy" onClick={continueFromMap} disabled={submitting}>{submitting ? 'Saving…' : 'Continue →'}</Button>
          </>
        )}
      </div>
    );
  };

  const renderConnected = () => {
    const needsOptions = person.deceased ? ['Bereavement support', ...JOIN_NEEDS_BASE] : JOIN_NEEDS_BASE;
    return (
      <div className="fmap-rise" key="s4">
        {stepHeader('Step 5 of 5 · Staying connected', 'Last step, tell us what would help.',
          'Everything here is optional except one question about the natural history study, that is how families get to it.')}

        <fieldset style={{ border: 'none', padding: 0, margin: '0 0 20px' }}>
          <legend style={{ ...JOIN_LABEL_STYLE, padding: 0 }}>What would be most helpful right now? <span style={{ fontWeight: 500, color: '#6B6659' }}>(optional, choose any)</span></legend>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 4 }}>
            {needsOptions.map(n => (
              <label key={n} style={{ display: 'flex', alignItems: 'flex-start', gap: 9, fontSize: 13, lineHeight: 1.5, color: '#0D1B2A', cursor: 'pointer' }}>
                <input type="checkbox" checked={connect.needs.includes(n)} onChange={() => setConnect({ needs: toggleIn(connect.needs, n) })}
                  style={{ marginTop: 2, width: 15, height: 15, flexShrink: 0, accentColor: '#1E3A6E' }} />
                {n}
              </label>
            ))}
          </div>
        </fieldset>

        <JoinGroupLabel>Research</JoinGroupLabel>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }} className="stack-mobile">
          <JoinField id="join-natural-history" label="Would you like to hear about the SCN2A natural history study?" error={errors.naturalHistoryStudy}>
            <select id="join-natural-history" value={connect.naturalHistoryStudy} onChange={(e) => setConnect({ naturalHistoryStudy: e.target.value })} style={joinFieldStyle(errors.naturalHistoryStudy)}>
              <option value="">Choose…</option>
              {JOIN_NATURAL_HISTORY.map(v => <option key={v} value={v}>{v}</option>)}
            </select>
          </JoinField>
          <JoinField id="join-research-contact" label="Willing to be contacted about other research opportunities?" optional error={errors.researchContact}>
            <select id="join-research-contact" value={connect.researchContact} onChange={(e) => setConnect({ researchContact: e.target.value })} style={joinFieldStyle(errors.researchContact)}>
              <option value="">Choose…</option>
              {JOIN_RESEARCH_CONTACT.map(v => <option key={v} value={v}>{v}</option>)}
            </select>
          </JoinField>
        </div>

        <JoinGroupLabel>Community</JoinGroupLabel>
        <fieldset style={{ border: 'none', padding: 0, margin: '0 0 20px' }}>
          <legend style={{ ...JOIN_LABEL_STYLE, padding: 0 }}>Would you like to be matched with another family for peer support? <span style={{ fontWeight: 500, color: '#6B6659' }}>(optional, choose any)</span></legend>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 4 }}>
            {JOIN_PEER_SUPPORT.map(p => (
              <label key={p} style={{ display: 'flex', alignItems: 'flex-start', gap: 9, fontSize: 13, lineHeight: 1.5, color: '#0D1B2A', cursor: 'pointer' }}>
                <input type="checkbox" checked={connect.peerSupport.includes(p)} onChange={() => setConnect({ peerSupport: toggleIn(connect.peerSupport, p) })}
                  style={{ marginTop: 2, width: 15, height: 15, flexShrink: 0, accentColor: '#1E3A6E' }} />
                {p}
              </label>
            ))}
          </div>
        </fieldset>

        <JoinField id="join-volunteer" label="Willing to volunteer with SCN2A Australia?" optional error={errors.volunteer}>
          <select id="join-volunteer" value={connect.volunteer} onChange={(e) => setConnect({ volunteer: e.target.value })} style={{ ...joinFieldStyle(errors.volunteer), maxWidth: 260 }}>
            <option value="">Choose…</option>
            {JOIN_VOLUNTEER.map(v => <option key={v} value={v}>{v}</option>)}
          </select>
        </JoinField>

        <JoinField id="join-anything-else" label="Anything else you would like us to know?" optional error={errors.anythingElse}
          hint={`${2000 - connect.anythingElse.length} characters left.`}>
          <textarea id="join-anything-else" value={connect.anythingElse} maxLength={2000} rows="3"
            onChange={(e) => setConnect({ anythingElse: e.target.value })}
            style={{ ...joinFieldStyle(errors.anythingElse), lineHeight: 1.5, resize: 'vertical' }} />
        </JoinField>

        {submitError && <JoinRetryBanner message={submitError} onRetry={continueFromConnected} busy={submitting} />}

        {navRow(
          <>
            <Button variant="outline" onClick={() => goTo(3)}>← Back</Button>
            <Button variant="mulberry" onClick={continueFromConnected} disabled={submitting}>{submitting ? 'Sending…' : 'Join SCN2A Australia →'}</Button>
          </>
        )}
        <p style={{ fontSize: 11.5, color: '#6B6659', lineHeight: 1.55, margin: '14px 0 0', textAlign: 'right' }}>
          You can update or remove your details at any time by emailing {JOIN_SUPPORT_EMAIL}.
        </p>
      </div>
    );
  };

  const renderDone = () => {
    // Every earlier step blocks on its own POST succeeding before it will
    // advance, so reaching this screen already means step 1 went through —
    // this branch exists only as a defensive fallback, never as the normal
    // path, so this screen is never left telling a family they have a record
    // with us when they do not.
    if (!step1Saved) {
      return (
        <div className="fmap-rise" key="s5-none" style={{ textAlign: 'center' }}>
          <div aria-hidden="true" style={{ width: 56, height: 56, borderRadius: 999, background: '#E8E3DA', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 18px', color: '#6B6659', fontSize: 26, fontWeight: 700 }}>·</div>
          <h2 className="h2-mobile" style={{ fontSize: 25, fontWeight: 700, letterSpacing: '-0.01em', margin: '0 0 10px', color: '#0D1B2A' }}>No worries, nothing has been saved.</h2>
          <p style={{ fontSize: 14.5, lineHeight: 1.65, color: '#0D1B2A', margin: '0 auto 24px', maxWidth: '46ch' }}>
            The door is always open, come back whenever it feels right, or say hello at {JOIN_SUPPORT_EMAIL}.
          </p>
          <div style={{ display: 'flex', gap: 10, justifyContent: 'center', flexWrap: 'wrap' }}>
            <Button variant="navy" onClick={() => navigate('family-map')}>See the Family Map</Button>
            <Button variant="outline" onClick={() => navigate('home')}>Back to home</Button>
          </div>
        </div>
      );
    }

    const deceased = person.deceased;
    const mapJoined = !deceased && !mapUnavailable && mapStep.mapConsent;
    const name = person.personFirstName.trim() || 'them';
    const title = deceased
      ? `Thank you for telling us about ${name}.`
      : mapJoined ? "Thank you, you're part of SCN2A Australia." : "Thank you, you're on the Family Registry.";
    const rows = [];
    if (deceased) {
      rows.push({ icon: 'heart-handshake', head: 'We will take care with what happens next', body: `Nothing about ${name} is ever published automatically. A member of our team will be in touch personally, and an In Memory page only ever goes up with your yes.` });
    } else if (mapJoined) {
      rows.push({ icon: 'map-pin', head: 'Your pin is with our team', body: 'Our team reviews every map submission by hand before your pin appears, usually within about two weeks. Once approved, your family shows as a first name, general area and an age only. If a fortnight passes and you cannot see your pin, please email us, it means something went wrong at our end, not yours.' });
    }
    rows.push({ icon: 'shield-check', head: 'Your details are saved, privately', body: 'Never published. We use them to connect you with support, resources and, only if you opted in, research.' });
    if (connect.peerSupport.length) rows.push({ icon: 'users', head: 'Your peer support request is with our team', body: 'Someone from SCN2A Australia will be in touch to help make a connection.' });
    rows.push({ icon: 'mail', head: 'Check your inbox, a welcome email is on its way', body: 'It covers what happens next and how to reach us. If it has not arrived within a day or two, check your spam folder or email us.' });

    return (
      <div className="fmap-rise" key="s5" style={{ textAlign: 'center' }}>
        <div aria-hidden="true" style={{ width: 56, height: 56, borderRadius: 999, background: '#E5EDF8', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 18px', color: '#4A7EC7', fontSize: 26, fontWeight: 700 }}>✓</div>
        <h2 className="h2-mobile" style={{ fontSize: 25, fontWeight: 700, letterSpacing: '-0.01em', margin: '0 0 10px', color: '#0D1B2A' }}>{title}</h2>
        <p style={{ fontSize: 14.5, lineHeight: 1.65, color: '#0D1B2A', margin: '0 auto 24px', maxWidth: '48ch' }}>
          {deceased
            ? 'Your family is on the Family Registry, saved privately. Here is what happens next.'
            : mapJoined
              ? 'Your family is joining the map and the registry. Here is what happens next.'
              : 'Your details are safely with us, privately. Here is what happens next.'}
        </p>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10, textAlign: 'left', margin: '0 auto 24px', maxWidth: 520 }}>
          {rows.map(r => (
            <div key={r.head} style={{ display: 'flex', gap: 14, background: '#F7F4F0', border: '1px solid #E8E3DA', borderRadius: 5, padding: '14px 16px' }}>
              <span style={{ flexShrink: 0, color: '#1E3A6E' }}><i data-lucide={r.icon} style={{ width: 20, height: 20 }} /></span>
              <div>
                <div style={{ fontSize: 14, fontWeight: 700, color: '#0D1B2A', marginBottom: 3 }}>{r.head}</div>
                <div style={{ fontSize: 13, lineHeight: 1.55, color: '#6B6659' }}>{r.body}</div>
              </div>
            </div>
          ))}
        </div>
        <div style={{ display: 'flex', gap: 10, justifyContent: 'center', flexWrap: 'wrap' }}>
          <Button variant="navy" onClick={() => navigate('family-map')}>See the Family Map</Button>
          <Button variant="outline" onClick={() => navigate('home')}>Back to home</Button>
        </div>
      </div>
    );
  };

  return (
    <div data-screen-label="Add your family">

      {/* ============ HERO (navy feature band) ============ */}
      <section style={{ background: '#1E3A6E', color: '#F7F4F0', position: 'relative', overflow: 'hidden' }}>
        <div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 6, background: '#8E3B5E', zIndex: 2 }} />
        <NetworkMotif opacity={0.12} stroke="#fff" dense />
        <div className="container" style={{ position: 'relative', zIndex: 2, padding: '72px 32px 64px' }}>
          <div style={{ display: 'grid', gridTemplateColumns: '1.25fr 1fr', gap: 56, alignItems: 'center' }} className="stack-mobile">
            <div>
              <div className="eyebrow" style={{ color: '#4A7EC7', marginBottom: 20 }}>Join SCN2A Australia · Add your family</div>
              <h1 className="hero-h1-mobile-lg" style={{ fontSize: 'clamp(34px, 4.2vw, 44px)', fontWeight: 800, letterSpacing: '-0.02em', lineHeight: 1.12, margin: '0 0 20px', color: '#fff', textWrap: 'balance' }}>
                Five short steps. You choose what is shared.
              </h1>
              <p className="hero-sub-mobile" style={{ fontSize: 17, lineHeight: 1.6, color: '#B9C2D6', maxWidth: '54ch', margin: '0 0 28px' }}>
                We save your answers after each step, so you can stop and come back without losing anything. A pin on the public Family Map is one optional step among five, everything else stays private.
              </p>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, alignItems: 'center' }}>
                <Button size="lg" variant="mulberry" onClick={scrollToFlow}>Start, it takes about five minutes →</Button>
                <Button size="lg" variant="outlineLight" onClick={() => navigate('family-map')}>See the map first</Button>
              </div>
              <p style={{ fontSize: 12, color: '#B9C2D6', margin: '18px 0 0', opacity: 0.85 }}>Private, secure, and reviewed by our team. You can change or remove your details at any time.</p>
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
              {[
                { icon: 'map-pin', title: 'The Family Map', body: 'Public and opt-in. First name, general area and an age only, never an address.' },
                { icon: 'lock', title: 'The Family Registry', body: 'Private and never published. It helps us connect you with support, resources and research.' },
                { icon: 'sliders-horizontal', title: 'Your call, always', body: 'Skip the map, or leave any optional question blank. Every share is a choice you make.' },
              ].map(c => (
                <div key={c.title} style={{ display: 'flex', gap: 16, background: '#18305B', border: '1px solid #26467F', borderRadius: 8, padding: '18px 22px' }}>
                  <span style={{ flexShrink: 0, color: '#4A7EC7' }}><i data-lucide={c.icon} style={{ width: 22, height: 22 }} /></span>
                  <div>
                    <h3 style={{ fontSize: 15, fontWeight: 700, margin: '0 0 4px', color: '#fff' }}>{c.title}</h3>
                    <p style={{ fontSize: 13, lineHeight: 1.5, color: '#B9C2D6', margin: 0 }}>{c.body}</p>
                  </div>
                </div>
              ))}
            </div>
          </div>
        </div>
      </section>

      <Breadcrumbs items={[{ label: 'Support', to: 'support' }, { label: 'Add your family' }]} navigate={navigate} />

      {/* ============ THE FLOW ============ */}
      <section id="join-flow" style={{ background: '#F7F4F0', padding: '56px 0 72px', scrollMarginTop: 90 }}>
        <div className="container" style={{ maxWidth: 820 }}>
          <JoinProgress step={step} />
          <div style={{ background: '#fff', border: '1px solid #D5CFBF', borderTop: '3px solid #4A7EC7', borderRadius: 8, padding: '36px 40px', boxShadow: '0 1px 2px rgba(13,27,42,0.06), 0 1px 3px rgba(13,27,42,0.08)' }} className="pad-mobile">
            {/* Honeypot — hidden from humans and screen readers; bots fill it. */}
            <div aria-hidden="true" style={{ position: 'absolute', left: '-9999px', height: 0, overflow: 'hidden' }}>
              <label htmlFor="join-website">Website</label>
              <input id="join-website" type="text" name="website" tabIndex={-1} autoComplete="off"
                value={website} onChange={(e) => setWebsite(e.target.value)} />
            </div>
            {step === 0 && renderAbout()}
            {step === 1 && renderPerson()}
            {step === 2 && renderAddress()}
            {step === 3 && renderMap()}
            {step === 4 && renderConnected()}
            {step === 5 && renderDone()}
          </div>
          {step < 5 && (
            <p style={{ fontSize: 12, color: '#6B6659', lineHeight: 1.6, margin: '16px 4px 0', textAlign: 'center' }}>
              Questions before you share anything? Read our{' '}
              <a href="/privacy" onClick={(e) => { e.preventDefault(); navigate('privacy'); }} style={{ color: '#1E3A6E' }}>Privacy Policy</a>
              {' '}or email {JOIN_SUPPORT_EMAIL}.
            </p>
          )}
        </div>
      </section>

      {/* ============ HOW YOUR INFORMATION IS HANDLED (sandstone band) ============ */}
      <section style={{ background: '#E8E3DA', padding: '64px 0' }}>
        <div className="container">
          <div className="eyebrow" style={{ color: '#6B6659', marginBottom: 12 }}>How your information is handled</div>
          <h2 className="h2-mobile" style={{ fontSize: 28, fontWeight: 700, letterSpacing: '-0.01em', margin: '0 0 36px', maxWidth: '26ch', color: '#0D1B2A' }}>Careful by design, and always reversible.</h2>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }} className="stack-mobile-2">
            {[
              { n: '01', title: 'Map pins are reviewed', body: 'Nothing appears on the Family Map automatically. Our team reviews every submission and places pins approximately, so no family can be identified by location.' },
              { n: '02', title: 'Your details stay private', body: 'Nothing you tell us beyond an optional map pin is ever published anywhere. We use it to connect you with support, resources and, only if you opt in, research opportunities.' },
              { n: '03', title: 'Change your mind any time', body: `Ask us to update your details, hide your pin, or remove everything, and we will. One email to ${JOIN_SUPPORT_EMAIL} is all it takes.` },
            ].map(s => (
              <div key={s.n} style={{ background: '#fff', border: '1px solid #D5CFBF', borderRadius: 8, padding: 28, borderTop: '3px solid #4A7EC7' }}>
                <div style={{ fontSize: 13, fontWeight: 800, color: '#4A7EC7', marginBottom: 14 }}>{s.n}</div>
                <h3 style={{ fontSize: 17, fontWeight: 700, margin: '0 0 10px', color: '#0D1B2A' }}>{s.title}</h3>
                <p style={{ fontSize: 14.5, lineHeight: 1.6, color: '#0D1B2A', margin: 0 }}>{s.body}</p>
              </div>
            ))}
          </div>
        </div>
      </section>
    </div>
  );
}

Object.assign(window, { JoinPage });
