3) “Orchid Interaction Explorer” widget (spec + code)

What it does
	•	Search an orchid species →
	•	Map: overlays GBIF occurrence density + clickable recent points
	•	Interactions: lists pollinators & mycorrhiza with evidence/citations
	•	Uses: food/medicine/trade with sources
	•	Attribution footer auto-generates dataset citations

Minimal data contract (what the widget expects)

Serve a single JSON at /api/continuum/species/{acceptedTaxonKey}.json matching the schema above.

Drop-in HTML (no external build tools)

Copy, paste, and replace the placeholder API URL. This uses GBIF’s tile servers for the basemap + density layer and renders your merged JSON. (If your site CSP blocks external tiles, proxy them through your server.)

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Orchid Interaction Explorer</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <style>
    body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; }
    header { padding: 12px 16px; border-bottom: 1px solid #eee; }
    .wrap { max-width: 1100px; margin: 0 auto; padding: 16px; }
    .row { display: grid; grid-template-columns: 1fr; gap: 16px; }
    @media (min-width: 900px){ .row { grid-template-columns: 2fr 1fr; } }
    #map { height: 420px; border: 1px solid #e5e5e5; border-radius: 8px; }
    h2 { margin: 12px 0 6px; font-size: 18px; }
    small.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #666; }
    .card { border: 1px solid #eee; border-radius: 8px; padding: 12px; }
    .pill { display:inline-block; padding:2px 8px; border:1px solid #ddd; border-radius:999px; margin:2px 4px 2px 0; font-size:12px;}
    footer { margin-top: 16px; font-size: 12px; color: #555; }
    a { color: #2a5bd7; text-decoration: none; }
  </style>
  <!-- MapLibre (lightweight, open) -->
  <link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet" />
  <script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
</head>
<body>
  <header>
    <div class="wrap">
      <strong>Orchid Interaction Explorer</strong>
      <div>
        <label>Search species (GBIF taxonKey):
          <input id="taxonKey" placeholder="e.g., 2877955" style="width:220px" />
        </label>
        <button id="load">Load</button>
        <small class="mono" id="sciName"></small>
      </div>
    </div>
  </header>

  <div class="wrap">
    <div class="row">
      <div id="map" class="card"></div>
      <div class="card">
        <h2>Interactions</h2>
        <div id="pollinators"></div>
        <h2>Mycorrhiza</h2>
        <div id="myco"></div>
        <h2>Uses</h2>
        <div id="uses"></div>
      </div>
    </div>
    <div class="card" style="margin-top:16px;">
      <h2>References & Attribution</h2>
      <div id="refs"></div>
    </div>
    <footer>
      Built on GBIF occurrence tiles and Orchid Continuum cross-linked datasets. Always verify original sources.
    </footer>
  </div>

  <script>
    const API_BASE = '/api/continuum/species'; // <-- replace with your endpoint

    const map = new maplibregl.Map({
      container: 'map',
      style: {
        "version": 8,
        "sources": {
          "basemap": {
            "type": "raster",
            "tiles": [
              "https://tile.gbif.org/3857/omt/{z}/{x}/{y}.png?style=gbif-light"
            ],
            "tileSize": 256
          }
        },
        "layers": [
          { "id": "basemap", "type": "raster", "source": "basemap" }
        ]
      },
      center: [0, 20],
      zoom: 2
    });

    function addDensityLayer(taxonKey){
      const id = 'density';
      if (map.getSource(id)) { map.removeLayer(id); map.removeSource(id); }
      map.addSource(id, {
        "type": "raster",
        "tiles": [
          `https://api.gbif.org/v2/map/occurrence/density/{z}/{x}/{y}.png?taxonKey=${taxonKey}&style=classic-v2.point`
        ],
        "tileSize": 256
      });
      map.addLayer({ "id": id, "type": "raster", "source": id, "paint": { "raster-opacity": 0.7 } });
    }

    function pill(text){ const span=document.createElement('span'); span.className='pill'; span.textContent=text; return span; }
    function citeList(arr){
      if (!arr || !arr.length) return '—';
      return arr.map(e => {
        if (e.type && e.value) return `<li><small>${e.type.toUpperCase()}: ${e.value}</small></li>`;
        return `<li><small>${e}</small></li>`;
      }).join('');
    }

    async function loadSpecies(taxonKey){
      const res = await fetch(`${API_BASE}/${taxonKey}.json`);
      if(!res.ok){ alert('Species JSON not found'); return; }
      const data = await res.json();

      document.getElementById('sciName').textContent = data.taxon?.acceptedScientificName || '';

      // Map layer
      addDensityLayer(taxonKey);

      // Interactions
      const pol = document.getElementById('pollinators'); pol.innerHTML = '';
      (data.interactions?.pollinators || []).forEach(p => {
        const div = document.createElement('div'); div.className='item';
        const title = p.pollinatorTaxon?.name || 'Unknown pollinator';
        div.innerHTML = `<strong>${title}</strong> <small>${p.interactionType||''}</small><ul>${citeList(p.evidence)}</ul>`;
        pol.appendChild(div);
      });
      if(!(data.interactions?.pollinators || []).length) pol.textContent = 'No pollinator records linked yet.';

      const my = document.getElementById('myco'); my.innerHTML = '';
      (data.interactions?.mycorrhiza || []).forEach(m => {
        const title = m.fungusTaxon?.name || 'Unknown fungus';
        const div = document.createElement('div');
        div.innerHTML = `<strong>${title}</strong> <small>${m.relationship||''} — ${m.lifeStage||''}</small><ul>${citeList(m.evidence)}</ul>`;
        my.appendChild(div);
      });
      if(!(data.interactions?.mycorrhiza || []).length) my.textContent = 'No mycorrhizal records linked yet.';

      const uses = document.getElementById('uses'); uses.innerHTML='';
      (data.uses?.food || []).forEach(u => {
        const div = document.createElement('div');
        div.appendChild(pill('food'));
        div.appendChild(document.createTextNode(` Part: ${u.part||'-'}; Prep: ${u.preparation||'-'}`));
        div.innerHTML += `<ul>${citeList(u.evidence)}</ul>`;
        uses.appendChild(div);
      });
      (data.uses?.medicine || []).forEach(u => {
        const div = document.createElement('div');
        div.appendChild(pill('medicine'));
        div.innerHTML += `<ul>${citeList(u.evidence)}</ul>`;
        uses.appendChild(div);
      });
      (data.uses?.trade || []).forEach(u => {
        const div = document.createElement('div');
        div.appendChild(pill('trade'));
        div.appendChild(document.createTextNode(` ${u.status||''}`));
        div.innerHTML += `<ul>${citeList(u.evidence)}</ul>`;
        uses.appendChild(div);
      });
      if(!uses.children.length) uses.textContent = 'No ethnobotanical/trade records linked yet.';

      // References & attribution
      const refs = document.getElementById('refs');
      const gbifList = (data.attribution?.gbifDatasets || [])
        .map(d => `<li><small>${d.title} — ${d.license}</small></li>`).join('');
      const intList = (data.attribution?.interactionSources || [])
        .map(s => `<li><small>${s}</small></li>`).join('');
      const ethList = (data.attribution?.ethnobotanySources || [])
        .map(s => `<li><small>${s}</small></li>`).join('');
      refs.innerHTML = `
        <strong>GBIF datasets</strong><ul>${gbifList || '<li><small>—</small></li>'}</ul>
        <strong>Interaction sources</strong><ul>${intList || '<li><small>—</small></li>'}</ul>
        <strong>Ethnobotany sources</strong><ul>${ethList || '<li><small>—</small></li>'}</ul>
      `;
    }

    document.getElementById('load').addEventListener('click', () => {
      const k = document.getElementById('taxonKey').value.trim();
      if (k) loadSpecies(k);
    });
  </script>
</body>
</html>