/* global React, mountCourseCard, Icon, money, Sparkline, Dial, PageSkeleton, DemoChip,
   useNetwork, useStaff, saveCourseLocation, placeAllFromAddress, addressQuery,
   PARTNER_TIERS, DADE, addBasemap */
// Network Overview + Map View — the two screens that answer "how is the
// whole network doing", which the admin portal has never had.

const pct = (v) => (v == null ? '—' : `${Math.round(v * 100)}%`);

// ─── TierPill ─────────────────────────────────────────────────────────
// Tier is shape as well as fill: Embed is solid, Growth outlined, Pilot a
// dashed outline. The same rule the match-result pills follow, so the tier
// still reads on a screenshot, in print, or to anyone who can't separate
// the greens.
function TierPill({ tier, founding }) {
  const style = {
    embed:  { background: 'var(--cream)', color: 'var(--forest)', border: '1px solid var(--cream)' },
    growth: { background: 'transparent', color: 'var(--ink)', border: '1px solid var(--ink-faint)' },
    pilot:  { background: 'transparent', color: 'var(--ink-muted)', border: '1px dashed var(--ink-faint)' },
  }[tier] || {};
  return (
    <span className="pill-mono" style={{ ...style, fontSize: 9.5, padding: '3px 8px' }}>
      {(PARTNER_TIERS[tier] || {}).label || tier}{founding ? ' ·  ★' : ''}
    </span>
  );
}

// ─── AlertDot ─────────────────────────────────────────────────────────
// Critical is a filled ring, warn is hollow — again shape first.
function AlertDot({ severity, size = 7 }) {
  const critical = severity === 'critical';
  return (
    <span aria-hidden="true" style={{
      width: size, height: size, borderRadius: 999, flexShrink: 0, display: 'inline-block',
      background: critical ? 'var(--loss, #9B3A2E)' : 'transparent',
      border: critical ? 'none' : '1.5px solid var(--ink-faint)',
      boxShadow: critical ? '0 0 0 3px rgba(155,58,46,0.22)' : 'none',
    }}/>
  );
}

// ─── KPI tile ─────────────────────────────────────────────────────────
function NetKpi({ label, value, sub, points }) {
  return (
    <div className="card" style={{ padding: '15px 16px 13px', display: 'flex', flexDirection: 'column', gap: 3, minHeight: 104 }}>
      <div className="eyebrow">{label}</div>
      <div style={{ fontFamily: 'var(--font-display)', fontSize: 27, lineHeight: 1.05, color: 'var(--paper)', fontVariantNumeric: 'tabular-nums' }}>{value}</div>
      {sub && <div style={{ fontSize: 11.5, color: 'var(--ink-muted)', lineHeight: 1.35 }}>{sub}</div>}
      {points && points.length > 1 && (
        <div style={{ marginTop: 'auto', paddingTop: 8, color: 'var(--paper)', opacity: 0.75 }}>
          <Sparkline points={points} height={22} stroke="var(--paper)"/>
        </div>
      )}
    </div>
  );
}

// ─── Course tile ──────────────────────────────────────────────────────
// One per partner. Everything you'd ask about a course at a glance, and a
// click opens it. Built as a button so it's keyboard-reachable — a grid of
// clickable divs is the classic way a dashboard becomes unusable without a
// mouse.
function CourseTile({ course, onOpen }) {
  const s = course.stat || {};
  const worst = course.alerts.some(a => a.severity === 'critical') ? 'critical'
    : course.alerts.length ? 'warn' : null;
  return (
    <button className="card" onClick={() => onOpen(course)}
      style={{
        padding: 16, textAlign: 'left', cursor: 'pointer', border: '1px solid var(--line)',
        display: 'flex', flexDirection: 'column', gap: 12, font: 'inherit', color: 'inherit',
      }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 10 }}>
        <div style={{ minWidth: 0 }}>
          <div style={{ fontSize: 15, fontWeight: 700, color: 'var(--paper)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
            {course.short_name || course.name}
          </div>
          <div style={{ fontSize: 11.5, color: 'var(--ink-muted)', marginTop: 1 }}>
            {[course.city, course.state].filter(Boolean).join(', ') || 'Location not set'}
          </div>
        </div>
        <TierPill tier={course.tier} founding={course.founding}/>
      </div>

      <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
        <Dial value={s.utilization != null ? s.utilization : 0} size={52} label="Util"
            color="var(--paper)" track="rgba(234,226,206,0.16)"/>
        <div style={{ display: 'grid', gap: 5, flex: 1, minWidth: 0 }}>
          <div>
            <div className="eyebrow" style={{ fontSize: 9 }}>Revenue MTD</div>
            <div style={{ fontFamily: 'var(--font-mono)', fontSize: 14.5, color: 'var(--paper)', fontVariantNumeric: 'tabular-nums' }}>
              {money(s.grossMtd || 0)}
            </div>
          </div>
          <div>
            <div className="eyebrow" style={{ fontSize: 9 }}>Our take</div>
            <div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, color: 'var(--ink-soft)', fontVariantNumeric: 'tabular-nums' }}>
              {money(s.takeMtd || 0)} · {course.sandbox_take_pct != null ? course.sandbox_take_pct : 15}%
            </div>
          </div>
        </div>
      </div>

      <div style={{
        display: 'flex', alignItems: 'center', gap: 8, paddingTop: 10,
        borderTop: '1px solid var(--line)', fontSize: 11.5, color: 'var(--ink-muted)',
      }}>
        {worst ? <AlertDot severity={worst}/> : (
          <span aria-hidden="true" style={{ width: 7, height: 7, borderRadius: 999, background: 'var(--moss-light, #3E8A57)', flexShrink: 0 }}/>
        )}
        <span style={{ flex: 1, minWidth: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
          {course.alerts.length
            ? `${course.alerts.length} need${course.alerts.length === 1 ? 's' : ''} attention`
            : `${s.roundsToday || 0} round${(s.roundsToday || 0) === 1 ? '' : 's'} today`}
        </span>
        <Icon name="chevron" size={13} style={{ transform: 'rotate(-90deg)', opacity: 0.5 }}/>
      </div>
    </button>
  );
}

// ─── Attention feed ───────────────────────────────────────────────────
function AttentionFeed({ courses, onOpen }) {
  const items = [];
  courses.forEach(c => c.alerts.forEach(a => items.push({ course: c, ...a })));
  items.sort((a, b) => (a.severity === b.severity ? 0 : a.severity === 'critical' ? -1 : 1));

  return (
    <div className="card" style={{ padding: 20 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 10 }}>
        <div className="eyebrow">Needs Attention</div>
        <div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-muted)' }}>{items.length}</div>
      </div>
      {!items.length ? (
        <div style={{ fontSize: 13.5, color: 'var(--ink-muted)', marginTop: 12, lineHeight: 1.5 }}>
          Nothing flagged. Every course has times published for tomorrow, yardages set today, and a location on the map.
        </div>
      ) : (
        <div style={{ marginTop: 12, display: 'flex', flexDirection: 'column' }}>
          {items.slice(0, 12).map((it, i) => (
            <button key={`${it.course.id}-${it.code}`} onClick={() => onOpen(it.course)}
              style={{
                display: 'flex', alignItems: 'center', gap: 10, textAlign: 'left', width: '100%',
                padding: '10px 0', background: 'none', border: 'none', font: 'inherit', color: 'inherit',
                borderTop: i ? '1px solid var(--line)' : 'none', cursor: 'pointer',
              }}>
              <AlertDot severity={it.severity}/>
              <span style={{ fontSize: 13, fontWeight: 700, color: 'var(--paper)', flexShrink: 0 }}>
                {it.course.short_name || it.course.name}
              </span>
              <span style={{ fontSize: 12.5, color: 'var(--ink-muted)', flex: 1, minWidth: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                {it.text}
              </span>
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

// ─── NetworkOverview ──────────────────────────────────────────────────
function NetworkOverview({ onOpenCourse }) {
  const [net] = useNetwork();
  if (!net) return <PageSkeleton/>;
  const t = net.totals || {};

  return (
    <div style={{ maxWidth: 1240, margin: '0 auto' }}>
      {net.demo && <div style={{ marginBottom: 12 }}><DemoChip corner={false}/></div>}

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 12 }}>
        <NetKpi label="Booking Value MTD" value={money(t.grossMtd || 0)} points={t.spark} sub="Gross across every course"/>
        <NetKpi label="Sandbox Take MTD" value={money(t.takeMtd || 0)}
          sub={t.grossMtd ? `${Math.round((t.takeMtd / t.grossMtd) * 100)}% blended` : 'No revenue yet'}/>
        <NetKpi label="Partner Courses" value={t.courses || 0} sub={`${t.active || 0} active`}/>
        <NetKpi label="Rounds Today" value={t.roundsToday || 0} sub="Across the network"/>
        <NetKpi label="Avg Utilization" value={pct(t.utilization)} sub="Mean of every course"/>
        <NetKpi label="Golfers MTD" value={(t.playersMtd || 0).toLocaleString()} sub="Unique, network-wide"/>
      </div>

      {!net.migrated && (
        <div className="card" style={{ padding: '14px 18px', marginTop: 16, borderLeft: '3px solid var(--cream)' }}>
          <div style={{ fontSize: 13, color: 'var(--ink-soft)', lineHeight: 1.5 }}>
            <strong style={{ color: 'var(--paper)' }}>Partnership data isn't in the database yet.</strong>{' '}
            Tier is being inferred from each course's revenue split, and map locations can't be
            saved. Run <code style={{ fontFamily: 'var(--font-mono)', fontSize: 12 }}>sql/admin-overhaul.sql</code> in
            Supabase to make it real.
          </div>
        </div>
      )}

      <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 2.1fr) minmax(260px, 1fr)', gap: 16, marginTop: 22, alignItems: 'start' }}>
        <div>
          <div className="eyebrow" style={{ marginBottom: 10 }}>The Network</div>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(258px, 1fr))', gap: 12 }}>
            {net.courses.map(c => <CourseTile key={c.id} course={c} onOpen={onOpenCourse}/>)}
          </div>
          {!net.courses.length && (
            <div className="card" style={{ padding: 30, textAlign: 'center', color: 'var(--ink-muted)' }}>
              No courses yet. Add one from SBX Courses.
            </div>
          )}
        </div>
        <AttentionFeed courses={net.courses} onOpen={onOpenCourse}/>
      </div>
    </div>
  );
}

// ─── Map View ─────────────────────────────────────────────────────────
// Leaflet with a muted dark basemap, and the Sandbox monogram as the pin
// rather than Leaflet's default blue marker — the courses should read as
// ours at a glance.
//
// Courses with no coordinates are not dropped at 0,0 (which is in the Gulf
// of Guinea); they sit in a rail beside the map, and clicking one arms
// placement so the next click on the map sets it. That turns a data gap
// into the one screen where it's easiest to fix.

function pinHtml(course, armed) {
  const critical = course.alerts.some(a => a.severity === 'critical');
  // Two kinds of pin, because they are two different businesses:
  //
  //   forest disc = an official partner. A Sandbox 9 is laid out on the
  //                 ground here — mats down, yardages set.
  //   cream disc  = a golf course we list so people can play it, with no
  //                 SBX setup on it.
  //
  // Each disc carries the monogram drawn for it — cream mark on forest,
  // forest mark on cream. A single masked asset was tried first so one file
  // could take either colour; the mask fills the counters inside the
  // letterforms and the mark becomes a blob at 21px.
  const ring = critical ? 'rgba(155,58,46,0.95)' : armed ? 'var(--forest)' : 'rgba(14,40,24,0.55)';
  const kind = course.hasSbx ? ' sbx-pin--partner' : ' sbx-pin--listed';
  const title = course.hasSbx ? 'Sandbox partner course' : 'Non-partner course — no Sandbox 9 set up';
  return `
    <div class="sbx-pin${critical ? ' sbx-pin--alert' : ''}${kind}" style="--pin-ring:${ring}" title="${title}">
      <span class="sbx-pin__disc"><img src="assets/monogram-${course.hasSbx ? 'cream' : 'forest'}.svg" alt=""/></span>
      <span class="sbx-pin__label">${(course.short_name || course.name || '').replace(/[<>&]/g, '')}</span>
    </div>`;
}

function MapView({ onOpenCourse }) {
  const [net, reload] = useNetwork();
  // The hover card names who holds the keys, and useNetwork does not carry
  // that. Without it every pin would claim "No manager assigned", which is
  // worse than saying nothing — one small query buys an honest card.
  const [staff] = useStaff();
  const holder = React.useRef(null);
  const mapRef = React.useRef(null);
  const layerRef = React.useRef(null);
  const [arming, setArming] = React.useState(null);   // course awaiting a click
  const [err, setErr] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [geo, setGeo] = React.useState(null);        // geocoding progress
  const [pinFilter, setPinFilter] = React.useState('all');  // all | partner | listed
  const [listQuery, setListQuery] = React.useState('');
  const armingRef = React.useRef(null);
  armingRef.current = arming;

  const hasLeaflet = typeof window.L !== 'undefined';

  // Create the map once — but only once the container is actually in the
  // DOM. This effect first runs while the skeleton is still up, when the
  // holder ref is null; without `ready` in the dependencies it would bail
  // then and never run again, leaving an empty box where the map goes.
  const ready = !!net;
  React.useEffect(() => {
    if (!hasLeaflet || !ready || !holder.current || mapRef.current) return undefined;
    const L = window.L;
    const map = L.map(holder.current, { zoomControl: false, attributionControl: true })
      .setView([DADE.lat, DADE.lng], 10);
    const basemap = addBasemap(map, { maxZoom: 19 });
    L.control.zoom({ position: 'bottomright' }).addTo(map);
    layerRef.current = L.layerGroup().addTo(map);
    mapRef.current = map;

    map.on('click', async (e) => {
      const course = armingRef.current;
      if (!course) return;
      setBusy(true); setErr('');
      try {
        await saveCourseLocation(course.id, e.latlng.lat, e.latlng.lng);
        setArming(null);
        reload();
      } catch (ex) {
        setErr(ex.message || 'Could not save the location.');
      } finally {
        setBusy(false);
      }
    });
    return () => { basemap.destroy(); map.remove(); mapRef.current = null; layerRef.current = null; };
  }, [hasLeaflet, ready, reload]);

  // Redraw pins whenever the network or the armed course changes.
  // One predicate for the pins and for the list beneath them, so pressing a
  // legend row cannot leave the two disagreeing about what is on screen.
  const matchesFilter = React.useCallback(
    (c) => pinFilter === 'all' || (pinFilter === 'partner' ? !!c.hasSbx : !c.hasSbx),
    [pinFilter]);

  // course_managers → the shape CourseCard wants. Empty until useStaff
  // resolves, which is why `staff` is in this effect's dependencies: the
  // pins are rebuilt once the names are in, rather than caching a card that
  // says nobody has access.
  const managersFor = React.useCallback((courseId) => {
    const entry = staff && staff.byCourse && staff.byCourse[courseId];
    return (entry ? entry.managers : []).map(m => {
      const nm = m.user
        ? ([m.user.first_name, m.user.last_name].filter(Boolean).join(' ').trim()
           || (m.user.handle ? `@${String(m.user.handle).replace(/^@/, '')}` : 'Unknown'))
        : 'Unknown account';
      return {
        id: m.id, name: nm, avatar_url: m.user ? m.user.avatar_url : null,
        initials: nm.split(' ').filter(Boolean).slice(0, 2).map(x => x[0].toUpperCase()).join('') || '·',
      };
    });
  }, [staff]);

  React.useEffect(() => {
    if (!mapRef.current || !layerRef.current || !net) return;
    const L = window.L;
    const layer = layerRef.current;
    layer.clearLayers();
    const placed = net.courses.filter(c => c.geo && matchesFilter(c));
    placed.forEach(c => {
      const icon = L.divIcon({
        className: 'sbx-pin-wrap',
        html: pinHtml(c, arming && arming.id === c.id),
        iconSize: [34, 34], iconAnchor: [17, 34], popupAnchor: [0, -32],
      });
      const marker = L.marker([c.geo.lat, c.geo.lng], { icon, title: c.short_name || c.name, keyboard: true })
        .on('click', () => { if (!armingRef.current) onOpenCourse(c); })
        .addTo(layer);

      // Hover preview: the same CourseCard the Course Staff grid uses, not a
      // second hand-written popup. Leaflet wants a DOM node, so the real
      // component is mounted into one — a duplicate HTML version would drift
      // from the card within a week and nobody would notice.
      //
      // Built lazily on first hover and kept on the marker afterwards: most
      // pins are never hovered, and mounting a React root per pin up front
      // would cost more than the feature is worth.
      // Keep the card inside the map. Leaflet's own answer is autoPan, which
      // moves the map out from under the cursor on a mere hover — so instead
      // the popup's offset is computed from where the pin actually sits in
      // the container: flipped below the pin when there is no room above,
      // and nudged sideways when it would run off an edge.
      //
      // The height is measured from the rendered popup rather than assumed,
      // because it varies — a course with three managers is taller than one
      // with none — and cached on the marker so later hovers place it right
      // the first time.
      const CARD_W = 216, PAD = 10, PIN_LIFT = 32;
      // A popup that has just opened measures 1px tall: Leaflet has created
      // the element but React has not painted the card inside it yet. Taking
      // that at face value made the second pass decide there was plenty of
      // room and undo the first pass's flip — the bug that kept the card off
      // the top of the map. Anything implausibly short is ignored in favour
      // of the last good measurement.
      const measure = () => {
        const popup = marker.getPopup();
        const el = popup && popup.getElement();
        const h = Math.max(el ? el.offsetHeight : 0, marker.__sbxNode ? marker.__sbxNode.offsetHeight : 0);
        if (h > 40) marker.__sbxH = h;
        return marker.__sbxH || 210;
      };

      const place = () => {
        const map = mapRef.current;
        const popup = marker.getPopup();
        if (!map || !popup) return;
        const h = measure();

        const size = map.getSize();
        const pt = map.latLngToContainerPoint(marker.getLatLng());

        // Above by default; below when the top would clear the map's edge.
        let dy = -6;
        if (pt.y - PIN_LIFT - 6 - h < PAD) dy = PIN_LIFT + h + 16;

        // Leaflet centres the popup on the anchor, so an edge pin needs a
        // horizontal nudge rather than a flip.
        let dx = 0;
        const half = CARD_W / 2;
        if (pt.x - half < PAD) dx = Math.round(PAD + half - pt.x);
        else if (pt.x + half > size.x - PAD) dx = Math.round(size.x - PAD - half - pt.x);

        const cur = popup.options.offset;
        if (!cur || cur.x !== dx || cur.y !== dy) {
          popup.options.offset = window.L.point(dx, dy);
          popup.update();
        }
      };

      const preview = () => {
        if (!marker.__sbxNode) {
          const node = document.createElement('div');
          node.className = 'map-card';
          mountCourseCard(node, { course: c, managers: managersFor(c.id), compact: true });
          marker.__sbxNode = node;
          marker.bindPopup(node, {
            className: 'sbx-map-popup', closeButton: false, autoPan: false,
            offset: [0, -6], minWidth: CARD_W, maxWidth: CARD_W,
          });
        }
        place();          // right first time once the height is known
        marker.openPopup();
        // Nothing exists to measure until it is open, and the card inside
        // paints a frame or two later, so correct twice and then stop. Both
        // are no-ops once the height has been cached.
        requestAnimationFrame(place);
        clearTimeout(marker.__sbxFit);
        marker.__sbxFit = setTimeout(place, 80);
      };
      marker.on('mouseover', preview);
      marker.on('focus', preview);
      // A small grace period so crossing the gap between pin and popup does
      // not flicker it shut.
      marker.on('mouseout', () => {
        clearTimeout(marker.__sbxHide);
        marker.__sbxHide = setTimeout(() => marker.closePopup(), 140);
      });
      marker.on('blur', () => marker.closePopup());
    });
    if (placed.length) {
      const bounds = L.latLngBounds(placed.map(c => [c.geo.lat, c.geo.lng]));
      mapRef.current.fitBounds(bounds, { padding: [56, 56], maxZoom: 13 });
    }
  }, [net, arming, onOpenCourse, managersFor, matchesFilter]);

  if (!net) return <PageSkeleton/>;
  const sbxCount = net.courses.filter(c => c.hasSbx).length;
  const listedCount = net.courses.length - sbxCount;
  const unplaced = net.courses.filter(c => !c.placed);
  // Courses we can look up rather than ask someone to point at.
  const withAddress = unplaced.filter(c => !!addressQuery(c));

  async function geocodeAll() {
    setErr(''); setGeo({ i: 0, total: withAddress.length });
    const { done, failed } = await placeAllFromAddress(withAddress, setGeo);
    setGeo(null);
    reload();
    if (failed.length) {
      setErr(`Placed ${done.length} of ${withAddress.length}. ${failed.map(f => `${f.course.short_name || f.course.name}: ${f.message}`).join(' ')}`);
    }
  }

  const placedAll = net.courses.filter(c => c.geo);
  const shownOnMap = placedAll.filter(matchesFilter);
  const term = listQuery.trim().toLowerCase();
  const listed = shownOnMap.filter(c => !term
    || [c.short_name, c.name, c.city, c.state].some(v => String(v || '').toLowerCase().includes(term)));

  const LEGEND = [
    { key: 'all', n: net.courses.length, label: 'All courses', sub: 'Everything on the map' },
    { key: 'partner', n: sbxCount, label: 'Partner courses', sub: 'Sandbox 9 on the ground' },
    { key: 'listed', n: listedCount, label: 'Non-partner courses', sub: 'No Sandbox setup yet' },
  ];

  return (
    <div style={{ maxWidth: 1240, margin: '0 auto' }}>
      {net.demo && <div style={{ marginBottom: 12 }}><DemoChip corner={false}/></div>}

      {!hasLeaflet && (
        <div className="card" style={{ padding: 30, textAlign: 'center', color: 'var(--ink-muted)' }}>
          The map library didn&rsquo;t load. Check the network connection and refresh.
        </div>
      )}

      {hasLeaflet && (<>
        <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(230px, 282px)', gap: 16, alignItems: 'start' }}>
          <div className="card" style={{ padding: 0, overflow: 'hidden', position: 'relative' }}>
            <div ref={holder} className="sbx-map" style={{ height: 'min(70vh, 620px)', width: '100%' }}/>
            {arming && (
              <div style={{
                position: 'absolute', top: 14, left: 14, right: 14, zIndex: 500,
                background: 'var(--forest-dark)', border: '1px solid var(--cream)', borderRadius: 'var(--r-sm)',
                padding: '11px 14px', display: 'flex', alignItems: 'center', gap: 12, boxShadow: 'var(--shadow-float)',
              }}>
                <span style={{ fontSize: 13, color: 'var(--paper)', flex: 1 }}>
                  {busy ? 'Saving\u2026' : <>Click the map to place <strong>{arming.short_name || arming.name}</strong>.</>}
                </span>
                <button className="btn btn-ghost" onClick={() => { setArming(null); setErr(''); }}
                  style={{ padding: '5px 12px', fontSize: 12 }}>Cancel</button>
              </div>
            )}
          </div>

          {/* Right rail. The placement queue only appears when something is
              actually waiting — an "everything is fine" card every day is
              furniture you stop reading. */}
          <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
            {err && (
              <div className="card" style={{ padding: '13px 16px', borderLeft: '3px solid var(--loss, #9B3A2E)' }}>
                <div style={{ fontSize: 12.5, color: 'var(--ink-soft)', lineHeight: 1.5 }}>{err}</div>
              </div>
            )}

            <div className="card" style={{ padding: 18 }}>
              <div className="eyebrow">Courses</div>
              {/* The key doubles as the filter: the thing that explains the
                  colours is the thing you press to isolate one, so there is
                  no second control to learn. */}
              <div className="map-legend" style={{ marginTop: 12 }}>
                {LEGEND.map(o => {
                  const on = pinFilter === o.key;
                  return (
                    <button key={o.key} className={`map-legend__item${on ? ' is-on' : ''}`} aria-pressed={on}
                      onClick={() => setPinFilter(on && o.key !== 'all' ? 'all' : o.key)}>
                      <span className={`map-legend__dot map-legend__dot--${o.key}`} aria-hidden="true">
                        {o.key === 'partner' && <img src="assets/monogram-cream.svg" alt=""/>}
                        {o.key === 'listed' && <img src="assets/monogram-forest.svg" alt=""/>}
                      </span>
                      <span style={{ flex: 1, minWidth: 0 }}>
                        <span className="map-legend__label">{o.label}</span>
                        <span className="map-legend__sub">{o.sub}</span>
                      </span>
                      <span className="map-legend__n">{o.n}</span>
                    </button>
                  );
                })}
              </div>
              {pinFilter !== 'all' && (
                <div style={{ fontSize: 11.5, color: 'var(--ink-muted)', marginTop: 10, lineHeight: 1.45 }}>
                  Showing {shownOnMap.length} of {placedAll.length} placed.{' '}
                  <button onClick={() => setPinFilter('all')}
                    style={{ background: 'none', border: 'none', padding: 0, font: 'inherit', color: 'var(--ink)', textDecoration: 'underline', cursor: 'pointer' }}>
                    Show all
                  </button>
                </div>
              )}
            </div>

            {!!unplaced.length && (
              <div className="card" style={{ padding: 18 }}>
                <div className="eyebrow">Not Placed Yet</div>
                <div style={{ fontSize: 12.5, color: 'var(--ink-muted)', marginTop: 6, lineHeight: 1.45 }}>
                  {withAddress.length
                    ? `${withAddress.length} of these already have an address on file.`
                    : 'Pick one, then click where it belongs on the map.'}
                </div>
                {withAddress.length > 0 && (
                  <button className="btn btn-forest" onClick={geocodeAll} disabled={!!geo}
                    style={{ marginTop: 10, width: '100%' }}>
                    {geo
                      ? `Placing ${geo.i + 1} of ${geo.total}\u2026`
                      : `Place ${withAddress.length} from ${withAddress.length === 1 ? 'its address' : 'their addresses'}`}
                  </button>
                )}
                <div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 6 }}>
                  {unplaced.map(c => {
                    const on = arming && arming.id === c.id;
                    return (
                      <button key={c.id} onClick={() => { setArming(on ? null : c); setErr(''); }}
                        style={{
                          display: 'flex', alignItems: 'center', gap: 9, textAlign: 'left', width: '100%',
                          padding: '9px 11px', borderRadius: 'var(--r-sm)', cursor: 'pointer', font: 'inherit',
                          background: on ? 'var(--cream)' : 'var(--surface-sunken)',
                          color: on ? 'var(--forest)' : 'var(--ink)',
                          border: `1px solid ${on ? 'var(--cream)' : 'var(--line)'}`,
                        }}>
                        <Icon name="pin" size={14}/>
                        <span style={{ flex: 1, minWidth: 0, fontSize: 13, fontWeight: 700, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                          {c.short_name || c.name}
                        </span>
                        <span style={{ fontSize: 11, color: on ? 'var(--forest)' : 'var(--ink-muted)' }}>
                          {[c.city, c.state].filter(Boolean).join(', ')}
                        </span>
                      </button>
                    );
                  })}
                </div>
              </div>
            )}
          </div>
        </div>

        {/* On the map, full width beneath it — where a list of every course
            has room to be a list rather than a column of clipped names. */}
        <div className="card" style={{ padding: 18, marginTop: 16 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
            <div className="eyebrow" style={{ flexShrink: 0 }}>On The Map</div>
            <div style={{ position: 'relative', flex: '1 1 240px', minWidth: 200 }}>
              <input className="input" value={listQuery} onChange={e => setListQuery(e.target.value)}
                placeholder="Search placed courses" aria-label="Search placed courses"
                style={{ paddingLeft: 32 }}/>
              <Icon name="pin" size={14} style={{ position: 'absolute', left: 11, top: '50%', transform: 'translateY(-50%)', opacity: 0.45, pointerEvents: 'none' }}/>
            </div>
            <div style={{ fontSize: 11.5, color: 'var(--ink-muted)', fontFamily: 'var(--font-mono)' }}>
              {listed.length} shown
            </div>
          </div>

          {listed.length ? (
            <div style={{ marginTop: 10, display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(270px, 1fr))', gap: '0 22px' }}>
              {listed.map(c => (
                <button key={c.id} onClick={() => onOpenCourse(c)}
                  style={{
                    display: 'flex', alignItems: 'center', gap: 9, textAlign: 'left', width: '100%',
                    padding: '9px 0', background: 'none', border: 'none', font: 'inherit', color: 'inherit',
                    borderTop: '1px solid var(--line)', cursor: 'pointer',
                  }}>
                  <span className={`map-legend__dot map-legend__dot--${c.hasSbx ? 'partner' : 'listed'}`}
                    aria-hidden="true" style={{ width: 16, height: 16, borderWidth: 1.5 }}>
                    <img src={`assets/monogram-${c.hasSbx ? 'cream' : 'forest'}.svg`} alt="" style={{ height: 9 }}/>
                  </span>
                  <span style={{ flex: 1, minWidth: 0, fontSize: 13, fontWeight: 700, color: 'var(--paper)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                    {c.short_name || c.name}
                  </span>
                  {c.geo.demo && <span className="pill-mono" style={{ fontSize: 8.5, padding: '2px 6px', border: '1px dashed var(--ink-faint)', color: 'var(--ink-muted)' }}>demo spot</span>}
                  <TierPill tier={c.tier} founding={c.founding}/>
                </button>
              ))}
            </div>
          ) : (
            <div style={{ fontSize: 13, color: 'var(--ink-muted)', marginTop: 12 }}>
              {term ? `Nothing placed matches \u201C${listQuery}\u201D.` : 'No courses match this filter.'}
            </div>
          )}
        </div>
      </>)}
    </div>
  );
}

Object.assign(window, { NetworkOverview, MapView, TierPill, AlertDot });
