// PublicationsView — full publications page with tabs:
// In Press · Articles · Proceedings · Book Chapters.
// Entries grouped by year, color-coded topic tags, DOI + BibTeX actions.
// Data parsed from the .bib (window.WINTEL_PUBLICATIONS); arXiv excluded.
const PV = window.WINTELAcademicDesignSystem_045d4f;

const CONF = {
  'IEEECONF': 'Asilomar Conf. Signals, Syst., Comput.',
  'ACSSC': 'Asilomar Conf. Signals, Syst., Comput.',
  'ICC': 'IEEE Int. Conf. Commun. (ICC)',
  'ICC Workshops': 'IEEE ICC Workshops',
  'GLOBECOM': 'IEEE Global Commun. Conf. (GLOBECOM)',
  'GLOBECOM (Workshops)': 'IEEE GLOBECOM Workshops',
  'GLOBECOM Workshops': 'IEEE GLOBECOM Workshops',
  'PIMRC': 'IEEE Int. Symp. Pers., Indoor Mobile Radio Commun. (PIMRC)',
  'WCNC': 'IEEE Wireless Commun. Netw. Conf. (WCNC)',
  'SPAWC': 'IEEE Int. Workshop Signal Process. Adv. Wireless Commun. (SPAWC)',
  'SAM': 'IEEE Sensor Array Multichannel Signal Process. Workshop (SAM)',
  'ICCSPA': 'Int. Conf. Commun., Signal Process., Appl. (ICCSPA)',
  'EMBC': 'IEEE Eng. Med. Biol. Soc. Conf. (EMBC)',
  'HealthCom': 'IEEE Int. Conf. E-Health Netw., Appl. Services (HealthCom)',
  'BSN': 'IEEE Body Sensor Networks Conf. (BSN)',
  'VTC Spring': 'IEEE Veh. Technol. Conf. (VTC-Spring)',
  'VTC Fall': 'IEEE Veh. Technol. Conf. (VTC-Fall)',
  'CCNC': 'IEEE Consum. Commun. Netw. Conf. (CCNC)',
  'ISCAS': 'IEEE Int. Symp. Circuits Syst. (ISCAS)',
  'MWSCAS': 'IEEE Int. Midwest Symp. Circuits Syst. (MWSCAS)',
  'ICTC': 'Int. Conf. ICT Convergence (ICTC)',
  'ICASSP': 'IEEE Int. Conf. Acoust., Speech, Signal Process. (ICASSP)',
  'ICNC': 'Int. Conf. Comput., Netw. Commun. (ICNC)',
};

// ---- Topic taxonomy (order = priority; each pub shows up to 3) ----
const TOPICS = [
  { label: 'ISAC', color: '#0ea5e9', re: /\bISAC\b|integrated sensing|sensing and communication|joint .*sensing|beam-?squint/i },
  { label: 'RIS', color: '#2563eb', re: /\bRIS\b|reconfigurable intelligent surface|star-?ris|intelligent surface/i },
  { label: 'AI-Native', color: '#8b5cf6', re: /\bAI\b|deep learning|machine learning|\bDRL\b|\bGAN\b|\bLLM\b|explainabl|generative|neural|\bXAI\b|agentic|autoencoder|reinforcement/i },
  { label: 'Near-Field', color: '#d97706', re: /near-?field|beamfocus|polar-?domain|spatial degrees of freedom|\bURA\b|\bUCA\b|circular array/i },
  { label: 'NTN', color: '#059669', re: /\bNTN\b|non-?terrestrial|\bUAV\b|airborne|satellite|aerial|drone/i },
  { label: 'OWC', color: '#4f46e5', re: /optical wireless|\bOWC\b|opto-?acoustic|underwater|visible light|\bVLC\b|\blaser\b|photonic/i },
  { label: 'IoB', color: '#84cc16', re: /internet of bodies|body communication|human body|\bIoB\b|\bHBC\b|biomedical|\bECG\b|wearable|implant/i },
  { label: 'NOMA', color: '#0d9488', re: /\bNOMA\b|non-?orthogonal|multiple access|rate splitting|\bRSMA\b|\bSWIPT\b/i },
  { label: 'mMIMO', color: '#ca8a04', re: /massive mimo|\bmMIMO\b|beamforming|codebook|channel estimation|cell-?free|\bMIMO\b|mmwave/i },
  { label: 'Security', color: '#dc2626', re: /physical layer security|\bsecurity\b|\bsecure\b|protected zone|jamming|eavesdrop/i },
  { label: 'CRN', color: '#c026d3', re: /cognitive radio|\bCRN\b|spectrum sensing|spectrum sharing|\bcognitive\b/i },
  { label: 'IoT', color: '#65a30d', re: /internet of things|\bIoT\b|device-?to-?device|\bD2D\b|machine-?type|\bM2M\b/i },
];

// ---- Journal venue short codes (short label + full title for tooltip) ----
const JOURNAL_VENUE = {
  'IEEE Trans. Wirel. Commun.': ['IEEE TWC', 'IEEE Transactions on Wireless Communications'],
  'IEEE Trans. Commun.': ['IEEE TCOM', 'IEEE Transactions on Communications'],
  'IEEE Trans. Mob. Comput.': ['IEEE TMC', 'IEEE Transactions on Mobile Computing'],
  'IEEE Trans. Cogn. Commun. Netw.': ['IEEE TCCN', 'IEEE Transactions on Cognitive Communications and Networking'],
  'IEEE Trans. Green Commun. Netw.': ['IEEE TGCN', 'IEEE Transactions on Green Communications and Networking'],
  'IEEE Trans. Netw. Serv. Manag.': ['IEEE TNSM', 'IEEE Transactions on Network and Service Management'],
  'IEEE Trans. Circuits Syst. I Regul. Pap.': ['IEEE TCAS-I', 'IEEE Transactions on Circuits and Systems I: Regular Papers'],
  'IEEE Trans. Biomed. Eng.': ['IEEE TBME', 'IEEE Transactions on Biomedical Engineering'],
  'IEEE Trans. Instrum. Meas.': ['IEEE TIM', 'IEEE Transactions on Instrumentation and Measurement'],
  'IEEE J. Sel. Top. Signal Process.': ['IEEE JSTSP', 'IEEE Journal of Selected Topics in Signal Processing'],
  'IEEE Open J. Commun. Soc.': ['IEEE OJ-COMS', 'IEEE Open Journal of the Communications Society'],
  'IEEE Trans. Mach. Learn. Commun. Netw.': ['IEEE TMLCN', 'IEEE Transactions on Machine Learning in Communications and Networking'],
  'Nat. Rev. Electr. Eng.': ['Nat. Rev. EE', 'Nature Reviews Electrical Engineering'],
  'Results Eng.': ['Results Eng.', 'Results in Engineering (Elsevier)'],
  'Smart Health': ['Smart Health', 'Smart Health (Elsevier)'],
  'Balkan J. Elect. Comp. Eng.': ['BAJECE', 'Balkan Journal of Electrical and Computer Engineering'],
  'IEEE Commun. Mag.': ['IEEE ComMag', 'IEEE Communications Magazine'],
  'IEEE Commun. Stand. Mag.': ['IEEE StdMag', 'IEEE Communications Standards Magazine'],
  'IEEE Commun. Lett.': ['IEEE ComLett', 'IEEE Communications Letters'],
  'IEEE Commun. Surv. Tutorials': ['IEEE COMST', 'IEEE Communications Surveys & Tutorials'],
  'IEEE Wirel. Commun.': ['IEEE WCM', 'IEEE Wireless Communications'],
  'IEEE Wirel. Commun. Lett.': ['IEEE WCL', 'IEEE Wireless Communications Letters'],
  'IEEE Wireless Commun. Lett.': ['IEEE WCL', 'IEEE Wireless Communications Letters'],
  'IEEE Signal Process. Mag.': ['IEEE SPM', 'IEEE Signal Processing Magazine'],
  'IEEE Internet Things J.': ['IEEE IoT-J', 'IEEE Internet of Things Journal'],
  'IEEE Sensors J.': ['IEEE SensJ', 'IEEE Sensors Journal'],
  'IEEE Internet Things Mag.': ['IEEE IoT-M', 'IEEE Internet of Things Magazine'],
  'IEEE Veh. Technol. Mag.': ['IEEE VTM', 'IEEE Vehicular Technology Magazine'],
  'IEEE Syst. J.': ['IEEE SysJ', 'IEEE Systems Journal'],
  'IEEE Access': ['IEEE Access', 'IEEE Access'],
  'Ad Hoc Networks': ['AdHocNet', 'Ad Hoc Networks (Elsevier)'],
  'IET Commun.': ['IET COM', 'IET Communications'],
  'Sci. China Inf. Sci.': ['SCIS', 'Science China Information Sciences'],
  'Sensors': ['Sensors', 'Sensors (MDPI)'],
};

// ---- Journal quartile ranking (Scimago / SJR, current) ----
// Keyed by the same venue string as pub.v. Source: scimagojr.com.
// >>> VERIFY / EDIT THESE to match your preferred ranking (Scimago vs JCR) <<<
// Use 'Q1'..'Q4', or null / omit for unranked or too-new venues (no badge shown).
const QUARTILE = {
  'IEEE Trans. Wirel. Commun.': 'Q1',
  'IEEE Trans. Commun.': 'Q1',
  'IEEE Trans. Mob. Comput.': 'Q1',
  'IEEE Trans. Cogn. Commun. Netw.': 'Q1',
  'IEEE Trans. Green Commun. Netw.': 'Q1',
  'IEEE Trans. Netw. Serv. Manag.': 'Q1',
  'IEEE Trans. Circuits Syst. I Regul. Pap.': 'Q1',
  'IEEE Trans. Biomed. Eng.': 'Q1',
  'IEEE Trans. Instrum. Meas.': 'Q1',
  'IEEE J. Sel. Top. Signal Process.': 'Q1',
  'IEEE Open J. Commun. Soc.': 'Q1',
  'IEEE Trans. Mach. Learn. Commun. Netw.': null, // too new for an SJR quartile
  'Nat. Rev. Electr. Eng.': 'Q1',
  'Results Eng.': 'Q1',
  'Smart Health': 'Q2',
  'Balkan J. Elect. Comp. Eng.': null, // not indexed in Scimago
  'IEEE Commun. Mag.': 'Q1',
  'IEEE Commun. Stand. Mag.': 'Q1',
  'IEEE Commun. Lett.': 'Q2',
  'IEEE Commun. Surv. Tutorials': 'Q1',
  'IEEE Wirel. Commun.': 'Q1',
  'IEEE Wirel. Commun. Lett.': 'Q1',
  'IEEE Wireless Commun. Lett.': 'Q1',
  'IEEE Signal Process. Mag.': 'Q1',
  'IEEE Internet Things J.': 'Q1',
  'IEEE Sensors J.': 'Q1',
  'IEEE Internet Things Mag.': 'Q1',
  'IEEE Veh. Technol. Mag.': 'Q1',
  'IEEE Syst. J.': 'Q1',
  'IEEE Access': 'Q1',
  'Ad Hoc Networks': 'Q1',
  'IET Commun.': 'Q2',
  'Sci. China Inf. Sci.': 'Q1',
  'Sensors': 'Q1',
  'Nature Sensors': 'Q1',
  'Nature Reviews Electrical Engineering': 'Q1',
};

const QUARTILE_COLOR = {
  Q1: '#059669', // emerald
  Q2: '#2563eb', // blue
  Q3: '#d97706', // amber
  Q4: '#6b7280', // slate
};

// Quartile only applies to journal articles (not conferences, not book chapters).
function quartileFor(pub) {
  if (pub.conf || pub.chapter) return null;
  return QUARTILE[pub.v] || null;
}

function QuartileTag({ q }) {
  const color = QUARTILE_COLOR[q] || '#6b7280';
  return (
    <span className="pubtag-quartile" title={'Journal quartile ' + q + ' (Scimago / SJR)'} style={{
      display: 'inline-flex', alignItems: 'center',
      fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', fontWeight: 'var(--weight-semibold)',
      letterSpacing: '0.02em', lineHeight: 1, padding: '0.28rem 0.5rem', borderRadius: 'var(--radius-sm)', whiteSpace: 'nowrap',
      color: '#fff', background: color, border: '1px solid ' + color,
    }}>{q}</span>
  );
}

function venueTag(pub) {
  if (pub.chapter) return { short: 'Book Ch.', full: pub.v + (pub.pub ? ' · ' + pub.pub : '') };
  if (!pub.conf) {
    const m = JOURNAL_VENUE[pub.v];
    return m ? { short: m[0], full: m[1] } : { short: pub.v, full: pub.v };
  }
  if (CONF[pub.v]) {
    const full = CONF[pub.v];
    const abbr = (full.match(/\(([^)]+)\)/) || [])[1] || pub.v;
    return { short: abbr, full };
  }
  if (/asilomar/i.test(pub.v)) {
    return { short: 'ASILOMAR', full: 'Asilomar Conference on Signals, Systems, and Computers, Pacific Grove' };
  }
  const KW = [
    [/globecom workshop|glocomw|gcwkshp|gc wkshp/i, 'GC Wkshps', 'IEEE GLOBECOM Workshops'],
    [/globecom|glocom/i, 'GLOBECOM', 'IEEE Global Communications Conference (GLOBECOM)'],
    [/icc workshop|iccw/i, 'ICC Wkshps', 'IEEE ICC Workshops'],
  ];
  for (const [re, short, full] of KW) { if (re.test(pub.v)) return { short, full }; }
  const abbr = (pub.v.match(/\(([^)]+)\)/) || [])[1];
  return { short: abbr || 'Conf.', full: pub.v.split(',')[0] };
}

function VenueTag({ info }) {
  return (
    <span className="pubtag-venue" title={info.full} style={{
      display: 'inline-flex', alignItems: 'center',
      fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', fontWeight: 'var(--weight-medium)',
      letterSpacing: '0.02em', lineHeight: 1, padding: '0.28rem 0.5rem', borderRadius: 'var(--radius-sm)', whiteSpace: 'nowrap',
      color: 'var(--text-muted)', background: 'var(--surface-muted)', border: '1px solid var(--border-default)',
    }}>{info.short}</span>
  );
}

function topicsFor(pub) {
  const hay = (pub.t || '') + ' ' + (pub.v || '');
  return TOPICS.filter((t) => t.re.test(hay)).slice(0, 3);
}

// Country → flag emoji for conference locations (matched against loc + venue string).
const COUNTRY_FLAGS = [
  [/\bU\.?S\.?A\.?\b|United States/i, '\uD83C\uDDFA\uD83C\uDDF8'],
  [/\bUK\b|United Kingdom|England|Scotland|Glasgow|London|Manchester|Guildford|Southampton/i, '\uD83C\uDDEC\uD83C\uDDE7'],
  [/\bUAE\b|United Arab Emirates|Dubai|Abu Dhabi/i, '\uD83C\uDDE6\uD83C\uDDEA'],
  [/Saudi Arabia|Jeddah|Dammam|Riyadh|Thuwal/i, '\uD83C\uDDF8\uD83C\uDDE6'],
  [/T\u00fcrkiye|Turkey|Istanbul|Ankara/i, '\uD83C\uDDF9\uD83C\uDDF7'],
  [/Canada|Ottawa|Montreal|Toronto/i, '\uD83C\uDDE8\uD83C\uDDE6'],
  [/Macau|Macao/i, '\uD83C\uDDF2\uD83C\uDDF4'],
  [/\bChina\b|Beijing|Shanghai|Shenzhen/i, '\uD83C\uDDE8\uD83C\uDDF3'],
  [/Sweden|Stockholm|Gothenburg/i, '\uD83C\uDDF8\uD83C\uDDEA'],
  [/Italy|Bologna|Padova|Padua|Rome|Milan/i, '\uD83C\uDDEE\uD83C\uDDF9'],
  [/Egypt|Alexandria|Cairo/i, '\uD83C\uDDEA\uD83C\uDDEC'],
  [/South Africa|Cape Town|Johannesburg/i, '\uD83C\uDDFF\uD83C\uDDE6'],
  [/Malaysia|Kuala Lumpur/i, '\uD83C\uDDF2\uD83C\uDDFE'],
  [/Singapore/i, '\uD83C\uDDF8\uD83C\uDDEC'],
  [/South Korea|\bKorea\b|Seoul|Busan/i, '\uD83C\uDDF0\uD83C\uDDF7'],
  [/Germany|Berlin|Munich|Hamburg/i, '\uD83C\uDDE9\uD83C\uDDEA'],
  [/France|Paris/i, '\uD83C\uDDEB\uD83C\uDDF7'],
  [/Spain|Barcelona|Madrid/i, '\uD83C\uDDEA\uD83C\uDDF8'],
  [/Greece|Athens/i, '\uD83C\uDDEC\uD83C\uDDF7'],
  [/Japan|Tokyo|Osaka/i, '\uD83C\uDDEF\uD83C\uDDF5'],
  [/Australia|Sydney|Melbourne/i, '\uD83C\uDDE6\uD83C\uDDFA'],
  [/\bIndia\b|Delhi|Mumbai|Bangalore/i, '\uD83C\uDDEE\uD83C\uDDF3'],
  [/Finland|Helsinki/i, '\uD83C\uDDEB\uD83C\uDDEE'],
  [/Morocco|Marrakech|Marrakesh|Rabat/i, '\uD83C\uDDF2\uD83C\uDDE6'],
  [/Kazakhstan|Astana|Almaty/i, '\uD83C\uDDF0\uD83C\uDDFF'],
  [/Brazil|Rio de Janeiro|S\u00e3o Paulo|Sao Paulo/i, '\uD83C\uDDE7\uD83C\uDDF7'],
];

function flagForText(str) {
  const hay = str || '';
  for (const [re, flag] of COUNTRY_FLAGS) { if (re.test(hay)) return flag; }
  return null;
}

function flagFor(pub) {
  if (!pub.conf) return null;
  return flagForText((pub.loc || '') + ' ' + (pub.v || ''));
}

function TopicTag({ topic }) {
  return (
    <span className="pubtag" style={{
      display: 'inline-flex', alignItems: 'center',
      fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', fontWeight: 'var(--weight-medium)',
      letterSpacing: '0.02em', lineHeight: 1, padding: '0.28rem 0.5rem', borderRadius: 'var(--radius-sm)', whiteSpace: 'nowrap',
      color: topic.color, background: topic.color + '1A', border: '1px solid ' + topic.color + '33',
    }}>{topic.label}</span>
  );
}

// ---- BibTeX synthesis ----
function bibKey(pub) {
  const first = (pub.a && pub.a[0] && pub.a[0].name) || 'celik';
  const last = first.replace(/\./g, '').trim().split(/\s+/).pop().toLowerCase().replace(/[^a-z]/g, '');
  const word = (pub.t || '').toLowerCase().replace(/[^a-z0-9 ]/g, '').split(/\s+/).find((w) => w.length > 3) || 'paper';
  return last + (pub.y || '') + word;
}

function bibtex(pub) {
  const type = pub.conf ? 'inproceedings' : (pub.chapter ? 'incollection' : 'article');
  const authors = (pub.a || []).map((x) => x.name).join(' and ');
  const doi = pub.u ? pub.u.replace(/^https?:\/\/doi\.org\//, '') : null;
  const L = [`@${type}{${bibKey(pub)},`];
  L.push(`  author    = {${authors}},`);
  L.push(`  title     = {${pub.t || ''}},`);
  if (pub.conf) {
    L.push(`  booktitle = {Proc. ${CONF[pub.v] || pub.v}},`);
    if (pub.loc) L.push(`  address   = {${pub.loc}},`);
  } else if (pub.chapter) {
    L.push(`  booktitle = {${pub.v}},`);
    L.push(`  chapter   = {${pub.chapter}},`);
  } else {
    L.push(`  journal   = {${pub.v || ''}},`);
  }
  if (pub.vol) L.push(`  volume    = {${pub.vol}},`);
  if (pub.no) L.push(`  number    = {${pub.no}},`);
  if (pub.p) L.push(`  pages     = {${String(pub.p).replace(/–/g, '--')}},`);
  if (pub.y) L.push(`  year      = {${pub.y}},`);
  if (doi) L.push(`  doi       = {${doi}},`);
  if (pub.ip) L.push(`  note      = {in press},`);
  L.push(`  publisher = {${pub.pub || 'IEEE'}},`);
  L.push(`}`);
  return L.join('\n');
}

function AuthorList({ list }) {
  return (
    <span>
      {list.map((au, i) => {
        let sep = '';
        if (i > 0) sep = i === list.length - 1 ? (list.length > 2 ? ', and ' : ' and ') : ', ';
        return (
          <React.Fragment key={i}>
            {sep}
            <span style={{ fontWeight: au.owner ? 'var(--weight-semibold)' : 'var(--weight-light)', color: au.owner ? 'var(--text-primary)' : undefined }}>{au.name}</span>
          </React.Fragment>
        );
      })}
    </span>
  );
}

function Citation({ pub, index, onBib }) {
  const isConf = pub.conf;
  const topics = topicsFor(pub);
  const venue = venueTag(pub);
  const quartile = quartileFor(pub);
  const flag = flagFor(pub);
  return (
    <li style={{ listStyle: 'none', display: 'flex', gap: '0.85rem' }}>
      <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-sm)', color: 'var(--text-muted)', flexShrink: 0, paddingTop: '0.1rem', minWidth: '2rem', textAlign: 'right' }}>
        {index}.
      </span>
      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '0.45rem' }}>
        <p style={{ margin: 0, fontSize: 'var(--text-base)', lineHeight: 'var(--leading-snug)', color: 'var(--text-body)', fontWeight: 'var(--weight-light)', textWrap: 'pretty' }}>
          <AuthorList list={pub.a} />
          {', \u201C'}
          <span style={{ color: 'var(--text-primary)' }}>{pub.t}</span>
          {',\u201D '}
          {isConf ? (pub.full
            ? <span>in <em>Proc. {pub.v}</em>{pub.p ? <span>, pp. {pub.p}</span> : null}.</span>
            : <span>in <em>Proc. {CONF[pub.v] || pub.v}</em>{pub.loc ? <span>, {pub.loc}</span> : null}, {pub.y}{pub.ip ? <span> <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-sm)', color: 'var(--text-muted)' }}>[in press]</span></span> : (pub.p ? <span>, pp. {pub.p}</span> : null)}.</span>)
            : pub.chapter ? <span>in <em>{pub.v}</em>, ch. {pub.chapter}{pub.pub ? <span>. {pub.pub}</span> : null}{pub.p ? <span>, pp. {pub.p}</span> : null}, {pub.y}.</span>
            : pub.ip ? <span><em>{pub.v}</em>, <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-sm)', color: 'var(--text-muted)' }}>[in press]</span>.</span>
            : <span><em>{pub.v}</em>{pub.vol ? <span>, vol. {pub.vol}</span> : null}{pub.no ? <span>, no. {pub.no}</span> : null}{pub.p ? <span>, pp. {pub.p}</span> : null}, {pub.y}.</span>}
          {flag ? <span style={{ marginLeft: '0.4rem', fontSize: '1.05em', verticalAlign: 'baseline' }}>{flag}</span> : null}
        </p>
        <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '0.5rem', marginTop: '0.1rem' }}>
          {pub.u ? (
            <PV.Button variant="outline" size="sm" href={pub.u} target="_blank" aria-label="DOI link"
              style={{ height: '1.85rem', padding: '0 0.6rem', fontSize: 'var(--text-xs)', fontFamily: 'var(--font-mono)', letterSpacing: '0.03em' }}>
              <i className="ri-external-link-line" style={{ fontSize: 14 }} /> DOI
            </PV.Button>
          ) : null}
          <PV.Button variant="outline" size="sm" onClick={() => onBib(pub)} aria-label="Extract BibTeX"
            style={{ height: '1.85rem', padding: '0 0.6rem', fontSize: 'var(--text-xs)', fontFamily: 'var(--font-mono)', letterSpacing: '0.03em' }}>
            <i className="ri-braces-line" style={{ fontSize: 14 }} /> BibTeX
          </PV.Button>
          <span style={{ display: 'inline-flex', flexWrap: 'wrap', alignItems: 'center', gap: '0.35rem', marginLeft: '0.25rem' }}>
            <VenueTag info={venue} />
            {quartile ? <QuartileTag q={quartile} /> : null}
            {topics.map((t) => <TopicTag key={t.label} topic={t} />)}
          </span>
        </div>
      </div>
    </li>
  );
}

function groupByYear(list) {
  const groups = {};
  list.forEach((p) => { const y = p.y || 'Forthcoming'; (groups[y] = groups[y] || []).push(p); });
  return Object.keys(groups)
    .sort((a, b) => (a === 'Forthcoming' ? -1 : b === 'Forthcoming' ? 1 : Number(b) - Number(a)))
    .map((y) => ({ year: y, items: groups[y] }));
}

// ============================================================================
//  Publication analytics
// ============================================================================

// >>> EDIT THESE WITH YOUR EXACT GOOGLE SCHOLAR FIGURES <<<
// Source: https://scholar.google.com/citations?user=Xp0rnxQAAAAJ
// `citationsByYear` is the "Cited by" histogram; h-index / i10-index are the
// current all-time values shown in the metrics box.
const SCHOLAR_METRICS = {
  hIndex: 38,
  i10Index: 81,
  totalCitations: 4700,
  asOf: 'Jun 22, 2026',
  placeholder: false, // set to false once you've entered your real numbers
  profileUrl: 'https://scholar.google.com/citations?user=Xp0rnxQAAAAJ&hl=en',
  citationsByYear: [
    { year: 2017, c: 32 },
    { year: 2018, c: 104 },
    { year: 2019, c: 204 },
    { year: 2020, c: 295 },
    { year: 2021, c: 366 },
    { year: 2022, c: 517 },
    { year: 2023, c: 579 },
    { year: 2024, c: 804 },
    { year: 2025, c: 1167 },
    { year: 2026, c: 550 },
  ],
};

// Live chart accent — tracks the --accent token so the accent tweak recolors charts/map.
function chartAccent() {
  try { const v = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim(); if (v) return v; } catch (e) {}
  return '#2563eb';
}

function tallyVenues(P) {
  const all = [...(P.inPress || []), ...(P.articles || []), ...(P.proceedings || []), ...(P.bookChapters || [])];
  const counts = {};
  all.forEach((p) => {
    const v = venueTag(p);
    if (!counts[v.short]) counts[v.short] = { short: v.short, full: v.full, n: 0 };
    counts[v.short].n += 1;
  });
  return Object.values(counts).sort((a, b) => b.n - a.n);
}

function tallyTopics(P) {
  const all = [...(P.inPress || []), ...(P.articles || []), ...(P.proceedings || []), ...(P.bookChapters || [])];
  const counts = {};
  TOPICS.forEach((t) => { counts[t.label] = { label: t.label, color: t.color, n: 0 }; });
  all.forEach((p) => { topicsFor(p).forEach((t) => { counts[t.label].n += 1; }); });
  return Object.values(counts).filter((t) => t.n > 0).sort((a, b) => b.n - a.n);
}

// Journals & magazines only (conferences and book chapters excluded).
function tallyJournalVenues(P) {
  const all = [...(P.inPress || []), ...(P.articles || [])];
  const counts = {};
  all.forEach((p) => {
    if (p.conf || p.chapter) return;
    const v = venueTag(p);
    counts[v.short] = counts[v.short] || { short: v.short, full: v.full, n: 0 };
    counts[v.short].n += 1;
  });
  return Object.values(counts).sort((a, b) => b.n - a.n);
}

// --- Conference geography: city gazetteer (lat/lon + ISO3) for the world map ---
const CITIES = {
  'Glasgow': { lat: 55.8642, lon: -4.2518, iso: 'GBR', country: 'United Kingdom' },
  'Pacific Grove': { lat: 36.6177, lon: -121.9166, iso: 'USA', country: 'United States' },
  'Montreal': { lat: 45.5019, lon: -73.5674, iso: 'CAN', country: 'Canada' },
  'Istanbul': { lat: 41.0082, lon: 28.9784, iso: 'TUR', country: 'T\u00fcrkiye' },
  'Orlando': { lat: 28.5383, lon: -81.3792, iso: 'USA', country: 'United States' },
  'Cape Town': { lat: -33.9249, lon: 18.4241, iso: 'ZAF', country: 'South Africa' },
  'Nara': { lat: 34.6851, lon: 135.8048, iso: 'JPN', country: 'Japan' },
  'Corvallis': { lat: 44.5646, lon: -123.262, iso: 'USA', country: 'United States' },
  'Valencia': { lat: 39.4699, lon: -0.3763, iso: 'ESP', country: 'Spain' },
  'Lucca': { lat: 43.8430, lon: 10.5079, iso: 'ITA', country: 'Italy' },
  'Dubai': { lat: 25.2048, lon: 55.2708, iso: 'ARE', country: 'United Arab Emirates' },
  'Las Vegas': { lat: 36.1699, lon: -115.1398, iso: 'USA', country: 'United States' },
  'Kuala Lumpur': { lat: 3.1390, lon: 101.6869, iso: 'MYS', country: 'Malaysia' },
  'Rome': { lat: 41.9028, lon: 12.4964, iso: 'ITA', country: 'Italy' },
  'Monterey': { lat: 36.6002, lon: -121.8947, iso: 'USA', country: 'United States' },
  'Tempe': { lat: 33.4255, lon: -111.9400, iso: 'USA', country: 'United States' },
  'Ioannina': { lat: 39.6650, lon: 20.8537, iso: 'GRC', country: 'Greece' },
  'Seoul': { lat: 37.5665, lon: 126.9780, iso: 'KOR', country: 'South Korea' },
  'Helsinki': { lat: 60.1699, lon: 24.9384, iso: 'FIN', country: 'Finland' },
  'London': { lat: 51.5074, lon: -0.1278, iso: 'GBR', country: 'United Kingdom' },
  'Madrid': { lat: 40.4168, lon: -3.7038, iso: 'ESP', country: 'Spain' },
  'Jeju': { lat: 33.4996, lon: 126.5312, iso: 'KOR', country: 'South Korea' },
  'Marrakesh': { lat: 31.6295, lon: -7.9811, iso: 'MAR', country: 'Morocco' },
  'Abu Dhabi': { lat: 24.4539, lon: 54.3773, iso: 'ARE', country: 'United Arab Emirates' },
  'Calgary': { lat: 51.0447, lon: -114.0719, iso: 'CAN', country: 'Canada' },
  'Kansas City': { lat: 39.0997, lon: -94.5786, iso: 'USA', country: 'United States' },
  'Kalamata': { lat: 37.0366, lon: 22.1144, iso: 'GRC', country: 'Greece' },
  'Barcelona': { lat: 41.3851, lon: 2.1734, iso: 'ESP', country: 'Spain' },
  'Singapore': { lat: 1.3521, lon: 103.8198, iso: 'SGP', country: 'Singapore' },
  'Paris': { lat: 48.8566, lon: 2.3522, iso: 'FRA', country: 'France' },
  'Toronto': { lat: 43.6532, lon: -79.3832, iso: 'CAN', country: 'Canada' },
  'Washington': { lat: 38.9072, lon: -77.0369, iso: 'USA', country: 'United States' },
  'Garden Grove': { lat: 33.7739, lon: -117.9414, iso: 'USA', country: 'United States' },
  'Austin': { lat: 30.2672, lon: -97.7431, iso: 'USA', country: 'United States' },
  'Ottawa': { lat: 45.4215, lon: -75.6972, iso: 'CAN', country: 'Canada' },
  'Stockholm': { lat: 59.3293, lon: 18.0686, iso: 'SWE', country: 'Sweden' },
  'Gothenburg': { lat: 57.7089, lon: 11.9746, iso: 'SWE', country: 'Sweden' },
  'Bologna': { lat: 44.4949, lon: 11.3426, iso: 'ITA', country: 'Italy' },
  'Padova': { lat: 45.4064, lon: 11.8768, iso: 'ITA', country: 'Italy' },
  'Santa Cruz': { lat: 36.9741, lon: -122.0308, iso: 'USA', country: 'United States' },
  'Irvine': { lat: 33.6846, lon: -117.8265, iso: 'USA', country: 'United States' },
  'Ankara': { lat: 39.9334, lon: 32.8597, iso: 'TUR', country: 'T\u00fcrkiye' },
  'Jeddah': { lat: 21.4858, lon: 39.1925, iso: 'SAU', country: 'Saudi Arabia' },
  'Dammam': { lat: 26.4207, lon: 50.0888, iso: 'SAU', country: 'Saudi Arabia' },
  'Ames': { lat: 42.0308, lon: -93.6319, iso: 'USA', country: 'United States' },
  'Macau': { lat: 22.1987, lon: 113.5439, iso: 'CHN', country: 'China' },
  'Alexandria': { lat: 31.2001, lon: 29.9187, iso: 'EGY', country: 'Egypt' },
  'Manchester': { lat: 53.4808, lon: -2.2426, iso: 'GBR', country: 'United Kingdom' },
  'Southampton': { lat: 50.9097, lon: -1.4044, iso: 'GBR', country: 'United Kingdom' },
  'Guildford': { lat: 51.2362, lon: -0.5704, iso: 'GBR', country: 'United Kingdom' },
  'Astana': { lat: 51.1605, lon: 71.4704, iso: 'KAZ', country: 'Kazakhstan' },
  'Rio de Janeiro': { lat: -22.9068, lon: -43.1729, iso: 'BRA', country: 'Brazil' },
  'Cairo': { lat: 30.0444, lon: 31.2357, iso: 'EGY', country: 'Egypt' },
  'San Antonio': { lat: 29.4241, lon: -98.4936, iso: 'USA', country: 'United States' },
  'San Francisco': { lat: 37.7749, lon: -122.4194, iso: 'USA', country: 'United States' },
};

function resolveCity(pub) {
  const hay = ((pub.v || '') + ' ' + (pub.loc || '')).toLowerCase();
  for (const key of Object.keys(CITIES)) { if (hay.includes(key.toLowerCase())) return Object.assign({ key }, CITIES[key]); }
  return null;
}

// Extra map locations that aren't tied to a talk/paper entry (e.g. award trips).
// Shown as pins + country fill on BOTH the conference and dissemination maps.
const EXTRA_PINS = [
  { key: 'Rio de Janeiro', n: 1 },
];

function mergeExtraGeo(geo) {
  const countries = geo.countries.map((c) => ({ ...c }));
  const cities = geo.cities.map((c) => ({ ...c }));
  const isoIndex = {}; countries.forEach((c) => { isoIndex[c.iso] = c; });
  const cityIndex = {}; cities.forEach((c) => { cityIndex[c.key] = c; });
  EXTRA_PINS.forEach((e) => {
    const loc = CITIES[e.key]; if (!loc) return;
    const n = e.n || 1;
    if (isoIndex[loc.iso]) { isoIndex[loc.iso].n += n; }
    else { const o = { iso: loc.iso, name: loc.country, n }; isoIndex[loc.iso] = o; countries.push(o); }
    if (cityIndex[e.key]) { cityIndex[e.key].n += n; }
    else { const o = { key: e.key, lon: loc.lon, lat: loc.lat, country: loc.country, n }; cityIndex[e.key] = o; cities.push(o); }
  });
  return { countries: countries.sort((a, b) => b.n - a.n), cities };
}

function tallyConfGeo(P) {
  const counts = {}; const cities = {};
  (P.proceedings || []).forEach((p) => {
    const c = resolveCity(p); if (!c) return;
    counts[c.iso] = counts[c.iso] || { iso: c.iso, name: c.country, n: 0 }; counts[c.iso].n += 1;
    cities[c.key] = cities[c.key] || { key: c.key, lon: c.lon, lat: c.lat, country: c.country, n: 0 }; cities[c.key].n += 1;
  });
  return mergeExtraGeo({ countries: Object.values(counts), cities: Object.values(cities) });
}

const SERIES = [
  [/globecom/i, 'GLOBECOM'], [/\bICC\b|International Conference on Communications/i, 'ICC'],
  [/PIMRC|Personal, Indoor and Mobile/i, 'PIMRC'], [/WCNC|Wireless Communications and Networking/i, 'WCNC'],
  [/SPAWC|Signal Processing Advances/i, 'SPAWC'], [/asilomar|ACSSC/i, 'ASILOMAR'],
  [/Vehicular Technology|\bVTC\b/i, 'VTC'], [/Medicine and Biology|EMBC/i, 'EMBC'],
  [/E-?health|HealthCom/i, 'HealthCom'], [/Body Sensor|\bBSN\b/i, 'BSN'],
  [/Consumer Communications|CCNC/i, 'CCNC'], [/Midwest|MWSCAS/i, 'MWSCAS'],
  [/ISCAS|Symposium on Circuits/i, 'ISCAS'], [/ICASSP|Acoustics, Speech/i, 'ICASSP'],
  [/ICT Convergence|ICTC/i, 'ICTC'], [/Computing, Networking|ICNC/i, 'ICNC'],
  [/ICCSPA/i, 'ICCSPA'], [/Sensor Array|\bSAM\b/i, 'SAM'],
  [/Middle East Conf|MECOM/i, 'MECOM'], [/Biomedical Circuits|BIOCAS|BioCAS/i, 'BIOCAS'],
  [/Radar Conf|RadarConf/i, 'RadarConf'], [/\bSPIE\b|Photonics West/i, 'SPIE'],
  [/Biomedical and Healthcare|\bBHI\b/i, 'BHI'],
];

function seriesFor(pub) {
  const hay = (pub.v || '') + ' ' + (pub.loc || '');
  for (const [re, s] of SERIES) { if (re.test(hay)) return s; }
  return (pub.v || 'Other').split(',')[0].slice(0, 16);
}

function tallyConfSeries(P) {
  const counts = {};
  (P.proceedings || []).forEach((p) => { const s = seriesFor(p); counts[s] = counts[s] || { key: s, n: 0 }; counts[s].n += 1; });
  return Object.values(counts).sort((a, b) => b.n - a.n);
}

function WorldMap({ countries, cities, title, hint }) {
  const [geo, setGeo] = React.useState(null);
  const [failed, setFailed] = React.useState(false);
  React.useEffect(() => {
    const CK = 'wintel_world_geo_v1';
    try { const c = localStorage.getItem(CK); if (c) { setGeo(JSON.parse(c)); return; } } catch (e) {}
    fetch('https://cdn.jsdelivr.net/gh/johan/world.geo.json@master/countries.geo.json')
      .then((r) => r.ok ? r.json() : Promise.reject())
      .then((g) => { setGeo(g); try { localStorage.setItem(CK, JSON.stringify(g)); } catch (e) {} })
      .catch(() => setFailed(true));
  }, []);
  const countByIso = {}; countries.forEach((c) => { countByIso[c.iso] = c.n; });
  const max = Math.max(1, ...countries.map((c) => c.n));
  const proj = (lon, lat) => [lon + 180, 90 - lat];
  const pathFor = (geom) => {
    const polys = geom.type === 'Polygon' ? [geom.coordinates] : geom.coordinates;
    let d = '';
    polys.forEach((poly) => poly.forEach((ring) => {
      ring.forEach((pt, i) => { const xy = proj(pt[0], pt[1]); d += (i === 0 ? 'M' : 'L') + xy[0].toFixed(2) + ' ' + xy[1].toFixed(2); });
      d += 'Z';
    }));
    return d;
  };

  // --- pan & zoom ---
  const BASE = { x: 0, y: 16, w: 360, h: 124 };
  const clampVB = (x, y, w, h) => ({ x: Math.max(0, Math.min(360 - w, x)), y: Math.max(0, Math.min(180 - h, y)), w, h });
  const [vb, setVb] = React.useState(BASE);
  const [dragging, setDragging] = React.useState(false);
  const svgRef = React.useRef(null);
  const vbRef = React.useRef(vb); vbRef.current = vb;
  const dragRef = React.useRef(null);
  React.useEffect(() => {
    const svg = svgRef.current; if (!svg) return;
    const onWheel = (e) => {
      e.preventDefault();
      const rect = svg.getBoundingClientRect();
      const px = (e.clientX - rect.left) / rect.width, py = (e.clientY - rect.top) / rect.height;
      const v = vbRef.current;
      const factor = e.deltaY < 0 ? 0.85 : 1 / 0.85;
      const nw = Math.max(24, Math.min(360, v.w * factor)); const nh = nw * (BASE.h / BASE.w);
      const cx = v.x + px * v.w, cy = v.y + py * v.h;
      setVb(clampVB(cx - px * nw, cy - py * nh, nw, nh));
    };
    svg.addEventListener('wheel', onWheel, { passive: false });
    return () => svg.removeEventListener('wheel', onWheel);
  }, []);
  const zoomBy = (factor) => setVb((v) => { const nw = Math.max(24, Math.min(360, v.w * factor)); const nh = nw * (BASE.h / BASE.w); const cx = v.x + v.w / 2, cy = v.y + v.h / 2; return clampVB(cx - nw / 2, cy - nh / 2, nw, nh); });
  const onPointerDown = (e) => { dragRef.current = { x: e.clientX, y: e.clientY, vb: vbRef.current, rect: e.currentTarget.getBoundingClientRect() }; setDragging(true); e.currentTarget.setPointerCapture(e.pointerId); };
  const onPointerMove = (e) => { const d = dragRef.current; if (!d) return; const dx = (e.clientX - d.x) / d.rect.width * d.vb.w; const dy = (e.clientY - d.y) / d.rect.height * d.vb.h; setVb(clampVB(d.vb.x - dx, d.vb.y - dy, d.vb.w, d.vb.h)); };
  const onPointerUp = () => { dragRef.current = null; setDragging(false); };
  const vbStr = vb.x.toFixed(2) + ' ' + vb.y.toFixed(2) + ' ' + vb.w.toFixed(2) + ' ' + vb.h.toFixed(2);
  const mapBtn = { width: '1.6rem', height: '1.6rem', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0, borderRadius: 'var(--radius-sm)', border: '1px solid var(--border-default)', background: 'var(--surface-card)', color: 'var(--text-body)', cursor: 'pointer', fontSize: 15, lineHeight: 1 };

  return (
    <ChartPanel title={title || 'Conference footprint'} hint={hint || "scroll to zoom \u00b7 drag to pan \u00b7 fill intensity = papers presented there"}>
      <div style={{ position: 'relative', width: '100%', borderRadius: 'var(--radius-md)', overflow: 'hidden', border: '1px solid var(--border-default)', background: 'var(--surface-muted)' }}>
        <svg ref={svgRef} viewBox={vbStr} onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={onPointerUp} onPointerLeave={onPointerUp}
          style={{ display: 'block', width: '100%', height: 'auto', cursor: dragging ? 'grabbing' : 'grab', touchAction: 'none' }}>
          {!geo ? [30, 60, 90, 120].map((y) => <line key={'g' + y} x1="0" y1={y} x2="360" y2={y} style={{ stroke: 'var(--border-default)', strokeWidth: 0.2 }} />) : null}
          {geo ? geo.features.map((f, i) => {
            const n = countByIso[f.id] || 0;
            return (
              <path key={i} d={pathFor(f.geometry)}
                style={{ fill: n ? chartAccent() : 'var(--surface-card)', fillOpacity: n ? (0.32 + 0.68 * (n / max)) : 1, stroke: 'var(--border-default)', strokeWidth: 0.15, vectorEffect: 'non-scaling-stroke' }}>
                {n ? <title>{f.properties.name + ': ' + n + ' paper' + (n > 1 ? 's' : '')}</title> : null}
              </path>
            );
          }) : null}
          {cities.map((c, i) => { const xy = proj(c.lon, c.lat); const r = (0.9 + Math.sqrt(c.n) * 1.0) * (vb.w / BASE.w); return (
            <circle key={'c' + i} cx={xy[0]} cy={xy[1]} r={r} style={{ fill: '#f59e0b', stroke: '#ffffff', strokeWidth: 0.3, vectorEffect: 'non-scaling-stroke' }}>
              <title>{c.key + ', ' + c.country + ': ' + c.n + ' paper' + (c.n > 1 ? 's' : '')}</title>
            </circle>
          ); })}
        </svg>
        <div style={{ position: 'absolute', right: '0.5rem', bottom: '0.5rem', display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
          <button aria-label="Zoom in" onClick={() => zoomBy(0.7)} style={mapBtn}><i className="ri-add-line" /></button>
          <button aria-label="Zoom out" onClick={() => zoomBy(1 / 0.7)} style={mapBtn}><i className="ri-subtract-line" /></button>
          <button aria-label="Reset view" onClick={() => setVb(BASE)} style={mapBtn}><i className="ri-restart-line" /></button>
        </div>
      </div>
      {failed ? <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: 'var(--text-muted)' }}>Country outlines need a connection; showing city markers only.</span> : null}
    </ChartPanel>
  );
}

function ChartPanel({ title, hint, children }) {
  return (
    <section style={{ display: 'flex', flexDirection: 'column', gap: '1rem', background: 'var(--surface-card)', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-xl)', padding: '1.25rem 1.35rem' }}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: '0.15rem' }}>
        <h3 style={{ margin: 0, fontSize: 'var(--text-base)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-primary)', letterSpacing: 'var(--tracking-tight)' }}>{title}</h3>
        {hint ? <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: 'var(--text-muted)' }}>{hint}</span> : null}
      </div>
      {children}
    </section>
  );
}

function StatBox({ value, label }) {
  return (
    <div style={{ flex: 1, minWidth: '5.5rem', display: 'flex', flexDirection: 'column', gap: '0.1rem', padding: '0.6rem 0.8rem', background: 'var(--surface-muted)', borderRadius: 'var(--radius-md)', border: '1px solid var(--border-default)' }}>
      <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-2xl)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-primary)', lineHeight: 1.05 }}>{value}</span>
      <span style={{ fontSize: 'var(--text-xs)', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>{label}</span>
    </div>
  );
}

function CitationsChart() {
  const m = SCHOLAR_METRICS;
  const max = Math.max(...m.citationsByYear.map((d) => d.c));
  const fmt = (n) => n >= 1000 ? (n / 1000).toFixed(n >= 10000 ? 0 : 1).replace(/\.0$/, '') + 'k' : String(n);
  return (
    <ChartPanel title="Citations by year" hint={m.placeholder ? 'placeholder data \u2014 replace with Google Scholar figures' : ('Source: Google Scholar' + (m.asOf ? ' as of ' + m.asOf : ''))}>
      <div style={{ display: 'flex', alignItems: 'flex-end', gap: '0.5rem', height: '180px', paddingTop: '0.5rem' }}>
        {m.citationsByYear.map((d) => (
          <div key={d.year} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.35rem', height: '100%', justifyContent: 'flex-end' }}>
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: 'var(--text-muted)' }}>{fmt(d.c)}</span>
            <div title={d.year + ': ' + d.c + ' citations'} style={{ width: '100%', maxWidth: '2.4rem', height: (d.c / max * 100) + '%', minHeight: '3px', background: chartAccent(), borderRadius: '3px 3px 0 0', transition: 'height var(--duration-base) var(--ease-standard)' }} />
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: 'var(--text-muted)' }}>{String(d.year).slice(2)}</span>
          </div>
        ))}
      </div>
      {m.profileUrl ? (
        <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
          <PV.Button variant="outline" size="sm" href={m.profileUrl} target="_blank" aria-label="View Google Scholar profile"
            style={{ height: '1.85rem', padding: '0 0.7rem', fontSize: 'var(--text-xs)' }}>
            <i className="ri-external-link-line" style={{ fontSize: 14 }} /> View on Google Scholar
          </PV.Button>
        </div>
      ) : null}
    </ChartPanel>
  );
}

// Count journal articles (not conf / chapters) by quartile.
function tallyQuartiles(P) {
  const all = [...(P.inPress || []), ...(P.articles || [])];
  const counts = { Q1: 0, Q2: 0, Q3: 0, Q4: 0 };
  let unranked = 0;
  all.forEach((p) => {
    if (p.conf || p.chapter) return;
    const q = QUARTILE[p.v];
    if (q && counts[q] !== undefined) counts[q] += 1; else unranked += 1;
  });
  return { counts, unranked };
}

function QuartilePie() {
  const P = window.WINTEL_PUBLICATIONS || {};
  const { counts, unranked } = tallyQuartiles(P);
  const order = ['Q1', 'Q2', 'Q3', 'Q4'];
  const data = order.map((q) => ({ q, n: counts[q], color: QUARTILE_COLOR[q] })).filter((d) => d.n > 0);
  const total = data.reduce((s, d) => s + d.n, 0);
  const cx = 80, cy = 80, R = 62, r = 38;
  let angle = -Math.PI / 2;
  const arcs = data.map((d) => {
    const frac = d.n / total;
    const a0 = angle, a1 = angle + frac * 2 * Math.PI - (data.length > 1 ? 0.015 : 0);
    angle += frac * 2 * Math.PI;
    const large = (a1 - a0) > Math.PI ? 1 : 0;
    const p = (ang, rad) => [cx + rad * Math.cos(ang), cy + rad * Math.sin(ang)];
    const [x0, y0] = p(a0, R), [x1, y1] = p(a1, R), [xi1, yi1] = p(a1, r), [xi0, yi0] = p(a0, r);
    const dPath = `M${x0.toFixed(2)} ${y0.toFixed(2)} A${R} ${R} 0 ${large} 1 ${x1.toFixed(2)} ${y1.toFixed(2)} L${xi1.toFixed(2)} ${yi1.toFixed(2)} A${r} ${r} 0 ${large} 0 ${xi0.toFixed(2)} ${yi0.toFixed(2)} Z`;
    return { ...d, dPath, pct: Math.round(frac * 100) };
  });
  return (
    <ChartPanel title="Journal quartiles" hint={"Scimago / SJR \u00b7 journal articles only"}>
      <div style={{ display: 'flex', alignItems: 'center', gap: '1.25rem', flexWrap: 'wrap' }}>
        <svg viewBox="0 0 160 160" style={{ width: '160px', height: '160px', flexShrink: 0 }}>
          {arcs.length === 0 ? (
            <circle cx={cx} cy={cy} r={R} style={{ fill: 'var(--surface-muted)' }} />
          ) : arcs.map((a) => (
            <path key={a.q} d={a.dPath} style={{ stroke: 'var(--surface-card)', strokeWidth: 1 }} fill={a.color}>
              <title>{a.q + ': ' + a.n + ' (' + a.pct + '%)'}</title>
            </path>
          ))}
          <text x={cx} y={cy - 4} textAnchor="middle" style={{ fontFamily: 'var(--font-mono)', fontSize: '26px', fontWeight: 700, fill: 'var(--text-primary)' }}>{total}</text>
          <text x={cx} y={cy + 16} textAnchor="middle" style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', fill: 'var(--text-muted)', letterSpacing: '0.08em' }}>RANKED</text>
        </svg>
        <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', flex: 1, minWidth: '8rem' }}>
          {data.map((d) => (
            <div key={d.q} style={{ display: 'grid', gridTemplateColumns: '0.8rem 1fr auto', alignItems: 'center', gap: '0.5rem' }}>
              <span style={{ width: '0.8rem', height: '0.8rem', borderRadius: '3px', background: d.color }} />
              <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-sm)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-primary)' }}>{d.q}</span>
              <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>{d.n} · {Math.round(d.n / total * 100)}%</span>
            </div>
          ))}
          {unranked ? <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: 'var(--text-muted)', marginTop: '0.15rem' }}>+ {unranked} unranked / too new</span> : null}
        </div>
      </div>
    </ChartPanel>
  );
}

function HBarChart({ title, hint, rows, total, colorFor }) {
  const max = Math.max(...rows.map((r) => r.n), 1);
  return (
    <ChartPanel title={title} hint={hint}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: '0.55rem' }}>
        {rows.map((r) => (
          <div key={r.key} title={r.full || r.key} style={{ display: 'grid', gridTemplateColumns: '5.5rem 1fr 2rem', alignItems: 'center', gap: '0.6rem' }}>
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: 'var(--text-body)', textAlign: 'right', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.key}</span>
            <div style={{ height: '0.9rem', background: 'var(--surface-muted)', borderRadius: '3px', overflow: 'hidden' }}>
              <div style={{ width: (r.n / max * 100) + '%', height: '100%', background: colorFor(r), borderRadius: '3px', transition: 'width var(--duration-base) var(--ease-standard)' }} />
            </div>
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: 'var(--text-muted)', textAlign: 'right' }}>{r.n}</span>
          </div>
        ))}
      </div>
    </ChartPanel>
  );
}

function PubStatBoxes() {
  const m = SCHOLAR_METRICS;
  const P = window.WINTEL_PUBLICATIONS || {};
  const fmt = (n) => n >= 1000 ? (n / 1000).toFixed(n >= 10000 ? 0 : 1).replace(/\.0$/, '') + 'k' : String(n);
  const journals = (P.inPress || []).length + (P.articles || []).length;
  const proceedings = (P.proceedings || []).length;
  const chapters = (P.bookChapters || []).length;
  const totalPubs = journals + proceedings + chapters;
  return (
    <div style={{ display: 'flex', gap: '0.6rem', flexWrap: 'wrap' }}>
      <StatBox value={fmt(m.totalCitations)} label="Citations" />
      <StatBox value={m.hIndex} label="h-index" />
      <StatBox value={m.i10Index} label="i10-index" />
      <StatBox value={totalPubs} label="Publications" />
      <StatBox value={journals} label="Journals" />
      <StatBox value={proceedings} label="Proceedings" />
    </div>
  );
}

function PubStats() {
  const P = window.WINTEL_PUBLICATIONS;
  const journalVenues = React.useMemo(() => tallyJournalVenues(P).slice(0, 12).map((v) => ({ ...v, key: v.short })), [P]);
  const topics = React.useMemo(() => tallyTopics(P).map((t) => ({ ...t, key: t.label })), [P]);
  const geo = React.useMemo(() => tallyConfGeo(P), [P]);
  const series = React.useMemo(() => tallyConfSeries(P), [P]);
  const countryRows = geo.countries.map((c) => ({ key: c.name, full: c.name, n: c.n }));
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '1rem' }}>
        <CitationsChart />
        <QuartilePie />
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '1rem' }}>
        <HBarChart title="Journal &amp; magazine venues" hint={"top 12 \u00b7 hover for full name"} rows={journalVenues} colorFor={() => chartAccent()} />
        <HBarChart title="Research topics" hint={"multi-tagged \u00b7 by relevance"} rows={topics} colorFor={(r) => r.color} />
      </div>
      <WorldMap countries={geo.countries} cities={geo.cities} />
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '1rem' }}>
        <HBarChart title="Conferences by country" hint={"proceedings papers"} rows={countryRows} colorFor={() => chartAccent()} />
        <HBarChart title="Conference series" hint={"proceedings papers"} rows={series} colorFor={() => chartAccent()} />
      </div>
    </div>
  );
}

function BibModal({ pub, onClose }) {
  const [copied, setCopied] = React.useState(false);
  const text = React.useMemo(() => bibtex(pub), [pub]);
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [onClose]);
  const copy = () => {
    navigator.clipboard.writeText(text).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1600); });
  };
  const download = () => {
    const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = bibKey(pub) + '.bib';
    a.click();
    setTimeout(() => URL.revokeObjectURL(a.href), 1000);
  };
  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 60, background: 'rgba(0,0,0,0.55)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '1.5rem' }}>
      <div onClick={(e) => e.stopPropagation()} style={{ width: '100%', maxWidth: '640px', background: 'var(--surface-card)', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-xl)', boxShadow: 'var(--shadow-lg, 0 20px 50px rgba(0,0,0,0.3))', display: 'flex', flexDirection: 'column', maxHeight: '80vh', overflow: 'hidden' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '1rem', padding: '1rem 1.25rem', borderBottom: '1px solid var(--border-default)' }}>
          <h3 style={{ margin: 0, fontSize: 'var(--text-base)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-primary)' }}>BibTeX record</h3>
          <PV.Button variant="ghost" size="icon" aria-label="Close" onClick={onClose}>
            <i className="ri-close-line" style={{ fontSize: 18 }} />
          </PV.Button>
        </div>
        <pre style={{ margin: 0, padding: '1.25rem', overflow: 'auto', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-relaxed)', color: 'var(--text-body)', background: 'var(--surface-muted)', whiteSpace: 'pre', flex: 1 }}>{text}</pre>
        <div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end', padding: '1rem 1.25rem', borderTop: '1px solid var(--border-default)' }}>
          <PV.Button variant="outline" size="sm" onClick={download}>
            <i className="ri-download-line" style={{ fontSize: 14 }} /> Download .bib
          </PV.Button>
          <PV.Button variant="default" size="sm" onClick={copy}>
            <i className={copied ? 'ri-check-line' : 'ri-file-copy-line'} style={{ fontSize: 14 }} /> {copied ? 'Copied' : 'Copy'}
          </PV.Button>
        </div>
      </div>
    </div>
  );
}

function PublicationsView({ data, onNavigate }) {
  const P = window.WINTEL_PUBLICATIONS;
  const tabs = [
    { key: 'articles', label: 'Articles', list: [...(P.inPress || []), ...P.articles] },
    { key: 'proceedings', label: 'Proceedings', list: P.proceedings },
    { key: 'bookChapters', label: 'Book Chapters', list: P.bookChapters },
  ];
  const [active, setActive] = React.useState('articles');
  const [bibPub, setBibPub] = React.useState(null);
  const [showStats, setShowStats] = React.useState(false);
  const current = tabs.find((t) => t.key === active);
  const grouped = groupByYear(current.list);
  let counter = 0;

  return (
    <main style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '1rem', flexWrap: 'wrap' }}>
        <PV.SectionHeading title="Publications" />
        <button onClick={() => setShowStats((v) => !v)} aria-expanded={showStats} aria-label="Toggle analytics"
          style={{ display: 'inline-flex', alignItems: 'center', gap: '0.4rem', padding: '0.4rem 0.75rem', background: showStats ? 'var(--surface-muted)' : 'var(--accent)', border: '1px solid ' + (showStats ? 'var(--border-default)' : 'var(--accent)'), borderRadius: 'var(--radius-full)', cursor: 'pointer', color: showStats ? 'var(--text-primary)' : 'var(--text-inverse, #fff)', font: 'inherit', fontSize: 'var(--text-sm)', fontWeight: 'var(--weight-medium)', whiteSpace: 'nowrap', transition: 'all var(--duration-base) var(--ease-standard)' }}>
          <i className="ri-bar-chart-2-fill" style={{ fontSize: 15 }} />
          <span>{showStats ? 'Hide bibliometrics' : 'Bibliometrics'}</span>
          <i className="ri-arrow-down-s-line dissem-arrow" style={{ fontSize: 16, transform: showStats ? 'rotate(180deg)' : 'none', transition: 'transform var(--duration-base) var(--ease-standard)', animation: showStats ? 'none' : 'dissemBounce 1.4s ease-in-out infinite' }} />
        </button>
      </div>
      <div style={{ borderBottom: '1px solid var(--border-default)', paddingBottom: '1.25rem' }}>
        <PubStatBoxes />
        {showStats ? <PubStats /> : null}
      </div>

      <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', borderBottom: '1px solid var(--border-default)', paddingBottom: '1rem' }}>
        {tabs.map((t) => (
          <PV.Button key={t.key} size="sm" variant={active === t.key ? 'default' : 'outline'} onClick={() => setActive(t.key)}>
            {t.label}
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', opacity: 0.7, marginLeft: '0.1rem' }}>{t.list.length}</span>
          </PV.Button>
        ))}
      </div>

      {current.list.length === 0 ? (
        <div style={{ padding: '2.5rem 1rem', textAlign: 'center', color: 'var(--text-muted)', border: '1px dashed var(--border-default)', borderRadius: 'var(--radius-xl)' }}>
          <p style={{ margin: 0, fontSize: 'var(--text-base)' }}>No {current.label.toLowerCase()} in the provided bibliography.</p>
        </div>
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: '2.25rem' }}>
          {grouped.map((g) => (
            <section key={g.year} style={{ display: 'flex', flexDirection: 'column', gap: '1.1rem' }}>
              <div style={{ display: 'flex', alignItems: 'baseline', gap: '0.75rem', borderBottom: '1px solid var(--border-default)', paddingBottom: '0.4rem' }}>
                <h2 style={{ margin: 0, fontFamily: 'var(--font-mono)', fontSize: 'var(--text-lg)', fontWeight: 'var(--weight-semibold)', letterSpacing: 'var(--tracking-tight)', color: 'var(--text-primary)', whiteSpace: 'nowrap' }}>{g.year === 'Forthcoming' ? 'In Press' : g.year}</h2>
                <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: 'var(--text-muted)' }}>{g.items.length}</span>
              </div>
              <ol style={{ margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: '1.4rem' }}>
                {g.items.map((pub, i) => { counter += 1; return (
                  <Citation key={i} pub={pub} index={counter} onBib={setBibPub} />
                ); })}
              </ol>
            </section>
          ))}
        </div>
      )}

      {bibPub ? <BibModal pub={bibPub} onClose={() => setBibPub(null)} /> : null}
    </main>
  );
}

Object.assign(window, { PublicationsView, topicsFor, TopicTag, flagForText, TOPICS, WorldMap, ChartPanel, HBarChart, StatBox, chartAccent, resolveCity, mergeExtraGeo, venueTag, VenueTag });
