/* Bench2Bedside Singapore, frontend
   Plain JSX, compiled in the browser by Babel, so there is no build step. */

const { useState, useEffect, useRef, useMemo } = React;

/* ----------------------------------------------------------------- brand -- */
const Logo = () => (
  <svg viewBox="0 0 32 32" className="logo" aria-hidden="true">
  <g transform="translate(0 -0.6)">
    <g fill="none" stroke="#0B1F1C" strokeWidth="2.3" strokeLinecap="round">
      <line x1="10.71" y1="25.24" x2="9.29" y2="21.24" strokeWidth="1.84" strokeOpacity=".55"/>
      <line x1="21.29" y1="8.76" x2="22.71" y2="12.76" strokeWidth="1.84" strokeOpacity=".55"/>
      <path d="M4.07 27.00 L4.30 26.99 L4.52 26.98 L4.75 26.97 L4.98 26.95 L5.21 26.93 L5.44 26.91 L5.67 26.88 L5.90 26.85 L6.14 26.82 L6.37 26.78 L6.61 26.74 L6.85 26.69 L7.08 26.64 L7.32 26.59 L7.57 26.53 L7.81 26.46 L8.06 26.39 L8.30 26.32 L8.55 26.24 L8.80 26.15 L9.05 26.05 L9.31 25.95 L9.56 25.84 L9.82 25.72 L10.07 25.60 L10.33 25.46 L10.58 25.32 L10.84 25.16 L11.09 25.00 L11.34 24.82 L11.59 24.63 L11.84 24.44 L12.08 24.23 L12.31 24.00 L12.54 23.77 L12.76 23.52 L12.98 23.27 L13.19 23.00 L13.40 22.72 L13.59 22.43 L13.78 22.13 L13.97 21.82 L14.14 21.50 L14.32 21.17 L14.48 20.83 L14.65 20.48 L14.81 20.12 L14.96 19.75 L15.12 19.37 L15.27 18.99 L15.42 18.60 L15.56 18.20 L15.71 17.81 L15.85 17.40 L16.00 17.00 L16.15 16.60 L16.29 16.19 L16.44 15.80 L16.58 15.40 L16.73 15.01 L16.88 14.63 L17.04 14.25 L17.19 13.88 L17.35 13.52 L17.52 13.17 L17.68 12.83 L17.86 12.50 L18.03 12.18 L18.22 11.87 L18.41 11.57 L18.60 11.28 L18.81 11.00 L19.02 10.73 L19.24 10.48 L19.46 10.23 L19.69 10.00 L19.92 9.77 L20.16 9.56 L20.41 9.37 L20.66 9.18 L20.91 9.00 L21.16 8.84 L21.42 8.68 L21.67 8.54 L21.93 8.40 L22.18 8.28 L22.44 8.16 L22.69 8.05 L22.95 7.95 L23.20 7.85 L23.45 7.76 L23.70 7.68 L23.94 7.61 L24.19 7.54 L24.43 7.47 L24.68 7.41 L24.92 7.36 L25.15 7.31 L25.39 7.26 L25.63 7.22 L25.86 7.18 L26.10 7.15 L26.33 7.12 L26.56 7.09 L26.79 7.07 L27.02 7.05 L27.25 7.03 L27.48 7.02 L27.70 7.01 L27.93 7.00"/>
      <path d="M3.93 21.00 L4.14 21.00 L4.35 20.99 L4.56 20.99 L4.76 21.00 L4.97 21.00 L5.18 21.00 L5.38 21.01 L5.59 21.02 L5.79 21.03 L5.99 21.04 L6.19 21.05 L6.39 21.06 L6.59 21.07 L6.78 21.09 L6.98 21.10 L7.17 21.12 L7.36 21.13 L7.55 21.15 L7.74 21.16 L7.93 21.18 L8.11 21.19 L8.29 21.20 L8.48 21.22 L8.66 21.23 L8.84 21.24 L9.02 21.24 L9.20 21.24 L9.38 21.24 L9.56 21.24 L9.75 21.23 L9.94 21.22 L10.13 21.19 L10.32 21.16 L10.53 21.13 L10.73 21.08 L10.94 21.02 L11.16 20.95 L11.39 20.86 L11.62 20.76 L11.86 20.64 L12.11 20.50 L12.36 20.35 L12.62 20.18 L12.88 19.99 L13.15 19.79 L13.42 19.57 L13.70 19.33 L13.98 19.07 L14.27 18.81 L14.55 18.53 L14.84 18.23 L15.13 17.93 L15.42 17.63 L15.71 17.31 L16.00 17.00 L16.29 16.69 L16.58 16.37 L16.87 16.07 L17.16 15.77 L17.45 15.47 L17.73 15.19 L18.02 14.93 L18.30 14.67 L18.58 14.43 L18.85 14.21 L19.12 14.01 L19.38 13.82 L19.64 13.65 L19.89 13.50 L20.14 13.36 L20.38 13.24 L20.61 13.14 L20.84 13.05 L21.06 12.98 L21.27 12.92 L21.47 12.87 L21.68 12.84 L21.87 12.81 L22.06 12.78 L22.25 12.77 L22.44 12.76 L22.62 12.76 L22.80 12.76 L22.98 12.76 L23.16 12.76 L23.34 12.77 L23.52 12.78 L23.71 12.80 L23.89 12.81 L24.07 12.82 L24.26 12.84 L24.45 12.85 L24.64 12.87 L24.83 12.88 L25.02 12.90 L25.22 12.91 L25.41 12.93 L25.61 12.94 L25.81 12.95 L26.01 12.96 L26.21 12.97 L26.41 12.98 L26.62 12.99 L26.82 13.00 L27.03 13.00 L27.24 13.00 L27.44 13.01 L27.65 13.01 L27.86 13.00 L28.07 13.00"/>
    </g>
    <circle cx="16.00" cy="17.00" r="3.6" fill="#D6156B"/>
    </g>
  </svg>
);

/* --------------------------------------------------------------- pipeline -- */
const STAGES = [
  { key: '01', label: 'Build',    blurb: 'Build the science and the asset',
    say: 'Your job right now is to make the science hold up and secure the IP position. Funding at this point is mostly non-dilutive and institutional.' },
  { key: '02', label: 'Advance',  blurb: 'Make it investible / IND-ready',
    say: 'You are de-risking the asset so someone else will pay for it. Toxicology, GMP material and the first regulatory conversations all belong here.' },
  { key: '03', label: 'Spin off', blurb: 'Incorporate, licence the IP',
    say: 'This is a legal and structural stage, not a scientific one. Chain of title, the institutional licence and the entity itself are what gate your first priced round.' },
  { key: '04', label: 'Raise',    blurb: 'Grants, equity, VC',
    say: 'Money is the task. Most Singapore schemes want a co-investor or a specific milestone, so line those up before you start applying.' },
  { key: '05', label: 'Scale',    blurb: 'GMP, trials, HSA, markets',
    say: 'You are executing: clinical supply, trial approvals and the regulatory path in the market you actually intend to sell in.' },
];

/* Six families, six clearly separated hues, green, blue, orange, indigo,
   violet, magenta, so a page of results can be read by colour alone. */
const FAMILY_COLOUR = {
  funding:    { c: '#0F7A52', soft: '#E3F3EC', line: '#B9DECC', label: 'Funding' },
  science:    { c: '#0E6FA8', soft: '#E2EFF8', line: '#B7D6EB', label: 'Platform' },
  regulatory: { c: '#C2410C', soft: '#FDEDE3', line: '#F3D3BC', label: 'Regulatory' },
  legal:      { c: '#4338CA', soft: '#E9E8FA', line: '#C7C3F0', label: 'IP & legal' },
  build:      { c: '#A21CAF', soft: '#FAE7FC', line: '#EEC4F4', label: 'Build space' },
  network:    { c: '#D6156B', soft: '#FCE7F0', line: '#F5C3D8', label: 'People' },
};
const FAMILY_ORDER = ['funding', 'science', 'regulatory', 'legal', 'build', 'network'];

/* Anything that is not therapeutics-specific carries a visible tag, so the
   footer's promise is literally true on the card. 'core' is untagged because
   untagged is the default the reader should be able to assume. */
const FIT_LABEL = { adjacent: 'General startup', medtech: 'Medtech' };

/* Where the entry came from. An official page and a press announcement are not
   the same claim, and the card should not pretend otherwise. */
const SOURCE_LABEL = { press: 'Press-reported', manual: 'Community-reported' };

/* What a tick actually means. Anything not listed here shows as unchecked. */
const CHECKED = {
  'page-checked': 'Someone opened this page and confirmed the amounts and eligibility',
  'founder-checked': 'Confirmed directly by the directory’s editor',
  'auto-checked': 'The weekly refresh re-read this page and it still matched',
};

const SITUATIONS = [
  { label: 'I just filed an invention disclosure',
    q: 'I filed an invention disclosure with my institution and I do not know what happens next or how to get the IP out.' },
  { label: 'I need funding for preclinical work',
    q: 'I need non-dilutive funding and early investors for preclinical therapeutics work in Singapore.' },
  { label: 'Getting to IND-ready',
    q: 'What do I need to make my asset IND-ready, including toxicology, GMP material and regulatory meetings?' },
  { label: 'I want to spin out a company',
    q: 'I want to spin a company out of my institution, licence the IP and incorporate in Singapore.' },
  { label: 'Bringing a foreign asset into Singapore',
    q: 'We are a foreign biotech bringing our asset and IP into Singapore as a base for development and the US market.' },
  { label: 'I need lab space',
    q: 'Where can I get wet lab bench space or incubator space in Singapore, and what does it cost?' },
  { label: 'Planning a first-in-human trial',
    q: 'How do I run a first-in-human clinical trial in Singapore and what approvals and quality standards apply?' },
  { label: 'Looking for people and events',
    q: 'Where do I meet other biotech founders, advisors and possible co-founders in Singapore?' },
];

/* ------------------------------------------------- the sigmoid dose curve -- */
const K = 5.4;
const sig = t => {
  const lo = 1 / (1 + Math.exp(K)), hi = 1 / (1 + Math.exp(-K));
  return (1 / (1 + Math.exp(-(-K + 2 * K * t))) - lo) / (hi - lo);
};
const PX = 7, PY = 27;
const cx = t => PX + t * (100 - 2 * PX);
const cy = t => (100 - PY) - sig(t) * (100 - 2 * PY);
const NODE_T = [0.05, 0.29, 0.50, 0.71, 0.95];

function Pathway({ active, onPick }) {
  const idx = STAGES.findIndex(s => s.key === active);
  const { full, area, done } = useMemo(() => {
    const pts = n => {
      const a = [];
      for (let i = 0; i <= n; i++) {
        const t = (i / n) * (idx < 0 ? 1 : 1);
        a.push(`${cx(t).toFixed(2)},${cy(t).toFixed(2)}`);
      }
      return 'M' + a.join(' L');
    };
    const upto = () => {
      if (idx < 0) return '';
      const end = NODE_T[idx], a = [];
      for (let i = 0; i <= 80; i++) {
        const t = (i / 80) * end;
        a.push(`${cx(t).toFixed(2)},${cy(t).toFixed(2)}`);
      }
      return 'M' + a.join(' L');
    };
    const f = pts(140);
    const area = `${f} L ${cx(1).toFixed(2)},${100 - PY} L ${cx(0).toFixed(2)},${100 - PY} Z`;
    return { full: f, area, done: upto() };
  }, [idx]);

  return (
    <section className="pathway" aria-label="Pipeline">
      <div className="cap">
        <h2>The pipeline</h2>
        <p>Five stages from bench to market. Ask a question and we place you on it, or tap a stage to
           browse it directly.</p>
      </div>
      <div className="plot">
        <svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
          <defs>
            <linearGradient id="under" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stopColor="#12706B" stopOpacity=".22" />
              <stop offset="55%" stopColor="#12706B" stopOpacity=".07" />
              <stop offset="100%" stopColor="#12706B" stopOpacity="0" />
            </linearGradient>
            <filter id="curveshadow" x="-8%" y="-20%" width="116%" height="150%">
              <feDropShadow dx="0" dy="1.6" stdDeviation="1.4"
                            floodColor="#0B1F1C" floodOpacity=".18" />
            </filter>
          </defs>
          <path d={area} fill="url(#under)" />
          <path d={full} fill="none" stroke="#0B1F1C" strokeWidth="2.6"
                strokeLinecap="round" vectorEffect="non-scaling-stroke"
                filter="url(#curveshadow)" />
          {done ? <path d={done} fill="none" stroke="#12706B" strokeWidth="3.4"
                        strokeLinecap="round" vectorEffect="non-scaling-stroke" /> : null}
        </svg>

        {STAGES.map((s, i) => (
          <button key={s.key} type="button"
                  className={'node' + (idx >= 0 && i < idx ? ' done' : '') + (active === s.key ? ' on' : '')}
                  style={{ left: cx(NODE_T[i]) + '%', top: cy(NODE_T[i]) + '%' }}
                  onClick={() => onPick(s.key)} aria-pressed={active === s.key}
                  aria-label={`${s.key} ${s.label}: ${s.blurb}`}>
            <span className="dot" />
          </button>
        ))}

        {STAGES.map((s, i) => (
          <div key={s.key}
               className={'lab' + (idx >= 0 && i < idx ? ' done' : '') + (active === s.key ? ' here' : '')}
               style={{ left: cx(NODE_T[i]) + '%', top: `calc(${cy(NODE_T[i])}% + 16px)` }}>
            <span className="n">{s.key}</span>
            <b>{s.label}</b>
            <span className="d">{s.blurb}</span>
          </div>
        ))}
      </div>
    </section>
  );
}

/* --------------------------------------------------------------- results -- */
function Res({ r }) {
  const fam = FAMILY_COLOUR[r.family] || FAMILY_COLOUR.network;
  const track = () => { navigator.sendBeacon?.('/api/pulse?p=' + encodeURIComponent('/out/' + r.id)); };
  const flag = FIT_LABEL[r.biotech_fit] || null;
  const style = { '--fam': fam.c, '--fam-soft': fam.soft, '--fam-line': fam.line };

  return (
    <article className="res" style={style}>
      <div className="rhead">
        <h3>{r.name}</h3>
        <div className="pills">
          <span className="pill fam">{fam.label}</span>
          <span className="pill type">{r.type}</span>
          <span className="pill status">{r.status}</span>
          {flag ? <span className="pill flag">{flag}</span> : null}
          {SOURCE_LABEL[r.source]
            ? <span className="pill src" title="Not an official programme page, confirm before relying on it">
                {SOURCE_LABEL[r.source]}
              </span>
            : null}
        </div>
      </div>

      <p className="meta"><b>{r.org}</b>{r.amount ? <> · {r.amount}</> : null}</p>

      <p className="why"><em>Why this fits, </em> {r.why}</p>

      <div className="req">
        <span className="mono">Crucial requirements</span>
        <p>{r.requirements}</p>
      </div>

      <div className="foot">
        <span className="next">
          <b>Next step</b>{r.next_step}
          {r.contact ? <span className="who">No direct link, {r.contact}</span> : null}
        </span>
        <span className="right">
          <span className={'check' + (CHECKED[r.verification] ? '' : ' un')}
                title={CHECKED[r.verification] ||
                  'Listed from the source, but nobody has read the page line by line yet'}>
            {CHECKED[r.verification] ? '✓ checked ' + r.verified_on : '○ not yet checked'}
          </span>
          <a className="open" href={r.link} target="_blank" rel="noopener noreferrer" onClick={track}>
            Open <span className="arw">↗</span>
          </a>
        </span>
      </div>
    </article>
  );
}

/* ----------------------------------------------------------- submit sheet -- */
function SubmitSheet({ onClose }) {
  const [f, setF] = useState({ name: '', url: '', category: 'Grant', note: '', website: '' });
  const [state, setState] = useState('idle');
  const [msg, setMsg] = useState('');
  const [types, setTypes] = useState(typeof B2B_TYPES !== 'undefined' ? B2B_TYPES : null);
  const set = k => e => setF({ ...f, [k]: e.target.value });

  useEffect(() => {
    if (!types) fetch('/api/types').then(r => r.json()).then(d => setTypes(d.types)).catch(() => {});
  }, []);

  // group the real types out of the directory under their colour family
  const grouped = FAMILY_ORDER
    .map(fam => ({ fam, label: FAMILY_COLOUR[fam].label,
                   list: (types || []).filter(t => t.family === fam) }))
    .filter(g => g.list.length);

  const send = async () => {
    setState('sending');
    try {
      const res = await fetch('/api/submit', {
        method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(f),
      });
      const d = await res.json();
      if (!res.ok) { setMsg(d.error || 'That did not go through.'); setState('idle'); return; }
      setState('done');
    } catch { setMsg('Network problem. Try again.'); setState('idle'); }
  };

  return (
    <div className="veil" onClick={e => e.target === e.currentTarget && onClose()}>
      <div className="sheet" role="dialog" aria-modal="true" aria-label="Suggest a resource">
        {state === 'done' ? (
          <div>
            <h2>Got it, thank you.</h2>
            <p className="note">Every submission is checked by a human before it goes live, so it will
              not appear straight away. If it holds up, it joins the directory in the next weekly pass.
              Since we did not take your contact details, we cannot write back, which is the trade
              we would rather make.</p>
            <div className="actions"><button className="go" onClick={onClose}>Close</button></div>
          </div>
        ) : (
          <div>
            <h2>Suggest a resource</h2>
            <p className="note">Know a programme, fund, facility or event that belongs here?
              Submissions are anonymous, we only want the link.</p>
            <div className="field">
              <label htmlFor="s-name">Name</label>
              <input id="s-name" value={f.name} onChange={set('name')} />
            </div>
            <div className="field">
              <label htmlFor="s-url">Link to the exact page</label>
              <input id="s-url" value={f.url} onChange={set('url')} placeholder="https://…" inputMode="url" />
            </div>
            <div className="field">
              <label htmlFor="s-cat">Type</label>
              <select id="s-cat" value={f.category} onChange={set('category')}>
                {grouped.length
                  ? grouped.map(g => (
                      <optgroup key={g.fam} label={g.label}>
                        {g.list.map(t => <option key={t.type} value={t.type}>{t.type}</option>)}
                      </optgroup>
                    ))
                  : <option>Loading…</option>}
                <option value="Other">Other / not sure</option>
              </select>
              <p className="hint">Grouped by the colour family the card will use. Pick the closest, 
                we can recategorise it during review.</p>
            </div>
            <div className="field">
              <label htmlFor="s-note">What it is, and who it is for</label>
              <textarea id="s-note" value={f.note} onChange={set('note')} />
            </div>
            <p className="nodata">
              We do not ask for your name or email. Nothing you send here identifies you.
            </p>
            <input className="hp" tabIndex="-1" autoComplete="off" aria-hidden="true"
                   value={f.website} onChange={set('website')} />
            {msg ? <div className="err">{msg}</div> : null}
            <div className="actions">
              <button className="ghost" onClick={onClose}>Cancel</button>
              <button className="go" onClick={send} disabled={state === 'sending'}>
                {state === 'sending' ? 'Sending…' : 'Send it in'}
              </button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

/* --------------------------------------------------------------- matching -- */
/* A branded, indeterminate progress indicator. The real wait is the AI call,
   whose length we can't predict, so we show motion + honest rolling status
   rather than a fake percentage. */
function Matching() {
  const steps = [
    'Reading your situation…',
    'Searching the directory…',
    'Ranking the best-fit resources…',
    'Writing why each one fits…',
  ];
  const [i, setI] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setI(n => Math.min(n + 1, steps.length - 1)), 1300);
    return () => clearInterval(id);
  }, []);
  return (
    <div className="matching" role="status" aria-live="polite">
      <div className="spinner" aria-hidden="true" />
      <p className="matching-msg mono">{steps[i]}</p>
    </div>
  );
}

/* -------------------------------------------------------------------- app -- */
function App() {
  const [q, setQ] = useState('');
  const [stage, setStage] = useState('');
  const [data, setData] = useState(null);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState('');
  const [sheet, setSheet] = useState(false);
  const [showMore, setShowMore] = useState(false);
  const [meta, setMeta] = useState(typeof B2B_META !== 'undefined' ? B2B_META : null);
  const out = useRef(null);

  useEffect(() => {
    fetch('/api/pulse?p=/&r=' + encodeURIComponent(document.referrer || '')).catch(() => {});
    if (!meta) fetch('/api/meta').then(r => r.json()).then(setMeta).catch(() => {});
  }, []);

  const ask = async (text, st) => {
    const query = (text ?? q).trim();
    const useStage = st ?? stage;
    if (query.length < 3 && !useStage) { setErr('Describe your situation, or pick a stage on the pipeline.'); return; }
    setBusy(true); setErr(''); setData(null); setShowMore(false);
    try {
      const res = await fetch('/api/match', {
        method: 'POST', headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ query, stage: useStage }),
      });
      const d = await res.json();
      if (!res.ok) { setErr(d.error || 'That search did not work.'); setBusy(false); return; }
      setData(d);
      if (d.stage) setStage(d.stage);
      setTimeout(() => out.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 60);
    } catch (e) { setErr('That search could not run. ' + (e && e.message ? e.message : '')); }
    finally { setBusy(false); }
  };

  const stageObj = STAGES.find(s => s.key === (data?.stage || stage));

  return (
    <div>
      <header className="masthead">
        <div className="wrap bar">
          <a className="brand" href="/">
            <Logo />
            <span className="wm">Bench<i>2</i>Bedside <span className="sg">Singapore</span></span>
          </a>
          <button className="ghost" onClick={() => setSheet(true)}>Suggest a resource</button>
        </div>
      </header>

      <main className="wrap">
        <section className="hero">
          <p className="eyebrow mono">Singapore therapeutics ecosystem</p>
          <h1>
            <span>Know which door to knock on.</span>
            <span className="l2">And what it takes to get in.</span>
          </h1>
          <p className="lede">
            Singapore's support for drug development is real, but it's scattered across dozens of
            agencies. Say where you are, and get the few programmes, investors and regulators that
            actually fit your stage, plus what it takes to qualify for each.
          </p>
          <ul className="features">
            <li><b>{meta ? meta.count : ', '}</b> resources{meta && meta.checked
              ? <> · <b>{meta.checked}</b> confirmed against their page</> : null}</li>
            <li><b>Updated weekly</b>, {meta && meta.last_refresh
              ? <>the site last re-read {meta.per_day} pages on {meta.last_refresh}</>
              : <>every page re-read on a rolling cycle</>}</li>
            <li><b>Built by the community</b>, anyone can suggest a resource</li>
          </ul>
        </section>

        <section className="picker">
          <form onSubmit={e => { e.preventDefault(); ask(); }}>
            <label className="mono ask-label" htmlFor="ask">Describe where you are</label>
            <textarea id="ask" value={q} onChange={e => setQ(e.target.value)}
              placeholder="e.g. I'm a post-doc at a local institute. We have a small-molecule hit against a fibrosis target with mouse efficacy data, I filed a disclosure four months ago, and I want to know what it takes to spin this out and fund the next two years."
              onKeyDown={e => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) ask(); }} />

            <div className="sug">
              <span className="mono">Or start from a common situation</span>
              <div className="chips">
                {SITUATIONS.map(s => (
                  <button key={s.label} type="button" className="chip"
                          onClick={() => { setQ(s.q); ask(s.q, stage); }}>{s.label}</button>
                ))}
              </div>
            </div>

            <p className="privacy">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
              <path d="M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z" />
            </svg>
            <span>
              <b>Please do not type anything confidential.</b> No unpublished data, compound structures,
              sequences or deal terms. A general description works just as well. Questions are stored
              only so the directory can be improved, and are never shown to other users or shared.
            </span>
            </p>

            <div className="askfoot">
              <button className="go" type="submit" disabled={busy}>
                {busy ? 'Matching…' : 'Find resources'}
              </button>
            </div>
          </form>
          {busy ? <Matching /> : null}
        </section>

        <Pathway active={data?.stage || stage} onPick={key => { setStage(key); ask(q || '', key); }} />

        {stageObj ? (
          <section className="youare">
            <span className="mono">Where you are</span>
            <h2>{stageObj.key} · {stageObj.label}, {stageObj.blurb}</h2>
            <p>{data?.stage_note || stageObj.say}</p>
            {data?.orientation ? <p className="orient">{data.orientation}</p> : null}
          </section>
        ) : null}

        <section className="results" ref={out}>
          {err ? <div className="err">{err}</div> : null}

          {data ? (
            <div>
              <div className="lead">
                {!stageObj && data.orientation ? <h2>{data.orientation}</h2> : null}
                <span className="mono">
                  {data.results.length} {data.results.length === 1 ? 'match' : 'matches'}
                  {data.ai === false ? <span className="degraded"> · keyword only</span> : null}
                </span>
              </div>

              <ul className="key">
                {FAMILY_ORDER.filter(f => data.results.some(r => r.family === f)).map(f => (
                  <li key={f}><i style={{ background: FAMILY_COLOUR[f].c }} />{FAMILY_COLOUR[f].label}</li>
                ))}
              </ul>
              {data.results.length
                ? <div className="reslist">{data.results.map(r => <Res key={r.id} r={r} />)}</div>
                : <p className="empty">Nothing matched. Try a stage on the pipeline above.</p>}

              {data.more && data.more.length ? (
                showMore ? (
                  <div className="reslist more-list">
                    <div className="more-divider mono">
                      More possible matches, broader fit, same verified directory
                    </div>
                    {data.more.map(r => <Res key={r.id} r={r} />)}
                  </div>
                ) : (
                  <button type="button" className="more-btn" onClick={() => setShowMore(true)}>
                    Show {data.more.length} more {data.more.length === 1 ? 'resource' : 'resources'}
                  </button>
                )
              ) : null}
            </div>
          ) : null}
        </section>
      </main>

      <footer className="fineprint">
        <div className="wrap cols">
          <div>
            <div className="name">Bench2Bedside Singapore</div>
            <p>
              Biotech and therapeutics first; general-startup and medtech entries are tagged.
              Details indicative, confirm eligibility, amounts and deadlines on the official page
              before acting. Not financial, legal, tax or regulatory advice.
            </p>
            <p>
              An independent personal project, not affiliated with, endorsed by, or an official
              channel of any organisation listed on it. All names and marks belong to their owners.
            </p>
            <p className="legal-links"><a href="/terms.html">Terms of Use</a> · <a href="/privacy.html">Privacy Notice</a></p>
          </div>
          <button className="ghost" onClick={() => setSheet(true)}>Suggest a resource</button>
        </div>
      </footer>

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

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
