// ui.jsx — primitives + sample data + iconography
const { useState, useEffect, useRef, useMemo, useCallback, createContext, useContext } = React;

// ───────────────────────────────────────────────────────────── Icons
// Stroke icons in a uniform outlined style — kept geometric to match brutalist feel.
const Ic = {
  Search:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>,
  Cmd:     (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M9 6V4a2 2 0 1 0-2 2h2zm0 0v12m0 0v2a2 2 0 1 1-2-2h2zm0 0h6m0 0V6m0 12v2a2 2 0 1 0 2-2h-2zm0-12V4a2 2 0 1 1 2 2h-2z"/></svg>,
  Bell:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M6 8a6 6 0 1 1 12 0v5l2 3H4l2-3V8z"/><path d="M10 19a2 2 0 0 0 4 0"/></svg>,
  Plus:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M12 5v14M5 12h14"/></svg>,
  Disc:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="0.6" fill="currentColor"/></svg>,
  Mic:     (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><rect x="9" y="3" width="6" height="12" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/></svg>,
  Music:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M9 18V5l11-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="17" cy="16" r="3"/></svg>,
  User:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>,
  Build:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M4 21V7l8-4 8 4v14"/><path d="M9 21v-6h6v6M9 11h.01M15 11h.01"/></svg>,
  Tag:     (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M3 12V4h8l10 10-8 8L3 12z"/><circle cx="8" cy="8" r="1.5"/></svg>,
  File:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M14 3H6v18h12V7l-4-4z"/><path d="M14 3v4h4M9 12h6M9 16h6"/></svg>,
  Globe:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>,
  Shield:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M12 3 4 6v6c0 5 3.5 8.5 8 9 4.5-.5 8-4 8-9V6l-8-3z"/></svg>,
  Bar:     (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/></svg>,
  Coin:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><circle cx="12" cy="12" r="9"/><path d="M9 9h5a2 2 0 1 1 0 4H9m0 0h5a2 2 0 1 1 0 4H9m0-8v10m4-10v10"/></svg>,
  Send:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="m3 12 18-9-7 18-3-7-8-2z"/></svg>,
  Bolt:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M13 2 4 14h7l-1 8 9-12h-7l1-8z"/></svg>,
  Settings:(p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3 1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8 1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>,
  Up:      (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="m6 15 6-6 6 6"/></svg>,
  Down:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="m6 9 6 6 6-6"/></svg>,
  Right:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="m9 6 6 6-6 6"/></svg>,
  Left:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="m15 6-6 6 6 6"/></svg>,
  Check:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" {...p}><path d="m4 12 5 5L20 6"/></svg>,
  X:       (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M5 5l14 14M19 5 5 19"/></svg>,
  Edit:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M14.5 4.5l5 5L8 21H3v-5L14.5 4.5z"/><path d="M13 6l5 5"/></svg>,
  Filter:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M3 5h18l-7 9v6l-4-2v-4L3 5z"/></svg>,
  Spark:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M5.6 18.4l2.8-2.8M15.6 8.4l2.8-2.8"/></svg>,
  Grid:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>,
  Rows:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M3 6h18M3 12h18M3 18h18"/></svg>,
  Down2:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="m6 9 6 6 6-6"/></svg>,
  Play:    (p) => <svg viewBox="0 0 24 24" fill="currentColor" {...p}><path d="M8 5v14l11-7L8 5z"/></svg>,
  Wave:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M3 12h2M7 8v8M11 4v16M15 8v8M19 12h2"/></svg>,
  Ext:     (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M14 4h6v6M20 4l-9 9M10 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-4"/></svg>,
  Dot:     (p) => <svg viewBox="0 0 24 24" fill="currentColor" {...p}><circle cx="12" cy="12" r="3"/></svg>,
  More:    (p) => <svg viewBox="0 0 24 24" fill="currentColor" {...p}><circle cx="6" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="18" cy="12" r="1.6"/></svg>,
  Inbox:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M3 13V5h18v8m-18 0v6h18v-6m-18 0h6l1 2h4l1-2h6"/></svg>,
  Calendar:(p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><rect x="3" y="5" width="18" height="16"/><path d="M3 9h18M8 3v4M16 3v4"/></svg>,
  Spotify: (p) => <svg viewBox="0 0 24 24" fill="currentColor" {...p}><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeWidth="1.5"/><path d="M7 9c4-1 8-1 11 1M7.5 12.5c3-.8 6.5-.5 9 1M8 15.5c2.5-.5 5-.3 7 .8" stroke="currentColor" strokeWidth="1.3" fill="none" strokeLinecap="round"/></svg>,
  Apple:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" {...p}><path d="M16 3c0 2-1.5 3.5-3 3.5M5 16c0-4 2.5-7 6-7 1 0 2 .3 3 .8 1-.5 2-.8 3-.8 2.5 0 4.5 2 5 4-2 .5-3 2-3 4 0 1.5.8 3 2 4-1 2-3 4-5 4-1 0-1.7-.4-3-.4s-2 .4-3 .4c-3 0-5-4-5-9z"/></svg>,
  Youtube: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" {...p}><rect x="2" y="6" width="20" height="12"/><path d="m10 9 5 3-5 3V9z" fill="currentColor"/></svg>,
  Star:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="m12 3 2.6 5.6L20 9.5l-4 4 1 6-5-3-5 3 1-6-4-4 5.4-.9L12 3z"/></svg>,
  Eye:     (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>,
  Sun:     (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" {...p}><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>,
  Moon:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" {...p}><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>,
  ThemeAuto: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><circle cx="12" cy="12" r="9"/><path d="M12 3v18a9 9 0 0 0 0-18z" fill="currentColor"/></svg>,
  Refresh: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="M3 12a9 9 0 0 1 15-6.7L21 8M21 4v4h-4M21 12a9 9 0 0 1-15 6.7L3 16M3 20v-4h4"/></svg>,
  Branch:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><circle cx="6" cy="5" r="2"/><circle cx="18" cy="5" r="2"/><circle cx="12" cy="19" r="2"/><path d="M6 7v4a4 4 0 0 0 4 4h4a4 4 0 0 0 4-4V7"/><path d="M12 15v2"/></svg>,
  Layers:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><path d="m12 3 9 5-9 5-9-5 9-5z"/><path d="m3 13 9 5 9-5M3 18l9 5 9-5"/></svg>,
  Image:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...p}><rect x="3" y="3" width="18" height="18" rx="1"/><circle cx="9" cy="9" r="1.5"/><path d="m3 17 5-5 5 5 3-3 5 5"/></svg>,
  Logo:    (p) => <svg viewBox="0 0 32 32" fill="none" {...p}><circle cx="16" cy="16" r="15" stroke="currentColor" strokeWidth="1.5"/><circle cx="16" cy="16" r="6.5" stroke="currentColor" strokeWidth="1.5"/><circle cx="16" cy="16" r="1.5" fill="currentColor"/><path d="M16 1v8M16 23v8M1 16h8M23 16h8" stroke="currentColor" strokeWidth="1.5"/></svg>,
};

// ───────────────────────────────────────────────────────────── Primitives

function Pill({ tone='neutral', children, dot=false, className='' }) {
  const tones = {
    neutral: { bg:'transparent', bd:'var(--rule)', fg:'var(--ink)' },
    accent:  { bg:'var(--accent)', bd:'var(--accent)', fg:'var(--accent-ink)' },
    ok:      { bg:'transparent', bd:'var(--ok)', fg:'var(--ok)' },
    danger:  { bg:'transparent', bd:'var(--danger)', fg:'var(--danger)' },
    info:    { bg:'transparent', bd:'var(--info)', fg:'var(--info)' },
    soft:    { bg:'var(--bg-2)', bd:'var(--bg-2)', fg:'var(--ink-2)' },
  };
  const t = tones[tone] || tones.neutral;
  return (
    <span className={`upper ff-mono ${className}`}
      style={{display:'inline-flex',alignItems:'center',gap:6,fontSize:10,fontWeight:500,
        padding:'3px 7px',background:t.bg,border:`1px solid ${t.bd}`,color:t.fg,lineHeight:1}}>
      {dot && <span style={{width:5,height:5,background:'currentColor',borderRadius:0,display:'inline-block'}}/>}
      {children}
    </span>
  );
}

function Btn({ children, variant='primary', size='md', icon, onClick, disabled, type='button', style, className='' }) {
  const sizes = { sm:{p:'4px 10px',fs:11}, md:{p:'8px 14px',fs:12}, lg:{p:'12px 18px',fs:13} };
  const s = sizes[size];
  const variants = {
    primary: { bg:'var(--ink)', fg:'var(--bg)', bd:'var(--ink)' },
    secondary:{ bg:'transparent', fg:'var(--ink)', bd:'var(--ink)' },
    ghost:   { bg:'transparent', fg:'var(--ink-2)', bd:'transparent' },
    accent:  { bg:'var(--accent)', fg:'var(--accent-ink)', bd:'var(--accent)' },
    danger:  { bg:'var(--danger)', fg:'#fff', bd:'var(--danger)' },
  };
  const v = variants[variant] || variants.primary;
  return (
    <button type={type} onClick={onClick} disabled={disabled} className={`focus-ring ff-mono upper ${className}`}
      style={{display:'inline-flex',alignItems:'center',gap:6,padding:s.p,fontSize:s.fs,
        fontWeight:500,letterSpacing:'.06em',background:v.bg,color:v.fg,whiteSpace:'nowrap',flexShrink:0,
        border:`1px solid ${v.bd}`,opacity:disabled?.45:1,...style}}>
      {icon ? React.cloneElement(icon, { width: size==='sm'?12:14, height: size==='sm'?12:14 }) : null}
      {children}
    </button>
  );
}

function Kbd({ children }) {
  return <span className="ff-mono" style={{display:'inline-flex',alignItems:'center',justifyContent:'center',
    minWidth:18,height:18,padding:'0 4px',fontSize:10,fontWeight:500,
    border:'1px solid var(--rule)',color:'var(--ink-2)',background:'var(--paper)'}}>{children}</span>;
}

// Sparkline / mini chart
function Spark({ data, w=120, h=28, stroke='currentColor', fill='none', strokeWidth=1.25, area=false }) {
  if (!data?.length) return null;
  const min = Math.min(...data), max = Math.max(...data);
  const range = (max - min) || 1;
  const pts = data.map((v,i)=>[i*(w/(data.length-1)), h - ((v-min)/range)*h]);
  const path = pts.map((p,i)=>`${i?'L':'M'}${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' ');
  const areaPath = `${path} L ${w} ${h} L 0 ${h} Z`;
  return (
    <svg width={w} height={h} style={{display:'block'}}>
      {area && <path d={areaPath} fill={fill} opacity=".18"/>}
      <path d={path} fill="none" stroke={stroke} strokeWidth={strokeWidth} strokeLinejoin="round"/>
    </svg>
  );
}

// ASCII bar (newspaper-style)
function AsciiBar({ value, max=100, width=24, fill='█', empty='·' }) {
  const n = Math.max(0, Math.min(width, Math.round((value/max)*width)));
  return <span className="ff-mono num" style={{whiteSpace:'pre'}}>{fill.repeat(n)}{empty.repeat(width-n)}</span>;
}

// VU meter (decorative, animated)
function VuMeter({ bars=12, h=26, color='var(--ink)' }) {
  return (
    <span style={{display:'inline-flex',gap:2,alignItems:'flex-end',height:h}}>
      {Array.from({length:bars}).map((_,i)=>(
        <span key={i} className="vu-bar"
          style={{display:'inline-block',width:3,height:h,background:color,
            animationDelay:`${(i*0.07).toFixed(2)}s`,animationDuration:`${(0.7+(i%4)*0.1).toFixed(2)}s`}}/>
      ))}
    </span>
  );
}

// Static waveform (deterministic pseudo-random for given seed)
function Waveform({ w=560, h=80, bars=120, seed=7, peaks=null, color='currentColor', highlight=null, progress=null, responsive=false }) {
  // seedable
  const data = useMemo(()=>{
    if (peaks && peaks.length > 0) {
      // Resample provided peaks to the requested bar count
      const out = new Array(bars);
      for (let i = 0; i < bars; i++) {
        const idx = Math.floor((i / bars) * peaks.length);
        out[i] = Math.max(0.04, Math.min(1, peaks[idx]));
      }
      return out;
    }
    let s = seed;
    const r = ()=> { s = (s*9301+49297) % 233280; return s/233280; };
    return Array.from({length:bars},(_,i)=>{
      // shape envelope
      const t = i/bars;
      const env = Math.sin(t*Math.PI)*0.6 + 0.4;
      return env * (0.4 + r()*0.6);
    });
  },[seed,bars,peaks]);
  const bw = (w/bars);
  // If progress (0..1) is given, prefer it for smooth sub-bar highlighting via clipPath.
  const hasProgress = progress != null && isFinite(progress);
  const clipId = useMemo(()=>'wf-clip-'+Math.random().toString(36).slice(2,9),[]);
  const svgProps = responsive
    ? { viewBox:`0 0 ${w} ${h}`, preserveAspectRatio:'none', style:{display:'block',width:'100%',height:h} }
    : { width:w, height:h, style:{display:'block'} };
  return (
    <svg {...svgProps}>
      {hasProgress && (
        <defs>
          <clipPath id={clipId}>
            <rect x="0" y="0" width={Math.max(0, Math.min(1, progress)) * w} height={h}/>
          </clipPath>
        </defs>
      )}
      {/* base layer (unhighlighted) */}
      {data.map((v,i)=>{
        const bh = v*h*0.92 + 1;
        const isHi = !hasProgress && highlight!=null && i<=highlight;
        return <rect key={i} x={i*bw + bw*0.18} y={(h-bh)/2}
          width={bw*0.64} height={bh}
          fill={isHi ? 'var(--accent)' : color}/>;
      })}
      {/* highlight layer clipped to progress fraction (smooth, sub-bar) */}
      {hasProgress && (
        <g clipPath={`url(#${clipId})`}>
          {data.map((v,i)=>{
            const bh = v*h*0.92 + 1;
            return <rect key={i} x={i*bw + bw*0.18} y={(h-bh)/2}
              width={bw*0.64} height={bh}
              fill="var(--accent)"/>;
          })}
        </g>
      )}
    </svg>
  );
}

// useAudioPeaks — decode an audio URL via Web Audio API and return normalized peak data.
// Returns null while decoding or if decoding fails. Cached on window._DEMO_AUDIO_CACHE so
// re-renders don't re-decode the same URL.
function useAudioPeaks(url, binCount = 240) {
  const [peaks, setPeaks] = React.useState(null);
  React.useEffect(() => {
    if (!url) { setPeaks(null); return; }
    const cacheKey = 'peaks:' + binCount + ':' + url;
    const cache = window._DEMO_AUDIO_CACHE = window._DEMO_AUDIO_CACHE || {};
    if (cache[cacheKey]) { setPeaks(cache[cacheKey]); return; }
    let cancelled = false;
    setPeaks(null);
    (async () => {
      try {
        const Ctx = window.AudioContext || window.webkitAudioContext;
        if (!Ctx) return;
        const resp = await fetch(url);
        const buf = await resp.arrayBuffer();
        const ctx = new Ctx();
        const audioBuf = await ctx.decodeAudioData(buf.slice(0));
        ctx.close && ctx.close();
        const channel = audioBuf.getChannelData(0);
        const samplesPerBin = Math.floor(channel.length / binCount);
        const out = new Array(binCount);
        let maxPeak = 0;
        for (let i = 0; i < binCount; i++) {
          const start = i * samplesPerBin;
          const end = Math.min(channel.length, start + samplesPerBin);
          let peak = 0;
          for (let j = start; j < end; j++) {
            const v = Math.abs(channel[j]);
            if (v > peak) peak = v;
          }
          out[i] = peak;
          if (peak > maxPeak) maxPeak = peak;
        }
        if (maxPeak > 0) for (let i = 0; i < binCount; i++) out[i] /= maxPeak;
        if (!cancelled) { cache[cacheKey] = out; setPeaks(out); }
      } catch {
        if (!cancelled) setPeaks(null);
      }
    })();
    return () => { cancelled = true; };
  }, [url, binCount]);
  return peaks;
}

// SectionLabel — newspaper micro-header
function Section({ children, num, action }) {
  return (
    <div style={{display:'flex',alignItems:'baseline',justifyContent:'space-between',
      borderTop:'1px solid var(--rule)',paddingTop:8,marginBottom:8}}>
      <div style={{display:'flex',alignItems:'baseline',gap:10}}>
        {num && <span className="ff-mono num" style={{fontSize:10,color:'var(--ink-3)'}}>{num}</span>}
        <span className="ff-mono upper" style={{fontSize:11,fontWeight:600}}>{children}</span>
      </div>
      {action}
    </div>
  );
}

// ───────────────────────────────────────────────────────────── PageHeader
// Canonical screen header used across every top-level page (Catalog, Directory,
// Issues, Audience, Settings, etc). Brutalist treatment: mono eyebrow segs,
// big display title with one highlight word boxed in accent yellow + period,
// optional sub paragraph, optional right-side actions or meta line.
//
//   <PageHeader
//     eyebrow={['CATALOG', '563\u00a0WORKS', '814\u00a0RECS']}
//     title="The catalog"     highlight="catalog"
//     sub="Every work, recording, release..."
//     actions={<><Btn>Filter</Btn><Btn>+ New</Btn></>}/>
//
// `eyebrow` may be a string, a node, or an array of segments (joined with " · ").
// `highlight` is the substring inside `title` to wrap in the yellow box + period.
// If `highlight` is omitted, the title renders as plain display type.
// `meta` (right side, alt to actions) is for date-range / status text.
function PageHeader({ eyebrow, title, highlight, sub, actions, meta, children }) {
  // Eyebrow rendering — array → join with " · " and keep each segment unbreakable.
  const ribbon = Array.isArray(eyebrow)
    ? eyebrow.map((s, i) => (
        <React.Fragment key={i}>
          {i > 0 && ' · '}
          <span style={{whiteSpace:'nowrap'}}>{s}</span>
        </React.Fragment>
      ))
    : eyebrow;

  // Title rendering — plain display type. `highlight` prop ignored.
  const titleNode = (typeof title === 'string' && highlight) ? title + '.' : title;

  return (
    <div style={{
      display:'flex', justifyContent:'space-between', alignItems:'flex-end',
      marginBottom:32, flexWrap:'wrap', gap:24,
    }}>
      <div style={{minWidth:0, flex:'1 1 480px'}}>
        {ribbon && (
          <div className="ff-mono upper" style={{
            fontSize:10, color:'var(--ink-3)', marginBottom:18, letterSpacing:'.1em',
          }}>{ribbon}</div>
        )}
        <h1 className="heading-swap ff-display" style={{
          fontSize:'clamp(48px,6.5vw,96px)', fontWeight:700,
          letterSpacing:'-0.05em', lineHeight:.85, margin:0,
        }}>{titleNode}</h1>
        {sub && (
          <p style={{
            fontSize:14, color:'var(--ink-2)', marginTop:22,
            maxWidth:680, lineHeight:1.5,
          }}>{sub}</p>
        )}
        {children}
      </div>
      {(actions || meta) && (
        <div style={{display:'flex', alignItems:'flex-end', gap:8, flexWrap:'wrap'}}>
          {meta}
          {actions}
        </div>
      )}
    </div>
  );
}

// ───────────────────────────────────────────────────────────── Sample data
// Authentic music-rights vocabulary — works, recordings, releases, claims, CWR, etc.

const ARTISTS = [
  {id:'p_01',name:'Solange Knowles',ipi:'00578913241',pro:'ASCAP',type:'Person',country:'US',roster:18,claims:7,share:62,color:'#f4d34a',legal:true},
  {id:'p_02',name:'Floating Points',ipi:'00829441020',pro:'PRS',type:'Person',country:'GB',roster:24,claims:2,share:84,color:'#2a4d8f',legal:false,realName:'Sam Shepherd'},
  {id:'p_03',name:'Caroline Polachek',ipi:'00471920055',pro:'BMI',type:'Person',country:'US',roster:12,claims:3,share:55,color:'#e8693a',legal:true},
  {id:'p_04',name:'Tirzah',ipi:'00993841552',pro:'PRS',type:'Person',country:'GB',roster:8,claims:0,share:100,color:'#5a3d1c',legal:false,realName:'Tirzah Mastin'},
  {id:'p_05',name:'Helado Negro',ipi:'00184220998',pro:'ASCAP',type:'Person',country:'US',roster:31,claims:1,share:91,color:'#b04a3a',legal:false,realName:'Roberto Lange'},
  {id:'p_06',name:'KAYTRANADA',ipi:'00734820011',pro:'SOCAN',type:'Person',country:'CA',roster:46,claims:4,share:73,color:'#5c2a8a',legal:false,realName:'Louis Kevin Celestin'},
  {id:'p_07',name:'Yaeji',ipi:'00882011455',pro:'ASCAP',type:'Person',country:'US',roster:14,claims:0,share:100,color:'#0f4c2a',legal:false,realName:'Kathy Yaeji Lee'},
  {id:'p_08',name:'Arca',ipi:'00633210099',pro:'SACVEN',type:'Person',country:'VE',roster:22,claims:5,share:48,color:'#a02a4d',legal:false,realName:'Alejandra Ghersi'},
  {id:'p_09',name:'Sault',ipi:'00911320744',pro:'PRS',type:'Group',country:'GB',roster:62,claims:9,share:38,color:'#d4a02a',legal:false},
  {id:'p_10',name:'L\u2019Rain',ipi:'00455202183',pro:'BMI',type:'Person',country:'US',roster:9,claims:0,share:100,color:'#4a6a8a',legal:false,realName:'Taja Cheek'},
];

// ── ARTIST_DETAILS ─────────────────────────────────────────────────────
// Per-profile expanded data: identity, IDs, multi-society memberships,
// publisher relationship, payee/tax, representation, group members, ops.
// Read in addition to ARTISTS[] in ScreenPublic and the Add/Edit profile
// flows; missing keys fall back to safe defaults so unenriched records
// still render. Editable: writes are merged back into window.ARTIST_DETAILS.
const ARTIST_DETAILS = {
  p_01: {
    realName:'Solange Piaget Knowles', aliases:['Solange'], pronouns:'she/her', dob:'1986-06-24', nationality:'US',
    ipiBase:'I-001234567-8', isni:'0000 0001 1503 5732', dpid:'PA-DPIDA-2018051811-K',
    musicBrainzId:'b7539c32-53e7-4908-bda3-f4e62b1b1e25', wikidataId:'Q241956',
    spotifyUri:'2auiVi8sUZo17dLy1HwrTU', appleId:'74609529', discogsId:'251544',
    // DSP / streaming presence
    spotifyId:'2auiVi8sUZo17dLy1HwrTU', spotifyMonthlyListeners:8412044, spotifyFollowers:3284119, spotifyPopularity:71, spotifyForArtists:true, spotifyGenres:['neo soul','art pop','rnb'],
    appleMusicUrl:'https://music.apple.com/artist/74609529', appleMusicForArtists:true, appleShazamCount:412900,
    deezerId:'74811', deezerFans:284119,
    tidalId:'4827821', tidalRoles:['Main Artist','Composer'],
    amazonId:'B001N5GH3W', amazonForArtists:true,
    youtubeChannelId:'UCp4VR2hM4j5pjfFmEx_yhBg', youtubeSubscribers:912000, youtubeViews:842309441, youtubeMusicChannelId:'UCp4VR2hM4j5pjfFmEx_yhBgM',
    soundcloudId:'solangeknowles', soundcloudFollowers:284100, soundcloudUrl:'https://soundcloud.com/solangeknowles',
    bandcampUrl:'https://saintheron.bandcamp.com',
    audiomackId:'solange', audiomackFollowers:91200,
    qobuzId:'2118412', beatportId:null,
    // External IDs / metadata partners
    musicBrainzSyncedAt:'2026-04-12', discogsName:'Solange', discogsRealname:'Solange Piaget Knowles', discogsSyncedAt:'2026-03-22',
    geniusId:'58119', geniusFollowers:128400, geniusVerified:true,
    lastfmListeners:1284119, lastfmPlaycount:184221044,
    allmusicId:'solange-mn0000402181', rymUrl:'https://rateyourmusic.com/artist/solange',
    gkgId:'/m/02p7rvf', gkgWikipediaUrl:'https://en.wikipedia.org/wiki/Solange_Knowles',
    // Social presence
    instagramHandle:'saintrecords', instagramFollowers:5418022, instagramVerified:true,
    tiktokHandle:'solangeknowles', tiktokFollowers:412900,
    twitterHandle:'solangeknowles', twitterFollowers:1102844,
    facebookUrl:'https://facebook.com/solangeknowles', facebookFollowers:2918044,
    threadsHandle:'saintrecords', threadsFollowers:248100, threadsVerified:true,
    blueskyHandle:'solange.bsky.social', blueskyFollowers:18400,
    // Live / fan platforms
    bandsintownTrackers:42100, bandsintownUpcoming:0, songkickOnTour:false,
    // Songwriter / linkfire / distribution
    spotifyForSongwriters:'2auiVi8sUZo17dLy1HwrTU-sw', spotifyWrittenByPlaylist:'37i9dQZF1DXcW8x0WxQzN1', spotifySongwriterPageActive:true,
    linktreeUrl:'https://linktr.ee/solange', linkfireUrl:'https://linkfire.com/saintheron',
    affiliations:[
      {kind:'PRO', org:'ASCAP', memberNumber:'9924118',  since:'2003', territory:'United States', status:'Member'},
      {kind:'MRO', org:'HFA',   memberNumber:'A-7728204',since:'2008', territory:'United States', status:'Member'},
      {kind:'NRO', org:'SoundExchange', memberNumber:'SX-119028', since:'2010', territory:'United States', status:'Member'},
    ],
    // Additional IPIs — primary lives on the legacy `ipi` field; these are
    // secondary IPIs issued by other societies (PRS for UK touring/CWR, etc.)
    additionalIpis:[
      {ipi:'00712840218', pro:'PRS', memberNumber:'PRS-WSK-2014'},
    ],
    // Additional ISNIs — performer ISNI vs. writer/publisher ISNI.
    additionalIsnis:[
      {isni:'0000000371842105', role:'Performer (Solange)'},
    ],
    nrPerformer:true,
    publisher:'Saint Heron Publishing', publisherType:'self', publisherStart:'2016-09-30', publisherTerritory:'Worldwide', publisherScope:'100% writer share',
    payee:'Saint Heron LLC', taxId:'**-***5189', taxForm:'W-9', taxFormExpiry:'2027-01-15', withholding:'0%', treatyCountry:'United States', paymentMethod:'ACH · Chase ****2287',
    manager:'Yvette Noel-Schure', managerCo:'Schure Media Group', agent:'Cara Lewis', agency:'Cara Lewis Group', lawyer:'Kenneth Meiselas', lawFirm:'Grubman Shire Meiselas & Sacks',
    email:'rights@saintheron.com', phone:'+1 (212) 555-0142',
    tags:['Priority','RnB'], notes:'Estate planning underway — payee may shift to Saint Heron Trust in Q3 2026.',
    createdAt:'2014-08-12', lastVerifiedAt:'2026-04-21', conflictsOpen:1, conflictsResolved:14,
  },
  p_02: {
    realName:'Sam Shepherd', aliases:['Floating Points'], pronouns:'he/him', dob:'1986-04-09', nationality:'GB',
    ipiBase:'I-002912004-2', isni:'0000 0003 8157 9912', dpid:'PA-DPIDA-2019073102-S',
    musicBrainzId:'7ed29074-30ab-46c5-b728-9bbf0e0fe5b9', wikidataId:'Q3076923',
    spotifyUri:'68kEuyFKyqrdQQLLsmiatm', appleId:'292842076',
    // DSP / streaming presence
    spotifyId:'68kEuyFKyqrdQQLLsmiatm', spotifyMonthlyListeners:2841029, spotifyFollowers:912447, spotifyPopularity:64, spotifyForArtists:true, spotifyGenres:['electronic','idm','modern jazz'],
    appleMusicUrl:'https://music.apple.com/artist/292842076', appleShazamCount:184200,
    deezerId:'1208422', deezerFans:78400,
    tidalId:'5128400', tidalRoles:['Main Artist','Composer','Producer'],
    youtubeChannelId:'UCcoyA3sPQq2Hh2WTRJgkJjA', youtubeSubscribers:148000, youtubeViews:42184022,
    soundcloudId:'floatingpoints', soundcloudFollowers:184022, soundcloudUrl:'https://soundcloud.com/floatingpoints',
    bandcampUrl:'https://floatingpoints.bandcamp.com',
    qobuzId:'412800', beatportId:118402,
    // External IDs / metadata partners
    musicBrainzSyncedAt:'2026-04-08', discogsName:'Floating Points', discogsRealname:'Sam Shepherd', discogsSyncedAt:'2026-03-14',
    geniusId:'82910', geniusFollowers:24400, geniusVerified:false,
    lastfmListeners:412844, lastfmPlaycount:48222019,
    allmusicId:'floating-points-mn0002510892', rymUrl:'https://rateyourmusic.com/artist/floating_points',
    gkgWikipediaUrl:'https://en.wikipedia.org/wiki/Floating_Points',
    // Social presence
    instagramHandle:'floatingpoints', instagramFollowers:284100, instagramVerified:true,
    twitterHandle:'floatingpoints', twitterFollowers:118400,
    facebookUrl:'https://facebook.com/floatingpointsmusic', facebookFollowers:148200,
    // Live / fan platforms
    bandsintownTrackers:38400, bandsintownUpcoming:14, songkickOnTour:true, songkickUpcoming:14,
    linktreeUrl:'https://linktr.ee/floatingpoints',
    additionalIpis:[
      {ipi:'00541209831', pro:'GEMA',  memberNumber:'GEMA-184220'},
      {ipi:'00611087221', pro:'SACEM', memberNumber:'SACEM-FR-220994'},
    ],
    additionalIsnis:[
      {isni:'0000000406173882', role:'DJ / Producer (Floating Points)'},
    ],
    affiliations:[
      {kind:'PRO',  org:'PRS',  memberNumber:'PRS-410992', since:'2010', territory:'United Kingdom', status:'Member'},
      {kind:'MRO',  org:'MCPS', memberNumber:'M-318204',   since:'2010', territory:'United Kingdom', status:'Member'},
      {kind:'NRO',  org:'PPL',  memberNumber:'PPL-902118', since:'2011', territory:'United Kingdom', status:'Member'},
      {kind:'PRO',  org:'GEMA', memberNumber:'(via ICE hub)', since:'2014', territory:'Germany',     status:'Reciprocal'},
    ],
    nrPerformer:true,
    publisher:'Pluralis Music', publisherType:'admin', publisherStart:'2019-02-01', publisherTerritory:'Worldwide ex. UK', publisherScope:'Admin · 75% net',
    payee:'Pluralis Music Ltd', taxId:'GB **8 *2** 90', taxForm:'W-8BEN', taxFormExpiry:'2026-12-31', withholding:'0% (UK treaty)', treatyCountry:'United Kingdom', paymentMethod:'Wire · Barclays ****0118',
    manager:'Marina Lalovic', managerCo:'AMF Artists', agent:'WME', agency:'WME · London',
    email:'sam@pluralis.co.uk', phone:'+44 20 7946 0992',
    tags:['Electronic','Crossover'], notes:'Admin deal only; masters with Ninja Tune.',
    createdAt:'2010-06-18', lastVerifiedAt:'2026-03-02', conflictsOpen:0, conflictsResolved:6,
  },
  p_09: { // Sault — Group
    aliases:['SAULT'], dob:'2018-02-14', nationality:'GB',
    ipiBase:'I-004812190-3', isni:'0000 0004 7629 1188',
    spotifyUri:'6O74knDqdv3XaWtkII7Xjp',
    // DSP / streaming presence (Sault — anonymous-by-policy, sparse on socials)
    spotifyId:'6O74knDqdv3XaWtkII7Xjp', spotifyMonthlyListeners:1284022, spotifyFollowers:412800, spotifyPopularity:68, spotifyGenres:['neo soul','uk soul','art pop'],
    appleMusicUrl:'https://music.apple.com/artist/1488118019',
    deezerId:'74821920', deezerFans:42100,
    youtubeChannelId:'UC1pX-cvVzGE3y9rFSZP3Ksg', youtubeSubscribers:284100, youtubeViews:128400229,
    bandcampUrl:'https://forever-living-originals.bandcamp.com',
    musicBrainzSyncedAt:'2026-03-30', discogsName:'Sault', discogsSyncedAt:'2026-02-22',
    geniusId:'2841090', lastfmListeners:412800, lastfmPlaycount:18244012,
    gkgWikipediaUrl:'https://en.wikipedia.org/wiki/Sault_(band)',
    // Social — minimal by group preference
    instagramHandle:'forever_living_originals', instagramFollowers:128400,
    bandsintownTrackers:18400, songkickOnTour:false,
    affiliations:[
      {kind:'PRO', org:'PRS',  memberNumber:'PRS-902441 (group)', since:'2018', territory:'United Kingdom', status:'Member'},
      {kind:'MRO', org:'MCPS', memberNumber:'M-902441',          since:'2018', territory:'United Kingdom', status:'Member'},
    ],
    members:[
      {aid:null, name:'Inflo (Dean Cover)', role:'Producer / Writer', joined:'2018-02-14'},
      {aid:null, name:'Cleo Sol',           role:'Vocals / Writer',   joined:'2018-02-14'},
      {aid:null, name:'Kid Sister',         role:'Vocals',            joined:'2019-06-01'},
      {aid:null, name:'Michael Kiwanuka',   role:'Featured writer',   joined:'2020-09-01', left:'2021-01-01'},
    ],
    splits:'per-credit', formed:'2018-02-14',
    publisher:'Forever Living Originals', publisherType:'exclusive', publisherStart:'2018-02-14', publisherTerritory:'Worldwide', publisherScope:'Group catalog · per-track splits',
    payee:'Forever Living Originals Ltd', taxId:'GB **3 *7** 18', taxForm:'W-8BEN-E', taxFormExpiry:'2027-06-30', withholding:'0% (UK treaty)', treatyCountry:'United Kingdom', paymentMethod:'Wire · HSBC ****1144',
    manager:'(undisclosed)', email:'rights@forever-living-originals.com',
    tags:['Group','Anonymous-by-policy','Active'], notes:'Members released anonymously by group preference; per-track writer splits filed via PRS.',
    createdAt:'2019-05-20', lastVerifiedAt:'2026-02-14', conflictsOpen:2, conflictsResolved:11,
  },
};
window.ARTIST_DETAILS = ARTIST_DETAILS;
function getArtistDetail(id){ return (window.ARTIST_DETAILS && window.ARTIST_DETAILS[id]) || {}; }
window.getArtistDetail = getArtistDetail;

const WORKS = [
  {id:'w_01',iswc:'T-308.901.522-0',title:'Cranes In The Sky',writers:['Solange Knowles','Raphael Saadiq'],pro:'ASCAP',status:'registered',shares:'62/38',catalog:'SK Catalog',copyright:'© 2016 SK / Saadiq Publ.',duration:243,plays:412394818,societies:6},
  {id:'w_02',iswc:'T-921.044.881-1',title:'Birds',writers:['Sam Shepherd'],pro:'PRS',status:'registered',shares:'100',catalog:'PluralPub',copyright:'© 2019 Pluralis Music',duration:362,plays:88401203,societies:4},
  {id:'w_03',iswc:'T-700.301.522-9',title:'Bunny Is A Rider',writers:['Caroline Polachek','Danny L Harle'],pro:'BMI',status:'pending',shares:'70/30',catalog:'Perpetual Pub',copyright:'© 2021 Perpetual',duration:171,plays:64227118,societies:3},
  {id:'w_04',iswc:'T-503.211.097-2',title:'Send Me',writers:['Tirzah Mastin','Mica Levi'],pro:'PRS',status:'registered',shares:'50/50',catalog:'TM Works',copyright:'© 2018 Tirzah Music',duration:204,plays:18412044,societies:5},
  {id:'w_05',iswc:'T-129.501.482-7',title:'Far In',writers:['Roberto Lange'],pro:'ASCAP',status:'registered',shares:'100',catalog:'HN Pub',copyright:'© 2021 Helado Negro',duration:194,plays:12203456,societies:4},
  {id:'w_06',iswc:'T-998.221.001-4',title:'10%',writers:['Louis Kevin Celestin','Kali Uchis'],pro:'SOCAN',status:'conflict',shares:'60/40',catalog:'KAY Pub',copyright:'© 2019 RCA Records',duration:207,plays:822401205,societies:7},
  {id:'w_07',iswc:'T-440.220.198-5',title:'Raingurl',writers:['Kathy Yaeji Lee'],pro:'ASCAP',status:'registered',shares:'100',catalog:'YAE Cat',copyright:'© 2017 Godmode',duration:265,plays:42301844,societies:4},
  {id:'w_08',iswc:'T-188.501.227-3',title:'Nonbinary',writers:['Alejandra Ghersi'],pro:'SACVEN',status:'pending',shares:'100',catalog:'Arca Cat',copyright:'© 2020 XL Recordings',duration:173,plays:15822036,societies:3},
  {id:'w_09',iswc:'T-822.444.118-2',title:'Wildfires',writers:['Sault Collective'],pro:'PRS',status:'conflict',shares:'splits',catalog:'Forever Living',copyright:'© 2020 Forever Living Originals',duration:288,plays:194002214,societies:6},
  {id:'w_10',iswc:'T-651.882.300-0',title:'Two Face',writers:['Taja Cheek'],pro:'BMI',status:'registered',shares:'100',catalog:'L\u2019R Cat',copyright:'© 2021 Mexican Summer',duration:199,plays:4402188,societies:4},
];

const RECENT = [
  {time:'2m', who:'a.cohen', what:'imported 412 recordings from', target:'Spotify For Artists', kind:'import'},
  {time:'14m',who:'system',  what:'CWR transmission accepted by', target:'GEMA', kind:'cwr'},
  {time:'31m',who:'k.davis', what:'resolved conflict on', target:'10% (T-998.221.001-4)', kind:'claim'},
  {time:'48m',who:'system',  what:'matched 1,204 ISRCs to', target:'YouTube Channel #UC0918', kind:'match'},
  {time:'1h', who:'m.lee',   what:'created agreement', target:'PluralPub × Domino Recording', kind:'agreement'},
  {time:'1h', who:'system',  what:'royalty statement parsed', target:'Q3 2025 / SoundExchange', kind:'royalty'},
  {time:'2h', who:'r.peters',what:'flagged duplicate profile', target:'Solange (× 3 candidates)', kind:'profile'},
  {time:'3h', who:'system',  what:'DDEX ERN 4.3 delivered to', target:'Apple Music (Europe)', kind:'ddex'},
];

const SOCIETIES = [
  // US
  {acronym:'ASCAP', name:'American Society of Composers, Authors and Publishers', country:'US', territory:'United States', kind:'PRO',  ackRate:99.2, lastSent:'2h ago',  members:920000, ipiPrefix:'1', cwrAck:'CWR 2.1, 3.0'},
  {acronym:'BMI',   name:'Broadcast Music, Inc.',                                  country:'US', territory:'United States', kind:'PRO',  ackRate:97.8, lastSent:'2h ago',  members:1300000,ipiPrefix:'1', cwrAck:'CWR 2.1, 3.0'},
  {acronym:'SESAC', name:'Society of European Stage Authors and Composers',        country:'US', territory:'United States', kind:'PRO',  ackRate:95.0, lastSent:'5d ago',  members:30000,  ipiPrefix:'1', cwrAck:'CWR 2.1'},
  {acronym:'GMR',   name:'Global Music Rights',                                    country:'US', territory:'United States', kind:'PRO',  ackRate:96.7, lastSent:'9d ago',  members:120,    ipiPrefix:'1', cwrAck:'CWR 2.1'},
  {acronym:'HFA',   name:'Harry Fox Agency',                                       country:'US', territory:'United States', kind:'MRO',  ackRate:94.5, lastSent:'1d ago',  members:48000,  ipiPrefix:'1', cwrAck:'CWR 2.1, 3.0'},
  {acronym:'MLC',   name:'The Mechanical Licensing Collective',                    country:'US', territory:'United States', kind:'MRO',  ackRate:98.1, lastSent:'14h ago', members:35000,  ipiPrefix:'1', cwrAck:'CWR 3.0'},
  // UK / Europe
  {acronym:'PRS',   name:'PRS for Music',                                          country:'GB', territory:'United Kingdom',kind:'PRO',  ackRate:99.8, lastSent:'1d ago',  members:160000, ipiPrefix:'2', cwrAck:'CWR 2.1, 3.0'},
  {acronym:'MCPS',  name:'Mechanical-Copyright Protection Society',                country:'GB', territory:'United Kingdom',kind:'MRO',  ackRate:98.6, lastSent:'1d ago',  members:155000, ipiPrefix:'2', cwrAck:'CWR 2.1, 3.0'},
  {acronym:'GEMA',  name:'Gesellschaft für musikalische Aufführungs- und mechanische Vervielfältigungsrechte', country:'DE', territory:'Germany', kind:'CMO', ackRate:98.5, lastSent:'12m ago', members:88000, ipiPrefix:'2', cwrAck:'CWR 3.0'},
  {acronym:'SACEM', name:'Société des Auteurs, Compositeurs et Éditeurs de Musique',country:'FR', territory:'France',        kind:'CMO',  ackRate:96.4, lastSent:'1d ago',  members:170000, ipiPrefix:'2', cwrAck:'CWR 2.1, 3.0'},
  {acronym:'SIAE',  name:'Società Italiana degli Autori ed Editori',               country:'IT', territory:'Italy',         kind:'CMO',  ackRate:93.8, lastSent:'4d ago',  members:99000,  ipiPrefix:'2', cwrAck:'CWR 2.1'},
  {acronym:'SGAE',  name:'Sociedad General de Autores y Editores',                 country:'ES', territory:'Spain',         kind:'CMO',  ackRate:91.2, lastSent:'3d ago',  members:120000, ipiPrefix:'2', cwrAck:'CWR 2.1'},
  {acronym:'STIM',  name:'Svenska Tonsättares Internationella Musikbyrå',          country:'SE', territory:'Sweden',        kind:'CMO',  ackRate:99.1, lastSent:'8h ago',  members:91000,  ipiPrefix:'2', cwrAck:'CWR 3.0'},
  {acronym:'BUMA',  name:'Buma/Stemra',                                            country:'NL', territory:'Netherlands',   kind:'CMO',  ackRate:97.9, lastSent:'2d ago',  members:32000,  ipiPrefix:'2', cwrAck:'CWR 2.1, 3.0'},
  {acronym:'SUISA', name:'SUISA',                                                  country:'CH', territory:'Switzerland',   kind:'CMO',  ackRate:98.4, lastSent:'1d ago',  members:42000,  ipiPrefix:'2', cwrAck:'CWR 3.0'},
  {acronym:'SOZA',  name:'Slovenský ochranný zväz autorský',                       country:'SK', territory:'Slovakia',      kind:'CMO',  ackRate:88.7, lastSent:'12d ago', members:5400,   ipiPrefix:'2', cwrAck:'CWR 2.1'},
  // Americas (non-US)
  {acronym:'SOCAN', name:'Society of Composers, Authors and Music Publishers of Canada', country:'CA', territory:'Canada', kind:'PRO',  ackRate:99.0, lastSent:'3d ago',  members:185000, ipiPrefix:'1', cwrAck:'CWR 2.1, 3.0'},
  {acronym:'CMRRA', name:'Canadian Musical Reproduction Rights Agency',            country:'CA', territory:'Canada',        kind:'MRO',  ackRate:96.0, lastSent:'2d ago',  members:6000,   ipiPrefix:'1', cwrAck:'CWR 2.1'},
  {acronym:'SACVEN',name:'Sociedad de Autores y Compositores de Venezuela',        country:'VE', territory:'Venezuela',     kind:'CMO',  ackRate:84.2, lastSent:'21d ago', members:3200,   ipiPrefix:'1', cwrAck:'CWR 2.1'},
  // Asia-Pacific
  {acronym:'JASRAC',name:'Japanese Society for Rights of Authors, Composers and Publishers', country:'JP', territory:'Japan', kind:'CMO', ackRate:88.1, lastSent:'8d ago', members:21000, ipiPrefix:'4', cwrAck:'CWR 2.1'},
  {acronym:'APRA',  name:'Australasian Performing Right Association',              country:'AU', territory:'Australia / NZ',kind:'PRO',  ackRate:99.5, lastSent:'6h ago',  members:115000, ipiPrefix:'5', cwrAck:'CWR 3.0'},
  {acronym:'CASH',  name:'Composers and Authors Society of Hong Kong',             country:'HK', territory:'Hong Kong',     kind:'CMO',  ackRate:92.0, lastSent:'5d ago',  members:3800,   ipiPrefix:'4', cwrAck:'CWR 2.1'},
  {acronym:'MCSC',  name:'Music Copyright Society of China',                       country:'CN', territory:'China',         kind:'CMO',  ackRate:78.4, lastSent:'14d ago', members:9300,   ipiPrefix:'4', cwrAck:'CWR 2.1'},
  // Africa
  {acronym:'SAMRO', name:'Southern African Music Rights Organisation',             country:'ZA', territory:'South Africa',  kind:'CMO',  ackRate:90.1, lastSent:'9d ago',  members:18000,  ipiPrefix:'3', cwrAck:'CWR 2.1'},
  // Neighboring rights (performers + masters — paid alongside PROs but for sound recordings)
  {acronym:'SX', name:'SoundExchange',                                              country:'US', territory:'United States', kind:'NRO',  ackRate:97.4, lastSent:'1d ago',  members:670000, ipiPrefix:'1', cwrAck:'DDEX RDR-N'},
  {acronym:'PPL',   name:'Phonographic Performance Limited',                       country:'GB', territory:'United Kingdom',kind:'NRO',  ackRate:98.7, lastSent:'9h ago',  members:140000, ipiPrefix:'2', cwrAck:'DDEX RDR-N'},
  {acronym:'GVL',   name:'Gesellschaft zur Verwertung von Leistungsschutzrechten', country:'DE', territory:'Germany',       kind:'NRO',  ackRate:96.5, lastSent:'2d ago',  members:170000, ipiPrefix:'2', cwrAck:'DDEX RDR-N'},
  {acronym:'SCPP',  name:'Société Civile des Producteurs Phonographiques',         country:'FR', territory:'France',        kind:'NRO',  ackRate:94.0, lastSent:'4d ago',  members:3200,   ipiPrefix:'2', cwrAck:'DDEX RDR-N'},
  {acronym:'PPCA',  name:'Phonographic Performance Company of Australia',          country:'AU', territory:'Australia',     kind:'NRO',  ackRate:95.8, lastSent:'3d ago',  members:3500,   ipiPrefix:'5', cwrAck:'DDEX RDR-N'},
  {acronym:'ReSound',name:'Re:Sound Music Licensing Company',                      country:'CA', territory:'Canada',        kind:'NRO',  ackRate:93.2, lastSent:'5d ago',  members:30000,  ipiPrefix:'1', cwrAck:'DDEX RDR-N'},
  // Rights hubs / admins — sit between publishers and societies; handle data + licensing on their behalf
  {acronym:'ICE',   name:'International Copyright Enterprise',                     country:'GB', territory:'PRS · GEMA · STIM joint hub', kind:'HUB', ackRate:99.4, lastSent:'4h ago', members:0, ipiPrefix:'2', cwrAck:'CWR 3.0'},
  {acronym:'MRI',   name:'Music Reports, Inc.',                                    country:'US', territory:'United States · DSP admin',  kind:'HUB', ackRate:96.2, lastSent:'12h ago', members:0, ipiPrefix:'1', cwrAck:'CWR 2.1, 3.0'},
  {acronym:'MINT',  name:'MINT Digital Services',                                  country:'CH', territory:'SACEM · SUISA joint hub',     kind:'HUB', ackRate:97.0, lastSent:'1d ago', members:0, ipiPrefix:'2', cwrAck:'CWR 3.0'},
  {acronym:'Armonia',name:'Armonia Online Music',                                  country:'FR', territory:'Multi-territory online',      kind:'HUB', ackRate:94.8, lastSent:'2d ago', members:0, ipiPrefix:'2', cwrAck:'CWR 3.0'},
  {acronym:'Latinautor',name:'Latinautor',                                         country:'UY', territory:'Latin America',               kind:'HUB', ackRate:91.0, lastSent:'6d ago', members:0, ipiPrefix:'1', cwrAck:'CWR 2.1'},
];

const CLAIMS = [
  {id:'C-2026-04-1882', work:'10%',           iswc:'T-998.221.001-4', claimant:'KAY Publishing',     party:'Sony/ATV',     pct:60, status:'open',    method:'manual', age:3, severity:'high'},
  {id:'C-2026-04-1881', work:'Wildfires',     iswc:'T-822.444.118-2', claimant:'Forever Living',     party:'Universal MP', pct:25, status:'open',    method:'cwr',    age:5, severity:'high'},
  {id:'C-2026-04-1875', work:'Bunny Is A Rider',iswc:'T-700.301.522-9', claimant:'Perpetual Pub',     party:'Concord',      pct:70, status:'review',  method:'manual', age:1, severity:'mid'},
  {id:'C-2026-04-1864', work:'Far In',        iswc:'T-129.501.482-7', claimant:'HN Pub',             party:'\u2014',       pct:100,status:'resolved',method:'cwr',    age:0, severity:'low'},
  {id:'C-2026-04-1859', work:'Send Me',       iswc:'T-503.211.097-2', claimant:'TM Works',           party:'Domino',       pct:50, status:'open',    method:'cwr',    age:8, severity:'mid'},
  {id:'C-2026-04-1842', work:'Cranes In The Sky',iswc:'T-308.901.522-0',claimant:'SK Catalog',       party:'Saadiq Publ.', pct:62, status:'open',    method:'cwr',    age:12,severity:'mid'},
];
// Procedurally append ~56 more claims so totals (41 open / 12 hot / 18 warm / 23 resolved-7d) match the queue.
(function _seedMoreClaims() {
  const works = [
    'Vermilion','Late Bloom','Stay Out','Old Reasons','Coastline','Half Moon','Dwell','Pyrite Sun',
    'Pulse Drift','Honey Pail','Soft Errors','Run Of Show','Civil Twilight','Glass Atlas','Mercy Hours',
    'Northbound','Slow Tomorrow','Tin Cathedral','Paper Houses','River Math','Fold In','Last Foothold',
    'Cargo','Midwood','Telegraph','Silver Tide','Field Recordings','August End','Brick And Light',
    'Echo Park','Tessellate','Hold Steady','Long Division','Marrow','Frequency 9','Open Hand',
    'Vapor Trail','Saltwater','Ten Cities','Granite','Half Step','Distant Wires','Common Hours',
    'Aperture','Plain Sight','Anthem 02','Citrine','Mercator','Ledger Line','Nightshift Choir',
    'Salt Cedar','Linework','Plumb','Quiet Math','Pinprick','Gentle Static','Rooftop Garden',
  ];
  const claimants = ['KAY Publishing','Forever Living','Perpetual Pub','HN Pub','TM Works','SK Catalog','Sunset Sound','Heller MP','Glasshouse Songs','Ninefold Music','Birch & Vale','Cardinal Pub','Aubergine Songs','Greenline Music'];
  const parties   = ['Sony/ATV','Universal MP','Concord','Warner Chappell','Domino','Saadiq Publ.','BMG','Kobalt','Reservoir','peermusic','—','Downtown','Pulse Pub','Round Hill'];
  const statuses  = ['open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open','open', 'review','review','review','review','review','review','review','review','review','review','review','review','review','review','resolved','resolved','resolved','resolved','resolved','resolved','resolved'];
  const methods   = ['cwr','manual','cwr','cwr','manual'];
  let s = 0x9e3779b9;
  const rnd = () => (s = (s * 16807) % 2147483647, s / 2147483647);
  const pick = (arr) => arr[Math.floor(rnd()*arr.length)];
  const idStart = 1841;
  for (let i = 0; i < 60; i++) {
    const status = statuses[i % statuses.length];
    const ageBase = status==='resolved' ? Math.floor(rnd()*7) : Math.floor(rnd()*18);
    const age = Math.max(0, ageBase);
    const severity = status==='resolved' ? 'low'
      : age > 5 ? 'high'
      : age > 2 ? 'mid'
      : (rnd() > 0.5 ? 'mid' : 'low');
    const id = `C-2026-04-${(idStart - i).toString().padStart(4,'0')}`;
    const work = pick(works);
    const claimant = pick(claimants);
    const party = pick(parties);
    const pct = [25,33,40,50,60,66,70,75,100][Math.floor(rnd()*9)];
    CLAIMS.push({
      id, work,
      iswc: `T-${(100 + Math.floor(rnd()*900))}.${(100+Math.floor(rnd()*900))}.${(100+Math.floor(rnd()*900))}-${Math.floor(rnd()*9)}`,
      claimant, party, pct, status,
      method: pick(methods), age, severity,
    });
  }
})();

// ───────────────────────────────────────────────────────────── CATALOG STATS
// Single source of truth for headline counters across the app.
// "Featured" arrays (WORKS, RECORDINGS, ARTISTS) are curated subsets
// for detail-screen demonstration; total counts represent the full
// catalog this workspace administers.
window.CATALOG_STATS = {
  works: {
    total: 18422,
    featured: WORKS.length,
    registered: WORKS.filter(w => w.status === 'registered').length,
    pending: WORKS.filter(w => w.status === 'pending').length,
  },
  recordings: {
    total: 32104,
    // featured count derived after RECORDING_GRAPH defined elsewhere; recompute on read
  },
  releases: { total: 4811 },
  agreements: { total: 218 },
  artists: {
    total: 342,
    featured: ARTISTS.length,
    legal: ARTISTS.filter(a => a.legal).length,
  },
  directory: {
    profiles:   5202,
    publishers: 142,
    labels:     38,
    // societies derived live from SOCIETIES array on read
  },
  claims: {
    open:     CLAIMS.filter(c => c.status === 'open').length,
    review:   CLAIMS.filter(c => c.status === 'review').length,
    resolved: CLAIMS.filter(c => c.status === 'resolved').length,
    hot:      CLAIMS.filter(c => c.status === 'open' && c.severity === 'high').length,
    warm:     CLAIMS.filter(c => c.status === 'open' && c.severity === 'mid').length,
    total:    CLAIMS.length,
  },
  cwrAckRate: (() => {
    if (typeof TXN === 'undefined' || !TXN.length) return 0.992;
    const ack = TXN.filter(t => t.status === 'acknowledged').length;
    return TXN.length ? ack / TXN.length : 0.992;
  })(),
};
// Recompute when window.RECORDING_GRAPH (defined in songs.jsx) becomes available.
window.__catalogStatsRefresh = function() {
  if (window.RECORDING_GRAPH) window.CATALOG_STATS.recordings.featured = window.RECORDING_GRAPH.length;
  return window.CATALOG_STATS;
};

const TXN = [
  {id:'CWR-2026-0418-001', file:'CW260418ASCAP.V21', recv:'ASCAP', count:144, status:'acknowledged', sent:'14:22', ack:'14:48'},
  {id:'CWR-2026-0418-002', file:'CW260418BMI.V21',   recv:'BMI',   count:144, status:'acknowledged', sent:'14:22', ack:'15:11'},
  {id:'CWR-2026-0418-003', file:'CW260418GEMA.V30',  recv:'GEMA',  count: 88, status:'submitted',    sent:'14:22', ack:'\u2014'},
  {id:'CWR-2026-0418-004', file:'CW260418PRS.V30',   recv:'PRS',   count:121, status:'rejected',     sent:'14:22', ack:'14:51'},
  {id:'CWR-2026-0418-005', file:'CW260418SACEM.V21', recv:'SACEM', count: 99, status:'acknowledged', sent:'14:22', ack:'18:09'},
  {id:'CWR-2026-0418-006', file:'CW260418SOCAN.V22', recv:'SOCAN', count:104, status:'submitted',    sent:'14:22', ack:'\u2014'},
];

// 30-day spark series
const sparkRoyalty = [22,28,24,31,35,29,40,46,38,33,42,55,48,52,58,61,52,67,72,68,77,82,79,88,94,90,101,98,108,116];
const sparkPlays   = [180,210,205,260,240,290,310,330,300,360,420,400,480,510,495,560,610,580,640,720,690,755,810,790,860,910,895,950,1020,1080];
const sparkRoster  = [212,214,219,221,222,228,231,235,240,242,247,250,255,260,262,268,271,276,279,283,288,293,295,300,305,309,313,318,322,328];

// ───────────────────────────────────────────────────────────── Plain-language glossary
const GLOSSARY = {
  ISWC:  'International Standard Musical Work Code — a unique ID for a song (composition).',
  ISRC:  'International Standard Recording Code — a unique ID for a specific recording.',
  IPI:   'Interested Parties Information — a global ID for a writer or publisher (also called CAE).',
  ISNI:  'International Standard Name Identifier — global ID for a person or organization.',
  CWR:   'Common Works Registration — the file format used to register songs with societies (ASCAP, BMI, PRS, etc.).',
  DDEX:  'Digital Data Exchange — the file format used to deliver music and metadata to streaming services.',
  ERN:   'Electronic Release Notification — a DDEX message that tells DSPs about a new release.',
  PRO:   'Performing Rights Organization — collects performance royalties (ASCAP, BMI, PRS).',
  CMO:   'Collective Management Organization — umbrella term for any rights society.',
  MRO:   'Mechanical Rights Organization — collects royalties when a song is reproduced.',
  NRO:   'Neighboring Rights Organization — collects royalties for performers and labels (e.g. SoundExchange).',
  DSP:   'Digital Service Provider — Spotify, Apple Music, YouTube, etc.',
  ACK:   'Acknowledgement — the response a society sends back after we register a work.',
  UPC:   'Universal Product Code — a barcode-style ID for a release (album/single).',
  GRid:  'Global Release Identifier — DDEX-issued ID for a release.',
  CISAC: 'International Confederation of Authors and Composers Societies — sets the global standards.',
  CAE:   'Old name for IPI — still used by some societies.',
  Split: 'How writing credit is divided between writers (must add up to 100%).',
  Controlled:'A writer/publisher you administer — you collect their royalties.',
  Uncontrolled:'A writer/publisher we know about but do not represent.',
  Sync:  'Sync licensing — using a song in film, TV, ads, games.',
  ISO:   'Two-letter country code (US, GB, DE, JP).',
};

// Inline help bubble — wrap any term to get a hover/tap explainer
function Term({ children, k }) {
  const def = GLOSSARY[k || (typeof children === 'string' ? children.trim() : '')];
  if (!def) return <span>{children}</span>;
  return (
    <span tabIndex={0} role="button" aria-label={`What is ${k||children}?`}
      style={{borderBottom:'1px dotted var(--ink-3)',cursor:'help',position:'relative'}}
      onMouseEnter={e=>{
        const tt = document.createElement('div');
        tt.className='__term_tt';
        tt.textContent=def;
        tt.style.cssText='position:fixed;z-index:9999;max-width:280px;background:var(--ink);color:var(--bg);padding:8px 10px;font:500 11px/1.4 "IBM Plex Mono",monospace;letter-spacing:.02em;pointer-events:none;border:1px solid var(--ink);';
        const r = e.currentTarget.getBoundingClientRect();
        tt.style.left = Math.max(8, Math.min(window.innerWidth-300, r.left)) + 'px';
        tt.style.top = (r.bottom + 6) + 'px';
        document.body.appendChild(tt);
        e.currentTarget._tt = tt;
      }}
      onMouseLeave={e=>{ if(e.currentTarget._tt){e.currentTarget._tt.remove();e.currentTarget._tt=null;} }}>
      {children}
    </span>
  );
}

// Plain-language hint banner shown at top of any screen — dismissable per screen
function HelpBanner({ id, title, children, action }) {
  const [open, setOpen] = useState(()=> {
    try { return localStorage.getItem('astro_help_'+id) !== '0'; } catch { return true; }
  });
  if (!open) return null;
  const dismiss = () => { try{ localStorage.setItem('astro_help_'+id,'0'); }catch{}; setOpen(false); };
  return (
    <div style={{display:'flex',alignItems:'flex-start',gap:14,padding:'12px 14px',background:'var(--bg-2)',border:'1px solid var(--rule)',borderLeft:'3px solid var(--accent)',marginBottom:18}}>
      <span className="ff-mono upper" style={{fontSize:9,fontWeight:600,color:'var(--accent-ink)',background:'var(--accent)',padding:'2px 6px',marginTop:1,letterSpacing:'.08em'}}>TIP</span>
      <div style={{flex:1,fontSize:13,lineHeight:1.45}}>
        {title && <div style={{fontWeight:600,marginBottom:2}}>{title}</div>}
        <div style={{color:'var(--ink-2)'}}>{children}</div>
      </div>
      {action}
      <button onClick={dismiss} title="Dismiss" style={{padding:4,color:'var(--ink-3)'}}><Ic.X width={12} height={12}/></button>
    </div>
  );
}

// ID validation badge — green check / red x with reason
function IdBadge({ kind, value, valid }) {
  return (
    <span style={{display:'inline-flex',alignItems:'center',gap:6,fontSize:11}} className="ff-mono">
      <span style={{color:valid?'var(--ok)':'var(--danger)',display:'inline-flex'}}>
        {valid ? <Ic.Check width={11} height={11}/> : <Ic.X width={11} height={11}/>}
      </span>
      <span style={{color:'var(--ink-3)'}}><Term k={kind}>{kind}</Term></span>
      <span style={{color:'var(--ink)'}}>{value}</span>
    </span>
  );
}

// Breadcrumb
function Crumbs({ items }) {
  return (
    <div className="ff-mono upper" style={{fontSize:10,color:'var(--ink-3)',marginBottom:10,display:'flex',alignItems:'center',gap:6,flexWrap:'wrap'}}>
      {items.map((it,i)=>(
        <React.Fragment key={i}>
          {i>0 && <span style={{color:'var(--ink-4)'}}>/</span>}
          {it.go ? <button onClick={it.go} style={{color:'var(--ink-3)'}}>{it.label}</button>
                 : <span style={{color:'var(--ink)'}}>{it.label}</span>}
        </React.Fragment>
      ))}
    </div>
  );
}

// Empty state
function EmptyState({ icon, title, hint, action }) {
  const Icon = icon || Ic.Inbox;
  return (
    <div style={{padding:'48px 24px',textAlign:'center',border:'1px dashed var(--rule-soft)',display:'flex',flexDirection:'column',alignItems:'center',gap:12}}>
      <Icon width={32} height={32} style={{color:'var(--ink-4)'}}/>
      <div style={{fontSize:16,fontWeight:600}}>{title}</div>
      {hint && <div style={{fontSize:13,color:'var(--ink-3)',maxWidth:380,lineHeight:1.5}}>{hint}</div>}
      {action}
    </div>
  );
}

// Sample releases / recordings
const RELEASES_DATA = [
  {id:'r_01', title:'A Seat At The Table', artist:'Solange', upc:'886446154213', grid:'A1-2W22-00000040-D', kind:'Album', year:2016, tracks:21, color:'#cf9b3e'},
  {id:'r_02', title:'Crush', artist:'Floating Points', upc:'5054429142655', grid:'A1-2NF8-00000222-K', kind:'Album', year:2019, tracks:9, color:'#1a3a6e'},
  {id:'r_03', title:'Pang', artist:'Caroline Polachek', upc:'192641143526', grid:'A1-2WC1-00000812-X', kind:'Album', year:2019, tracks:14, color:'#cf6438'},
  {id:'r_04', title:'Devotion', artist:'Tirzah', upc:'5400863001148', grid:'A1-2P55-00000071-J', kind:'Album', year:2018, tracks:11, color:'#3a2614'},
  {id:'r_05', title:'Far In', artist:'Helado Negro', upc:'4582116080726', grid:'A1-2T44-00000910-W', kind:'Album', year:2021, tracks:14, color:'#a0392c'},
  {id:'r_06', title:'BUBBA', artist:'KAYTRANADA', upc:'196589094339', grid:'A1-2X88-00000044-L', kind:'Album', year:2019, tracks:17, color:'#4a2270'},
];

// Society code helpers — derive ALL dropdown lists from the central SOCIETIES directory
// so adding a society in the Directory automatically wires through to filters, registrations, etc.
const SOC_CODES = SOCIETIES.map(s => s.acronym);                            // every society
const SOC_PROS  = SOCIETIES.filter(s => s.kind === 'PRO').map(s => s.acronym); // performance only
const SOC_MROS  = SOCIETIES.filter(s => s.kind === 'MRO').map(s => s.acronym); // mechanical only
const SOC_NROS  = SOCIETIES.filter(s => s.kind === 'NRO').map(s => s.acronym); // neighboring only
const SOC_CMOS  = SOCIETIES.filter(s => s.kind === 'CMO').map(s => s.acronym); // combined CMO only
const SOC_HUBS  = SOCIETIES.filter(s => s.kind === 'HUB').map(s => s.acronym); // hubs / admins
// Writers register with PROs and combined CMOs (not MROs/NROs/HUBs)
const SOC_WRITER = SOCIETIES.filter(s => s.kind === 'PRO' || s.kind === 'CMO').map(s => s.acronym);
// CWR is sent to PROs, MROs, combined CMOs, and hubs that route CWR (not pure NROs)
const SOC_CWR    = SOCIETIES.filter(s => s.kind !== 'NRO').map(s => s.acronym);
function getSocietyByCode(code) { return SOCIETIES.find(s => s.acronym === code) || null; }

// SocietyLink — anywhere a PRO/society acronym is shown, use this to make it
// clickable. It deep-links to Directory → Societies and highlights the row.
// Falls back to a plain span if the code isn't in the directory yet.
function SocietyLink({ code, style, children, dim }) {
  if (!code || code === '—' || code === '\u2014') return <span style={style}>{children || code || '—'}</span>;
  const known = getSocietyByCode(code);
  const open = (e) => {
    e.stopPropagation();
    if (typeof window.__astroGo === 'function') window.__astroGo('directory');
    // Slight defer so the directory mounts before we tell it which tab to open
    window.setTimeout(() => {
      window.dispatchEvent(new CustomEvent('astro-open-society', { detail: { code } }));
    }, 30);
  };
  const tip = known ? `${known.name} — ${known.territory}${known.kind ? ` · ${known.kind}` : ''}` : `${code} — not in directory`;
  return (
    <button onClick={open} title={tip}
      style={{
        background:'transparent', border:0, padding:0, margin:0, cursor:'pointer',
        font:'inherit', color: dim ? 'var(--ink-3)' : 'inherit',
        textDecoration:'underline', textDecorationStyle: known ? 'solid' : 'dotted',
        textDecorationThickness:'1px', textUnderlineOffset:'2px',
        textDecorationColor:'var(--rule)',
        ...(style||{})
      }}>
      {children || code}
    </button>
  );
}

// Export
Object.assign(window, { Ic, Pill, Btn, Kbd, Spark, AsciiBar, VuMeter, Waveform, useAudioPeaks, Section,
  Term, HelpBanner, IdBadge, Crumbs, EmptyState, GLOSSARY,
  ARTISTS, WORKS, RECENT, SOCIETIES, SOC_CODES, SOC_PROS, SOC_MROS, SOC_NROS, SOC_CMOS, SOC_HUBS, SOC_WRITER, SOC_CWR, getSocietyByCode, SocietyLink,
  CLAIMS, TXN, RELEASES_DATA, sparkRoyalty, sparkPlays, sparkRoster });
