// Prickly — interactive components
// Quiz · KitExplorer · ClosedLoop · Timeline · RotatingHeadline · RevealWords
// All components attached to window so prickly-site.jsx can use them.

// ── Helpers ────────────────────────────────────────────────────────────────
function useInView(ref, opts = {}) {
  const [seen, setSeen] = React.useState(false);
  React.useEffect(() => {
    const el = ref.current;
    if (!el || seen) return;
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => { if (e.isIntersecting) setSeen(true); });
    }, { threshold: 0.15, ...opts });
    io.observe(el);
    return () => io.disconnect();
  }, [ref, seen]);
  return seen;
}

function useScrollProgress(ref) {
  const [p, setP] = React.useState(0);
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    let raf = 0;
    const onScroll = () => {
      cancelAnimationFrame(raf);
      raf = requestAnimationFrame(() => {
        const rect = el.getBoundingClientRect();
        const vh = window.innerHeight;
        const total = rect.height + vh * 0.4;
        const seen = Math.max(0, vh - rect.top) - vh * 0.3;
        const v = Math.max(0, Math.min(1, seen / total));
        setP(v);
      });
    };
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll);
    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', onScroll);
    };
  }, [ref]);
  return p;
}

// ── Reveal wrapper ─────────────────────────────────────────────────────────
function Reveal({ children, d = 0, as = 'div', className = '', ...rest }) {
  const ref = React.useRef(null);
  const seen = useInView(ref);
  const Tag = as;
  return (
    <Tag
      ref={ref}
      data-reveal=""
      className={(seen ? 'in ' : '') + className}
      style={{ '--d': d, ...(rest.style || {}) }}
      {...rest}
    >
      {children}
    </Tag>
  );
}

// ── Word-by-word reveal (for big headlines) ────────────────────────────────
function RevealWords({ text, className = '', startDelay = 0 }) {
  const ref = React.useRef(null);
  const seen = useInView(ref, { threshold: 0.25 });
  const words = String(text).split(/(\s+)/);
  let i = 0;
  return (
    <span ref={ref} className={'pr-revealwords ' + (seen ? 'in ' : '') + className}>
      {words.map((w, idx) => {
        if (/^\s+$/.test(w)) return <span key={idx}>{w}</span>;
        const k = i++;
        return (
          <span key={idx} className="w" style={{ '--i': k + startDelay }}>
            <span>{w}</span>
          </span>
        );
      })}
    </span>
  );
}

// ── Rotating headline word ─────────────────────────────────────────────────
function RotatingHeadline({ words, interval = 2400 }) {
  const [active, setActive] = React.useState(0);
  React.useEffect(() => {
    const t = setInterval(() => setActive((a) => (a + 1) % words.length), interval);
    return () => clearInterval(t);
  }, [words.length, interval]);

  // measure widest word for stable width
  const ref = React.useRef(null);
  const [width, setWidth] = React.useState(null);
  React.useEffect(() => {
    if (!ref.current) return;
    const el = ref.current;
    const measure = () => {
      const ghosts = el.querySelectorAll('.measure');
      let w = 0;
      ghosts.forEach((g) => { w = Math.max(w, g.offsetWidth); });
      setWidth(w);
    };
    measure();
    window.addEventListener('resize', measure);
    return () => window.removeEventListener('resize', measure);
  }, [words]);

  return (
    <span ref={ref} className="pr-hero-rotator" style={width ? { width: width + 4 } : undefined}>
      {words.map((w, i) => (
        <span
          key={w}
          className={'word ' + (i === active ? 'active' : (i === (active - 1 + words.length) % words.length ? 'out' : ''))}
        >
          {w}
        </span>
      ))}
      {/* ghost copies for width measurement, hidden */}
      {words.map((w, i) => (
        <span key={'g' + i} className="measure" style={{ position: 'absolute', visibility: 'hidden', pointerEvents: 'none', whiteSpace: 'nowrap' }}>{w}</span>
      ))}
    </span>
  );
}

// ── Eligibility Quiz ──────────────────────────────────────────────────────
const QUIZ = [
  {
    q: "Where are you located?",
    help: null,
    opts: [
      { label: "Australia",       next: 1 },
      { label: "Another country", end: 'overseas' },
    ],
  },
  {
    q: "How old are you?",
    help: "We can't safely screen anyone under 16 through this service.",
    opts: [
      { label: "Under 16",     end: 'underage' },
      { label: "16 or older",  next: 2 },
    ],
  },
  {
    q: "Have you been sexually active since your last STI screen?",
    help: null,
    opts: [
      { label: "Yes",                              next: 3 },
      { label: "No",                               end: 'not-active' },
      { label: "I've never been sexually active",  end: 'never-active' },
    ],
  },
  {
    q: "Are you currently experiencing any symptoms?",
    help: "Discharge, pain, sores, bleeding, lumps, burning when peeing.",
    opts: [
      { label: "No, I'm feeling fine",          next: 4 },
      { label: "Yes, something is going on",    end: 'symptoms' },
    ],
  },
  {
    q: "Do you require urgent sexual health care?",
    help: null,
    opts: [
      { label: "I've had a potential exposure to HIV",  end: 'pep' },
      { label: "I need emergency contraception",        end: 'emergency-contraception' },
      { label: "I've been recently sexually assaulted", end: 'assault' },
      { label: "No, I just need a routine STI screen",  next: 5 },
    ],
  },
  {
    q: "Do you have a Medicare card?",
    help: null,
    opts: [
      { label: "Yes",  end: 'go' },
      { label: "No",   next: 6 },
    ],
  },
  {
    q: "Do you have OSHC or OVHC insurance?",
    help: "Overseas Student Health Cover or Overseas Visitor Health Cover.",
    opts: [
      { label: "Yes",  end: 'oshc' },
      { label: "No",   end: 'private' },
    ],
  },
];

const QUIZ_RESULTS = {
  go: {
    tone: 'go',
    tag: 'Eligible',
    h: "You look like a great fit for prickly.",
    p: "Based on your answers, you can move straight into the full clinical questionnaire within our secure electronic medical record. Your specimen collection equipment ships the next business day following clinician approval.",
    cta: 'Request your Medicare STI screen',
    sec: 'Prefer not to use Medicare?',
    secEnd: 'private',
    secNote: 'Nothing is claimed on Medicare, uploaded to My Health Record or sent to your GP. Your screen stays completely off the record — your business is your business, and we\'ll help you keep it that way.',
    secCta: 'Request your private STI screen',
  },
  'not-active': {
    tone: 'go',
    tag: 'Low risk',
    h: "Screening is most useful after sexual activity.",
    p: "Since you haven't been sexually active since your last screen, your risk is low. If that changes, come back and we'll get you sorted.",
    cta: null,
    sec: null,
  },
  'never-active': {
    tone: 'go',
    tag: 'No risk',
    h: "There's nothing to screen for.",
    p: "STI transmission requires sexual contact, so there's no risk to test for. If that ever changes, come back — we'll be here.",
    cta: null,
    sec: null,
  },
  symptoms: {
    tone: 'warn',
    tag: 'See a clinic',
    h: "Symptoms need a clinic, not a screen.",
    p: "Prickly is designed for asymptomatic screening. With symptoms you'll be better off with a GP or sexual health clinic — they can examine you, test on the spot, and get you treated straight away.",
    cta: 'Find a sexual health clinic',
    sec: null,
  },
  underage: {
    tone: 'warn',
    tag: "We can't help here",
    h: "Please see a GP or a sexual health clinic.",
    p: "Most state sexual health services are free for under-25s and confidential. We can't safely screen anyone under 16 through this service.",
    cta: 'Find youth services',
    sec: null,
  },
  'emergency-contraception': {
    tone: 'warn',
    tag: 'Time-critical',
    h: "Emergency contraception needs a clinic or pharmacy now.",
    p: "The morning-after pill is most effective within 72 hours. Head to your nearest pharmacy, GP, or sexual health clinic — no appointment needed at most pharmacies.",
    cta: 'Find a pharmacy near me',
    sec: null,
  },
  assault: {
    tone: 'warn',
    tag: 'Get support now',
    h: "Please reach out to a specialist service.",
    p: "If you've been sexually assaulted, you deserve immediate, specialist care. 1800RESPECT (1800 737 732) is available 24/7, and your nearest sexual assault service can provide medical care and support.",
    cta: 'Call 1800RESPECT',
    sec: null,
  },
  pep: {
    tone: 'warn',
    tag: 'Time-critical',
    h: "Get to a clinic or emergency department now.",
    p: "If a possible HIV exposure happened in the last 72 hours, post-exposure prophylaxis (PEP) is time-critical and not something we can dispense at home. Call 1800 MEDICARE or your nearest sexual health clinic immediately.",
    cta: 'Call 1800 MEDICARE',
    sec: null,
  },
  'no-medicare': {
    tone: 'go',
    tag: 'Likely eligible',
    h: "You can still order — just one extra step.",
    p: "Without Medicare we'll verify your identity using an IHI number you can request online. Pathology costs are the same; Telehealth follow-up is out-of-pocket, quoted upfront.",
    cta: 'Continue to identity step',
    sec: 'About IHI numbers',
  },
  oshc: {
    tone: 'go',
    tag: 'Eligible',
    h: "You can book with your OSHC/OVHC cover.",
    p: "Your overseas health cover may offset some costs. Continue to book — we'll confirm what's claimable during the clinical questionnaire.",
    cta: 'Request your OSHC/OVHC STI screen',
    sec: 'Prefer not to use OSHC/OVHC?',
    secEnd: 'private',
    secNote: 'Nothing is claimed on your insurance, uploaded to My Health Record or sent to your GP. Your screen stays completely off the record — your business is your business, and we\'ll help you keep it that way.',
    secCta: 'Request your private STI screen',
  },
  private: {
    tone: 'go',
    tag: 'Eligible',
    h: "You can book as a private patient.",
    p: "No Medicare or insurance needed. All costs are quoted upfront before you commit to anything.",
    cta: 'Request your private STI screen',
    sec: null,
  },
  overseas: {
    tone: 'warn',
    tag: 'Coming soon',
    h: "Prickly is Australia-only for now.",
    p: "We'll let you know when we ship to your country. In the meantime: in NZ, try Sexual Wellbeing Aotearoa; in the UK, NHS sexual health clinics offer at-home packages too.",
    cta: 'Notify me when we open',
    sec: null,
  },
};

function EligibilityQuiz() {
  const [step, setStep]   = React.useState(0);
  const [path, setPath]   = React.useState([]); // stack of step indices, for back
  const [result, setResult] = React.useState(null);

  const total = QUIZ.length;
  const progress = result ? 100 : (step / total) * 100;

  const pick = (opt) => {
    if (opt.end) { setResult(opt.end); return; }
    setPath((p) => [...p, step]);
    setStep(opt.next);
  };
  const back = () => {
    if (result) { setResult(null); return; }
    setPath((p) => {
      const prev = p[p.length - 1];
      if (prev === undefined) return p;
      setStep(prev);
      return p.slice(0, -1);
    });
  };
  const restart = () => { setStep(0); setPath([]); setResult(null); };

  return (
    <div className="pr-quiz">
      <div className="pr-quiz-progress" aria-hidden>
        <div className="pr-quiz-progress-bar" style={{ width: progress + '%' }} />
      </div>

      {!result && (
        <>
          <div className="pr-quiz-step-label">
            Eligibility check · Step {String(step + 1).padStart(2, '0')} / {String(total).padStart(2, '0')}
          </div>
          <h3 className="pr-quiz-question">{QUIZ[step].q}</h3>
          <p className="pr-p sm" style={{ opacity: 0.65, marginBottom: 24, maxWidth: 540 }}>{QUIZ[step].help}</p>
          <div className="pr-quiz-opts" role="radiogroup">
            {QUIZ[step].opts.map((o, i) => (
              <button key={i} className="pr-quiz-opt" onClick={() => pick(o)} type="button">
                <span style={{ display: 'inline-flex', alignItems: 'center', gap: 14 }}>
                  <span className="pr-quiz-opt-key">{String.fromCharCode(65 + i)}</span>
                  <span>{o.label}</span>
                </span>
                <span aria-hidden style={{ opacity: 0.5 }}>→</span>
              </button>
            ))}
          </div>
          <button className="pr-quiz-back" onClick={back} hidden={path.length === 0}>← back</button>
        </>
      )}

      {result && (
        <button type="button" onClick={restart} className="pr-quiz-back" style={{ position: 'absolute', bottom: 'clamp(28px, 4vw, 56px)', right: 'clamp(28px, 4vw, 56px)', marginTop: 0 }}>↺ restart</button>
      )}

      {result && (() => {
        const r = QUIZ_RESULTS[result];
        return (
          <div className={'pr-quiz-result ' + r.tone}>
            <span className="pr-pill pr-quiz-result-tag">{r.tag}</span>
            <h3>{r.h}</h3>
            <p className="pr-p" style={{ opacity: 0.82, fontSize: 17, lineHeight: 1.55, maxWidth: 540 }}>{r.p}</p>
            {r.cta && (
              <div className="pr-quiz-cta-row">
                <button type="button" className={'pr-btn ' + (r.tone === 'go' ? 'sage' : '')}>
                  {r.cta} <span className="pr-arrow">→</span>
                </button>
              </div>
            )}
            {r.sec && (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 20, paddingTop: 20, borderTop: '1px solid rgba(255,255,255,0.15)', maxWidth: 480 }}>
                <p style={{ fontWeight: 600, fontSize: 15, margin: 0 }}>{r.sec}</p>
                {r.secNote && <p className="pr-p" style={{ fontSize: 14, opacity: 0.6, margin: 0, lineHeight: 1.55 }}>{r.secNote}</p>}
                {r.secCta && (
                  <div className="pr-quiz-cta-row">
                    <button type="button" className="pr-btn ghost" onClick={r.secEnd ? () => setResult(r.secEnd) : undefined}>{r.secCta} <span className="pr-arrow">→</span></button>
                  </div>
                )}
              </div>
            )}
          </div>
        );
      })()}
    </div>
  );
}

// ── Kit explorer ──────────────────────────────────────────────────────────
const STIS = [
  { id: 'chlamydia',  label: 'Chlamydia',  method: 'Lab PCR',        parts: ['swab', 'urine'] },
  { id: 'gonorrhoea', label: 'Gonorrhoea', method: 'Lab PCR',        parts: ['swab', 'urine'] },
  { id: 'hiv',        label: 'HIV',        method: 'Rapid self test', parts: ['rapid'] },
  { id: 'syphilis',   label: 'Syphilis',   method: 'Rapid self test', parts: ['rapid'] },
];

const KIT_PARTS = [
  {
    id: 'swab',  label: 'Swabs',
    tests: ['chlamydia', 'gonorrhoea'],
    sites: 'Anal, throat, vaginal',
    test: 'Gold-standard PCR',
    h: 'Self-collect swabs',
  },
  {
    id: 'urine', label: 'Urine pot',
    tests: ['chlamydia', 'gonorrhoea'],
    sites: 'First pass urine',
    test: 'Gold-standard PCR',
    h: 'Self-collect urine pot',
  },
  {
    id: 'rapid', label: 'Self tests',
    tests: ['hiv', 'syphilis'],
    sites: 'Fingerprick blood',
    test: 'Gold-standard antibody',
    h: 'Fingerprick self tests',
  },
];

function KitExplorer() {
  const [active, setActive] = React.useState('swab');
  const part = KIT_PARTS.find((p) => p.id === active);

  // Row 1: Swabs full-width (short). Row 2: Urine 1/3, Self Tests 2/3 (taller).
  const boxes = {
    swab:  { x: 36,  y: 46,  w: 528, h: 90,  lines: ['SELF-COLLECT SWABS'] },
    urine: { x: 36,  y: 144, w: 168, h: 210, lines: ['SELF-COLLECT', 'URINE POT'] },
    rapid: { x: 220, y: 144, w: 344, h: 210, lines: ['FINGERPRICK', 'SELF TESTS'] },
  };

  return (
    <>
      <div className="pr-kit">
        <div className="pr-kit-stage">
          <svg className="pr-kit-svg" viewBox="0 0 600 400" preserveAspectRatio="xMidYMid meet" aria-hidden>
            {/* Tray border */}
            <rect x="20" y="30" width="560" height="350" rx="14" fill="none" stroke="currentColor" strokeOpacity="0.28" />

            {KIT_PARTS.map((p) => {
              const b = boxes[p.id];
              const isActive = p.id === active;
              return (
                <g
                  key={p.id}
                  className="pr-kit-hot"
                  data-active={isActive}
                  onClick={() => setActive(p.id)}
                  onMouseEnter={() => setActive(p.id)}
                >
                  <rect
                    x={b.x} y={b.y} width={b.w} height={b.h} rx="8"
                    fill={isActive ? 'var(--accent)' : 'none'}
                    stroke="currentColor"
                    strokeOpacity={isActive ? 0 : 0.5}
                    fillOpacity={isActive ? 1 : 0}
                  />
                  {!isActive && (
                    <pattern id={'pat-' + p.id} patternUnits="userSpaceOnUse" width="10" height="10" patternTransform="rotate(135)">
                      <line x1="0" y1="0" x2="0" y2="10" stroke="currentColor" strokeWidth="1" strokeOpacity="0.18" />
                    </pattern>
                  )}
                  {!isActive && (
                    <rect x={b.x} y={b.y} width={b.w} height={b.h} rx="8" fill={'url(#pat-' + p.id + ')'} />
                  )}
                  <text x={b.x + 14} y={b.y + 28} className="pr-kit-marker" style={{ fill: isActive ? 'var(--moss)' : 'currentColor', fontSize: 11, letterSpacing: '0.16em', fontWeight: 400, fillOpacity: 1 }}>
                    {b.lines.map((line, i) => (
                      <tspan key={i} x={b.x + 14} dy={i === 0 ? 0 : 22}>{line}</tspan>
                    ))}
                  </text>
                </g>
              );
            })}
          </svg>
        </div>

        <div className="pr-kit-info">
          <div className="pr-kit-detail" key={part.id}>
            <h3>{part.h}</h3>
            {part.tests && (
              <div className="pr-kit-tests">
                <span className="pr-kit-tests-label">Screens for</span>
                {STIS.filter((s) => part.tests.includes(s.id)).map((s) => (
                  <span key={s.id} className="pr-kit-sti-tag">{s.label}</span>
                ))}
              </div>
            )}
            {part.sites && (
              <div className="pr-kit-tests">
                <span className="pr-kit-tests-label">Sample site</span>
                <span className="pr-kit-sites-val">{part.sites}</span>
              </div>
            )}
            {part.test && (
              <div className="pr-kit-tests">
                <span className="pr-kit-tests-label">Analysis</span>
                <span className="pr-kit-sites-val">{part.test}</span>
              </div>
            )}
            <p className="pr-p sm" style={{ marginTop: 20, opacity: 0.5, fontSize: 13, lineHeight: 1.5 }}>
              Prickly does not screen for herpes (HSV), human papilloma virus (HPV), hepatitis B/C, MPOX, mycoplasma genitalium (MG), trichomonas or any other STI. If you have concerns about these or any other STI, please see a GP or sexual health clinic.
            </p>
          </div>
        </div>
      </div>
    </>
  );
}

// ── Closed-loop care diagram ──────────────────────────────────────────────
const STAGE_DURATION = 6400;
const LOOP_NODES = [
  { id: 'order',     label: 'Request',   num: '01',
    h: 'Eligibility & request',
    p: "An Australian-registered clinician reviews every request against the Australian STI Management Guidelines before issuing a Medicare-compliant pathology referral. Every request, every time — no exceptions." },
  { id: 'collect',   label: 'Collect',   num: '02',
    h: 'Self-collection',
    p: "TGA-listed collection equipment — the same used in GP and sexual health clinics — arrives in plain packaging. Samples are self-collected at any pace, with clinician support available if needed." },
  { id: 'lab',       label: 'Analyse',   num: '03',
    h: 'PCR analysis',
    p: "Samples go via Australia Post — compliant with their biological specimen requirements — to our partner lab for PCR analysis. Clinic-grade technology, validated to maintain accuracy in transit." },
  { id: 'review',    label: 'Review',    num: '04',
    h: 'Clinician review',
    p: "Every result — negative, positive, indeterminate — is reviewed by an Australian-registered clinician before anything is communicated. No bots, no automation anywhere in the clinical decision chain." },
  { id: 'followup',  label: 'Follow-up', num: '05',
    h: 'Telehealth follow-up',
    p: "Positive and indeterminate results trigger a mandatory telehealth appointment with an Australian-registered clinician — prescriptions, referrals, and (with consent) a summary sent to your regular GP." },
  { id: 'escalate',  label: 'Escalate',  num: '06',
    h: 'Result escalation',
    p: "After every completed request, if a positive or indeterminate result goes unaddressed after 14 days, a clinician follows up directly by phone and letter — and notifies the relevant Public Health authority if required." },
  { id: 'reminder',  label: 'Remind',    num: '07',
    h: 'Screening reminder',
    p: "Three months after every completed request, a reminder email is sent — a nudge to consider whether a repeat STI screen is recommended based on current guidelines and individual circumstances." },
];

function ClosedLoopDiagram() {
  const [active, setActive] = React.useState(0);
  const stageRef = React.useRef(null);
  const timerRef = React.useRef(null);

  function startTimer() {
    clearInterval(timerRef.current);
    timerRef.current = setInterval(() => {
      setActive((a) => (a + 1) % LOOP_NODES.length);
    }, STAGE_DURATION);
  }

  React.useEffect(() => {
    startTimer();
    return () => clearInterval(timerRef.current);
  }, []);

  const cx = 200, cy = 200, r = 182;
  const positions = LOOP_NODES.map((_, i) => {
    const angle = -Math.PI / 2 + (i / LOOP_NODES.length) * Math.PI * 2;
    return {
      x: cx + Math.cos(angle) * r,
      y: cy + Math.sin(angle) * r,
    };
  });

  const node = LOOP_NODES[active];

  return (
    <div className="pr-loop">
      <div className="pr-loop-stage" ref={stageRef}>
        <svg viewBox="-60 -60 520 520" aria-hidden>
          {/* dashed orbit */}
          <circle cx={cx} cy={cy} r={r} fill="none"
            stroke="currentColor" strokeOpacity="0.22"
            strokeDasharray="2 6" />

          {/* connecting arc to next active */}
          {positions.map((p, i) => {
            const next = positions[(i + 1) % positions.length];
            const isActive = i === active;
            return (
              <line key={i}
                x1={p.x} y1={p.y} x2={next.x} y2={next.y}
                stroke="currentColor"
                strokeOpacity={isActive ? 0.65 : 0.22}
                strokeWidth={isActive ? 1.5 : 1}
                style={{ transition: 'stroke-opacity .35s, stroke-width .35s' }}
              />
            );
          })}

          {/* central marker */}
          <circle cx={cx} cy={cy} r="68"
            fill="none"
            stroke="currentColor" strokeOpacity="0.16" strokeWidth="1" />
          <text x={cx} y={cy - 7} textAnchor="middle" fill="currentColor" opacity="0.55"
            style={{ fontFamily: '"JetBrains Mono", monospace', fontSize: 15, letterSpacing: '0.16em', textTransform: 'uppercase' }}>
            Closed
          </text>
          <text x={cx} y={cy + 16} textAnchor="middle" fill="currentColor" opacity="0.55"
            style={{ fontFamily: '"JetBrains Mono", monospace', fontSize: 15, letterSpacing: '0.16em', textTransform: 'uppercase' }}>
            loop
          </text>

          {positions.map((p, i) => {
            const n = LOOP_NODES[i];
            const isActive = i === active;
            return (
              <g
                key={n.id}
                className={'pr-loop-node ' + (isActive ? 'active' : '')}
                transform={`translate(${p.x}, ${p.y})`}
                onClick={() => { setActive(i); startTimer(); }}
                onMouseEnter={() => { setActive(i); startTimer(); }}
              >
                <circle r="58" fill="var(--moss)" />
                <circle r="58" className="pr-loop-node-circle" />
                <text y="-5" className="num" textAnchor="middle">{n.num}</text>
                <text y="15" textAnchor="middle">{n.label}</text>
              </g>
            );
          })}
        </svg>
      </div>

      <div className="pr-loop-info">
        <div className="pr-loop-counter">
          Stage {node.num} / {String(LOOP_NODES.length).padStart(2, '0')}
        </div>
        <h3 key={'h-' + node.id}>{node.h}</h3>
        <p key={'p-' + node.id}>{node.p}</p>
        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 8 }}>
          {LOOP_NODES.map((n, i) => (
            <button
              key={n.id}
              type="button"
              onClick={() => { setActive(i); startTimer(); }}
              aria-label={'Stage ' + n.label}
              style={{
                width: 32, height: 4,
                background: 'none',
                border: 0, cursor: 'pointer', padding: 0,
              }}
            >
              <div style={{
                position: 'relative', overflow: 'hidden',
                width: '100%', height: '100%',
                background: 'rgba(241,236,222,0.18)',
                borderRadius: 2,
              }}>
                {i === active && (
                  <span key={active} className="pr-loop-progress-fill" />
                )}
              </div>
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

// ── Scroll-progressed 5-day timeline ──────────────────────────────────────
const TIMELINE = [
  { d: 'Anytime',  h: "Complete the eligibility check and request your STI screen",
    body: "Reviewed by an Australian-registered clinician before anything is shipped. If you're not a fit, we say so and point you to the right service. Your medical record is stored securely, with integrated secure payment." },
  { d: 'Sent next business day · arrives 2–6 days',  h: "Receive your specimen collection equipment in plain packaging.",
    body: "Plain, unmarked packaging — your name and address on the outside, everything you need to collect your samples on the inside. Shipped via Australia Post with tracking by email and/or SMS." },
  { d: 'On your schedule', h: "Collect your samples, on your own schedule.",
    body: "Swabs, urine, fingerprick self tests — all TGA-listed and completed within 15 minutes. Fool-proof instructions included, with clinician support available if you need it." },
  { d: 'Send anytime · received 2–6 days', h: "Return your package to the lab.",
    body: "Repackage your sealed samples in the original box using the included pre-paid shipping label. Drop it into any Australia Post Office or red Post Box. Track via email, SMS, or the AusPost app." },
  { d: 'Within 1–3 days of samples being received',  h: "Receive your results, properly handled.",
    body: "Negative: a discreet email saying so. Positive or indeterminate: an email asking you to book a Telehealth follow-up — never a result by SMS. Every result is clinician reviewed, and any follow-up is at no extra cost." },
];

function Timeline() {
  const ref = React.useRef(null);
  const p = useScrollProgress(ref);
  const litCount = Math.max(0, Math.min(TIMELINE.length, Math.ceil(p * TIMELINE.length * 1.05)));

  return (
    <div className="pr-timeline" ref={ref}>
      <div className="pr-timeline-rail">
        <div className="pr-timeline-rail-fill" style={{ height: (p * 100) + '%' }} />
      </div>
      <div className="pr-timeline-items">
        {TIMELINE.map((it, i) => (
          <div key={it.d} className={'pr-timeline-item ' + (i < litCount ? 'lit' : '')}>
            <div>
              <div className="pr-timeline-day">{it.d}</div>
              <h3 className="pr-timeline-h">{it.h}</h3>
              <p className="pr-timeline-body">{it.body}</p>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// Export to window for the main site script
Object.assign(window, {
  Reveal, RevealWords, RotatingHeadline,
  EligibilityQuiz, KitExplorer, ClosedLoopDiagram, Timeline,
  useInView, useScrollProgress,
});
