const { Panel, Label, Badge, Button, Monogram } = window.MakiSClubDesignSystem_d11b87;
const MK = window.MK;

function SiteNav({ active, playersOnline }) {
  const tabs = [['Home', "Maki's Club.html"], ['Standings', 'Standings.html'], ['Skins', 'Skins.html'], ['House Rules', 'House Rules.html']];
  // Staff comes from the server session (MK_STAFF_STEAMIDS), not the client —
  // there is no localStorage flag to flip any more.
  if (MK.SESSION && MK.SESSION.isStaff) tabs.push(['Admin', 'Admin.html']);
  const players = playersOnline != null ? playersOnline : MK.SERVERS.filter(s => s.online).reduce((n, s) => n + s.players, 0);
  return (
    <header style={{position:'sticky',top:0,zIndex:50,background:'var(--mk-lacquer)',borderBottom:'1px solid var(--mk-border)'}}>
      <div style={{display:'flex',alignItems:'center',gap:28,padding:'18px 40px'}}>
        <a href="Maki's Club.html" style={{display:'flex',alignItems:'center',gap:14,textDecoration:'none'}}>
          <Monogram variant="tile" size={36} />
          <span style={{fontFamily:'var(--mk-font-display)',fontWeight:900,fontVariationSettings:"'wdth' 118",textTransform:'uppercase',letterSpacing:'-0.01em',fontSize:17,color:'var(--mk-ivory)'}}>Maki's <span style={{fontFamily:'var(--mk-font-serif)',fontStyle:'italic',fontWeight:400,textTransform:'none',color:'var(--mk-scarlet)'}}>Club</span></span>
        </a>
        <nav style={{display:'flex',gap:24,marginLeft:'auto'}}>
          {tabs.map(([t, href]) => t === 'Admin'
            ? <a key={t} href={href} style={{display:'inline-flex',alignItems:'center',gap:7,fontFamily:'var(--mk-font-mono)',fontSize:12,fontWeight:600,letterSpacing:'0.18em',textTransform:'uppercase',textDecoration:'none',color:t===active?'var(--mk-scarlet)':'var(--mk-smoke)',borderBottom:t===active?'2px solid var(--mk-scarlet)':'2px solid transparent',paddingBottom:4}}>{t}</a>
            : <a key={t} href={href} style={{fontFamily:'var(--mk-font-mono)',fontSize:12,fontWeight:600,letterSpacing:'0.18em',textTransform:'uppercase',textDecoration:'none',color:t===active?'var(--mk-ivory)':'var(--mk-smoke)',borderBottom:t===active?'2px solid var(--mk-violet)':'2px solid transparent',paddingBottom:4}}>{t}</a>)}
        </nav>
        <div style={{display:'flex',alignItems:'center',gap:14}}>
          <PlayerSearch />
          <SteamAuth />
        </div>
      </div>
      <TempoStripe players={players} height={3} gap={4} />
    </header>
  );
}

// Site-wide Steam sign-in control. Identity is a signed session cookie set by
// /api/auth/callback only after Steam confirms it signed the OpenID assertion —
// there is no client-side auth state, and no way to browse as someone else.
function SteamAuth() {
  const me = MK.SESSION;
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  React.useEffect(() => { const h = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h); }, []);
  const signIn = () => { location.href = 'api/auth/login'; };
  const signOut = () => { fetch('api/auth/logout', { method: 'POST', credentials: 'same-origin' }).catch(() => {}).then(() => location.reload()); };
  const steam = <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="12" cy="12" r="9"/><circle cx="15.5" cy="9" r="2.4"/><path d="M13.5 10.8 6 14" strokeLinecap="round"/><circle cx="6.6" cy="14.4" r="1.5" fill="currentColor" stroke="none"/></svg>;
  if (!me) return <button className="mk-authbtn" onClick={signIn}>{steam}<span>Sign in</span></button>;
  return (
    <div ref={ref} style={{position:'relative',flexShrink:0}}>
      <button className="mk-authchip" onClick={() => setOpen(o => !o)} aria-label="Your pass">
        <img src={me.avatar || MK.agentFor(me.steamid)} alt="" style={{width:28,height:28,borderRadius:'50%',objectFit:'cover',border:'1px solid var(--mk-line-violet)',background:'var(--mk-lacquer)'}} />
        <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="var(--mk-smoke)" strokeWidth="2.2" style={{transform:open?'rotate(180deg)':'none',transition:'transform .15s'}}><path d="m6 9 6 6 6-6"/></svg>
      </button>
      {open && (
        <div style={{position:'absolute',top:'calc(100% + 8px)',right:0,minWidth:200,background:'var(--mk-surface)',border:'1px solid var(--mk-border)',borderRadius:12,overflow:'hidden',zIndex:60,boxShadow:'0 18px 40px rgba(10,8,13,.5)'}}>
          <div style={{padding:'12px 15px',borderBottom:'1px solid var(--mk-border)'}}>
            <div style={{fontFamily:'var(--mk-font-mono)',fontSize:10,fontWeight:600,letterSpacing:'0.16em',textTransform:'uppercase',color:'var(--mk-smoke)'}}>signed in with steam</div>
            <div style={{fontSize:14,color:'var(--mk-ivory)',marginTop:4}}>{me.name}</div>
            {!me.isMember && <div style={{fontFamily:'var(--mk-font-serif)',fontStyle:'italic',fontSize:13,color:'var(--mk-smoke)',marginTop:4}}>no rounds on record yet</div>}
          </div>
          {me.slug && <a href={'Profile.html?p=' + me.slug} className="mk-authitem">View profile</a>}
          <a href="Skins.html" className="mk-authitem">My skins</a>
          <button className="mk-authitem" onClick={signOut} style={{borderTop:'1px solid var(--mk-border)'}}>Sign out</button>
        </div>
      )}
    </div>
  );
}

function MkFooter() {
  const linkCss = {fontFamily:'var(--mk-font-mono)',fontSize:11,fontWeight:600,letterSpacing:'0.16em',textTransform:'uppercase',textDecoration:'none',color:'var(--mk-violet)'};
  return (
    <footer style={{borderTop:'1px solid var(--mk-border)',padding:'32px 40px 40px',marginTop:64}}>
      <div style={{display:'flex',flexWrap:'wrap',alignItems:'center',gap:24}}>
        <Monogram variant="tile" size={28} />
        <nav style={{display:'flex',gap:22,flexWrap:'wrap'}}>
          {MK.SOCIALS.map(s => <a key={s.label} href={s.href} style={linkCss}>{s.label} ↗</a>)}
        </nav>
        <nav style={{display:'flex',gap:22,marginLeft:'auto',flexWrap:'wrap'}}>
          <a href="Appeal.html" style={{...linkCss,color:'var(--mk-smoke)'}}>Appeal a ban</a>
          <a href="Privacy Policy.html" style={{...linkCss,color:'var(--mk-smoke)'}}>Privacy policy</a>
          <a href="Terms & House Rules.html" style={{...linkCss,color:'var(--mk-smoke)'}}>Terms &amp; house rules</a>
        </nav>
      </div>
      <p style={{margin:'22px 0 0',maxWidth:'78ch',fontSize:12.5,lineHeight:1.7,color:'var(--mk-smoke)'}}>Maki's Club is an independent community project. It is not affiliated with, sponsored by, or endorsed by Valve Corporation. Counter-Strike, Counter-Strike 2, and Steam are trademarks and/or registered trademarks of Valve Corporation. Map imagery belongs to its respective owners.</p>
      <p style={{margin:'8px 0 0',fontFamily:'var(--mk-font-mono)',fontSize:11,letterSpacing:'0.14em',textTransform:'uppercase',color:'var(--mk-smoke)'}}>© 2026 Maki's Club · house rules apply</p>
    </footer>
  );
}

/* Approximate ping.

   A browser cannot open a UDP socket, so the real in-game latency is out of
   reach — the number the imported design showed was Math.random() jitter around
   a constant. What is measurable: an empty HTTPS request to the game hostname,
   which is a DNS-only record on the club's own connection, so the round trip
   covers the same internet path the game traffic takes.

   Warm up first (that request pays DNS + TCP + TLS), then take the fastest of
   three samples — the minimum is the one least polluted by scheduling noise.
   responseStart - requestStart is the request/response pair on its own, and is
   readable cross-origin only because /mk-ping sends Timing-Allow-Origin.

   undefined = still measuring, null = unavailable. Never a made-up number. */
const PING_NOTE = 'approximate — timed over HTTPS from your browser to the server’s network. Your in-game ping will differ.';

function hostOf(addr) { return String(addr || '').split(':')[0]; }

async function measurePing(host) {
  const url = n => 'https://' + host + '/mk-ping?n=' + n;
  const opts = { cache: 'no-store', mode: 'cors', credentials: 'omit' };
  try { await fetch(url('warm'), opts); } catch (e) { return null; }
  const samples = [];
  for (let i = 0; i < 3; i++) {
    const u = url(i + '-' + Math.random().toString(36).slice(2));
    const t0 = performance.now();
    try { await fetch(u, opts); } catch (e) { return null; }
    const wall = performance.now() - t0;
    const e = performance.getEntriesByName(u).pop();
    samples.push(e && e.responseStart && e.requestStart ? e.responseStart - e.requestStart : wall);
  }
  try { performance.clearResourceTimings(); } catch (e) {}
  return Math.max(1, Math.round(Math.min.apply(null, samples)));
}

/* Keyed by host, not by server: every room on one box shares one round trip. */
function useApproxPing(servers) {
  const [pings, setPings] = React.useState({});
  const hosts = Array.from(new Set((servers || []).filter(s => s.online).map(s => hostOf(s.addr)))).sort().join(',');
  React.useEffect(() => {
    let live = true;
    (hosts ? hosts.split(',') : []).forEach(h => {
      measurePing(h).then(ms => { if (live) setPings(p => Object.assign({}, p, { [h]: ms })); });
    });
    return () => { live = false; };
  }, [hosts]);
  return pings;
}

function PingText({ ms, style }) {
  const txt = ms === undefined ? '…' : ms === null ? '—' : '≈' + ms + ' ms';
  return <span title={PING_NOTE} style={style}>{txt}</span>;
}

function StatusDot({ online }) {
  return <span style={{display:'inline-flex',alignItems:'center',gap:7,fontFamily:'var(--mk-font-mono)',fontSize:11,fontWeight:600,letterSpacing:'0.14em',textTransform:'uppercase',color:'var(--mk-smoke)'}}>
    <span className={online ? 'mk-status-live' : ''} style={{width:8,height:8,borderRadius:'50%',background:online?'var(--mk-ivory)':'transparent',border:online?'none':'1px solid var(--mk-smoke)'}}></span>
    {online ? 'online' : 'offline'}
  </span>;
}

/* Two different connect paths, and they do not accept the same thing. Steam's
   steam://connect/ URL handler needs a numeric address (server.connect, built
   from the resolved IP by the API); the in-game console resolves DNS, so the
   copied string keeps the hostname and survives a WAN IP change. */
function ConnectActions({ server, size }) {
  const [copied, setCopied] = React.useState(false);
  const signal = () => { try { mkChime(); } catch (e) {} window.dispatchEvent(new CustomEvent('mk-connect')); };
  const copy = () => {
    navigator.clipboard.writeText('connect ' + server.addr).catch(()=>{});
    signal();
    setCopied(true); setTimeout(() => setCopied(false), 1600);
  };
  return (
    <div style={{display:'flex',gap:10,alignItems:'center'}}>
      <Button variant="secondary" size={size||'sm'} disabled={!server.online} onClick={() => { signal(); window.location.href = server.connect || ('steam://connect/' + server.addr); }}>Play</Button>
      <Button variant="ghost" size={size||'sm'} style={{color:copied?'var(--mk-ivory)':'var(--mk-smoke)',fontFamily:'var(--mk-font-mono)',fontWeight:600,fontSize:12,letterSpacing:'0.1em',textTransform:'uppercase',padding:'8px 10px'}} onClick={copy}>{copied ? 'copied' : 'copy connect'}</Button>
    </div>
  );
}

const mkMono = {fontFamily:'var(--mk-font-mono)',fontSize:12,fontWeight:600,letterSpacing:'0.12em',textTransform:'uppercase'};

// Marks the parts of the site that are NOT reading real data. Player stats and
// standings come from the K4-System tables and server status from A2S; the
// CounterStrikeSharp admin tables aren't installed, so moderation stays mock
// and says so rather than passing itself off.
function DemoNote({ children, style }) {
  return (
    <p style={{display:'flex',alignItems:'center',gap:9,margin:'10px 0 0',fontFamily:'var(--mk-font-mono)',fontSize:11,fontWeight:600,letterSpacing:'0.14em',textTransform:'uppercase',color:'var(--mk-smoke)',...style}}>
      <span style={{width:6,height:6,borderRadius:'50%',background:'var(--mk-smoke)',flexShrink:0}}></span>
      demo data — {children}
    </p>
  );
}

if (!document.getElementById('mk-modal-css')) {
  const st = document.createElement('style'); st.id = 'mk-modal-css';
  st.textContent = [
    '@keyframes mkFade{from{opacity:0}to{opacity:1}}',
    '@keyframes mkPop{from{opacity:0;transform:translateY(18px) scale(.965)}to{opacity:1;transform:none}}',
    '@keyframes mkRise{from{opacity:0;transform:translateY(22px)}to{opacity:1;transform:none}}',
    '@keyframes mkKey{0%,100%{opacity:.28}50%{opacity:1}}',
    '@keyframes mkKeyScarlet{0%,100%{opacity:.55}50%{opacity:1}}',
    '@keyframes mkPulse{0%{box-shadow:0 0 0 0 rgba(244,239,228,.5)}70%{box-shadow:0 0 0 6px rgba(244,239,228,0)}100%{box-shadow:0 0 0 0 rgba(244,239,228,0)}}',
    '@keyframes mkTick{0%,100%{transform:translateX(-2px)}50%{transform:translateX(2px)}}',
    '.mk-rise{animation:mkRise .55s cubic-bezier(.2,.7,.2,1) both}',
    '.mk-card-hit{cursor:pointer}',
    '.mk-card-hit img{transition:transform .5s cubic-bezier(.2,.7,.2,1),filter .3s}',
    '.mk-card-hit:hover img{transform:scale(1.045)}',
    '.mk-piano{display:flex;align-items:stretch}',
    '.mk-piano span{flex:1;min-height:100%;border-radius:2px;animation:mkKey 1.8s ease-in-out infinite both}',
    '.mk-piano span.sc{animation-name:mkKeyScarlet}',
    '.mk-piano.play span{animation-duration:.9s}',
    '.mk-reveal{opacity:0;transform:translateY(22px)}',
    '.mk-reveal.in{animation:mkRise .6s cubic-bezier(.2,.7,.2,1) both}',
    '.mk-live-dot{width:7px;height:7px;border-radius:50%;background:var(--mk-scarlet);animation:mkPulse 2s ease-out infinite}',
    '.mk-status-live{position:relative}',
    '.mk-status-live::after{content:"";position:absolute;inset:-4px;border-radius:50%;animation:mkPulse 2.4s ease-out infinite}',
    '.mk-metro{display:inline-block;animation:mkTick 1.1s ease-in-out infinite}',
    '.mk-tempo span{animation:mkKey var(--d,1.8s) ease-in-out infinite both}',
    '.mk-tempo span.sc{animation-name:mkKeyScarlet}',
    '.mk-tempo.flash span{animation:mkKey var(--d,1.8s) ease-in-out infinite both,mkTempoPop .62s cubic-bezier(.2,.7,.2,1) both}',
    '.mk-tempo.flash span.sc{animation-name:mkKeyScarlet,mkTempoPop}',
    '@keyframes mkTempoPop{0%{transform:scaleY(1)}32%{transform:scaleY(2.2);filter:brightness(1.6)}100%{transform:scaleY(1)}}',
    '.mk-modal-close{background:none;border:1px solid rgba(244,239,228,.25);border-radius:10px;color:var(--mk-smoke);font-family:var(--mk-font-mono);font-size:11px;font-weight:600;letter-spacing:.16em;text-transform:uppercase;padding:8px 14px;cursor:pointer}',
    '.mk-modal-close:hover{color:var(--mk-ivory);border-color:rgba(244,239,228,.5)}',
    '@media (prefers-reduced-motion:reduce){.mk-rise,.mk-reveal,.mk-reveal.in,.mk-piano span,.mk-live-dot,.mk-status-live::after,.mk-metro,.mk-tempo span{animation:none!important;opacity:1!important;transform:none!important}}'
  ].join('');
  document.head.appendChild(st);
}

function PianoStripe({ height = 10, gap = 5, play, style }) {
  return (
    <div className={'mk-piano' + (play ? ' play' : '')} style={{ gap, height, ...style }}>
      {[0,1,2,3,4].map(i => <span key={i} className={i === 2 ? 'sc' : ''} style={{ background: i === 2 ? 'var(--mk-scarlet)' : 'var(--mk-ivory)', animationDelay: (i * 0.16) + 's' }} />)}
    </div>
  );
}

// Tempo marking from how busy the club is right now (an-empty-room to packed).
function tempoMark(players) {
  if (players <= 0) return 'Tacet';
  if (players < 8) return 'Adagio';
  if (players < 18) return 'Andante';
  if (players < 30) return 'Allegro';
  return 'Presto';
}

// A five-note motif on connect — one soft note per key of the stripe.
function mkChime() {
  try {
    const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return;
    const ctx = mkChime._ctx || (mkChime._ctx = new AC());
    if (ctx.state === 'suspended') ctx.resume();
    const now = ctx.currentTime;
    [523.25, 587.33, 659.25, 783.99, 1046.5].forEach((f, i) => {
      const o = ctx.createOscillator(), g = ctx.createGain();
      o.type = 'triangle'; o.frequency.value = f;
      const t = now + i * 0.072;
      g.gain.setValueAtTime(0.0001, t);
      g.gain.exponentialRampToValueAtTime(0.12, t + 0.015);
      g.gain.exponentialRampToValueAtTime(0.0008, t + 0.34);
      o.connect(g).connect(ctx.destination);
      o.start(t); o.stop(t + 0.38);
    });
  } catch (e) {}
}

// The five-key stripe as a living heartbeat: tempo scales with players online,
// and every connect (Play / copy) ripples the keys via the 'mk-connect' event.
function TempoStripe({ players = 0, height = 4, gap = 5, style }) {
  const dur = Math.max(2.0, 3.8 - Math.min(players, 40) / 40 * 1.8);
  const [flash, setFlash] = React.useState(false);
  React.useEffect(() => {
    let id;
    const on = () => { setFlash(true); clearTimeout(id); id = setTimeout(() => setFlash(false), 660); };
    window.addEventListener('mk-connect', on);
    return () => { window.removeEventListener('mk-connect', on); clearTimeout(id); };
  }, []);
  return (
    <div className={'mk-tempo' + (flash ? ' flash' : '')} style={{ display:'flex', gap, height, alignItems:'stretch', ...style }}>
      {[0,1,2,3,4].map(i => <span key={i} className={i === 2 ? 'sc' : ''} style={{ flex:1, borderRadius:2, transformOrigin:'center', background: i === 2 ? 'var(--mk-scarlet)' : 'var(--mk-ivory)', animationDelay:(i*0.09)+'s', '--d': dur + 's' }} />)}
    </div>
  );
}

function useReveal(once) {
  const ref = React.useRef(null);
  const [seen, setSeen] = React.useState(false);
  React.useEffect(() => {
    const el = ref.current; if (!el) return;
    const io = new IntersectionObserver(([e]) => {
      if (e.isIntersecting) { setSeen(true); if (once) io.disconnect(); }
      else if (!once) setSeen(false);
    }, { threshold: 0.15 });
    io.observe(el); return () => io.disconnect();
  }, []);
  return [ref, seen];
}

function Reveal({ children, delay = 0, once, as = 'div', className = '', style }) {
  const [ref, seen] = useReveal(once);
  return React.createElement(as, { ref, className: ('mk-reveal ' + (seen ? 'in ' : '') + className).trim(), style: { animationDelay: delay + 's', ...style } }, children);
}

/* Who's connected, from A2S. The protocol reports score and time only — no
   deaths, so there is no k/d to show — and no SteamID, so a row links to a
   member page only when the display name matches one. */
function Roster({ server }) {
  const list = (MK.ROSTERS[server.id] || []).slice().sort((a, b) => b.score - a.score);
  const known = Object.fromEntries((MK.PLAYERS || []).map(p => [p.name, p.slug]));
  const rGrid = {display:'grid',gridTemplateColumns:'2fr .8fr .9fr',gap:8,padding:'8px 20px',alignItems:'center'};
  const since = m => m >= 60 ? Math.floor(m / 60) + 'h ' + (m % 60) + 'm' : m + 'm';
  if (!server.online) return <p style={{margin:0,padding:'14px 20px 18px',fontFamily:'var(--mk-font-serif)',fontStyle:'italic',fontSize:15,color:'var(--mk-smoke)'}}>Room's closed — no one inside.</p>;
  if (!list.length) return <p style={{margin:0,padding:'14px 20px 18px',fontFamily:'var(--mk-font-serif)',fontStyle:'italic',fontSize:15,color:'var(--mk-smoke)'}}>Empty room — first one in picks the music.</p>;
  return (
    <div style={{borderTop:'1px solid var(--mk-border)',paddingBottom:10}}>
      <div style={rGrid}>{['player','score','in the room'].map(h => <span key={h} style={{fontFamily:'var(--mk-font-mono)',fontSize:10,fontWeight:600,letterSpacing:'0.16em',textTransform:'uppercase',color:'var(--mk-smoke)'}}>{h}</span>)}</div>
      {list.map((r, i) => (
        <div key={r.name + i} style={{...rGrid,background:i%2?'transparent':'rgba(244,239,228,.025)'}}>
          {known[r.name] ? <a href={'Player.html?p=' + known[r.name]} style={{fontSize:13.5,color:'var(--mk-ivory)',textDecoration:'none',borderBottom:'1px dotted var(--mk-smoke)',width:'fit-content'}}>{r.name}</a> : <span style={{fontSize:13.5,color:'var(--mk-smoke)'}}>{r.name}</span>}
          <span style={{...mkMono,fontSize:12,color:'var(--mk-ivory)'}}>{r.score}</span>
          <span style={{...mkMono,fontSize:12,color:'var(--mk-smoke)'}}>{since(r.minutes)}</span>
        </div>
      ))}
    </div>
  );
}

function ServerModal({ server: s, ping, onClose }) {
  const [closing, setClosing] = React.useState(false);
  const close = () => { setClosing(true); setTimeout(onClose, 180); };
  React.useEffect(() => {
    const onKey = e => { if (e.key === 'Escape') close(); };
    document.addEventListener('keydown', onKey);
    const prev = document.body.style.overflow, prevH = document.documentElement.style.overflow;
    document.body.style.overflow = 'hidden'; document.documentElement.style.overflow = 'hidden';
    return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = prev; document.documentElement.style.overflow = prevH; };
  }, []);
  const full = s.online && s.players >= s.max;
  return ReactDOM.createPortal((
    <div onClick={close} style={{position:'fixed',inset:0,zIndex:100,background:'rgba(10,8,13,.72)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'6vh 20px 40px',overflowY:'auto',overscrollBehavior:'contain',animation:closing?'mkFade .18s reverse forwards':'mkFade .2s ease'}}>
      <Panel onClick={e => e.stopPropagation()} padding={0} style={{width:'min(680px,100%)',overflow:'hidden',animation:closing?'mkPop .18s reverse forwards':'mkPop .32s cubic-bezier(.2,.7,.2,1)'}}>
        <div style={{position:'relative'}}>
          <img src={MK.mapImg(s.map)} alt={s.map} style={{width:'100%',aspectRatio:'16/7',objectFit:'cover',display:'block',filter:s.online?'none':'grayscale(1) brightness(.8)'}} />
          <div style={{position:'absolute',inset:0,background:'linear-gradient(to top,rgba(20,16,24,.92),rgba(20,16,24,0) 55%)'}}></div>
          <button className="mk-modal-close" onClick={close} style={{position:'absolute',top:14,right:14,background:'rgba(20,16,24,.82)',color:'var(--mk-ivory)'}}>close ✕</button>
          <div style={{position:'absolute',left:24,right:24,bottom:18,display:'flex',alignItems:'flex-end',justifyContent:'space-between',gap:16,flexWrap:'wrap'}}>
            <div>
              <div style={{display:'flex',gap:8,marginBottom:10}}><Badge style={{background:'rgba(20,16,24,.82)',color:'var(--mk-ivory)',border:'none'}}>{s.map}</Badge>{full && <Badge style={{background:'rgba(20,16,24,.82)',color:'var(--mk-violet)',border:'none'}}>full</Badge>}</div>
              <h2 style={{margin:0,fontSize:34,textTransform:'uppercase',fontVariationSettings:"'wdth' 112",lineHeight:1}}>{s.name}</h2>
            </div>
            <StatusDot online={s.online} />
          </div>
        </div>
        <div style={{padding:'20px 24px',display:'flex',alignItems:'center',gap:28,flexWrap:'wrap',borderBottom:'1px solid var(--mk-border)'}}>
          <div><Label size={11}>players</Label><div style={{...mkMono,fontSize:18,color:'var(--mk-ivory)',marginTop:4}}>{s.online ? s.players + ' / ' + s.max : '— / ' + s.max}</div></div>
          <div><Label size={11}>approx. ping</Label><div style={{...mkMono,fontSize:18,color:'var(--mk-ivory)',marginTop:4}}><PingText ms={s.online ? ping : null} /></div></div>
          <div style={{marginLeft:'auto'}}><ConnectActions server={s} size="md" /></div>
          <p style={{margin:0,flexBasis:'100%',fontFamily:'var(--mk-font-serif)',fontStyle:'italic',fontSize:13,color:'var(--mk-smoke)'}}>{s.addr} · ping is {PING_NOTE}</p>
        </div>
        <div style={{padding:'6px 4px 10px'}}>
          <Label size={11} style={{display:'block',padding:'12px 20px 2px'}}>who's on</Label>
          <Roster server={s} />
        </div>
      </Panel>
    </div>
  ), document.body);
}

function ServerCard({ server: s, ping, layout, index }) {
  const [open, setOpen] = React.useState(false);
  const [ref, seen] = useReveal();
  const full = s.online && s.players >= s.max;
  const rise = { animationDelay: ((index || 0) * 0.09) + 's' };
  const revealCls = 'mk-reveal' + (seen ? ' in' : '');
  const modal = open && <ServerModal server={s} ping={ping} onClose={() => setOpen(false)} />;
  if (layout === 'row') {
    return (
      <div ref={ref} className={revealCls} style={{...rise,display:'flex'}}>
      <Panel padding={0} style={{flex:1,opacity:s.online?1:.65}}>
        <div style={{display:'grid',gridTemplateColumns:'150px 1.5fr 1fr .8fr auto',gap:20,alignItems:'center',padding:14}}>
          <div className="mk-card-hit" onClick={() => setOpen(true)} style={{borderRadius:8,overflow:'hidden'}}>
            <img src={MK.mapImg(s.map)} alt={s.map} style={{width:'100%',aspectRatio:'16/9',objectFit:'cover',display:'block',filter:s.online?'none':'grayscale(1)'}} />
          </div>
          <div className="mk-card-hit" onClick={() => setOpen(true)} style={{display:'flex',flexDirection:'column',gap:8,minWidth:0}}>
            <strong style={{fontSize:16,fontWeight:850,textTransform:'uppercase',fontVariationSettings:"'wdth' 112"}}>{s.name}</strong>
            <div style={{display:'flex',gap:8,flexWrap:'wrap'}}><Badge>{s.map}</Badge>{full && <Badge tone="violet">full</Badge>}</div>
          </div>
          <div style={{display:'flex',flexDirection:'column',gap:8}}>
            <StatusDot online={s.online} />
            <span style={{...mkMono,color:'var(--mk-ivory)'}}>{s.online ? s.players + ' / ' + s.max + ' playing' : '— / ' + s.max}</span>
          </div>
          <div style={{display:'flex',flexDirection:'column',gap:6}}>
            <span style={{...mkMono,color:'var(--mk-smoke)'}}>{s.addr}</span>
            <PingText ms={s.online ? ping : null} style={{...mkMono,color:'var(--mk-smoke)'}} />
          </div>
          <ConnectActions server={s} />
        </div>
        {modal}
      </Panel>
      </div>
    );
  }
  return (
    <div ref={ref} className={revealCls} style={{...rise,display:'flex'}}>
    <Panel padding={0} style={{flex:1,overflow:'hidden',opacity:s.online?1:.72,display:'flex',flexDirection:'column'}}>
      <div className="mk-card-hit" onClick={() => setOpen(true)}>
        <div style={{position:'relative',overflow:'hidden'}}>
          <img src={MK.mapImg(s.map)} alt={s.map} style={{width:'100%',aspectRatio:'16/9',objectFit:'cover',display:'block',filter:s.online?'none':'grayscale(1) brightness(.8)'}} />
          <div style={{position:'absolute',left:12,bottom:12,display:'flex',gap:8}}>
            <Badge style={{background:'rgba(20,16,24,.82)',color:'var(--mk-ivory)',border:'none'}}>{s.map}</Badge>
            {full && <Badge style={{background:'rgba(20,16,24,.82)',color:'var(--mk-violet)',border:'none'}}>full</Badge>}
          </div>
          <span style={{position:'absolute',top:12,right:12,fontFamily:'var(--mk-font-mono)',fontSize:10,fontWeight:600,letterSpacing:'0.14em',textTransform:'uppercase',color:'var(--mk-ivory)',background:'rgba(20,16,24,.72)',padding:'5px 9px',borderRadius:8}}>details</span>
        </div>
        <div style={{padding:'18px 20px',display:'flex',flexDirection:'column',gap:14,flex:1}}>
          <div style={{display:'flex',alignItems:'center',gap:12,justifyContent:'space-between'}}>
            <strong style={{fontSize:17,fontWeight:850,textTransform:'uppercase',fontVariationSettings:"'wdth' 112"}}>{s.name}</strong>
            <StatusDot online={s.online} />
          </div>
          <div style={{display:'flex',gap:18,flexWrap:'wrap',alignItems:'center'}}>
            <span style={{...mkMono,color:'var(--mk-ivory)'}}>{s.online ? s.players + ' / ' + s.max + ' playing' : '— / ' + s.max}</span>
            <PingText ms={s.online ? ping : null} style={{...mkMono,color:'var(--mk-smoke)'}} />
          </div>
          <span style={{...mkMono,fontSize:11,color:'var(--mk-smoke)'}}>{s.addr}</span>
        </div>
      </div>
      <div style={{padding:'0 20px 16px',marginTop:'auto'}}>
        <ConnectActions server={s} />
      </div>
      {modal}
    </Panel>
    </div>
  );
}

function LegalPage({ active, eyebrow, title, updated, children }) {
  return (
    <div style={{minHeight:'100vh',display:'flex',flexDirection:'column'}}>
      <SiteNav active={active} />
      <main className="legal" style={{padding:'56px 40px 0',maxWidth:820,width:'100%',margin:'0 auto',flex:1}}>
        <Label>{eyebrow}</Label>
        <h1 style={{margin:'12px 0 6px',fontSize:42}}>{title}</h1>
        <p style={{margin:0,fontFamily:'var(--mk-font-mono)',fontSize:11,letterSpacing:'0.14em',textTransform:'uppercase',color:'var(--mk-smoke)'}}>last updated — {updated}</p>
        {children}
      </main>
      <MkFooter />
    </div>
  );
}

// ── Nav player search: magnifier that expands into a live field on click ──
function PlayerSearch() {
  const [q, setQ] = React.useState('');
  const [open, setOpen] = React.useState(false);
  const [hi, setHi] = React.useState(0);
  const [exp, setExp] = React.useState(false);
  const boxRef = React.useRef(null);
  const inRef = React.useRef(null);
  const results = React.useMemo(() => {
    const s = q.trim().toLowerCase(); if (!s) return [];
    return (MK.PLAYERS || []).map(p => ({ p, i: (p.name + ' ' + p.rank).toLowerCase().indexOf(s) })).filter(x => x.i >= 0).sort((a, b) => a.i - b.i).slice(0, 6).map(x => x.p);
  }, [q]);
  React.useEffect(() => setHi(0), [q]);
  React.useEffect(() => { if (exp && inRef.current) inRef.current.focus(); }, [exp]);
  React.useEffect(() => { const onDoc = e => { if (boxRef.current && !boxRef.current.contains(e.target)) { setOpen(false); if (!q) setExp(false); } }; document.addEventListener('mousedown', onDoc); return () => document.removeEventListener('mousedown', onDoc); }, [q]);
  const go = p => { if (p) location.href = 'Profile.html?p=' + p.slug; };
  const onKey = e => {
    if (e.key === 'ArrowDown') { e.preventDefault(); setOpen(true); setHi(h => Math.min(results.length - 1, h + 1)); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); setHi(h => Math.max(0, h - 1)); }
    else if (e.key === 'Enter') go(results[hi]);
    else if (e.key === 'Escape') { setOpen(false); setExp(false); }
  };
  const mag = (color) => <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{flexShrink:0}}><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>;
  const show = exp && open && q.trim().length > 0;
  return (
    <div ref={boxRef} style={{position:'relative',flexShrink:0}}>
      {exp ? (
        <div style={{display:'flex',alignItems:'center',gap:8,background:'var(--mk-surface)',border:'1px solid var(--mk-border)',borderRadius:10,padding:'8px 11px',width:220}}>
          {mag('var(--mk-smoke)')}
          <input ref={inRef} className="mk-search-input" value={q} onChange={e => { setQ(e.target.value); setOpen(true); }} onKeyDown={onKey} onBlur={() => setTimeout(() => { if (!q) setExp(false); }, 140)} placeholder="search members" aria-label="Search members" style={{width:'100%',boxSizing:'border-box',background:'transparent',border:'none',color:'var(--mk-ivory)',fontFamily:'var(--mk-font-mono)',fontSize:12,letterSpacing:'0.06em',padding:0,outline:'none'}} />
        </div>
      ) : (
        <button onClick={() => setExp(true)} aria-label="Search members" style={{display:'flex',alignItems:'center',justifyContent:'center',width:38,height:38,background:'var(--mk-surface)',border:'1px solid var(--mk-border)',borderRadius:10,cursor:'pointer'}}>{mag('var(--mk-ivory)')}</button>
      )}
      {show && (
        <div style={{position:'absolute',top:'calc(100% + 6px)',right:0,width:240,background:'var(--mk-surface)',border:'1px solid var(--mk-border)',borderRadius:12,overflow:'hidden',zIndex:60}}>
          {results.length === 0 ? (
            <div style={{padding:'14px',fontFamily:'var(--mk-font-serif)',fontStyle:'italic',fontSize:14,color:'var(--mk-smoke)'}}>No members match “{q.trim()}”.</div>
          ) : results.map((p, idx) => (
            <a key={p.slug} href={'Profile.html?p=' + p.slug} onMouseDown={() => go(p)} onMouseEnter={() => setHi(idx)} style={{display:'flex',alignItems:'center',justifyContent:'space-between',gap:12,padding:'10px 13px',textDecoration:'none',background:idx===hi?'rgba(244,239,228,.06)':'transparent',borderTop:idx?'1px solid var(--mk-border)':'none'}}>
              <span style={{display:'flex',flexDirection:'column',gap:3}}>
                <span style={{fontSize:13.5,color:'var(--mk-ivory)'}}>{p.name}</span>
                <span style={{...mkMono,fontSize:9.5,color:'var(--mk-smoke)'}}>{p.rank}</span>
              </span>
              <span style={{...mkMono,fontSize:11,color:'var(--mk-violet)'}}>{p.points.toLocaleString('en-US')}</span>
            </a>
          ))}
        </div>
      )}
    </div>
  );
}

// ── Mock API fetch with loading / error states. ?api=slow or ?api=down to demo. ──
function useMockFetch(getData) {
  const [state, setState] = React.useState({ loading: true, error: null, data: null });
  const load = React.useCallback(() => {
    setState({ loading: true, error: null, data: null });
    const q = new URLSearchParams(location.search).get('api');
    const delay = q === 'slow' ? 2600 : q === 'down' ? 900 : 600;
    const id = setTimeout(() => {
      if (q === 'down') { setState({ loading: false, error: 'unreachable', data: null }); return; }
      try { const d = getData(); setState({ loading: false, error: null, data: d }); }
      catch (e) { setState({ loading: false, error: 'unreachable', data: null }); }
    }, delay);
    return () => clearTimeout(id);
  }, []);
  React.useEffect(() => load(), [load]);
  return { ...state, retry: load };
}

function SkeletonBlock({ w = '100%', h = 16, r = 8, style }) {
  return <div className="mk-shimmer" style={{ width: w, height: h, borderRadius: r, ...style }} />;
}

function PlayerSkeleton() {
  return (
    <main style={{padding:'44px 40px 0',flex:1,maxWidth:1100,width:'100%',margin:'0 auto'}}>
      <SkeletonBlock w={90} h={12} />
      <Panel padding={0} style={{overflow:'hidden',marginTop:18}}>
        <div style={{padding:'11px 24px',borderBottom:'1px solid var(--mk-border)'}}><SkeletonBlock w={260} h={11} /></div>
        <div style={{display:'flex',gap:24,padding:'26px 28px',alignItems:'center'}}>
          <SkeletonBlock w={96} h={96} r={14} />
          <div style={{flex:1,display:'flex',flexDirection:'column',gap:12}}>
            <SkeletonBlock w={140} h={11} />
            <SkeletonBlock w={'46%'} h={40} />
            <SkeletonBlock w={220} h={16} />
          </div>
        </div>
      </Panel>
      <div style={{display:'flex',alignItems:'center',gap:14,margin:'34px 0 20px'}}><SkeletonBlock w={60} h={12} /><SkeletonBlock w={180} h={12} /></div>
      <Panel padding={28}>
        <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(220px,1fr))',gap:'22px 40px'}}>
          {[0,1,2,3,4,5].map(i => <div key={i} style={{display:'flex',flexDirection:'column',gap:8}}><SkeletonBlock w={80} h={11} /><SkeletonBlock h={9} r={5} /></div>)}
        </div>
      </Panel>
      <div style={{display:'flex',alignItems:'center',gap:9,marginTop:22,fontFamily:'var(--mk-font-mono)',fontSize:11,letterSpacing:'0.14em',textTransform:'uppercase',color:'var(--mk-smoke)'}}><span className="mk-live-dot"></span>loading stats from K4-System…</div>
    </main>
  );
}

function ApiError({ onRetry, label }) {
  return (
    <main style={{padding:'80px 40px',flex:1,maxWidth:640,width:'100%',margin:'0 auto',textAlign:'center'}}>
      <Label>connection</Label>
      <h1 style={{margin:'14px 0 0',fontSize:40}}>Can't reach the server</h1>
      <p style={{fontFamily:'var(--mk-font-serif)',fontStyle:'italic',fontSize:20,color:'var(--mk-smoke)',margin:'14px 0 26px'}}>The stats desk isn't answering. Give it a moment.</p>
      <p style={{...mkMono,fontSize:11,color:'var(--mk-smoke)',marginBottom:26}}>{label || 'K4-System · request timed out'}</p>
      {onRetry && <Button variant="secondary" onClick={onRetry}>Try again</Button>}
    </main>
  );
}

function ApiEmpty({ slug }) {
  return (
    <main style={{padding:'80px 40px',flex:1,maxWidth:640,width:'100%',margin:'0 auto',textAlign:'center'}}>
      <Label>not found</Label>
      <h1 style={{margin:'14px 0 0',fontSize:40}}>No member by that name</h1>
      <p style={{fontFamily:'var(--mk-font-serif)',fontStyle:'italic',fontSize:20,color:'var(--mk-smoke)',margin:'14px 0 26px'}}>{slug ? '“' + slug + '” isn\'t on the books.' : 'Pick a member from the standings.'}</p>
      <a href="Standings.html" style={{...mkMono,fontSize:12,color:'var(--mk-violet)',textDecoration:'none'}}>← back to standings</a>
    </main>
  );
}

if (!document.getElementById('mk-api-css')) {
  const st = document.createElement('style'); st.id = 'mk-api-css';
  st.textContent = [
    '@keyframes mkShimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}',
    '.mk-shimmer{background:linear-gradient(90deg,rgba(244,239,228,.04) 25%,rgba(244,239,228,.10) 37%,rgba(244,239,228,.04) 63%);background-size:200% 100%;animation:mkShimmer 1.4s ease-in-out infinite;border-radius:8px}',
    '.mk-search-input:focus{border-color:var(--mk-violet)}',
    '.mk-authbtn{display:inline-flex;align-items:center;gap:8px;white-space:nowrap;font-family:var(--mk-font-mono);font-size:12px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--mk-ivory);background:var(--mk-surface);border:1px solid var(--mk-border);border-radius:10px;padding:9px 14px;cursor:pointer;transition:border-color .14s}',
    '.mk-authbtn:hover{border-color:rgba(244,239,228,.5)}',
    '.mk-authchip{display:flex;align-items:center;gap:9px;background:var(--mk-surface);border:1px solid var(--mk-border);border-radius:10px;padding:5px 11px 5px 6px;cursor:pointer;transition:border-color .14s}',
    '.mk-authchip:hover{border-color:var(--mk-line-violet)}',
    '.mk-authname{font-family:var(--mk-font-mono);font-size:12px;font-weight:600;letter-spacing:.06em;color:var(--mk-ivory);max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
    '.mk-authitem{display:block;width:100%;box-sizing:border-box;text-align:left;padding:11px 15px;font-family:var(--mk-font-mono);font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--mk-smoke);background:none;border:none;text-decoration:none;cursor:pointer}',
    '.mk-authitem+.mk-authitem{border-top:1px solid var(--mk-border)}',
    '.mk-authitem:hover{color:var(--mk-ivory);background:rgba(244,239,228,.05)}',
    '.mk-search-input::placeholder{color:var(--mk-smoke)}',
    '@media (prefers-reduced-motion:reduce){.mk-shimmer{animation:none}}'
  ].join('');
  document.head.appendChild(st);
}

Object.assign(window, { DemoNote, SiteNav, SteamAuth, MkFooter, useApproxPing, PingText, ServerCard, ServerModal, ConnectActions, StatusDot, LegalPage, Roster, PianoStripe, TempoStripe, tempoMark, mkChime, useReveal, Reveal, PlayerSearch, useMockFetch, SkeletonBlock, PlayerSkeleton, ApiError, ApiEmpty });
