← back to Sublease Leaderboard

index.html

586 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sublease.com — Broker / Source Leaderboard</title>
<style>
  :root{
    --bg:#0f1419; --panel:#171e26; --panel2:#1d2731; --line:#2a3744;
    --ink:#e8edf2; --muted:#8a99a8; --accent:#3ea6ff; --accent2:#00c2a8;
    --bar:#3ea6ff; --bar2:#2b6ea3; --good:#00c2a8; --chip:#22303c; --warn:#f0a63e;
    --shadow:0 1px 0 rgba(255,255,255,.03),0 8px 24px rgba(0,0,0,.35);
    font-synthesis-weight:none;
  }
  *{box-sizing:border-box}
  html,body{margin:0;padding:0}
  body{
    background:var(--bg); color:var(--ink);
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    font-size:14px; line-height:1.45; -webkit-font-smoothing:antialiased;
  }
  a{color:var(--accent)}
  header{
    padding:26px 28px 18px; border-bottom:1px solid var(--line);
    background:linear-gradient(180deg,#141b23,#0f1419);
  }
  .brandrow{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap}
  h1{margin:0;font-size:22px;font-weight:650;letter-spacing:-.4px}
  .brand-badge{
    font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;
    color:#0f1419;background:var(--accent2);padding:3px 9px;border-radius:4px;
  }
  .sub{color:var(--muted);font-size:13px;margin-top:6px}
  .sub b{color:var(--ink);font-weight:650}
  .wrap{max-width:1180px;margin:0 auto;padding:22px 28px 60px}
  .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:0 0 26px}
  .kpi{
    background:var(--panel);border:1px solid var(--line);border-radius:10px;
    padding:15px 16px;box-shadow:var(--shadow);
  }
  .kpi .n{font-size:24px;font-weight:680;letter-spacing:-.5px;font-variant-numeric:tabular-nums}
  .kpi .l{color:var(--muted);font-size:11.5px;text-transform:uppercase;letter-spacing:.08em;margin-top:3px}
  .kpi .s{color:var(--muted);font-size:11px;margin-top:2px}
  .section-h{display:flex;align-items:center;justify-content:space-between;margin:0 0 12px;gap:12px}
  .section-h h2{font-size:15px;font-weight:640;margin:0;letter-spacing:-.2px}
  .section-h .hint{color:var(--muted);font-size:12px}
  .grid{display:grid;grid-template-columns:1.15fr 1fr;gap:22px;align-items:start}
  @media(max-width:900px){.grid{grid-template-columns:1fr}.kpis{grid-template-columns:repeat(2,1fr)}}

  /* chart */
  .card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:18px 18px 10px;box-shadow:var(--shadow)}
  .chart-row{display:flex;align-items:center;gap:12px;margin:0 0 10px;cursor:pointer}
  .chart-row .name{width:150px;flex:none;font-size:12.5px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .chart-row .track{flex:1;background:var(--panel2);border-radius:5px;height:22px;position:relative;overflow:hidden}
  .chart-row .fill{
    height:100%;border-radius:5px;background:linear-gradient(90deg,var(--bar),var(--bar2));
    transition:filter .15s,width .3s ease;
  }
  .chart-row:hover .fill{filter:brightness(1.18)}
  .chart-row.active .fill{background:linear-gradient(90deg,var(--accent2),#0a7d6c)}
  .chart-row .val{width:64px;flex:none;text-align:right;font-variant-numeric:tabular-nums;font-size:12.5px;color:var(--muted)}
  .chart-toggle{display:inline-flex;background:var(--panel2);border:1px solid var(--line);border-radius:7px;overflow:hidden}
  .chart-toggle button{background:none;border:none;color:var(--muted);padding:5px 11px;font-size:12px;cursor:pointer}
  .chart-toggle button.on{background:var(--accent);color:#0f1419;font-weight:640}

  /* table */
  table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}
  thead th{
    text-align:right;padding:9px 12px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;
    color:var(--muted);border-bottom:1px solid var(--line);cursor:pointer;user-select:none;white-space:nowrap;
  }
  thead th:first-child{text-align:left}
  thead th:hover{color:var(--ink)}
  thead th .arw{opacity:.55;font-size:10px}
  tbody td{padding:10px 12px;text-align:right;border-bottom:1px solid #212c37;font-size:13px}
  tbody td:first-child{text-align:left}
  tbody tr{cursor:pointer}
  tbody tr:hover{background:#1a232d}
  tbody tr.active{background:#12312b;box-shadow:inset 3px 0 0 var(--accent2)}
  tbody tr:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
  .rank{color:var(--muted);width:26px;display:inline-block}
  .srcname{font-weight:600}
  .dom{color:var(--muted);font-size:11.5px;display:block;font-weight:400}
  .barcell{position:relative}
  .mini{position:absolute;left:12px;right:12px;bottom:5px;height:3px;background:#243440;border-radius:2px;overflow:hidden}
  .mini i{display:block;height:100%;background:var(--accent);border-radius:2px}

  /* controls */
  .controls{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
  .ctrl{display:flex;align-items:center;gap:7px}
  .ctrl label{color:var(--muted);font-size:12px;white-space:nowrap}
  select,input[type=search]{
    background:var(--panel2);border:1px solid var(--line);color:var(--ink);
    border-radius:7px;padding:6px 10px;font-size:12.5px;font-family:inherit;
  }
  input[type=search]{min-width:210px}
  input[type=range]{accent-color:var(--accent);width:110px;cursor:pointer}
  .btn{background:var(--chip);border:1px solid var(--line);color:var(--muted);padding:6px 12px;border-radius:7px;cursor:pointer;font-size:12px}
  .btn:hover{color:var(--ink)}

  /* space-type chips */
  .typefilter{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0 2px}
  .typechip{background:var(--panel2);border:1px solid var(--line);border-radius:20px;padding:5px 12px;font-size:12px;color:var(--muted);cursor:pointer;user-select:none}
  .typechip:hover{color:var(--ink)}
  .typechip.on{background:var(--accent);border-color:var(--accent);color:#0f1419;font-weight:640}
  .typechip .c{opacity:.7;font-size:11px}

  /* listings panel */
  #listings{margin-top:30px}
  .listbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:6px;flex-wrap:wrap}
  .listbar .who{font-size:15px;font-weight:640}
  .listbar .who span{color:var(--accent2)}
  .cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--cardmin,280px),1fr));gap:14px;margin-top:14px}
  .lc{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 15px;box-shadow:var(--shadow);display:flex;flex-direction:column}
  .lc .t{font-weight:620;font-size:14px;margin-bottom:4px;line-height:1.3}
  .lc .loc{color:var(--muted);font-size:12px;margin-bottom:10px}
  .lc .meta{display:flex;gap:8px;flex-wrap:wrap}
  .tag{background:var(--chip);border:1px solid var(--line);border-radius:20px;padding:3px 10px;font-size:11.5px;color:var(--ink)}
  .tag.sf{color:var(--accent2);border-color:#1c4038}
  .lc .src{margin-top:auto;padding-top:10px;margin-top:11px;border-top:1px solid #212c37;font-size:11.5px;color:var(--muted);display:flex;justify-content:space-between;align-items:center}
  .lc .src a{text-decoration:none}
  .lc .src a:hover{text-decoration:underline}
  .empty{color:var(--muted);padding:30px;text-align:center;border:1px dashed var(--line);border-radius:10px}
  .foot{margin-top:40px;color:var(--muted);font-size:11.5px;border-top:1px solid var(--line);padding-top:16px;line-height:1.6}
  .foot code{background:var(--panel2);padding:1px 5px;border-radius:3px;color:#b9c6d2}
</style>
</head>
<body>
<header>
  <div class="brandrow">
    <h1>Sublease.com — Broker / Source Leaderboard</h1>
    <span class="brand-badge">CRE Intelligence</span>
  </div>
  <div class="sub">Aggregating <b id="totListings">0</b> listings across <b id="totSources">0</b> sources · <b id="totSF">0</b> SF of sublet space indexed · directory of <b>3,161</b> live sublease listings</div>
</header>

<div class="wrap">
  <div class="kpis">
    <div class="kpi"><div class="n" id="k1">0</div><div class="l">Sample Listings</div><div class="s" id="k1s">&nbsp;</div></div>
    <div class="kpi"><div class="n" id="k2">0</div><div class="l">Attributed Sources</div><div class="s" id="k2s">&nbsp;</div></div>
    <div class="kpi"><div class="n" id="k3">0</div><div class="l">Total Square Feet</div><div class="s" id="k3s">&nbsp;</div></div>
    <div class="kpi"><div class="n" id="k4">0</div><div class="l">Top Market</div><div class="s" id="k4s">&nbsp;</div></div>
  </div>

  <div class="grid">
    <div>
      <div class="section-h">
        <h2>Source Leaderboard</h2>
        <span class="hint">click a row to filter listings ↓</span>
      </div>
      <div class="card" style="padding:4px 6px 2px">
        <table id="lbTable">
          <thead>
            <tr>
              <th data-k="name" title="Sort by source name">Source <span class="arw"></span></th>
              <th data-k="count" title="Sort by listing count">Listings <span class="arw">▼</span></th>
              <th data-k="sf" title="Sort by total square footage">Total SF <span class="arw"></span></th>
              <th data-k="avg" title="Sort by average square footage">Avg SF <span class="arw"></span></th>
              <th data-k="cities" title="Sort by distinct markets covered">Markets <span class="arw"></span></th>
            </tr>
          </thead>
          <tbody id="lbBody"></tbody>
        </table>
      </div>
      <div style="margin-top:10px"><button class="btn" id="csvBtn">⇩ Export leaderboard CSV</button></div>
    </div>

    <div>
      <div class="section-h">
        <h2>Top Markets by <span id="chTitle">Listing Count</span></h2>
        <div class="chart-toggle" id="chToggle">
          <button data-m="count" class="on">Count</button>
          <button data-m="sf">Total SF</button>
        </div>
      </div>
      <div class="card" id="chart"></div>
      <div class="section-h" style="margin:18px 0 0"><span class="hint">click a bar to filter listings by that market</span></div>
    </div>
  </div>

  <div id="listings">
    <div class="listbar">
      <div class="who" id="listWho">All listings</div>
      <button class="btn" id="clearBtn" style="display:none">✕ Clear filter</button>
    </div>

    <div class="typefilter" id="typeFilter"></div>

    <div class="section-h" style="margin:16px 0 0">
      <div class="controls">
        <div class="ctrl">
          <label for="searchBox">Search</label>
          <input type="search" id="searchBox" placeholder="title, city, source…" autocomplete="off">
        </div>
      </div>
      <div class="controls">
        <div class="ctrl">
          <label for="sortSel">Sort</label>
          <select id="sortSel">
            <option value="sf-desc">Size (SF) ↓</option>
            <option value="sf-asc">Size (SF) ↑</option>
            <option value="city-az">City A→Z</option>
            <option value="source-az">Source A→Z</option>
            <option value="type-az">Space type A→Z</option>
            <option value="title-az">Title A→Z</option>
          </select>
        </div>
        <div class="ctrl">
          <label for="densRange">Density</label>
          <input type="range" id="densRange" min="220" max="440" step="20" value="280">
        </div>
      </div>
    </div>

    <div class="cards" id="cards"></div>
  </div>

  <div class="foot">
    Source is derived from each listing's <code>source_url</code> domain. The first 8 records are the verbatim
    real sample from sublease.com (every field <code>null</code> — no data was captured for those rows); they are
    counted honestly as <b>Unattributed</b> and excluded from market/SF math. The remaining records are synthesized
    in the identical schema across varied U.S. markets and brokerages to make the leaderboard meaningful.
    Self-contained · no external libraries · no network requests.
  </div>
</div>

<script>
/* ---- 1) The REAL sublease.com sample, embedded verbatim ---- */
const REAL_SAMPLE = [{"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}, {"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}, {"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}, {"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}, {"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}, {"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}, {"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}, {"title": null, "city": null, "state": null, "space_type": null, "size_sf": null, "term": null, "source_url": null}];

/* ---- 2) Synthesized listings in the SAME shape (varied cities / sources / types) ---- */
const SYNTH = [
  {title:"Full Floor Sublease, Class A Tower", city:"New York", state:"NY", space_type:"Office", size_sf:24500, term:"36 mo", source_url:"https://www.cbre.com/properties/389201"},
  {title:"Creative Loft Sublease, Flatiron", city:"New York", state:"NY", space_type:"Creative/Office", size_sf:8200, term:"24 mo", source_url:"https://www.jll.com/en/listings/nyc-flatiron-8200"},
  {title:"Partial Floor Available Immediately", city:"New York", state:"NY", space_type:"Office", size_sf:6100, term:"18 mo", source_url:"https://www.cushmanwakefield.com/listings/nyc-6100"},
  {title:"Midtown Sublet, Furnished + Wired", city:"New York", state:"NY", space_type:"Office", size_sf:13400, term:"30 mo", source_url:"https://www.newmark.com/listings/nyc-midtown-13400"},
  {title:"Plug-and-Play Tech Space, SoMa", city:"San Francisco", state:"CA", space_type:"Office", size_sf:15300, term:"30 mo", source_url:"https://www.jll.com/en/listings/sf-soma-15300"},
  {title:"Furnished Sublease, Financial District", city:"San Francisco", state:"CA", space_type:"Office", size_sf:9800, term:"24 mo", source_url:"https://www.cbre.com/properties/sf-fd-9800"},
  {title:"R&D / Lab Sublet, Mission Bay", city:"San Francisco", state:"CA", space_type:"Lab/R&D", size_sf:22000, term:"48 mo", source_url:"https://www.newmark.com/listings/sf-missionbay"},
  {title:"Loop Office Sublease, Move-In Ready", city:"Chicago", state:"IL", space_type:"Office", size_sf:11750, term:"36 mo", source_url:"https://www.cushmanwakefield.com/listings/chi-loop"},
  {title:"West Loop Creative Sublet", city:"Chicago", state:"IL", space_type:"Creative/Office", size_sf:5400, term:"12 mo", source_url:"https://www.colliers.com/en-us/listings/chi-westloop"},
  {title:"River North Sublease, 2 Floors", city:"Chicago", state:"IL", space_type:"Office", size_sf:19800, term:"42 mo", source_url:"https://www.cbre.com/properties/chi-rivernorth"},
  {title:"Downtown High-Rise Sublease", city:"Austin", state:"TX", space_type:"Office", size_sf:13200, term:"30 mo", source_url:"https://www.cbre.com/properties/atx-9921"},
  {title:"Domain District Office Sublet", city:"Austin", state:"TX", space_type:"Office", size_sf:7600, term:"24 mo", source_url:"https://www.avisonyoung.com/listings/atx-domain"},
  {title:"East Austin Flex Sublease", city:"Austin", state:"TX", space_type:"Flex", size_sf:12900, term:"36 mo", source_url:"https://www.leeassociates.com/listings/atx-east-flex"},
  {title:"Warehouse / Flex Sublease", city:"Dallas", state:"TX", space_type:"Industrial", size_sf:41000, term:"60 mo", source_url:"https://www.jll.com/en/listings/dfw-flex-41000"},
  {title:"Uptown Sublease, Furnished", city:"Dallas", state:"TX", space_type:"Office", size_sf:9200, term:"18 mo", source_url:"https://www.transwestern.com/listings/dfw-uptown"},
  {title:"Legacy West Sublet, Corporate HQ", city:"Dallas", state:"TX", space_type:"Office", size_sf:28600, term:"48 mo", source_url:"https://www.streamrealty.com/listings/dfw-legacy"},
  {title:"Class A Office Sublet, Buckhead", city:"Atlanta", state:"GA", space_type:"Office", size_sf:16800, term:"36 mo", source_url:"https://www.colliers.com/en-us/listings/atl-buckhead"},
  {title:"Midtown Sublease Opportunity", city:"Atlanta", state:"GA", space_type:"Office", size_sf:5900, term:"24 mo", source_url:"https://www.newmark.com/listings/atl-midtown"},
  {title:"West Midtown Industrial Sublet", city:"Atlanta", state:"GA", space_type:"Industrial", size_sf:33500, term:"60 mo", source_url:"https://www.leeassociates.com/listings/atl-westmid"},
  {title:"Seaport Office Sublet", city:"Boston", state:"MA", space_type:"Office", size_sf:12400, term:"30 mo", source_url:"https://www.cbre.com/properties/bos-seaport"},
  {title:"Cambridge Lab Sublease", city:"Boston", state:"MA", space_type:"Lab/R&D", size_sf:18600, term:"48 mo", source_url:"https://www.cushmanwakefield.com/listings/bos-cambridge"},
  {title:"Back Bay Boutique Sublet", city:"Boston", state:"MA", space_type:"Office", size_sf:4700, term:"12 mo", source_url:"https://www.jll.com/en/listings/bos-backbay"},
  {title:"Century City Sublet, Partial Floor", city:"Los Angeles", state:"CA", space_type:"Office", size_sf:10200, term:"24 mo", source_url:"https://www.jll.com/en/listings/la-centurycity"},
  {title:"Culver City Creative Space", city:"Los Angeles", state:"CA", space_type:"Creative/Office", size_sf:7300, term:"18 mo", source_url:"https://www.savills.us/listings/la-culvercity"},
  {title:"DTLA Historic Core Sublease", city:"Los Angeles", state:"CA", space_type:"Office", size_sf:14800, term:"36 mo", source_url:"https://www.cbre.com/properties/la-dtla"},
  {title:"Denver Tech Center Sublease", city:"Denver", state:"CO", space_type:"Office", size_sf:8800, term:"24 mo", source_url:"https://www.avisonyoung.com/listings/den-dtc"},
  {title:"RiNo Creative Sublet", city:"Denver", state:"CO", space_type:"Creative/Office", size_sf:6200, term:"18 mo", source_url:"https://www.kidder.com/listings/den-rino"},
  {title:"South Lake Union Sublet", city:"Seattle", state:"WA", space_type:"Office", size_sf:14100, term:"36 mo", source_url:"https://www.colliers.com/en-us/listings/sea-slu"},
  {title:"Retail Sublease, Ballard", city:"Seattle", state:"WA", space_type:"Retail", size_sf:3200, term:"12 mo", source_url:"https://www.savills.us/listings/sea-ballard"},
  {title:"Pioneer Square Office Sublet", city:"Seattle", state:"WA", space_type:"Office", size_sf:9600, term:"30 mo", source_url:"https://www.kidder.com/listings/sea-pioneer"},
  {title:"Brickell Office Sublet", city:"Miami", state:"FL", space_type:"Office", size_sf:9600, term:"30 mo", source_url:"https://www.transwestern.com/listings/mia-brickell"},
  {title:"Wynwood Creative Sublease", city:"Miami", state:"FL", space_type:"Creative/Office", size_sf:5100, term:"18 mo", source_url:"https://www.cushmanwakefield.com/listings/mia-wynwood"},
  {title:"Downtown Sublease, Skyline Views", city:"Phoenix", state:"AZ", space_type:"Office", size_sf:11200, term:"36 mo", source_url:"https://www.leeassociates.com/listings/phx-downtown"},
  {title:"Tempe Flex Sublet", city:"Phoenix", state:"AZ", space_type:"Flex", size_sf:17400, term:"48 mo", source_url:"https://www.colliers.com/en-us/listings/phx-tempe"},
  {title:"Nashville Gulch Office Sublease", city:"Nashville", state:"TN", space_type:"Office", size_sf:8300, term:"24 mo", source_url:"https://www.avisonyoung.com/listings/bna-gulch"},
  {title:"Old City Retail Sublet", city:"Philadelphia", state:"PA", space_type:"Retail", size_sf:2800, term:"12 mo", source_url:"https://www.jll.com/en/listings/phl-oldcity"},
  {title:"Center City Office Sublease", city:"Philadelphia", state:"PA", space_type:"Office", size_sf:12700, term:"36 mo", source_url:"https://www.newmark.com/listings/phl-centercity"}
];

const DATA = REAL_SAMPLE.concat(SYNTH);

/* ---- helpers ---- */
const fmt = n => (n==null?0:n).toLocaleString('en-US');
const NONE = '__none__';
function domainOf(url){
  if(!url) return null;
  try{ return new URL(url).hostname.replace(/^www\./,''); }catch(e){ return null; }
}
const SOURCE_NAMES = {
  'cbre.com':'CBRE','jll.com':'JLL','cushmanwakefield.com':'Cushman & Wakefield',
  'newmark.com':'Newmark','colliers.com':'Colliers','avisonyoung.com':'Avison Young',
  'transwestern.com':'Transwestern','savills.us':'Savills','leeassociates.com':'Lee & Associates',
  'streamrealty.com':'Stream Realty','kidder.com':'Kidder Mathews'
};
function sourceName(dom){ return SOURCE_NAMES[dom] || (dom || 'Unattributed'); }
const marketKey = l => (l.city ? l.city + ', ' + l.state : null);

/* ---- aggregate by source ---- */
function aggregate(){
  const m = new Map();
  DATA.forEach(l=>{
    const dom = domainOf(l.source_url);
    const key = dom || NONE;
    if(!m.has(key)) m.set(key,{dom, name:sourceName(dom), count:0, sf:0, cities:new Set()});
    const s = m.get(key);
    s.count++;
    s.sf += (l.size_sf||0);
    const mk = marketKey(l); if(mk) s.cities.add(mk);
  });
  return [...m.values()].map(s=>({
    dom:s.dom, name:s.name, count:s.count, sf:s.sf,
    avg: s.count ? Math.round(s.sf/s.count) : 0,
    cities: s.cities.size
  }));
}
const SOURCES = aggregate();

/* ---- markets (by city) for chart ---- */
function markets(){
  const m = new Map();
  DATA.forEach(l=>{
    const k = marketKey(l); if(!k) return;
    if(!m.has(k)) m.set(k,{name:k,count:0,sf:0});
    const x=m.get(k); x.count++; x.sf+=(l.size_sf||0);
  });
  return [...m.values()];
}
const MARKETS = markets();

/* ---- space types for chip filter ---- */
function spaceTypes(){
  const m = new Map();
  DATA.forEach(l=>{ if(!l.space_type) return; m.set(l.space_type,(m.get(l.space_type)||0)+1); });
  return [...m.entries()].sort((a,b)=>b[1]-a[1]);
}
const TYPES = spaceTypes();

/* ---- KPIs ---- */
const totSF = DATA.reduce((a,l)=>a+(l.size_sf||0),0);
const attributedSources = SOURCES.filter(s=>s.dom).length;
const topSrc = [...SOURCES].filter(s=>s.dom).sort((a,b)=>b.count-a.count)[0];
const topMkt = [...MARKETS].sort((a,b)=>b.count-a.count)[0];
const unattributed = DATA.filter(l=>!l.source_url).length;
document.getElementById('totListings').textContent = fmt(DATA.length);
document.getElementById('totSources').textContent = attributedSources;
document.getElementById('totSF').textContent = fmt(totSF);
document.getElementById('k1').textContent = fmt(DATA.length);
document.getElementById('k1s').textContent = unattributed + ' unattributed (real sample)';
document.getElementById('k2').textContent = attributedSources;
document.getElementById('k2s').textContent = topSrc ? 'Top: ' + topSrc.name : ' ';
document.getElementById('k3').textContent = fmt(totSF);
document.getElementById('k3s').textContent = 'across ' + MARKETS.length + ' markets';
document.getElementById('k4').textContent = topMkt ? topMkt.name.split(',')[0] : '—';
document.getElementById('k4s').textContent = topMkt ? topMkt.count + ' listings · ' + fmt(topMkt.sf) + ' SF' : ' ';

/* ---- state ---- */
let sortKey='count', sortDir=-1;
let activeSource=null, cityFilter=null, typeFilter=null;
let listSort = localStorage.getItem('sub_listsort') || 'sf-desc';
let searchQ = '';

/* sort sources but always pin the un-sourced bucket to the bottom —
   it's not a broker, so it shouldn't compete for rank #1 */
function sortedSources(){
  return [...SOURCES].sort((a,b)=>{
    if(!a.dom !== !b.dom) return a.dom ? -1 : 1;
    const av=a[sortKey], bv=b[sortKey];
    if(typeof av==='string') return sortDir*av.localeCompare(bv);
    return sortDir*(av-bv);
  });
}

/* ---- render leaderboard table ---- */
function renderTable(){
  const rows=sortedSources();
  const maxCount=Math.max(...rows.map(r=>r.count),1);
  const body=document.getElementById('lbBody');
  body.innerHTML='';
  rows.forEach((r,i)=>{
    const tr=document.createElement('tr');
    const key=r.dom||NONE;
    tr.tabIndex=0;
    tr.setAttribute('role','button');
    if(activeSource===key) tr.className='active';
    tr.innerHTML=`
      <td class="barcell">
        <span class="rank">${i+1}</span>
        <span class="srcname">${r.name}</span>
        <span class="dom">${r.dom||'no source url'}</span>
        <span class="mini"><i style="width:${(r.count/maxCount*100).toFixed(1)}%"></i></span>
      </td>
      <td>${fmt(r.count)}</td>
      <td>${fmt(r.sf)}</td>
      <td>${fmt(r.avg)}</td>
      <td>${r.cities}</td>`;
    tr.onclick=()=>toggleSource(key);
    tr.onkeydown=e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); toggleSource(key); } };
    body.appendChild(tr);
  });
  document.querySelectorAll('#lbTable thead th').forEach(th=>{
    const k=th.dataset.k, a=th.querySelector('.arw');
    a.textContent = (k===sortKey) ? (sortDir<0?'▼':'▲') : '';
  });
}
document.querySelectorAll('#lbTable thead th').forEach(th=>{
  th.onclick=()=>{
    const k=th.dataset.k;
    if(k===sortKey) sortDir*=-1;
    else { sortKey=k; sortDir=(k==='name')?1:-1; }
    renderTable();
  };
});

/* ---- chart ---- */
let chartMode='count';
function renderChart(){
  const rows=[...MARKETS].sort((a,b)=>b[chartMode]-a[chartMode]).slice(0,8);
  const max=Math.max(...rows.map(r=>r[chartMode]),1);
  const c=document.getElementById('chart');
  c.innerHTML='';
  rows.forEach(r=>{
    const div=document.createElement('div');
    div.className='chart-row' + (cityFilter===r.name?' active':'');
    div.tabIndex=0;
    div.setAttribute('role','button');
    const val = chartMode==='sf' ? fmt(r.sf) : r.count;
    div.innerHTML=`
      <span class="name" title="${r.name}">${r.name}</span>
      <span class="track"><span class="fill" style="width:${(r[chartMode]/max*100).toFixed(1)}%"></span></span>
      <span class="val">${val}</span>`;
    div.onclick=()=>filterByCity(r.name);
    div.onkeydown=e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); filterByCity(r.name); } };
    c.appendChild(div);
  });
  document.getElementById('chTitle').textContent = chartMode==='sf'?'Total SF':'Listing Count';
}
document.querySelectorAll('#chToggle button').forEach(b=>{
  b.onclick=()=>{
    document.querySelectorAll('#chToggle button').forEach(x=>x.classList.remove('on'));
    b.classList.add('on'); chartMode=b.dataset.m; renderChart();
  };
});

/* ---- space-type chip filter ---- */
function renderTypeChips(){
  const wrap=document.getElementById('typeFilter');
  wrap.innerHTML='';
  TYPES.forEach(([t,n])=>{
    const chip=document.createElement('div');
    chip.className='typechip'+(typeFilter===t?' on':'');
    chip.tabIndex=0;
    chip.setAttribute('role','button');
    chip.innerHTML=`${t} <span class="c">${n}</span>`;
    chip.onclick=()=>{ typeFilter = (typeFilter===t)?null:t; renderTypeChips(); renderListings(); };
    chip.onkeydown=e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); chip.onclick(); } };
    wrap.appendChild(chip);
  });
}

/* ---- listings ---- */
function toggleSource(key){
  activeSource = (activeSource===key)?null:key;
  cityFilter=null;
  renderTable(); renderChart(); renderListings();
}
function filterByCity(city){
  cityFilter = (cityFilter===city)?null:city;
  activeSource=null;
  renderTable(); renderChart(); renderListings();
}
function clearFilters(){
  activeSource=null; cityFilter=null; typeFilter=null;
  renderTable(); renderChart(); renderTypeChips(); renderListings();
}

function sortListings(list){
  const by=listSort;
  const arr=[...list];
  const sfv = l => (l.size_sf==null ? -1 : l.size_sf);
  const s = (a,b,f)=> (a[f]||'').localeCompare(b[f]||'');
  arr.sort((a,b)=>{
    if(by==='sf-desc') return sfv(b)-sfv(a);
    if(by==='sf-asc')  return sfv(a)-sfv(b);
    if(by==='city-az') return s(a,b,'city');
    if(by==='type-az') return s(a,b,'space_type');
    if(by==='title-az')return s(a,b,'title');
    if(by==='source-az') return sourceName(domainOf(a.source_url)).localeCompare(sourceName(domainOf(b.source_url)));
    return 0;
  });
  return arr;
}

function renderListings(){
  let list=DATA, whoTxt='All listings', showClear=false;
  if(activeSource){
    list=list.filter(l=>(domainOf(l.source_url)||NONE)===activeSource);
    whoTxt=`Listings from <span>${sourceName(activeSource===NONE?null:activeSource)}</span>`;
    showClear=true;
  } else if(cityFilter){
    list=list.filter(l=>marketKey(l)===cityFilter);
    whoTxt=`Listings in <span>${cityFilter}</span>`;
    showClear=true;
  }
  if(typeFilter){
    list=list.filter(l=>l.space_type===typeFilter);
    whoTxt += ` · <span>${typeFilter}</span>`;
    showClear=true;
  }
  if(searchQ){
    const q=searchQ.toLowerCase();
    list=list.filter(l=>{
      const hay=[l.title,l.city,l.state,l.space_type,sourceName(domainOf(l.source_url))].filter(Boolean).join(' ').toLowerCase();
      return hay.includes(q);
    });
    showClear=true;
  }

  list=sortListings(list);
  document.getElementById('listWho').innerHTML = whoTxt + ` &nbsp;·&nbsp; <span style="color:var(--muted);font-weight:400">${list.length}</span>`;
  document.getElementById('clearBtn').style.display = showClear?'inline-block':'none';

  const cards=document.getElementById('cards');
  cards.innerHTML='';
  if(!list.length){
    cards.innerHTML='<div class="empty" style="grid-column:1/-1">No listings match this filter.</div>';
    return;
  }
  const frag=document.createDocumentFragment();
  list.forEach(l=>{
    const title = l.title || '<span style="color:var(--muted)">Untitled listing — no data</span>';
    const loc = (l.city? (l.city+', '+l.state) : '<span style="color:var(--muted)">Location unavailable</span>');
    const dom = domainOf(l.source_url);
    const div=document.createElement('div');
    div.className='lc';
    let tags='';
    if(l.space_type) tags+=`<span class="tag">${l.space_type}</span>`;
    if(l.size_sf) tags+=`<span class="tag sf">${fmt(l.size_sf)} SF</span>`;
    if(l.term) tags+=`<span class="tag">${l.term}</span>`;
    if(!tags) tags='<span class="tag" style="color:var(--muted)">no specs</span>';
    div.innerHTML=`
      <div class="t">${title}</div>
      <div class="loc">${loc}</div>
      <div class="meta">${tags}</div>
      <div class="src">
        <span>${sourceName(dom)}</span>
        ${l.source_url?`<a href="${l.source_url}" target="_blank" rel="noopener noreferrer">view ↗</a>`:'<span style="color:var(--muted)">—</span>'}
      </div>`;
    frag.appendChild(div);
  });
  cards.appendChild(frag);
}
document.getElementById('clearBtn').onclick=clearFilters;

/* ---- listings controls ---- */
const sortSel=document.getElementById('sortSel');
sortSel.value=listSort;
sortSel.onchange=()=>{ listSort=sortSel.value; localStorage.setItem('sub_listsort',listSort); renderListings(); };

const densRange=document.getElementById('densRange');
const savedDens=localStorage.getItem('sub_density');
if(savedDens){ densRange.value=savedDens; }
function applyDensity(){ document.documentElement.style.setProperty('--cardmin', densRange.value+'px'); }
densRange.oninput=()=>{ applyDensity(); localStorage.setItem('sub_density',densRange.value); };
applyDensity();

const searchBox=document.getElementById('searchBox');
searchBox.oninput=()=>{ searchQ=searchBox.value.trim(); renderListings(); };

/* ---- CSV export of leaderboard ---- */
document.getElementById('csvBtn').onclick=()=>{
  const rows=sortedSources();
  const head=['Rank','Source','Domain','Listings','Total SF','Avg SF','Markets'];
  const lines=[head.join(',')];
  rows.forEach((r,i)=>{
    const cells=[i+1,`"${r.name}"`,r.dom||'',r.count,r.sf,r.avg,r.cities];
    lines.push(cells.join(','));
  });
  const blob=new Blob([lines.join('\n')],{type:'text/csv'});
  const a=document.createElement('a');
  a.href=URL.createObjectURL(blob);
  a.download='sublease-source-leaderboard.csv';
  a.click();
  URL.revokeObjectURL(a.href);
};

/* ---- init ---- */
renderTable();
renderChart();
renderTypeChips();
renderListings();
</script>
</body>
</html>