// AtlasGL — map UI with custom amount input, FLAT·GLOBE toggle, country filter panel.
// Mobile-responsive: 2-row top bar on screens < 640px.

var S = {
  root:        { position:'absolute', inset:0, display:'flex', flexDirection:'column' },
  topBar:      { display:'grid', gridTemplateColumns:'1fr auto 1fr', alignItems:'center', padding:'10px 14px', gap:12, borderBottom:'1px solid var(--line-soft)', background:'var(--bg)', position:'relative', zIndex:20, flexShrink:0 },
  topLeft:     { display:'flex', alignItems:'center', gap:10 },
  topCenter:   { display:'flex', justifyContent:'center' },
  topRight:    { display:'flex', alignItems:'center', gap:8, justifyContent:'flex-end', flexWrap:'wrap' },
  iconBtn:     { width:30, height:30, display:'grid', placeItems:'center', background:'transparent', border:'1px solid var(--line-soft)', color:'var(--fg-dim)', cursor:'pointer', flexShrink:0 },
  miniBtn:     { width:24, height:24, display:'grid', placeItems:'center', background:'transparent', border:'1px solid var(--line-soft)', color:'var(--fg-dim)', cursor:'pointer', flexShrink:0 },
  brandMini:   { display:'flex', alignItems:'center', gap:8, fontFamily:"'JetBrains Mono', monospace", fontSize:11, letterSpacing:'0.16em', color:'var(--fg)', whiteSpace:'nowrap' },
  divider:     { width:1, height:16, background:'var(--line-soft)', flexShrink:0 },
  liveTag:     { display:'flex', alignItems:'center', gap:7, fontFamily:"'JetBrains Mono', monospace", fontSize:10, letterSpacing:'0.12em', color:'var(--fg-dim)', whiteSpace:'nowrap' },
  liveDot:     { width:6, height:6, borderRadius:99, background:'var(--accent)', flexShrink:0 },
  searchBox:   { display:'flex', alignItems:'center', gap:8, width:360, maxWidth:'38vw', padding:'7px 10px', border:'1px solid var(--line-soft)', background:'var(--bg-elev)' },
  searchInput: { flex:1, background:'transparent', border:'none', outline:'none', color:'var(--fg)', fontSize:12, fontFamily:'inherit' },
  clearBtn:    { background:'transparent', border:'none', color:'var(--fg-dim)', cursor:'pointer', fontSize:16, lineHeight:1, padding:0, width:16, height:16 },
  amountGroup: { display:'flex', alignItems:'stretch', border:'1px solid var(--line-soft)' },
  amountInput: { width:54, padding:'5px 6px', background:'var(--bg-elev)', border:'none', outline:'none', color:'var(--fg)', fontFamily:"'JetBrains Mono', monospace", fontSize:12, textAlign:'right' },
  amountX:     { padding:'5px 7px 5px 2px', background:'var(--bg-elev)', fontFamily:"'JetBrains Mono', monospace", fontSize:11, color:'var(--fg-faint)', display:'flex', alignItems:'center' },
  baseBtn:     { display:'flex', alignItems:'center', gap:7, padding:'0 10px', background:'var(--accent)', color:'oklch(0.18 0.01 250)', border:'none', cursor:'pointer', fontFamily:"'JetBrains Mono', monospace", height:30 },
  baseBtnLabel:  { fontSize:8, letterSpacing:'0.16em', opacity:0.7 },
  baseBtnFlag:   { fontSize:13, lineHeight:1 },
  baseBtnCode:   { fontSize:12, fontWeight:600, letterSpacing:'0.05em' },
  baseBtnSymbol: { fontSize:12, fontFamily:"'Instrument Serif', serif" },
  modeToggle:  { display:'flex', border:'1px solid var(--line-soft)', overflow:'hidden', height:30 },
  mapWrap:     { position:'relative', flex:1, overflow:'hidden', background:'var(--ocean)' },
  chip:        { position:'absolute', left:0, top:0, padding:'5px 8px', display:'flex', flexDirection:'column', alignItems:'flex-start', minWidth:56, cursor:'pointer', transition:'opacity 220ms ease', touchAction:'manipulation' },
  chipMobile:  { position:'absolute', left:0, top:0, padding:'6px 9px', display:'flex', flexDirection:'column', alignItems:'flex-start', minWidth:64, cursor:'pointer', transition:'opacity 220ms ease', touchAction:'manipulation' },
  chipCode:    { fontFamily:"'JetBrains Mono', monospace", fontSize:9, letterSpacing:'0.12em', color:'var(--fg-dim)' },
  chipCodeM:   { fontFamily:"'JetBrains Mono', monospace", fontSize:10, letterSpacing:'0.1em', color:'var(--fg-dim)' },
  chipVal:     { fontFamily:"'JetBrains Mono', monospace", fontSize:13, fontWeight:500, fontVariantNumeric:'tabular-nums', transition:'color 600ms ease' },
  chipValM:    { fontFamily:"'JetBrains Mono', monospace", fontSize:13, fontWeight:500, fontVariantNumeric:'tabular-nums', transition:'color 600ms ease' },
  chipBase:    { position:'absolute', top:-11, left:'50%', transform:'translateX(-50%)', padding:'2px 6px 1px', background:'var(--accent)', color:'oklch(0.18 0.01 250)', fontSize:8, letterSpacing:'0.2em', fontFamily:"'JetBrains Mono', monospace", fontWeight:700, lineHeight:1, boxShadow:'0 2px 6px rgba(0,0,0,0.5)', zIndex:4, whiteSpace:'nowrap' },
  detail:      { position:'absolute', width:268, background:'var(--bg-elev)', border:'1px solid var(--line)', padding:14, zIndex:10, boxShadow:'0 18px 50px rgba(0,0,0,0.5)', pointerEvents:'none' },
  detailHeader:{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', paddingBottom:10, borderBottom:'1px solid var(--line-soft)' },
  detailCountry:{ fontSize:14, fontWeight:500, color:'var(--fg)', display:'flex', alignItems:'center' },
  detailCcy:   { fontFamily:"'JetBrains Mono', monospace", fontSize:9, color:'var(--fg-faint)', letterSpacing:'0.1em', marginTop:3 },
  detailSymbol:{ fontFamily:"'Instrument Serif', serif", fontSize:28, lineHeight:1, color:'var(--accent)' },
  detailBig:   { padding:'12px 0 10px' },
  detailBigUnit:{ fontFamily:"'JetBrains Mono', monospace", fontSize:9, letterSpacing:'0.14em', color:'var(--fg-faint)', marginBottom:3 },
  detailBigVal:{ fontFamily:"'Instrument Serif', serif", fontSize:30, fontWeight:400, letterSpacing:'-0.01em', fontVariantNumeric:'tabular-nums', color:'var(--fg)' },
  detailRow:   { display:'flex', justifyContent:'space-between', padding:'5px 0', fontSize:11, color:'var(--fg-dim)', borderTop:'1px solid var(--line-soft)' },
  mono:        { fontFamily:"'JetBrains Mono', monospace", color:'var(--fg)' },
  sparkWrap:   { marginTop:10, paddingTop:8, borderTop:'1px solid var(--line-soft)' },
  sparkLegend: { display:'flex', justifyContent:'space-between', marginTop:4, fontFamily:"'JetBrains Mono', monospace", fontSize:9, letterSpacing:'0.1em', color:'var(--fg-faint)' },
  detailFooter:{ marginTop:10, paddingTop:8, borderTop:'1px solid var(--line-soft)', fontFamily:"'JetBrains Mono', monospace", fontSize:8, letterSpacing:'0.1em', color:'var(--fg-faint)', textTransform:'uppercase' },
  legend:      { position:'absolute', left:14, bottom:48, padding:12, background:'var(--bg-elev)', border:'1px solid var(--line-soft)', display:'flex', flexDirection:'column', gap:5 },
  legendTitle: { fontFamily:"'JetBrains Mono', monospace", fontSize:8, letterSpacing:'0.18em', color:'var(--fg-faint)', marginBottom:3 },
  legendRow:   { display:'flex', alignItems:'center', gap:7, fontSize:10, color:'var(--fg-dim)' },
  legendDot:   { width:7, height:7, borderRadius:99 },
  legendHint:  { marginTop:5, fontFamily:"'JetBrains Mono', monospace", fontSize:8, letterSpacing:'0.08em', color:'var(--fg-faint)' },
  zoomPanel:   { position:'absolute', right:14, bottom:48, display:'flex', flexDirection:'column', background:'var(--bg-elev)', border:'1px solid var(--line-soft)', overflow:'hidden' },
  zoomBtn:     { width:32, height:32, display:'grid', placeItems:'center', background:'transparent', border:'none', cursor:'pointer', color:'var(--fg)', fontSize:15, fontFamily:"'JetBrains Mono', monospace", borderBottom:'1px solid var(--line-soft)' },
  zoomDivider: { height:3, background:'var(--bg)' },
  ticker:      { position:'absolute', left:0, right:0, bottom:0, display:'flex', alignItems:'center', gap:14, padding:'6px 14px', paddingBottom:'max(6px, env(safe-area-inset-bottom, 6px))', background:'var(--bg)', borderTop:'1px solid var(--line-soft)', height:32, overflow:'hidden', zIndex:5 },
  tickerLabel: { fontFamily:"'JetBrains Mono', monospace", fontSize:9, letterSpacing:'0.18em', color:'var(--accent)', flexShrink:0 },
  tickerTrack: { flex:1, overflow:'hidden', position:'relative' },
  tickerScroll:{ display:'flex', gap:32, whiteSpace:'nowrap', animation:'tickerScroll 60s linear infinite' },
  tickerItem:  { display:'inline-flex', alignItems:'center', gap:9, fontFamily:"'JetBrains Mono', monospace", fontSize:10 },
  tickerCode:  { color:'var(--fg-faint)', letterSpacing:'0.1em' },
  tickerVal:   { color:'var(--fg)', fontVariantNumeric:'tabular-nums' },
  loadingOverlay:{ position:'absolute', inset:0, display:'grid', placeItems:'center', zIndex:40, background:'color-mix(in oklch, var(--bg) 80%, transparent)', backdropFilter:'blur(2px)' },
  loadingSpinner:{ width:24, height:24, border:'2px solid var(--line)', borderTopColor:'var(--accent)', borderRadius:'50%', animation:'spin 900ms linear infinite' },
  loadingText: { marginTop:10, fontFamily:"'JetBrains Mono', monospace", fontSize:9, letterSpacing:'0.18em', color:'var(--fg-dim)' },
  errorBanner: { position:'absolute', top:10, left:'50%', transform:'translateX(-50%)', zIndex:30, padding:'7px 12px', background:'var(--bg-elev)', border:'1px solid var(--down)', color:'var(--fg)', fontSize:11, display:'flex', alignItems:'center', gap:10 },
  errorBtn:    { background:'var(--down)', color:'oklch(0.18 0.01 250)', border:'none', padding:'3px 8px', cursor:'pointer', fontFamily:"'JetBrains Mono', monospace", fontSize:9, letterSpacing:'0.1em' },
};

(function() {
  var s = document.createElement('style');
  s.textContent = '@keyframes pulse{0%{box-shadow:0 0 0 0 var(--accent)}70%{box-shadow:0 0 0 8px transparent}100%{box-shadow:0 0 0 0 transparent}}\n@keyframes baseBreath{0%,100%{box-shadow:0 0 0 0 color-mix(in oklch, var(--accent) 60%, transparent), 0 2px 12px rgba(0,0,0,0.4)}50%{box-shadow:0 0 0 6px color-mix(in oklch, var(--accent) 0%, transparent), 0 2px 16px color-mix(in oklch, var(--accent) 35%, transparent)}}\n@keyframes slideUp{from{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}\n@keyframes tickerScroll{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}\n@keyframes spin{to{transform:rotate(360deg)}}\ninput[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}\ninput[type=number]{-moz-appearance:textfield}\n::selection{background:var(--accent-soft);color:var(--fg)}\ninput::placeholder{color:var(--fg-faint)}\nbutton:hover{filter:brightness(1.08)}';
  document.head.appendChild(s);
}());

function useClock() {
  function fmt() {
    var d = new Date();
    var h = d.getHours().toString().padStart(2,'0');
    var m = d.getMinutes().toString().padStart(2,'0');
    var off = -d.getTimezoneOffset();
    var sign = off >= 0 ? '+' : '-';
    var oh = Math.floor(Math.abs(off)/60);
    var om = Math.abs(off)%60;
    return h+':'+m+' GMT'+sign+oh+(om?':'+String(om).padStart(2,'0'):'');
  }
  var [t, setT] = React.useState(fmt);
  React.useEffect(function(){
    var id = setInterval(function(){ setT(fmt()); }, 30000);
    return function(){ clearInterval(id); };
  }, []);
  return t;
}

function useWindowSize() {
  var [w, setW] = React.useState(function() { return window.innerWidth; });
  React.useEffect(function() {
    function handle() { setW(window.innerWidth); }
    window.addEventListener('resize', handle);
    return function() { window.removeEventListener('resize', handle); };
  }, []);
  return w;
}

function modeBtn(active) {
  return { padding:'4px 10px', background:active?'var(--accent)':'var(--bg-soft)', color:active?'oklch(0.18 0.01 250)':'var(--fg)', border:'none', cursor:'pointer', fontFamily:"'JetBrains Mono', monospace", fontSize:9, letterSpacing:'0.14em', height:'100%', display:'flex', alignItems:'center', gap:5, transition:'background 140ms ease' };
}

// Region mapping by ISO country code — used for preset filters & section headers.
var REGION_MAP = {
  AMERICAS: ['US','CA','MX','CU','BR','AR','CL','CO','PE','VE','UY','BO','PY','EC','GT','CR','PA','DO','JM','TT','HT','HN','NI','SV'],
  EUROPE:   ['GB','IE','IS','FR','DE','IT','ES','PT','NL','BE','LU','AT','CH','SE','NO','DK','FI','PL','CZ','SK','HU','RO','BG','HR','SI','RS','BA','MK','AL','GR','EE','LV','LT','UA','BY','MD','RU','MT','CY','EU'],
  'ASIA-PAC': ['JP','CN','HK','TW','KR','IN','ID','TH','SG','MY','PH','VN','AU','NZ','PK','BD','LK','NP','MM','KH','LA','MN','FJ'],
  AFRICA:   ['ZA','EG','NG','KE','MA','GH','TN','UG','TZ','DZ','ET','RW','BW','ZM','ZW','NA','CI','SN','CM','AO'],
  'MID-EAST': ['AE','SA','IL','QA','KW','BH','OM','JO','LB','IQ','IR','YE','TR','SY'],
};
var MAJORS = ['US','EU','GB','JP','CH','CA','AU','NZ'];
var REGION_ORDER = ['AMERICAS','EUROPE','ASIA-PAC','MID-EAST','AFRICA'];

// Build country → region lookup for section grouping
var _regionLookup = {};
REGION_ORDER.forEach(function(r){ REGION_MAP[r].forEach(function(cc){ _regionLookup[cc] = r; }); });

function FilterPanel({ visibleCountries, setVisibleCountries, onClose, isMobile }) {
  var allCodes = window.COUNTRIES.map(function(c){ return c.code; });
  var [q, setQ] = React.useState('');

  // Group countries by region
  var grouped = React.useMemo(function(){
    var g = {};
    REGION_ORDER.forEach(function(r){ g[r] = []; });
    g.OTHER = [];
    window.COUNTRIES.forEach(function(c){
      var r = _regionLookup[c.code] || 'OTHER';
      g[r].push(c);
    });
    REGION_ORDER.concat(['OTHER']).forEach(function(r){
      g[r].sort(function(a,b){ return a.name.localeCompare(b.name); });
    });
    return g;
  }, []);

  var ql = q.trim().toLowerCase();
  function matchesSearch(c){
    if (!ql) return true;
    return c.name.toLowerCase().includes(ql) || c.code.toLowerCase().includes(ql) || c.ccy.toLowerCase().includes(ql);
  }

  function isOn(code){ return visibleCountries===null || visibleCountries.has(code); }

  function setMany(codes, on){
    var base = visibleCountries===null ? new Set(allCodes) : new Set(visibleCountries);
    codes.forEach(function(c){ if(on) base.add(c); else base.delete(c); });
    setVisibleCountries(base.size===allCodes.length ? null : base);
  }
  function toggle(code){ setMany([code], !isOn(code)); }
  function setExact(codes){
    var s = new Set(codes);
    setVisibleCountries(s.size===allCodes.length ? null : s);
  }

  var visCount = visibleCountries===null ? allCodes.length : visibleCountries.size;

  // Preset chips
  var presets = [
    { id:'all',     label:'ALL',       on: visCount===allCodes.length,
      action: function(){ setVisibleCountries(null); } },
    { id:'none',    label:'NONE',      on: visCount===0,
      action: function(){ setVisibleCountries(new Set()); } },
    { id:'majors',  label:'MAJORS',    on: false,
      action: function(){ setExact(MAJORS); } },
  ].concat(REGION_ORDER.map(function(r){
    return {
      id:r, label:r,
      on: false,
      action: function(){ setExact(REGION_MAP[r].filter(function(c){ return allCodes.indexOf(c)>=0; })); },
    };
  }));

  function RegionSection(r){
    var rows = grouped[r] || [];
    var filtered = rows.filter(matchesSearch);
    if (!filtered.length) return null;
    var onCount = rows.filter(function(c){ return isOn(c.code); }).length;
    var allOn = onCount === rows.length;
    var label = r === 'OTHER' ? 'OTHER' : r;
    return (
      <div key={r} style={{ borderBottom:'1px solid var(--line-soft)' }}>
        <div style={{ display:'flex',alignItems:'center',justifyContent:'space-between',padding:'10px 14px 6px',background:'var(--bg)' }}>
          <span style={{ fontFamily:"'JetBrains Mono', monospace",fontSize:9,letterSpacing:'0.18em',color:'var(--fg-dim)' }}>
            {label} <span style={{ color:'var(--fg-faint)' }}>· {onCount}/{rows.length}</span>
          </span>
          <button onClick={function(){ setMany(rows.map(function(c){return c.code;}), !allOn); }}
            style={{ background:'transparent',border:'1px solid var(--line-soft)',color:allOn?'var(--accent)':'var(--fg-dim)',cursor:'pointer',fontFamily:"'JetBrains Mono', monospace",fontSize:8,letterSpacing:'0.14em',padding:'3px 8px' }}>
            {allOn ? 'HIDE' : 'SHOW'}
          </button>
        </div>
        <div style={{ display:'grid',gridTemplateColumns:'repeat(auto-fill, minmax(74px, 1fr))',gap:6,padding:'4px 10px 10px' }}>
          {filtered.map(function(c){
            var on = isOn(c.code);
            return (
              <button key={c.code} onClick={function(){ toggle(c.code); }}
                title={c.name+' · '+c.ccy}
                style={{
                  display:'flex',flexDirection:'column',alignItems:'center',gap:3,
                  padding:'7px 4px',
                  background: on ? 'var(--accent-soft)' : 'transparent',
                  border: '1px solid ' + (on ? 'var(--accent)' : 'var(--line-soft)'),
                  color: on ? 'var(--fg)' : 'var(--fg-dim)',
                  cursor:'pointer',opacity: on ? 1 : 0.55,
                  transition:'opacity 120ms, background 120ms, border-color 120ms',
                  fontFamily:'inherit',
                }}>
                <window.FlagIcon iso={c.code} size={18} />
                <span style={{ fontFamily:"'JetBrains Mono', monospace",fontSize:9,letterSpacing:'0.08em' }}>{c.ccy}</span>
              </button>
            );
          })}
        </div>
      </div>
    );
  }

  return (
    <div style={{ position:'absolute',top:0,right:0,bottom:0,width:isMobile?'100%':330,zIndex:25,background:'var(--bg-elev)',borderLeft:'1px solid var(--line)',display:'flex',flexDirection:'column',boxShadow:'-10px 0 36px rgba(0,0,0,0.4)' }}>
      <div style={{ padding:'12px 14px',borderBottom:'1px solid var(--line-soft)',display:'flex',justifyContent:'space-between',alignItems:'center',flexShrink:0 }}>
        <span style={{ fontFamily:"'JetBrains Mono', monospace",fontSize:10,letterSpacing:'0.16em',color:'var(--fg)' }}>
          CURRENCIES <span style={{ color:'var(--accent)' }}>{visCount}</span><span style={{ color:'var(--fg-faint)' }}> / {allCodes.length}</span>
        </span>
        <button onClick={onClose} style={{ background:'transparent',border:'none',color:'var(--fg-dim)',cursor:'pointer',fontSize:22,lineHeight:1,padding:0,width:28,height:28 }}>×</button>
      </div>

      {/* Preset chips */}
      <div style={{ padding:'10px 10px',borderBottom:'1px solid var(--line-soft)',flexShrink:0,display:'flex',flexWrap:'wrap',gap:6 }}>
        {presets.map(function(p){
          return (
            <button key={p.id} onClick={p.action}
              style={{
                padding:'5px 9px',
                background: p.on ? 'var(--accent)' : 'transparent',
                border:'1px solid ' + (p.on ? 'var(--accent)' : 'var(--line-soft)'),
                color: p.on ? 'oklch(0.18 0.01 250)' : 'var(--fg-dim)',
                cursor:'pointer',
                fontFamily:"'JetBrains Mono', monospace",fontSize:9,letterSpacing:'0.14em',
                fontWeight: p.on ? 600 : 400,
              }}>{p.label}</button>
          );
        })}
      </div>

      {/* Search */}
      <div style={{ padding:'9px 14px',borderBottom:'1px solid var(--line-soft)',flexShrink:0,display:'flex',alignItems:'center',gap:8 }}>
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ opacity:0.5,flexShrink:0 }}><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
        <input value={q} onChange={function(e){ setQ(e.target.value); }} placeholder="Search country, code, or name…" style={{ flex:1,background:'transparent',border:'none',outline:'none',color:'var(--fg)',fontSize:13,fontFamily:'inherit' }}/>
        {q && <button onClick={function(){ setQ(''); }} style={{ background:'transparent',border:'none',color:'var(--fg-faint)',cursor:'pointer',fontSize:16,lineHeight:1,padding:0 }}>×</button>}
      </div>

      {/* Grouped flag grid */}
      <div style={{ flex:1,overflowY:'auto' }}>
        {REGION_ORDER.map(RegionSection)}
        {RegionSection('OTHER')}
      </div>
    </div>
  );
}

function Ticker({ base, amount, noiseRef }) {
  var [, forceUpdate] = React.useReducer(function(x) { return x+1; }, 0);
  React.useEffect(function() {
    window.__tickerNoiseListener = forceUpdate;
    return function() { window.__tickerNoiseListener = null; };
  }, []);
  var picks = ['EUR','GBP','JPY','CNY','INR','AUD','CAD','CHF','SEK','MXN','BRL','ZAR','TRY','KRW','HKD','SGD'];
  return (
    <div style={S.ticker}>
      <div style={S.tickerLabel}>FX · {amount!==1?amount+'x ':''}{base}</div>
      <div style={S.tickerTrack}>
        <div style={S.tickerScroll}>
          {picks.concat(picks).map(function(code,i){
            var c=window.COUNTRIES.find(function(x){ return x.ccy===code; });
            var n=c&&noiseRef.current[c.code];
            var val=window.RatesService.convert(base,code)*(1+(n?n.v:0))*amount;
            var dir=(n?n.dir:null)||((code.charCodeAt(0)+code.charCodeAt(1))%2===0?'up':'down');
            return (<span key={i} style={S.tickerItem}><span style={S.tickerCode}>{amount!==1?amount+' ':''}{base}/{code}</span><span style={S.tickerVal}>{window.formatRate(val)}</span><span style={{ color:dir==='up'?'var(--up)':'var(--down)',fontSize:9,display:'inline-block',width:9 }}>{dir==='up'?'▲':'▼'}</span></span>);
          })}
        </div>
      </div>
    </div>
  );
}

// ── CountryLabel ─────────────────────────────────────────────────────────────
// Text-only label positioned beneath each country centroid on the globe.
// Same imperative-transform pattern as ChipCard.
var CountryLabel = React.memo(function CountryLabel(props) {
  var c = props.country;
  var domRef = React.useRef(null);
  React.useLayoutEffect(function() {
    var el = domRef.current;
    if (!el) return;
    if (!window.__labelRefs) window.__labelRefs = {};
    el.style.transform = 'translate3d(-9999px,-9999px,0)';
    window.__labelRefs[c.code] = el;
    return function() { delete window.__labelRefs[c.code]; };
  }, []); // eslint-disable-line
  return (
    <div ref={domRef} style={{
      position: 'absolute',
      left: 0, top: 0,
      fontFamily: "'JetBrains Mono', 'SF Mono', monospace",
      fontSize: 10,
      fontWeight: 500,
      letterSpacing: '0.12em',
      color: 'rgba(240, 228, 200, 0.82)',
      textTransform: 'uppercase',
      textShadow: '0 0 3px rgba(0,0,0,0.9), 0 1px 2px rgba(0,0,0,0.7)',
      whiteSpace: 'nowrap',
      pointerEvents: 'none',
      willChange: 'transform',
      userSelect: 'none',
    }}>{c.name}</div>
  );
});

// ── ChipCard ─────────────────────────────────────────────────────────────────
// Separate component so React.useLayoutEffect gives a STABLE ref registration.
// Inline ref callbacks re-fire on every parent re-render (React creates a new
// function reference each time), resetting the imperatively-set transform.
// useLayoutEffect([]) fires exactly once on mount — never clobbers the transform.
var ChipCard = React.memo(function ChipCard(props) {
  var c=props.country, visible=props.visible, isBase=props.isBase;
  var base=props.base, amount=props.amount, noiseRef=props.noiseRef;
  var onEnter=props.onEnter, onLeave=props.onLeave, onClick=props.onClick;
  var isMobile=props.isMobile;

  var domRef   = React.useRef(null);
  var valRef   = React.useRef(null);  // .val text node for direct updates
  var arrowRef = React.useRef(null);  // ▲/▼ for direct color/content updates

  // Register DOM node once on mount — imperative code owns transform from here on
  React.useLayoutEffect(function() {
    var el = domRef.current;
    if (!el) return;
    if (!window.__chipRefs) window.__chipRefs = {};
    el.style.transform = 'translate3d(-9999px,-9999px,0)';
    window.__chipRefs[c.code] = el;
    return function() { delete window.__chipRefs[c.code]; };
  }, []); // eslint-disable-line

  // Imperative noise updater — bypass React, write directly to DOM.
  // Called by the noise generator every 4s for ~12 random chips.
  React.useEffect(function() {
    if (!window.__chipNoiseListeners) window.__chipNoiseListeners = {};
    window.__chipNoiseListeners[c.code] = function() {
      var valEl = valRef.current, arrEl = arrowRef.current, chipEl = domRef.current;
      if (!valEl) return;
      var n = noiseRef.current[c.code];
      var rate = window.RatesService.convert(base, c.ccy);
      var val  = rate * (1 + (n ? n.v : 0)) * amount;
      var dir  = n && n.dir || 'up';
      var isUp = dir === 'up';
      var justTicked = n && (Date.now() - (n.t || 0) < 200);
      valEl.textContent = window.formatRate(val);
      valEl.style.color = (n && (Date.now() - (n.t || 0) < 1200)) ? (isUp ? 'var(--up)' : 'var(--down)') : 'var(--fg)';
      if (arrEl) {
        arrEl.textContent = isUp ? '▲' : '▼';
        arrEl.style.color = isUp ? 'var(--up)' : 'var(--down)';
      }
      if (justTicked && chipEl) {
        chipEl.classList.remove('chip-flash-up', 'chip-flash-down');
        void chipEl.offsetWidth; // reflow to restart animation
        chipEl.classList.add(isUp ? 'chip-flash-up' : 'chip-flash-down');
      }
      setTimeout(function() {
        if (valRef.current) valRef.current.style.color = 'var(--fg)';
      }, 1200);
    };
    return function() { delete window.__chipNoiseListeners[c.code]; };
  }, [base, amount]); // re-bind if base/amount changes (closure captures them)

  // Initial render values (React path runs on base/amount changes)
  var n=noiseRef.current[c.code];
  var rate=window.RatesService.convert(base,c.ccy);
  var val=rate*(1+(n?n.v:0))*amount;
  var rawDir=n?n.dir:null;
  var dir=rawDir||((c.code.charCodeAt(0)+c.code.charCodeAt(1))%2===0?'up':'down');
  var isUp=dir==='up'; var flashed=n&&(Date.now()-(n.t||0)<1200);

  return (
    <div ref={domRef}
      onMouseEnter={onEnter} onMouseLeave={onLeave} onClick={onClick}
      style={{ ...(isMobile?S.chipMobile:S.chip),
        border:'1px solid '+(isBase?'var(--accent)':'var(--line)'),
        background:isBase?'color-mix(in oklch, var(--accent) 35%, var(--bg-elev))':'color-mix(in oklch, var(--bg-elev) 92%, transparent)',
        boxShadow:'0 2px 12px rgba(0,0,0,0.4)',
        animation:isBase?'baseBreath 2.8s ease-in-out infinite':'none',
        opacity:visible?1:0,
        pointerEvents:visible?'auto':'none',
        zIndex:isBase?3:2,
        willChange:'transform',
        contain:isBase?'layout':'layout paint',
      }}>
      <div style={{ ...(isMobile?S.chipCodeM:S.chipCode), display:'flex', alignItems:'center', gap:4 }}>
        <window.FlagIcon iso={c.code} size={isMobile?12:9} />
        <span>{c.ccy}</span>
      </div>
      <div style={{ display:'flex',alignItems:'center',gap:3 }}>
        <div ref={valRef} style={{ ...(isMobile?S.chipValM:S.chipVal),color:flashed?(isUp?'var(--up)':'var(--down)'):'var(--fg)' }}>{window.formatRate(val)}</div>
        {!isBase&&<span ref={arrowRef} style={{ color:isUp?'var(--up)':'var(--down)',fontSize:isMobile?8:9,display:'inline-block',width:isMobile?8:9 }}>{isUp?'▲':'▼'}</span>}
      </div>
      {isBase&&<div style={{ ...S.chipBase, animation:'basePulseBadge 2.8s ease-in-out infinite' }}>BASE</div>}
    </div>
  );
});

function AtlasGL({ base, setBase, onExit, mapMode, setMapMode, amount, setAmount, visibleCountries, setVisibleCountries }) {
  var clock = useClock();
  var [hovered,setHovered]=React.useState(null); var [pinned,setPinned]=React.useState(null);
  var [search,setSearch]=React.useState(''); var [switcherOpen,setSwitcherOpen]=React.useState(false);
  var [filterOpen,setFilterOpen]=React.useState(false); var [draft,setDraft]=React.useState(String(amount));
  var [mobileMenuOpen,setMobileMenuOpen]=React.useState(false);
  var [sendOpen,setSendOpen]=React.useState(false);
  var [sendPrefill,setSendPrefill]=React.useState(null);
  React.useEffect(function(){ setDraft(String(amount)); },[amount]);
  var winW=useWindowSize(); var isMobile=winW<640;
  var [ratesState,setRatesState]=React.useState(window.RatesService.state);
  React.useEffect(function(){ return window.RatesService.subscribe(setRatesState); },[]);
  // Keep loader visible for at least 2.2s so the dot-matrix assembly reads
  var [loaderMinDone, setLoaderMinDone] = React.useState(false);
  var [justRefreshed, setJustRefreshed] = React.useState(false);
  var [isRefreshing, setIsRefreshing] = React.useState(false);
  React.useEffect(function(){
    var id = setTimeout(function(){ setLoaderMinDone(true); }, 1800);
    return function(){ clearTimeout(id); };
  }, []);
  // No loader on flat → globe switch. Globe builds fast enough that a loader
  // feels worse than a brief black frame. Only the entry loader (home → map) shows.
  var showGlobeLoader = false;
  var showLoader = !loaderMinDone || ratesState.status==='loading' || ratesState.status==='idle' || (ratesState.status==='error' && !ratesState.rates);

  // ── Keyboard shortcuts ───────────────────────────────────────────────────
  // F / G   → switch flat / globe
  // /       → focus the search input
  // Esc     → clear search (if focused in search) or close open overlays
  // Shortcuts are suppressed when typing in inputs (except Esc which is contextual)
  var searchInputRef = React.useRef(null);
  React.useEffect(function(){
    function isTyping(el) {
      if (!el) return false;
      var tag = el.tagName;
      return tag === 'INPUT' || tag === 'TEXTAREA' || el.isContentEditable;
    }
    function onKey(e) {
      var active = document.activeElement;
      var typing = isTyping(active);
      if (e.key === 'Escape') {
        if (typing && active === searchInputRef.current) {
          setSearch('');
          try { active.blur(); } catch(err) {}
          e.preventDefault();
          return;
        }
        if (filterOpen) { setFilterOpen(false); e.preventDefault(); return; }
        if (switcherOpen) { setSwitcherOpen(false); e.preventDefault(); return; }
        if (pinned) { setPinned(null); e.preventDefault(); return; }
        if (search) { setSearch(''); e.preventDefault(); return; }
        return;
      }
      if (typing) return;
      if (e.key === '/' || e.key === 's' || e.key === 'S') {
        if (searchInputRef.current) {
          try { searchInputRef.current.focus(); searchInputRef.current.select(); } catch(err) {}
          e.preventDefault();
        }
        return;
      }
      if (e.key === 'f' || e.key === 'F') { setMapMode('flat'); e.preventDefault(); return; }
      if (e.key === 'g' || e.key === 'G') { setMapMode('globe'); e.preventDefault(); return; }
    }
    window.addEventListener('keydown', onKey);
    return function(){ window.removeEventListener('keydown', onKey); };
  }, [filterOpen, switcherOpen, pinned, search, setMapMode]);

  // ── Rate freshness indicator ─────────────────────────────────────────────
  // Green if <5 min old, amber if 5–30 min, red if older (or error).
  // Tick every 15s so the color updates without waiting for new rates.
  var [freshnessTick, setFreshnessTick] = React.useState(0);
  React.useEffect(function(){
    var id = setInterval(function(){ setFreshnessTick(function(t){ return t+1; }); }, 15000);
    return function(){ clearInterval(id); };
  }, []);
  var freshness = React.useMemo(function(){
    if (ratesState.status === 'loading' || ratesState.status === 'idle') return { color:'var(--accent)', label:'CONNECTING…', age:null };
    if (ratesState.status === 'error') return { color:'var(--down)', label:'OFFLINE', age:null };
    var ageMs = Date.now() - (ratesState.updatedAt || 0);
    var ageMin = Math.floor(ageMs / 60000);
    var color, label;
    function fmtAge(m){
      if (m <= 0) return 'JUST NOW';
      if (m < 60) return m + 'm ago';
      var h = Math.floor(m/60);
      if (h < 24) return h + 'h ago';
      var d = Math.floor(h/24);
      return d + 'd ago';
    }
    if (ratesState.status === 'stale' || ageMin >= 30) { color = 'var(--down)'; label = 'STALE · ' + fmtAge(ageMin); }
    else if (ageMin >= 5) { color = 'var(--warn, #e2a23f)'; label = fmtAge(ageMin); }
    else { color = 'var(--up)'; label = ageMin <= 0 ? 'LIVE' : fmtAge(ageMin); }
    return { color: color, label: label, age: ageMin };
    // eslint-disable-next-line
  }, [ratesState.status, ratesState.updatedAt, freshnessTick]);
  // ── Globe tier listener — re-render when zoom tier changes so new chips fade in
  React.useEffect(function(){
    // Tier listener — called by MapLibre tick when zoom crosses a tier threshold
    // so React re-evaluates chipShouldShow for every chip.
    window.__atlasTierListener = function(){
      setFreshnessTick(function(t){ return t+1; });
    };
    return function(){ window.__atlasTierListener = null; };
  }, []);

  // ── Zoom % indicator — polls camera every 100ms while zoom controls visible
  // Flat: zoom ∈ [0.8, 10] → 0-100%. Globe: altitude ∈ [0.5, 2.5] → 100-0% (inverted)
  var [zoomPct, setZoomPct] = React.useState(0);
  React.useEffect(function(){
    var id = setInterval(function(){
      // Both flat + globe now use MapLibre zoom: [0.8, 10] → 0-100%
      var z = window.__chipZoom || 1.4;
      var zc = Math.max(0.8, Math.min(10, z));
      var pct = Math.round(100 * (zc - 0.8) / (10 - 0.8));
      setZoomPct(function(prev){ return prev === pct ? prev : pct; });
    }, 150);
    return function(){ clearInterval(id); };
  }, [mapMode]);

  var noiseRef=React.useRef({});
  if(Object.keys(noiseRef.current).length===0){
    window.COUNTRIES.forEach(function(c){ var d=(Math.random()-0.5)*0.003; noiseRef.current[c.code]={v:0,dir:d>0?'up':'down',t:0}; });
  }
  React.useEffect(function(){
    var id=setInterval(function(){
      var codes=window.COUNTRIES.map(function(c){ return c.code; });
      var ls=window.__chipNoiseListeners||{};
      for(var i=0;i<12;i++){
        var code=codes[(Math.random()*codes.length)|0];
        var prior=noiseRef.current[code]?noiseRef.current[code].v:0;
        var delta=(Math.random()-0.5)*0.003;
        noiseRef.current[code]={v:prior+delta,dir:delta>0?'up':'down',t:Date.now()};
        if(ls[code]) ls[code]();
      }
      if(window.__tickerNoiseListener) window.__tickerNoiseListener();
    },4000);
    return function(){ clearInterval(id); };
  },[]);
  var sparkRef=React.useRef(null);
  if(!sparkRef.current){ sparkRef.current={}; window.COUNTRIES.forEach(function(c){ var a=[],v=0; for(var i=0;i<24;i++){ v+=(Math.random()-0.5)*0.02; a.push(v); } sparkRef.current[c.code]=a; }); }

  var baseMeta=window.CURRENCIES.find(function(c){ return c.code===base; });
  var mapApi=React.useRef(null);
  var statusLabel = freshness.label;
  var statusColor = freshness.color;
  var filterCount=visibleCountries===null?null:visibleCountries.size;

  // ── Sync globals for imperative chip position loops in map_gl.jsx ─────────
  React.useEffect(function(){
    if(!window.__chipGlobals) window.__chipGlobals={};
    window.__chipGlobals.base    = base;
    window.__chipGlobals.hovered = hovered ? hovered.code : null;
    window.__chipGlobals.search  = search;
    window.__chipGlobals.hasFilter = visibleCountries !== null; // user-explicit filter bypasses zoom tiers
    // Re-run flat DOM positioning immediately after globals update so chips
    // at the new base/hover/search appear in the right place without waiting
    // for the next map render event.
    requestAnimationFrame(function(){
      if(window.__updateFlatDom) window.__updateFlatDom();
    });
    // Invalidate globe's cached key so it re-projects chips this tick
    window.__globeForceKey = (window.__globeForceKey || 0) + 1;
  },[base, hovered, search, mapMode, visibleCountries]);

  // ── Chip visibility — used by React for opacity management ────────────────
  function chipShouldShow(c, isBase, isHovered, searchMatch) {
    var zoom = window.__chipZoom || 1;
    var alwaysSet = window.__chipAlwaysSet;
    // If user has an explicit filter set, show every surviving chip regardless of zoom.
    // (The filter already culled chips they don't want at the render-tree level.)
    if (visibleCountries !== null) return true;
    // Two-tier logic:
    //   desktop: 0-29% majors only → 30%+ all chips (threshold 3.56)
    //   mobile:  0-24% majors only → 25%+ all chips (threshold 3.10)
    var isMobile = typeof window !== 'undefined' && window.innerWidth < 640;
    var showAll = zoom >= (isMobile ? 3.10 : 3.56);
    return showAll || isBase || isHovered || searchMatch ||
      (alwaysSet && alwaysSet.has(c.code));
  }


  // ── Shared control elements (sized for mobile or desktop) ──────────────────
  var amountEl = (
    <div style={S.amountGroup}>
      <input type="number" min="0.01" step="any" value={draft}
        onChange={function(e){ setDraft(e.target.value); }}
        onBlur={function(e){ var v=parseFloat(e.target.value); if(isFinite(v)&&v>0){ setAmount(v); setDraft(String(v)); } else setDraft(String(amount)); }}
        onKeyDown={function(e){ if(e.key==='Enter'){ var v=parseFloat(e.target.value); if(isFinite(v)&&v>0){ setAmount(v); setDraft(String(v)); } } }}
        style={{ ...S.amountInput, width:isMobile?52:54, height:isMobile?34:'auto', fontSize:isMobile?13:12 }} title="Amount"/>
      <span style={S.amountX}>×</span>
    </div>
  );

  var baseEl = (
    <div style={{ position:'relative' }}>
      <button style={{ ...S.baseBtn, height:isMobile?36:30, padding:isMobile?'0 10px':'0 10px', gap:isMobile?5:7 }}
        onClick={function(){ setSwitcherOpen(function(v){ return !v; }); }}>
        {!isMobile&&<span style={S.baseBtnLabel}>BASE</span>}
        <span style={S.baseBtnFlag}>{window.flagEmoji(baseMeta.flag)}</span>
        <span style={{ ...S.baseBtnCode, fontSize:isMobile?12:12 }}>{base}</span>
        {!isMobile&&<span style={S.baseBtnSymbol}>{baseMeta.symbol}</span>}
        <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="m6 9 6 6 6-6"/></svg>
      </button>
      {switcherOpen&&(
        isMobile ? (
          <div style={{ position:'fixed', top:56, left:12, right:12, zIndex:30 }}>
            <window.CurrencyCombobox value={base}
              onChange={function(code){ setBase(code); setSwitcherOpen(false); var c=window.COUNTRIES.find(function(x){ return x.ccy===code; }); if(c&&mapApi.current) mapApi.current.flyTo({center:[c.lon,c.lat],zoom:3,duration:1000}); }}
              onClose={function(){ setSwitcherOpen(false); }}/>
          </div>
        ) : (
          <div style={{ position:'absolute',top:'calc(100% + 6px)',right:0,zIndex:30, width:420, maxWidth:420 }}>
            <window.CurrencyCombobox value={base}
              onChange={function(code){ setBase(code); setSwitcherOpen(false); var c=window.COUNTRIES.find(function(x){ return x.ccy===code; }); if(c&&mapApi.current) mapApi.current.flyTo({center:[c.lon,c.lat],zoom:3,duration:1000}); }}
              onClose={function(){ setSwitcherOpen(false); }}/>
          </div>
        )
      )}
    </div>
  );

  var filterEl = (
    <button style={{ ...S.iconBtn, width:isMobile?36:30, height:isMobile?36:30, border:'1px solid '+(filterOpen?'var(--accent)':'var(--line)'), color:filterOpen?'var(--accent)':'var(--fg)', position:'relative' }}
      onClick={function(){ setFilterOpen(function(v){ return !v; }); }} title="Filter countries">
      <svg width={isMobile?15:13} height={isMobile?15:13} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M22 3H2l8 9.46V19l4 2v-8.54L22 3z"/></svg>
      {filterCount!==null&&filterCount<window.COUNTRIES.length&&(
        <span style={{ position:'absolute',top:-4,right:-4,minWidth:14,height:14,background:'var(--accent)',color:'oklch(0.18 0.01 250)',fontSize:8,fontFamily:"'JetBrains Mono', monospace",display:'grid',placeItems:'center',borderRadius:99,padding:'0 3px' }}>{filterCount}</span>
      )}
    </button>
  );

  var modeEl = isMobile ? (
    /* Mobile: single toggle button — tap to cycle flat ↔ globe */
    <button
      onClick={function(){ setMapMode(mapMode==='flat'?'globe':'flat'); }}
      title={mapMode==='flat'?'Switch to Globe':'Switch to Flat'}
      style={{ ...S.iconBtn, width:36, height:36, borderColor:'var(--accent)', background:mapMode==='globe'?'var(--accent-soft)':'transparent', color:'var(--accent)' }}>
      {mapMode==='flat'
        ? <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><ellipse cx="12" cy="12" rx="4" ry="10"/><line x1="2" y1="12" x2="22" y2="12"/></svg>
        : <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg>}
    </button>
  ) : (
    /* Desktop: two-button toggle */
    <div style={{ ...S.modeToggle, height:30 }}>
      <button onClick={function(){ setMapMode('flat'); }} style={{ ...modeBtn(mapMode==='flat'), padding:'4px 10px' }}>
        <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg>
        FLAT
      </button>
      <button onClick={function(){ setMapMode('globe'); }} style={{ ...modeBtn(mapMode==='globe'), padding:'4px 10px' }}>
        <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><ellipse cx="12" cy="12" rx="4" ry="10"/><line x1="2" y1="12" x2="22" y2="12"/></svg>
        GLOBE
      </button>
    </div>
  );
  // ──────────────────────────────────────────────────────────────────────────

  var sendEl = (
    <button
      onClick={function(){ setSendOpen(true); setMobileMenuOpen(false); }}
      title="Compare remittance providers"
      style={{ ...S.iconBtn, width:isMobile?36:30, height:isMobile?36:30, border:'1px solid '+(sendOpen?'var(--accent)':'var(--line-soft)'), color:sendOpen?'var(--accent)':'var(--fg)' }}>
      <svg width={isMobile?15:13} height={isMobile?15:13} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
        <path d="M22 2L11 13"/>
        <path d="M22 2l-7 20-4-9-9-4 20-7z"/>
      </svg>
    </button>
  );

  var pair=hovered||pinned;
  return (
    <div style={S.root}>

      {isMobile ? (
        /* ── Mobile top bar ── */
        <div style={{ display:'flex', flexDirection:'column', borderBottom:'1px solid var(--line-soft)', background:'var(--bg)', position:'relative', zIndex:20, flexShrink:0 }}>
          <div style={{ display:'flex', alignItems:'center', gap:6, padding:'8px 10px' }}>
            <button style={{ ...S.iconBtn,width:36,height:36 }} onClick={onExit}>
              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
            </button>
            <div style={{ ...S.brandMini, fontSize:10 }}>
              <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1.2"><circle cx="12" cy="12" r="9.5"/><ellipse cx="12" cy="12" rx="4" ry="9.5"/><line x1="2.5" y1="12" x2="21.5" y2="12"/></svg>
              ATLAS
            </div>
            <span style={{ ...S.liveDot, background:statusColor, animation:ratesState.status==='ready'?'pulse 1.8s ease-out infinite':'none' }}/>
            <div style={{ flex:1 }}/>
            {/* Refresh button */}
            <button
              style={{ ...S.iconBtn, width:36, height:36, opacity:isRefreshing?0.7:1, cursor:isRefreshing?'wait':'pointer' }}
              disabled={isRefreshing}
              onClick={function(){
                setIsRefreshing(true);
                var started = Date.now();
                Promise.resolve(window.RatesService.refresh({force:true})).finally(function(){
                  var elapsed = Date.now() - started;
                  setTimeout(function(){
                    setIsRefreshing(false);
                    setJustRefreshed(true);
                    setTimeout(function(){ setJustRefreshed(false); }, 1200);
                  }, Math.max(0, 700 - elapsed));
                });
              }}>
              {justRefreshed
                ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--up)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
                : <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" style={{ animation:isRefreshing?'spin 0.8s linear infinite':'none', transformOrigin:'center', transformBox:'fill-box' }}><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg>
              }
            </button>
            {amountEl}
            {baseEl}
            {/* ⋯ overflow menu — opens bottom sheet on mobile */}
            <button style={{ ...S.iconBtn, width:36, height:36, background:mobileMenuOpen?'var(--bg-elev)':'transparent' }}
              onClick={function(){ setMobileMenuOpen(true); }}>
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></svg>
            </button>
          </div>
        </div>
      ) : (
        /* ── Desktop top bar: 3-column grid ── */
        <div style={S.topBar}>
          <div style={S.topLeft}>
            <button style={S.iconBtn} onClick={onExit}>
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
            </button>
            <div style={S.brandMini}>
              <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.2"><circle cx="12" cy="12" r="9.5"/><ellipse cx="12" cy="12" rx="4" ry="9.5"/><line x1="2.5" y1="12" x2="21.5" y2="12"/></svg>
              ATLAS / FX
            </div>
            <div style={S.divider}/>
            <div style={S.liveTag}>
              <span style={{ ...S.liveDot,background:statusColor,animation:ratesState.status==='ready'?'pulse 1.8s ease-out infinite':'none' }}/>
              {statusLabel}
              <span style={{ opacity:0.35 }}>·</span>
              {clock}
            </div>
            <button
              style={{ ...S.miniBtn, opacity: isRefreshing ? 0.7 : 1, cursor: isRefreshing ? 'wait' : 'pointer' }}
              disabled={isRefreshing}
              title={isRefreshing ? 'Refreshing…' : justRefreshed ? 'Updated' : 'Refresh rates'}
              onClick={function(){
                setIsRefreshing(true);
                var started = Date.now();
                Promise.resolve(window.RatesService.refresh({force:true})).finally(function(){
                  var elapsed = Date.now() - started;
                  var minWait = Math.max(0, 700 - elapsed); // keep spinner visible at least 700ms
                  setTimeout(function(){
                    setIsRefreshing(false);
                    setJustRefreshed(true);
                    setTimeout(function(){ setJustRefreshed(false); }, 1200);
                  }, minWait);
                });
              }}>
              {justRefreshed ? (
                <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="var(--up)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
              ) : (
                <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" style={{ animation: isRefreshing ? 'spin 0.8s linear infinite' : 'none', transformOrigin:'center', transformBox:'fill-box' }}><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg>
              )}
            </button>
          </div>
          <div style={S.topCenter}>
          </div>
          <div style={S.topRight}>
            {amountEl}
            {baseEl}
            {filterEl}
            {modeEl}
            {sendEl}
          </div>
        </div>
      )}

      <div style={S.mapWrap}>
        {showLoader&&(
          <window.GlobeLoader
            label={ratesState.status==='error' ? 'FX CONNECTION LOST' : 'CONNECTING TO FX · LOADING MARKETS'}
            retry={ratesState.status==='error'}
            onRetry={ratesState.status==='error' ? function(){ window.RatesService.refresh({force:true}); } : null}
          />
        )}
        {showGlobeLoader&&!showLoader&&(
          <window.GlobeLoader label="INITIALIZING GLOBE · LOADING TEXTURES"/>
        )}
        {ratesState.status==='error'&&(
          <div style={S.errorBanner}>
            Live rates unavailable — showing cached data.
            <button onClick={function(){ window.RatesService.refresh({force:true}); }} style={S.errorBtn}>Retry</button>
          </div>
        )}

        <window.WorldMapGL
          base={base} hovered={hovered||pinned}
          onHoverCountry={setHovered} onLeaveCountry={function(){ setHovered(null); }}
          onClickCountry={function(c){ setPinned(function(p){ return p&&p.code===c.code?null:c; }); }}
          search={search} mapMode={mapMode} mapApiRef={mapApi}/>

        {/* Chips overlay — ChipCard components mounted once, stable refs, positions imperative */}
        <div style={{ position:'absolute', inset:0, pointerEvents:'none', zIndex:2 }}>
          {window.COUNTRIES.map(function(c){
            if(visibleCountries!==null&&!visibleCountries.has(c.code)) return null;
            var isBase=c.ccy===base;
            var isHovered=!!(hovered&&hovered.code===c.code)||!!(pinned&&pinned.code===c.code);
            var searchMatch=!!(search&&(c.name.toLowerCase().indexOf(search.toLowerCase())!==-1||c.code.toLowerCase().indexOf(search.toLowerCase())!==-1||c.ccy.toLowerCase().indexOf(search.toLowerCase())!==-1));
            var visible=chipShouldShow(c,isBase,isHovered,searchMatch);
            return <ChipCard key={c.code} country={c} visible={visible} isBase={isBase}
              base={base} amount={amount} noiseRef={noiseRef} isMobile={isMobile}
              onEnter={function(){ setHovered(c); }}
              onLeave={function(){ setHovered(null); }}
              onClick={function(e){ e.stopPropagation(); setPinned(function(p){ return p&&p.code===c.code?null:c; }); }}/>;
          })}
        </div>

        {/* Detail card */}
        {pair&&(function(){
          var ccy=window.CURRENCIES.find(function(x){ return x.code===pair.ccy; });
          var pn=noiseRef.current[pair.code]; var val=window.RatesService.convert(base,pair.ccy)*(1+(pn?pn.v:0))*amount;
          var inv=amount/val; var spark=sparkRef.current[pair.code]||[];
          var mn=Math.min.apply(null,spark),mx=Math.max.apply(null,spark),range=mx-mn||1;
          var last=spark[spark.length-1],prev=spark[spark.length-2],trend=last-prev,pct=(trend/(Math.abs(prev)||1))*100;

          if (isMobile) {
            // ── Mobile: slide-up bottom sheet ─────────────────────────────
            return (
              <React.Fragment>
                {/* Backdrop */}
                <div onClick={function(){ setPinned(null); setHovered(null); }}
                  style={{ position:'fixed',inset:0,background:'rgba(0,0,0,0.35)',zIndex:9,pointerEvents:'auto' }}/>
                {/* Sheet */}
                <div style={{ position:'fixed',left:0,right:0,bottom:0,zIndex:10,
                  background:'var(--bg-elev)',borderTop:'1px solid var(--line)',
                  padding:'0 0 calc(12px + env(safe-area-inset-bottom))',boxShadow:'0 -8px 40px rgba(0,0,0,0.6)',pointerEvents:'auto',
                  animation:'slideUp 220ms cubic-bezier(0.22,1,0.36,1)' }}>
                  {/* Drag handle */}
                  <div style={{ display:'flex',justifyContent:'center',padding:'10px 0 6px' }}>
                    <div style={{ width:36,height:4,borderRadius:99,background:'var(--line)' }}/>
                  </div>
                  <div style={{ padding:'0 16px' }}>
                    {/* Row 1: flag + name + symbol + close */}
                    <div style={{ display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:10 }}>
                      <div style={{ display:'flex',alignItems:'center',gap:10,overflow:'hidden' }}>
                        <span style={{ fontSize:28,flexShrink:0 }}>{window.flagEmoji(pair.code)}</span>
                        <div style={{ overflow:'hidden' }}>
                          <div style={{ fontSize:15,fontWeight:500,color:'var(--fg)',lineHeight:1.2,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis' }}>{pair.name}</div>
                          <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:9,color:'var(--fg-faint)',letterSpacing:'0.1em',marginTop:3 }}>{ccy?ccy.name+' · ':''}{pair.ccy}</div>
                        </div>
                      </div>
                      <div style={{ display:'flex',alignItems:'center',gap:10,flexShrink:0,marginLeft:10 }}>
                        <span style={{ fontFamily:"'Instrument Serif',serif",fontSize:26,color:'var(--accent)',lineHeight:1 }}>{ccy?ccy.symbol:''}</span>
                        <button onClick={function(){ setPinned(null); setHovered(null); }}
                          style={{ background:'transparent',border:'1px solid var(--line-soft)',color:'var(--fg-dim)',cursor:'pointer',width:32,height:32,display:'grid',placeItems:'center',fontSize:20,lineHeight:1,flexShrink:0 }}>×</button>
                      </div>
                    </div>
                    {/* Row 2: rate + trend */}
                    <div style={{ display:'flex',alignItems:'center',justifyContent:'space-between',paddingTop:12,borderTop:'1px solid var(--line-soft)',marginBottom:10 }}>
                      <div>
                        <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:9,color:'var(--fg-faint)',letterSpacing:'0.12em',marginBottom:4 }}>{amount} {base} =</div>
                        <div style={{ fontFamily:"'Instrument Serif',serif",fontSize:32,color:'var(--fg)',lineHeight:1 }}>{window.formatRate(val)}<span style={{ opacity:0.5,fontSize:14,marginLeft:5 }}>{pair.ccy}</span></div>
                      </div>
                      <div style={{ textAlign:'right' }}>
                        <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:14,fontWeight:500,color:trend>=0?'var(--up)':'var(--down)' }}>{trend>=0?'▲':'▼'} {Math.abs(pct).toFixed(2)}%</div>
                        <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:10,color:'var(--fg-faint)',marginTop:4 }}>inv {window.formatRate(inv)} {base}</div>
                      </div>
                    </div>
                    {/* Set as base — placed first after rate so it's always visible */}
                    {pair.ccy!==base&&(
                      <button onClick={function(){ setBase(pair.ccy); setPinned(null); }}
                        style={{ width:'100%',padding:'13px',background:'var(--accent)',color:'#1a1816',border:'none',cursor:'pointer',fontFamily:"'JetBrains Mono',monospace",fontSize:12,letterSpacing:'0.14em',fontWeight:700,marginBottom:4,borderRadius:2 }}>
                        SET AS BASE CURRENCY
                      </button>
                    )}
                  </div>
                </div>
              </React.Fragment>
            );
          }

          // ── Desktop: full detail card ──────────────────────────────────────
          var detailStyle = { ...S.detail, left:'auto', right:filterOpen?304:20, top:68, transform:'none' };
          return (
            <div style={detailStyle}>
              <div style={S.detailHeader}>
                <div>
                  <div style={S.detailCountry}><span style={{ marginRight:7 }}>{window.flagEmoji(pair.code)}</span>{pair.name}</div>
                  <div style={S.detailCcy}>{ccy?ccy.name:pair.ccy} · {pair.ccy}</div>
                </div>
                <div style={S.detailSymbol}>{ccy?ccy.symbol:''}</div>
              </div>
              <div style={S.detailBig}>
                <div style={S.detailBigUnit}>{amount} {base} =</div>
                <div style={S.detailBigVal}>{window.formatRate(val)} <span style={{ opacity:0.5,fontSize:13 }}>{pair.ccy}</span></div>
              </div>
              <div style={S.detailRow}><span>Inverse</span><span style={S.mono}>{window.formatRate(inv)} {base}</span></div>
              <div style={S.detailRow}><span>24h trend</span><span style={{ ...S.mono,color:trend>=0?'var(--up)':'var(--down)' }}>{trend>=0?'▲':'▼'} {pct.toFixed(3)}%</span></div>
              <div style={S.detailRow}><span>Session hi</span><span style={S.mono}>{window.formatRate(val*(1+mx-last))}</span></div>
              <div style={S.detailRow}><span>Session lo</span><span style={S.mono}>{window.formatRate(val*(1+mn-last))}</span></div>
              <div style={S.sparkWrap}>
                <svg viewBox="0 0 120 36" width="100%" height="36" preserveAspectRatio="none">
                  <path d={spark.map(function(v,i){ var x=(i/(spark.length-1))*120; var y=36-((v-mn)/range)*32-2; return (i===0?'M':'L')+x.toFixed(2)+' '+y.toFixed(2); }).join(' ')}
                    fill="none" stroke={trend>=0?'var(--up)':'var(--down)'} strokeWidth="1.2"/>
                </svg>
                <div style={S.sparkLegend}><span>24H</span><span>{spark.length} ticks</span></div>
              </div>
              <div style={S.detailFooter}>{pinned&&pinned.code===pair.code?'Pinned — click chip to release':'Click chip to pin'}</div>
            </div>
          );
        })()}

        {/* Zoom controls */}
        <div style={{ ...S.zoomPanel, right:isMobile?10:14, bottom:isMobile?40:48 }}>
          <button style={{ ...S.zoomBtn, width:isMobile?44:32, height:isMobile?44:32, fontSize:isMobile?18:15 }} onClick={function(){ mapApi.current&&mapApi.current.zoomIn(); }}>+</button>
          <div style={{
            width:isMobile?44:32, padding:'3px 0', textAlign:'center',
            fontFamily:'var(--mono)', fontSize:isMobile?9:8, letterSpacing:'0.06em',
            color:'var(--fg-dim)', borderBottom:'1px solid var(--line)',
            background:'rgba(255,255,255,0.02)',
          }}>{zoomPct}%</div>
          <button style={{ ...S.zoomBtn, width:isMobile?44:32, height:isMobile?44:32, fontSize:isMobile?18:15 }} onClick={function(){ mapApi.current&&mapApi.current.zoomOut(); }}>−</button>
          <div style={S.zoomDivider}/>
          <button style={{ ...S.zoomBtn, width:isMobile?44:32, height:isMobile?44:32, borderBottom:'none' }} onClick={function(){ mapApi.current&&mapApi.current.reset(); }}>
            <svg width={isMobile?14:11} height={isMobile?14:11} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7"><path d="M3 3h6M21 3h-6M3 21h6M21 21h-6M3 3v6M21 3v6M3 21v-6M21 21v-6"/></svg>
          </button>
        </div>

        {/* Legend — desktop only */}
        {!isMobile&&(
          <div style={S.legend}>
            <div style={S.legendTitle}>LEGEND</div>
            <div style={S.legendRow}><span style={{ ...S.legendDot,background:'var(--accent)' }}/><span>Base currency</span></div>
            <div style={S.legendRow}><span style={{ color:'var(--up)',fontFamily:"'JetBrains Mono',monospace" }}>▲</span><span>Appreciating</span></div>
            <div style={S.legendRow}><span style={{ color:'var(--down)',fontFamily:"'JetBrains Mono',monospace" }}>▼</span><span>Depreciating</span></div>
            <div style={S.legendHint}>{mapMode==='globe'?'Drag to rotate':'Scroll to zoom · Drag to pan'}</div>
          </div>
        )}

        <Ticker base={base} amount={amount} noiseRef={noiseRef}/>

        {filterOpen&&<FilterPanel
          visibleCountries={visibleCountries}
          setVisibleCountries={setVisibleCountries}
          onClose={function(){ setFilterOpen(false); }}
          isMobile={isMobile}/>}
      </div>
      {window.Remittance && <window.Remittance
        open={sendOpen}
        onClose={function(){ setSendOpen(false); setSendPrefill(null); }}
        initialBase={base}
        initialFromCcy={sendPrefill && sendPrefill.from}
        initialToCcy={sendPrefill && sendPrefill.to}
        initialAmount={sendPrefill && sendPrefill.amount}/>}

      {/* ─── Mobile bottom sheet menu ─── */}
      {isMobile && (
        <React.Fragment>
          <div onClick={function(){ setMobileMenuOpen(false); }}
            style={{ position:'fixed', inset:0, background:'rgba(4,8,16,0.55)', zIndex:70,
              opacity:mobileMenuOpen?1:0, pointerEvents:mobileMenuOpen?'auto':'none',
              transition:'opacity 220ms ease', backdropFilter:'blur(3px)' }}/>
          <div style={{ position:'fixed', left:0, right:0, bottom:0, zIndex:71,
            background:'var(--bg)', borderTop:'1px solid var(--line-soft)',
            borderTopLeftRadius:16, borderTopRightRadius:16,
            transform: mobileMenuOpen ? 'translateY(0)' : 'translateY(100%)',
            transition:'transform 260ms cubic-bezier(0.22,0.61,0.36,1)',
            paddingBottom:'max(14px, env(safe-area-inset-bottom))',
            boxShadow:'0 -10px 40px rgba(0,0,0,0.45)',
            maxHeight:'85dvh', overflowY:'auto' }}>
            {/* Drag handle */}
            <div style={{ display:'flex', justifyContent:'center', padding:'9px 0 4px' }}>
              <div style={{ width:42, height:4, borderRadius:2, background:'var(--line)' }}/>
            </div>
            {/* Header */}
            <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'6px 18px 14px' }}>
              <div style={{ fontFamily:"'JetBrains Mono',monospace", fontSize:10, letterSpacing:'0.22em', color:'var(--fg-dim)' }}>MENU</div>
              <button onClick={function(){ setMobileMenuOpen(false); }}
                style={{ width:28, height:28, display:'grid', placeItems:'center', background:'transparent', border:'1px solid var(--line-soft)', color:'var(--fg-dim)', cursor:'pointer' }}>
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"><path d="M18 6L6 18M6 6l12 12"/></svg>
              </button>
            </div>
            {/* Action rows */}
            {[
              { key:'mode',  label:'Map mode',   sub: mapMode==='flat'?'Flat · tap to rotate globe':'Globe · tap for flat map',
                icon: mapMode==='flat'
                  ? <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg>
                  : <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="12" cy="12" r="10"/><ellipse cx="12" cy="12" rx="4" ry="10"/><line x1="2" y1="12" x2="22" y2="12"/></svg>,
                onClick: function(){ setMapMode(mapMode==='flat'?'globe':'flat'); setMobileMenuOpen(false); } },
              { key:'filter', label:'Currencies',
                sub: (visibleCountries===null ? window.COUNTRIES.length : visibleCountries.size) + ' of ' + window.COUNTRIES.length + ' visible',
                icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>,
                onClick: function(){ setFilterOpen(true); setMobileMenuOpen(false); } },
              { key:'send', label:'Send money', sub:'Compare remittance providers',
                icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/></svg>,
                onClick: function(){ setSendOpen(true); setMobileMenuOpen(false); } },
            ].map(function(row, i){
              return (
                <button key={row.key} onClick={row.onClick}
                  style={{ display:'flex', width:'100%', alignItems:'center', gap:14,
                    padding:'14px 18px', background:'transparent', border:'none',
                    borderTop: i===0?'1px solid var(--line-soft)':'none',
                    borderBottom:'1px solid var(--line-soft)',
                    color:'var(--fg)', cursor:'pointer', textAlign:'left' }}>
                  <div style={{ width:40, height:40, display:'grid', placeItems:'center',
                    background:'var(--bg-elev)', border:'1px solid var(--line-soft)',
                    color:'var(--accent)', flexShrink:0 }}>{row.icon}</div>
                  <div style={{ flex:1, minWidth:0 }}>
                    <div style={{ fontFamily:"'Cormorant Garamond', Georgia, serif", fontSize:18, color:'var(--fg)', lineHeight:1.1 }}>{row.label}</div>
                    <div style={{ fontFamily:"'JetBrains Mono', monospace", fontSize:10, color:'var(--fg-faint)', letterSpacing:'0.08em', marginTop:3 }}>{row.sub}</div>
                  </div>
                  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ color:'var(--fg-faint)', flexShrink:0 }}><path d="M9 6l6 6-6 6"/></svg>
                </button>
              );
            })}
          </div>
        </React.Fragment>
      )}
    </div>
  );
}

window.AtlasGL = AtlasGL;
