[object Object]

← back to Rentv

Add CRE Market Pulse (rates terminal) page + front-page ticker link, nav, and live embed

78d7b01db19f511e5cea2fbebca380f5e464ce91 · 2026-07-24 23:44:42 -0700 · Steve

Files touched

Diff

commit 78d7b01db19f511e5cea2fbebca380f5e464ce91
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Jul 24 23:44:42 2026 -0700

    Add CRE Market Pulse (rates terminal) page + front-page ticker link, nav, and live embed
---
 public/index.html        |  58 ++++-
 public/market-pulse.html | 550 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 605 insertions(+), 3 deletions(-)

diff --git a/public/index.html b/public/index.html
index b3800ca1..72d812a3 100644
--- a/public/index.html
+++ b/public/index.html
@@ -43,6 +43,8 @@
   .datastrip{border-bottom:1px solid #e8e4e0;background:#f7f5f2}
   .datastrip .in{max-width:1240px;margin:0 auto;display:flex;gap:34px;padding:9px 22px;font-size:11px;font-weight:500;letter-spacing:.06em;color:#4a4f55;overflow:hidden;white-space:nowrap}
   .datastrip .m b{color:#14171a;font-weight:700;margin-left:6px}.datastrip .m b.dn{color:var(--red)}.datastrip .m b.up{color:var(--up)}
+  .datastrip .mp-link{margin-left:auto;white-space:nowrap;color:var(--red);font-weight:800;letter-spacing:.08em;text-transform:uppercase;font-size:10.5px;flex:none}
+  .datastrip .mp-link:hover{text-decoration:underline}
   .fold{min-height:calc(100vh - 54px);display:flex;flex-direction:column}
   .wrap{max-width:1240px;margin:0 auto;padding:0 22px;width:100%}
   .hero{flex:1;display:grid;grid-template-columns:1.55fr 1fr;gap:34px;align-items:center;padding:28px 0}
@@ -92,6 +94,30 @@
   footer a{display:block;color:#9aa4ad;padding:3px 0}footer .logo{font-family:'Playfair Display',serif;font-size:22px;font-weight:800;color:#fff}footer .logo span{color:var(--red)}
   .fine{max-width:1240px;margin:18px auto 0;padding:14px 22px 0;border-top:1px solid #1c232b;color:#6b747d}
   @media(max-width:900px){.hero,.river,footer .cols{grid-template-columns:1fr}.lead{padding-right:0;border-right:0}.river{grid-template-columns:1fr 1fr}}
+
+  /* ---------- CRE Market Pulse embed (dark rates terminal on the light homepage) ---------- */
+  .mp{background:linear-gradient(180deg,#0b0f16,#080b11);border:1px solid #1c2536;border-radius:14px;color:#e8edf5;padding:22px 22px 20px;margin:8px 0 6px;
+    --mp-mono:"SFMono-Regular",ui-monospace,"JetBrains Mono","Menlo",Consolas,monospace}
+  .mp .mp-top{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;flex-wrap:wrap;margin-bottom:16px}
+  .mp .mp-k{color:#f0b429;font-weight:800;font-size:11px;letter-spacing:.16em;text-transform:uppercase}
+  .mp .mp-h{font-family:'Playfair Display',serif;font-size:clamp(22px,2.4vw,30px);font-weight:700;letter-spacing:-.01em;margin-top:3px}
+  .mp .mp-sub{color:#8a97ad;font-size:12px;margin-top:2px}
+  .mp .mp-view{color:#38bdf8;font-weight:800;font-size:12px;letter-spacing:.06em;text-transform:uppercase;white-space:nowrap}
+  .mp .mp-view:hover{text-decoration:underline}
+  .mp .mp-kpis{display:grid;grid-template-columns:repeat(6,1fr);gap:10px}
+  .mp .mp-kpi{background:linear-gradient(180deg,#111826,#0e131c);border:1px solid #1c2536;border-radius:10px;padding:12px 13px;position:relative;overflow:hidden}
+  .mp .mp-kpi::after{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:#38bdf8;opacity:.5}
+  .mp .mp-kpi.up::after{background:#22c55e}.mp .mp-kpi.down::after{background:#f04141}.mp .mp-kpi.flat::after{background:#8a97ad}
+  .mp .mp-kpi .l{font-size:10px;letter-spacing:1.1px;text-transform:uppercase;color:#5b6879}
+  .mp .mp-kpi .v{font-family:var(--mp-mono);font-size:21px;font-weight:600;margin-top:6px;font-variant-numeric:tabular-nums}
+  .mp .mp-kpi .d{margin-top:6px;display:inline-flex;align-items:center;gap:4px;font-family:var(--mp-mono);font-size:11px;padding:2px 7px;border-radius:6px;font-variant-numeric:tabular-nums}
+  .mp .mp-kpi.up .d{color:#22c55e;background:rgba(34,197,94,.14)}.mp .mp-kpi.down .d{color:#f04141;background:rgba(240,65,65,.14)}.mp .mp-kpi.flat .d{color:#8a97ad;background:rgba(138,151,173,.12)}
+  .mp .mp-funds{display:flex;gap:26px;flex-wrap:wrap;margin-top:16px;padding-top:14px;border-top:1px solid #161d2b}
+  .mp .mp-fund .fl{font-size:10px;letter-spacing:1px;text-transform:uppercase;color:#5b6879}
+  .mp .mp-fund .fv{font-family:var(--mp-mono);font-size:19px;font-weight:600;color:#f0b429;margin-top:3px}
+  .mp .mp-fund .fm{font-size:11px;color:#8a97ad;margin-top:1px}
+  @media(max-width:900px){.mp .mp-kpis{grid-template-columns:repeat(3,1fr)}}
+  @media(max-width:560px){.mp .mp-kpis{grid-template-columns:repeat(2,1fr)}}
 </style></head>
 <body>
 <header class="top">
@@ -104,7 +130,7 @@
   <div class="dh"><span class="w">REN<span>TV</span></span><button id="dclose" aria-label="Close">✕</button></div>
   <div class="snap" id="snap"></div>
   <div class="grp"><div class="gt">Discover</div></div>
-  <nav><a href="/hub" style="font-weight:700">✨ Intelligence Hub — all tools</a><a href="/search">🔎 Search</a><a href="/pulse">📊 Market Pulse</a><a href="/sector">🏙️ Sector Deep-Dives</a><a href="/metros">📍 Deal Flow by Metro</a><a href="/tools">🧮 CRE Toolkit</a><a href="/watchlist">⭐ Deal Watchlist</a><a href="/directory">🏢 CRE Directory</a><a href="/compare">⚖️ Deal Comparison</a><a href="/brief">📰 The RENTV Brief</a><a href="/syndicate">📡 Syndicate</a></nav>
+  <nav><a href="/hub" style="font-weight:700">✨ Intelligence Hub — all tools</a><a href="/search">🔎 Search</a><a href="/market-pulse">📈 Rates &amp; Capital Markets</a><a href="/pulse">📊 Market Pulse</a><a href="/sector">🏙️ Sector Deep-Dives</a><a href="/metros">📍 Deal Flow by Metro</a><a href="/tools">🧮 CRE Toolkit</a><a href="/watchlist">⭐ Deal Watchlist</a><a href="/directory">🏢 CRE Directory</a><a href="/compare">⚖️ Deal Comparison</a><a href="/brief">📰 The RENTV Brief</a><a href="/syndicate">📡 Syndicate</a></nav>
   <div class="grp"><div class="gt">Sections</div></div>
   <nav><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a><a href="/development">Development</a><a href="/multifamily">Multifamily</a><a href="/industrial">Industrial</a><a href="/retail">Retail</a></nav>
   <div class="grp"><div class="gt">Intelligence</div></div>
@@ -123,6 +149,20 @@
 </div>
 
 <main class="wrap">
+  <section class="sec" style="padding-bottom:0">
+    <a class="mp" id="mpEmbed" href="/market-pulse" style="display:block">
+      <div class="mp-top">
+        <div>
+          <div class="mp-k">CRE Market Pulse</div>
+          <div class="mp-h">Rates &amp; Capital Markets</div>
+          <div class="mp-sub">U.S. Treasury curve · credit spreads · SoCal fundamentals — live</div>
+        </div>
+        <span class="mp-view">View full pulse →</span>
+      </div>
+      <div class="mp-kpis" id="mpKpis"></div>
+      <div class="mp-funds" id="mpFunds"></div>
+    </a>
+  </section>
   <section class="sec"><h2 class="rule">Across the market</h2><div class="river" id="river"></div></section>
   <div class="cta"><h3>The morning CRE briefing, in your inbox.</h3><input id="email" type="email" placeholder="you@company.com"><button id="subbtn">Subscribe</button><span class="ok" id="subok">✓ You're in.</span></div>
 </main>
@@ -194,11 +234,23 @@ async function loadTicker(){
   const t=await jget('/api/ticker'); if(!t)return;
   const metrics=Object.values(t).filter(m=>m&&m.label&&m.value!=null);
   const cell=m=>{const cls=m.dir==='up'?'up':m.dir==='down'?'dn':'';const chg=m.change_pct!=null?` ${m.change_pct>0?'+':''}${m.change_pct}%`:'';return `<span class="m">${esc(m.label)}<b class="${cls}">${esc(m.value)}${chg}</b></span>`;};
-  $('datastrip').innerHTML=metrics.map(cell).join('');
+  $('datastrip').innerHTML=metrics.map(cell).join('')+'<a class="mp-link" href="/market-pulse">CRE Market Pulse →</a>';
   $('snap').innerHTML=metrics.map(m=>`<div class="m"><div class="k">${esc(m.label)}</div><div class="v ${m.dir==='up'?'up':m.dir==='down'?'dn':''}">${esc(m.value)}</div></div>`).join('');
 }
 $('subbtn').onclick=function(){var em=($('email').value||'').trim();if(!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(em)){$('email').focus();return;}
   this.setAttribute('disabled','');fetch('/api/subscribe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:em,source:'homepage'})}).then(r=>r.json()).then(j=>{if(j&&j.ok){$('subok').style.display='inline';$('email').setAttribute('disabled','');}else this.removeAttribute('disabled');}).catch(()=>this.removeAttribute('disabled'));};
-load(); setInterval(load,120000); loadTicker(); setInterval(loadTicker,120000);
+async function loadPulseEmbed(){
+  const box=$('mpEmbed'); if(!box)return;
+  const d=await jget('/api/markets');
+  if(!d||!Array.isArray(d.curve)||!d.curve.length){box.style.display='none';return;}
+  const arr=x=>x==='up'?'▲':x==='down'?'▼':'●';
+  const nb=s=>{const n=parseFloat(String(s).replace(/[^0-9.\-]/g,''));return isNaN(n)?0:n;};
+  const curve=(d.curve||[]).map(c=>`<div class="mp-kpi ${esc(c.dir)}"><div class="l">${esc(c.label)} UST</div><div class="v">${esc(c.value)}</div><div class="d">${arr(c.dir)} ${Math.abs(c.change_bps||0)} bps</div></div>`);
+  const spreads=(d.spreads||[]).map(s=>`<div class="mp-kpi ${esc(s.dir)}"><div class="l">${esc(s.label)}</div><div class="v">${esc(String(s.value).replace(/\s*bps/,''))}<span style="font-size:12px;color:#5b6879"> bps</span></div><div class="d">${arr(s.dir)} ${nb(s.value)>=0?'steepening':'inverted'}</div></div>`);
+  $('mpKpis').innerHTML=curve.concat(spreads).join('');
+  $('mpFunds').innerHTML=(d.fundamentals||[]).map(f=>`<div class="mp-fund"><div class="fl">${esc(f.label)}</div><div class="fv">${esc(f.value)}</div><div class="fm">As of ${esc(f.as_of)} · ${esc(f.source)}</div></div>`).join('');
+  box.style.display='block';
+}
+load(); setInterval(load,120000); loadTicker(); setInterval(loadTicker,120000); loadPulseEmbed(); setInterval(loadPulseEmbed,120000);
 </script>
 </body></html>
diff --git a/public/market-pulse.html b/public/market-pulse.html
new file mode 100644
index 00000000..05ad55ba
--- /dev/null
+++ b/public/market-pulse.html
@@ -0,0 +1,550 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>CRE Market Pulse — RENTV.com</title>
+<meta name="description" content="RENTV CRE Market Pulse — live U.S. Treasury yield curve, credit spreads, REIT performance and Western-U.S. commercial-real-estate fundamentals.">
+<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;0,800;1,600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+<style>
+  :root{
+    --bg:#080b11;
+    --panel:#0e131c;
+    --panel-2:#111826;
+    --line:#1c2536;
+    --line-soft:#161d2b;
+    --ink:#e8edf5;
+    --ink-dim:#8a97ad;
+    --ink-faint:#5b6879;
+    --up:#22c55e;
+    --up-soft:rgba(34,197,94,.14);
+    --down:#f04141;
+    --down-soft:rgba(240,65,65,.14);
+    --flat:#8a97ad;
+    --accent:#f0b429;      /* RENTV amber */
+    --accent-2:#38bdf8;    /* data cyan */
+    --mono:"SFMono-Regular",ui-monospace,"JetBrains Mono","Menlo",Consolas,monospace;
+    --sans:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
+  }
+  *{box-sizing:border-box}
+  html,body{margin:0;padding:0}
+  a{color:inherit;text-decoration:none}
+  body{
+    background:
+      radial-gradient(1200px 600px at 80% -10%, rgba(56,189,248,.06), transparent 60%),
+      radial-gradient(900px 500px at -10% 110%, rgba(240,180,41,.05), transparent 55%),
+      var(--bg);
+    color:var(--ink);
+    font-family:var(--sans);
+    -webkit-font-smoothing:antialiased;
+    letter-spacing:.1px;
+    min-height:100vh;
+  }
+
+  /* ---------- House chrome (white top bar + dark drawer) — colors hardcoded to
+       the RENTV house palette so the dark page's :root vars can't recolor them. ---------- */
+  .top{position:sticky;top:0;z-index:900;display:flex;align-items:center;justify-content:space-between;height:54px;padding:0 16px;background:#fff;border-bottom:1px solid #e4e7ea}
+  .top .burger{display:inline-flex;align-items:center;gap:10px;border:0;background:none;cursor:pointer;font:inherit;color:#14171a;padding:7px 9px;border-radius:8px}
+  .top .burger:hover{background:rgba(0,0,0,.06)}
+  .top .burger .lbl{font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:#5b636b}
+  .top .brand{position:absolute;left:50%;transform:translateX(-50%);font-family:'Playfair Display',Georgia,serif;font-weight:800;font-size:25px;letter-spacing:.02em;color:#14171a}
+  .top .brand span{color:#c8102e}
+  .top .user{width:37px;height:37px;border-radius:50%;border:1.5px solid #e4e7ea;background:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:#14171a}
+  .top .user:hover{border-color:#c8102e;color:#c8102e}
+  .scrim{position:fixed;inset:0;background:rgba(6,9,12,.5);opacity:0;pointer-events:none;transition:opacity .22s;z-index:950}
+  .scrim.open{opacity:1;pointer-events:auto}
+  .drawer{position:fixed;top:0;left:0;height:100%;width:360px;max-width:88vw;background:#0b0e12;color:#e7ecf1;transform:translateX(-102%);transition:transform .26s cubic-bezier(.4,0,.2,1);z-index:951;display:flex;flex-direction:column;overflow-y:auto;box-shadow:2px 0 40px rgba(0,0,0,.4)}
+  .drawer.open{transform:none}
+  .drawer .dh{display:flex;align-items:center;justify-content:space-between;padding:16px 18px;border-bottom:1px solid #1c232b}
+  .drawer .dh .w{font-family:'Playfair Display',serif;font-size:23px;font-weight:800;color:#fff}.drawer .dh .w span{color:#c8102e}
+  .drawer .dh button{background:none;border:0;color:#8b949e;font-size:22px;cursor:pointer;line-height:1}
+  .drawer .grp{padding:14px 18px 4px}.drawer .grp .gt{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:#6b747d;margin-bottom:6px}
+  .drawer nav a{display:block;padding:9px 18px;color:#dbe1e7;font-size:14.5px;border-left:3px solid transparent}
+  .drawer nav a:hover{background:#141a21;border-left-color:#c8102e;color:#fff}
+  .drawer nav a.active{background:#141a21;border-left-color:#c8102e;color:#fff;font-weight:700}
+  .drawer .df{margin-top:auto;padding:16px 18px;border-top:1px solid #1c232b;font-size:11px;color:#6b747d}
+
+  .wrap{max-width:1360px;margin:0 auto;padding:22px 26px 46px}
+
+  /* ---------- Page title band ---------- */
+  .pulse-band{
+    display:flex;align-items:center;justify-content:space-between;
+    gap:20px;flex-wrap:wrap;
+    padding-bottom:18px;margin-bottom:22px;
+    border-bottom:1px solid var(--line);
+  }
+  .pb-head{display:flex;flex-direction:column;gap:4px}
+  .pb-head .k{color:var(--accent);font-weight:800;font-size:11px;letter-spacing:2px;text-transform:uppercase}
+  .pb-head h1{font-family:'Playfair Display',serif;font-size:clamp(28px,3vw,40px);font-weight:700;letter-spacing:-.01em;color:var(--ink)}
+  .pb-head .d{font-size:12px;letter-spacing:.5px;color:var(--ink-faint)}
+  .status{display:flex;align-items:center;gap:22px;flex-wrap:wrap}
+  .clock{display:flex;flex-direction:column;align-items:flex-end;line-height:1.3}
+  .clock .lbl{font-size:10px;letter-spacing:1.6px;color:var(--ink-faint);text-transform:uppercase}
+  .clock .val{font-family:var(--mono);font-size:14px;color:var(--ink);font-variant-numeric:tabular-nums}
+  .live{
+    display:flex;align-items:center;gap:8px;
+    font-size:11px;letter-spacing:1.4px;text-transform:uppercase;color:var(--up);
+    padding:5px 11px;border:1px solid rgba(34,197,94,.3);border-radius:999px;
+    background:var(--up-soft);
+  }
+  .dot{width:8px;height:8px;border-radius:50%;background:var(--up);box-shadow:0 0 0 0 rgba(34,197,94,.6);animation:pulse 2s infinite}
+  @keyframes pulse{0%{box-shadow:0 0 0 0 rgba(34,197,94,.55)}70%{box-shadow:0 0 0 7px rgba(34,197,94,0)}100%{box-shadow:0 0 0 0 rgba(34,197,94,0)}}
+
+  /* ---------- Section titles ---------- */
+  .sec-title{
+    display:flex;align-items:center;gap:10px;
+    font-size:11px;letter-spacing:2px;text-transform:uppercase;color:var(--ink-dim);
+    margin:0 0 12px;
+  }
+  .sec-title::before{content:"";width:3px;height:13px;background:var(--accent);border-radius:2px}
+  .sec-title .hint{margin-left:auto;letter-spacing:.5px;text-transform:none;color:var(--ink-faint);font-size:11px}
+
+  /* ---------- KPI tiles ---------- */
+  .kpis{
+    display:grid;grid-template-columns:repeat(6,1fr);gap:12px;margin-bottom:26px;
+  }
+  .kpi{
+    background:linear-gradient(180deg,var(--panel-2),var(--panel));
+    border:1px solid var(--line);border-radius:11px;padding:14px 15px;
+    position:relative;overflow:hidden;
+  }
+  .kpi::after{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--accent-2);opacity:.5}
+  .kpi.up::after{background:var(--up)}
+  .kpi.down::after{background:var(--down)}
+  .kpi.flat::after{background:var(--flat)}
+  .kpi .k-lbl{font-size:10.5px;letter-spacing:1.2px;text-transform:uppercase;color:var(--ink-faint)}
+  .kpi .k-val{font-family:var(--mono);font-size:25px;font-weight:600;margin-top:8px;font-variant-numeric:tabular-nums}
+  .kpi .k-delta{
+    margin-top:7px;display:inline-flex;align-items:center;gap:5px;
+    font-family:var(--mono);font-size:12px;padding:2px 8px;border-radius:6px;font-variant-numeric:tabular-nums;
+  }
+  .up .k-delta{color:var(--up);background:var(--up-soft)}
+  .down .k-delta{color:var(--down);background:var(--down-soft)}
+  .flat .k-delta{color:var(--flat);background:rgba(138,151,173,.12)}
+  .arrow{font-size:10px;line-height:1}
+
+  /* ---------- Grid of panels ---------- */
+  .grid{display:grid;grid-template-columns:1.35fr 1fr;gap:16px;margin-bottom:16px}
+  .grid.lower{grid-template-columns:1fr 1fr}
+  .panel{
+    background:linear-gradient(180deg,var(--panel-2),var(--panel));
+    border:1px solid var(--line);border-radius:14px;padding:18px 18px 16px;
+  }
+  canvas{display:block;width:100%}
+
+  /* legend / caption under curve */
+  .curve-foot{
+    display:flex;justify-content:space-between;gap:12px;margin-top:12px;
+    padding-top:12px;border-top:1px solid var(--line-soft);
+    font-size:11px;color:var(--ink-faint);flex-wrap:wrap;
+  }
+  .curve-foot b{color:var(--ink-dim);font-weight:600}
+
+  /* ---------- Credit spreads ---------- */
+  .spreads{display:flex;flex-direction:column;gap:16px;margin-top:4px}
+  .spread .row1{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:8px}
+  .spread .s-lbl{font-size:13px;color:var(--ink)}
+  .spread .s-val{font-family:var(--mono);font-size:16px;font-weight:600}
+  .spread .s-val.up{color:var(--up)}
+  .spread .s-val.down{color:var(--down)}
+  .bar-track{height:9px;background:var(--line-soft);border-radius:6px;overflow:hidden;position:relative}
+  .bar-fill{height:100%;border-radius:6px;background:linear-gradient(90deg,rgba(56,189,248,.5),var(--accent-2));transition:width .8s cubic-bezier(.2,.7,.2,1)}
+  .spread small{color:var(--ink-faint);font-size:10.5px;letter-spacing:.3px}
+
+  /* ---------- REIT bars ---------- */
+  .reits{display:flex;flex-direction:column;gap:15px;margin-top:2px}
+  .reit .r-top{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:8px}
+  .reit .r-lbl{font-size:13px;color:var(--ink)}
+  .reit .r-val{font-family:var(--mono);font-size:15px;color:var(--ink);font-variant-numeric:tabular-nums}
+  .reit .r-track{position:relative;height:26px;background:var(--line-soft);border-radius:7px;display:flex;align-items:center}
+  .reit .r-mid{position:absolute;left:50%;top:0;bottom:0;width:1px;background:var(--line);opacity:.9}
+  .reit .r-bar{position:absolute;top:5px;bottom:5px;border-radius:5px;transition:width .8s cubic-bezier(.2,.7,.2,1)}
+  .reit .r-bar.up{left:50%;background:linear-gradient(90deg,rgba(34,197,94,.35),var(--up))}
+  .reit .r-bar.down{right:50%;background:linear-gradient(270deg,rgba(240,65,65,.35),var(--down))}
+  .reit .r-pct{position:absolute;font-family:var(--mono);font-size:12px;font-weight:600;font-variant-numeric:tabular-nums}
+  .reit .r-pct.up{left:calc(50% + 10px);color:var(--up)}
+  .reit .r-pct.down{right:calc(50% + 10px);color:var(--down)}
+  .reit .r-pct.flat{left:calc(50% + 10px);color:var(--flat)}
+  .reit-scale{display:flex;justify-content:space-between;font-size:10px;color:var(--ink-faint);font-family:var(--mono);margin-top:10px;padding-top:8px;border-top:1px solid var(--line-soft)}
+
+  /* ---------- Fundamentals ---------- */
+  .funds{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:2px}
+  .fund{
+    background:var(--panel);border:1px solid var(--line);border-radius:11px;padding:15px;
+    display:flex;flex-direction:column;gap:2px;
+  }
+  .fund .f-lbl{font-size:10.5px;letter-spacing:1.1px;text-transform:uppercase;color:var(--ink-faint)}
+  .fund .f-val{font-family:var(--mono);font-size:28px;font-weight:600;color:var(--accent);margin:6px 0 4px}
+  .fund .f-meta{font-size:11px;color:var(--ink-dim)}
+  .fund .f-src{font-size:10.5px;color:var(--ink-faint);margin-top:3px}
+  .tag{display:inline-block;font-size:9.5px;letter-spacing:.6px;color:var(--accent-2);border:1px solid rgba(56,189,248,.25);border-radius:5px;padding:1px 6px;margin-top:8px;width:fit-content}
+
+  footer.pulse-foot{
+    margin-top:30px;padding-top:16px;border-top:1px solid var(--line);
+    display:flex;justify-content:space-between;gap:14px;flex-wrap:wrap;
+    font-size:11px;color:var(--ink-faint);
+  }
+  footer.pulse-foot .disc{max-width:70%}
+
+  @media (max-width:1040px){
+    .kpis{grid-template-columns:repeat(3,1fr)}
+    .grid,.grid.lower{grid-template-columns:1fr}
+  }
+  @media (max-width:560px){
+    .kpis{grid-template-columns:repeat(2,1fr)}
+    .funds{grid-template-columns:1fr}
+    .wrap{padding:16px 14px 40px}
+  }
+</style>
+</head>
+<body>
+
+<!-- ── House chrome ── -->
+<header class="top">
+  <button class="burger" id="burger" aria-label="Menu">
+    <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
+    <span class="lbl">Menu</span>
+  </button>
+  <a class="brand" href="/">REN<span>TV</span></a>
+  <a class="user" href="/search" aria-label="Search" title="Search">
+    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
+  </a>
+</header>
+
+<div class="scrim" id="scrim"></div>
+<aside class="drawer" id="drawer">
+  <div class="dh"><div class="w">REN<span>TV</span></div><button id="dx" aria-label="Close">&times;</button></div>
+  <div class="grp"><div class="gt">Discover</div></div>
+  <nav><a href="/search">🔎 Search</a><a href="/market-pulse" class="active">📈 Rates &amp; Capital Markets</a><a href="/pulse">📊 Market Pulse (Deal Flow)</a><a href="/markets">🏙️ Markets</a></nav>
+  <div class="grp"><div class="gt">Sections</div></div>
+  <nav><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a><a href="/development">Development</a><a href="/multifamily">Multifamily</a><a href="/industrial">Industrial</a><a href="/retail">Retail</a></nav>
+  <div class="grp"><div class="gt">Intelligence</div></div>
+  <nav><a href="/deals">Deals</a><a href="/la-commercial">LA Commercial</a><a href="/desk">Broker &amp; Owner Desk</a><a href="/review">The REview</a><a href="/cre-talk">CRE Talk</a><a href="/conferences">Conferences</a><a href="/blog">Blog</a></nav>
+  <div class="grp"><div class="gt">Company</div></div>
+  <nav><a href="/about">About</a><a href="/advertise">Advertise</a><a href="/subscribe">Subscribe / Newsletters</a></nav>
+  <div class="df">RENTV.com — Commercial real estate news, Western U.S.</div>
+</aside>
+
+<div class="wrap">
+
+  <div class="pulse-band">
+    <div class="pb-head">
+      <span class="k">CRE Market Pulse</span>
+      <h1>Rates &amp; Capital Markets</h1>
+      <span class="d">U.S. Treasury curve · credit spreads · REIT performance · SoCal fundamentals</span>
+    </div>
+    <div class="status">
+      <div class="live"><span class="dot"></span>Live Feed</div>
+      <div class="clock">
+        <span class="lbl">Data as of</span>
+        <span class="val" id="timestamp">—</span>
+      </div>
+    </div>
+  </div>
+
+  <!-- KPI TILES -->
+  <div class="sec-title">US Treasury Yield Curve <span class="hint" id="curveHint"></span></div>
+  <div class="kpis" id="kpis"></div>
+
+  <!-- CHARTS -->
+  <div class="grid">
+    <div class="panel">
+      <div class="sec-title">Yield Curve <span class="hint">Constant-maturity Treasury yields (%)</span></div>
+      <canvas id="yieldCurve" height="300"></canvas>
+      <div class="curve-foot" id="curveFoot"></div>
+    </div>
+    <div class="panel">
+      <div class="sec-title">Credit &amp; Curve Spreads <span class="hint">basis points</span></div>
+      <div class="spreads" id="spreads"></div>
+    </div>
+  </div>
+
+  <!-- LOWER -->
+  <div class="grid lower">
+    <div class="panel">
+      <div class="sec-title">REIT Performance <span class="hint">session change</span></div>
+      <div class="reits" id="reits"></div>
+      <div class="reit-scale"><span>−1.0%</span><span>flat</span><span>+1.0%</span></div>
+    </div>
+    <div class="panel">
+      <div class="sec-title">Market Fundamentals <span class="hint">SoCal CRE</span></div>
+      <div class="funds" id="funds"></div>
+    </div>
+  </div>
+
+  <footer class="pulse-foot">
+    <div class="disc">
+      Snapshot for informational purposes only — not investment advice. Yields are constant-maturity
+      Treasury rates; REIT levels are last-session close. Fundamentals sourced from cited market reports.
+    </div>
+    <div>© <span id="yr"></span> RENTV.com · CRE Market Pulse</div>
+  </footer>
+
+</div>
+
+<script>
+/* ============================================================
+   Live source: /api/markets (cron-refreshed via Yahoo Finance).
+   Falls back to the embedded snapshot below if the fetch fails,
+   so the terminal always renders even offline. Same payload shape.
+   ============================================================ */
+const SNAPSHOT = {
+  "curve": [
+    {"label": "3-Month", "value": "3.80%", "change_bps": 5, "dir": "up"},
+    {"label": "5-Year",  "value": "4.46%", "change_bps": 5, "dir": "up"},
+    {"label": "10-Year", "value": "4.70%", "change_bps": 5, "dir": "up"},
+    {"label": "30-Year", "value": "5.17%", "change_bps": 2, "dir": "up"}
+  ],
+  "spreads": [
+    {"label": "30Y – 10Y (curve)", "value": "+47 bps", "dir": "up"},
+    {"label": "10Y – 3M (curve)",  "value": "+90 bps", "dir": "up"}
+  ],
+  "reits": [
+    {"label": "DJ U.S. Real Estate",       "value": "394.84",   "change_pct": -0.22, "dir": "down"},
+    {"label": "MSCI U.S. REIT",            "value": "1,289.55", "change_pct": 0,     "dir": "flat"},
+    {"label": "Vanguard REIT ETF (VNQ)",   "value": "$98.69",   "change_pct": -0.84, "dir": "down"}
+  ],
+  "fundamentals": [
+    {"label": "LA OFFICE VAC",      "value": "23.1%", "as_of": "Q2 2026", "source": "CBRE/JLL market reports"},
+    {"label": "IE INDUSTRIAL CAP",  "value": "5.2%",  "as_of": "Q2 2026", "source": "Colliers IE industrial"}
+  ]
+};
+let marketData = SNAPSHOT;
+
+/* ---------- helpers ---------- */
+const $  = s => document.querySelector(s);
+const num = s => parseFloat(String(s).replace(/[^0-9.\-]/g, ''));
+const arrow = d => d === 'up' ? '▲' : d === 'down' ? '▼' : '●';
+function getCSS(v){ return getComputedStyle(document.documentElement).getPropertyValue(v).trim() || 'monospace'; }
+
+/* ---------- drawer ---------- */
+const drawer = $('#drawer'), scrim = $('#scrim');
+$('#burger').onclick = () => { drawer.classList.add('open'); scrim.classList.add('open'); };
+const closeDrawer = () => { drawer.classList.remove('open'); scrim.classList.remove('open'); };
+$('#dx').onclick = closeDrawer; scrim.onclick = closeDrawer;
+document.addEventListener('keydown', e => { if (e.key === 'Escape') closeDrawer(); });
+
+/* ---------- 1. KPI tiles (curve tenors + spreads) ---------- */
+function renderKPIs(){
+  const el = $('#kpis');
+  const curveTiles = marketData.curve.map(c => `
+    <div class="kpi ${c.dir}">
+      <div class="k-lbl">${c.label} UST</div>
+      <div class="k-val">${c.value}</div>
+      <div class="k-delta"><span class="arrow">${arrow(c.dir)}</span>${Math.abs(c.change_bps)} bps</div>
+    </div>`);
+  const spreadTiles = marketData.spreads.map(s => `
+    <div class="kpi ${s.dir}">
+      <div class="k-lbl">${s.label}</div>
+      <div class="k-val">${s.value.replace(/\s*bps/,'')}<span style="font-size:13px;color:var(--ink-faint)"> bps</span></div>
+      <div class="k-delta"><span class="arrow">${arrow(s.dir)}</span>${num(s.value) >= 0 ? 'steepening' : 'inverted'}</div>
+    </div>`);
+  el.innerHTML = curveTiles.concat(spreadTiles).join('');
+
+  const dirs = marketData.curve.map(c => c.dir);
+  const allUp = dirs.every(d => d === 'up');
+  const allDn = dirs.every(d => d === 'down');
+  $('#curveHint').textContent = allUp ? 'Bear steepening — all tenors higher'
+    : allDn ? 'Bull move — all tenors lower' : '';
+}
+
+/* ---------- 2. Yield curve line chart (hand-drawn canvas, DPR-aware) ---------- */
+function drawYieldCurve(){
+  const cv  = $('#yieldCurve');
+  const dpr = window.devicePixelRatio || 1;
+  const cssW = cv.clientWidth || 640;
+  const cssH = parseInt(cv.getAttribute('height'), 10);
+  cv.width  = cssW * dpr;
+  cv.height = cssH * dpr;
+  const ctx = cv.getContext('2d');
+  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+  ctx.clearRect(0, 0, cssW, cssH);
+
+  const data = marketData.curve;
+  const padL = 46, padR = 22, padT = 30, padB = 34;
+  const W = cssW - padL - padR;
+  const H = cssH - padT - padB;
+
+  const vals = data.map(d => num(d.value));
+  const dMin = Math.min(...vals), dMax = Math.max(...vals);
+  const lo = Math.floor((dMin - 0.25) * 4) / 4;
+  const hi = Math.ceil((dMax + 0.25) * 4) / 4;
+  const x = i => padL + (data.length === 1 ? W/2 : i * (W / (data.length - 1)));
+  const y = v => padT + H - ((v - lo) / (hi - lo)) * H;
+
+  ctx.font = '11px ' + getCSS('--mono');
+  ctx.textBaseline = 'middle';
+
+  const steps = 4;
+  for (let i = 0; i <= steps; i++){
+    const v = lo + (hi - lo) * (i / steps);
+    const yy = y(v);
+    ctx.strokeStyle = 'rgba(28,37,54,.9)';
+    ctx.lineWidth = 1;
+    ctx.beginPath(); ctx.moveTo(padL, yy); ctx.lineTo(padL + W, yy); ctx.stroke();
+    ctx.fillStyle = '#5b6879';
+    ctx.textAlign = 'right';
+    ctx.fillText(v.toFixed(2) + '%', padL - 8, yy);
+  }
+
+  const grad = ctx.createLinearGradient(0, padT, 0, padT + H);
+  grad.addColorStop(0, 'rgba(56,189,248,.28)');
+  grad.addColorStop(1, 'rgba(56,189,248,0)');
+  ctx.beginPath();
+  ctx.moveTo(x(0), y(vals[0]));
+  for (let i = 1; i < data.length; i++) ctx.lineTo(x(i), y(vals[i]));
+  ctx.lineTo(x(data.length - 1), padT + H);
+  ctx.lineTo(x(0), padT + H);
+  ctx.closePath();
+  ctx.fillStyle = grad;
+  ctx.fill();
+
+  ctx.beginPath();
+  ctx.moveTo(x(0), y(vals[0]));
+  for (let i = 1; i < data.length; i++) ctx.lineTo(x(i), y(vals[i]));
+  ctx.strokeStyle = '#38bdf8';
+  ctx.lineWidth = 2.5;
+  ctx.lineJoin = 'round';
+  ctx.shadowColor = 'rgba(56,189,248,.5)';
+  ctx.shadowBlur = 10;
+  ctx.stroke();
+  ctx.shadowBlur = 0;
+
+  data.forEach((d, i) => {
+    const px = x(i), py = y(vals[i]);
+    ctx.beginPath(); ctx.arc(px, py, 4.5, 0, Math.PI * 2);
+    ctx.fillStyle = '#0e131c'; ctx.fill();
+    ctx.lineWidth = 2.5; ctx.strokeStyle = '#38bdf8'; ctx.stroke();
+
+    ctx.fillStyle = '#e8edf5';
+    ctx.font = 'bold 12px ' + getCSS('--mono');
+    if (i === 0)                    { ctx.textAlign = 'left';  ctx.fillText(d.value, px - 4,  py - 16); }
+    else if (i === data.length - 1) { ctx.textAlign = 'right'; ctx.fillText(d.value, px + 4,  py - 16); }
+    else                            { ctx.textAlign = 'center';ctx.fillText(d.value, px,      py - 16); }
+
+    ctx.fillStyle = '#8a97ad';
+    ctx.font = '11px ' + getCSS('--sans');
+    ctx.fillText(d.label.replace('-', ' '), px, padT + H + 18);
+  });
+
+  // caption footer — computed from live data so it never contradicts the tiles
+  const front = data[0], back = data[data.length - 1];
+  const spread = Math.round((num(back.value) - num(front.value)) * 100);
+  $('#curveFoot').innerHTML = `
+    <span>Front end <b>${front.value}</b> · Long end <b>${back.value}</b></span>
+    <span>Curve is <b>${spread >= 0 ? 'upward-sloping' : 'inverted'}</b> (${spread >= 0 ? '+' : ''}${spread} bps ${front.label.replace('-',' ')}→${back.label.replace('-',' ')})</span>`;
+}
+
+/* ---------- 3. Credit / curve spreads ---------- */
+function renderSpreads(){
+  const el = $('#spreads');
+  const vals = marketData.spreads.map(s => Math.abs(num(s.value)));
+  const max = Math.max(...vals, 1);
+  el.innerHTML = marketData.spreads.map(s => {
+    const v = num(s.value);
+    const pct = Math.max(6, Math.abs(v) / max * 100);
+    return `
+      <div class="spread">
+        <div class="row1">
+          <span class="s-lbl">${s.label}</span>
+          <span class="s-val ${s.dir}">${arrow(s.dir)} ${s.value}</span>
+        </div>
+        <div class="bar-track"><div class="bar-fill" style="width:0%" data-w="${pct}"></div></div>
+        <small>${Math.abs(v)} bps ${v >= 0 ? 'positive' : 'inverted'} · steeper vs prior</small>
+      </div>`;
+  }).join('');
+  requestAnimationFrame(() => el.querySelectorAll('.bar-fill')
+    .forEach(b => b.style.width = b.dataset.w + '%'));
+}
+
+/* ---------- 4. REIT performance (diverging bars around zero) ---------- */
+function renderReits(){
+  const el = $('#reits');
+  const SCALE = 1.0; // ±1% = full half-width
+  el.innerHTML = marketData.reits.map(r => {
+    const p = r.change_pct;
+    const w = Math.min(50, Math.abs(p) / SCALE * 50);
+    const bar = r.dir === 'up'
+      ? `<div class="r-bar up" style="width:0%" data-w="${w}"></div>`
+      : r.dir === 'down'
+        ? `<div class="r-bar down" style="width:0%" data-w="${w}"></div>`
+        : '';
+    const pctTxt = (p > 0 ? '+' : '') + p.toFixed(2) + '%';
+    return `
+      <div class="reit">
+        <div class="r-top">
+          <span class="r-lbl">${r.label}</span>
+          <span class="r-val">${r.value}</span>
+        </div>
+        <div class="r-track">
+          <div class="r-mid"></div>
+          ${bar}
+          <span class="r-pct ${r.dir}">${arrow(r.dir)} ${pctTxt}</span>
+        </div>
+      </div>`;
+  }).join('');
+  requestAnimationFrame(() => el.querySelectorAll('.r-bar')
+    .forEach(b => b.style.width = b.dataset.w + '%'));
+}
+
+/* ---------- 5. Fundamentals KPI tiles ---------- */
+function renderFunds(){
+  $('#funds').innerHTML = marketData.fundamentals.map(f => `
+    <div class="fund">
+      <span class="f-lbl">${f.label}</span>
+      <span class="f-val">${f.value}</span>
+      <span class="f-meta">As of ${f.as_of}</span>
+      <span class="f-src">${f.source}</span>
+      <span class="tag">MARKET REPORT</span>
+    </div>`).join('');
+}
+
+/* ---------- live timestamp ---------- */
+let feedAsOf = null;
+function tick(){
+  const now = feedAsOf || new Date();
+  const opts = { year:'numeric', month:'short', day:'numeric',
+                 hour:'2-digit', minute:'2-digit', second:'2-digit' };
+  $('#timestamp').textContent = now.toLocaleString(undefined, opts) +
+    ' ' + (Intl.DateTimeFormat().resolvedOptions().timeZone || '');
+}
+
+/* ---------- render everything from the current marketData ---------- */
+function renderAll(){
+  renderKPIs();
+  renderSpreads();
+  renderReits();
+  renderFunds();
+  drawYieldCurve();
+}
+
+/* ---------- pull live data, keep the snapshot as fallback ---------- */
+async function loadLive(){
+  try{
+    const r = await fetch('/api/markets', { credentials:'same-origin' });
+    if(!r.ok) return;
+    const d = await r.json();
+    if(d && Array.isArray(d.curve) && d.curve.length){
+      marketData = d;
+      feedAsOf = d.fetched_at ? new Date(d.fetched_at) : null;
+      renderAll();
+    }
+  }catch(e){ /* keep snapshot */ }
+}
+
+/* ---------- boot ---------- */
+function boot(){
+  $('#yr').textContent = new Date().getFullYear();
+  renderAll();          // instant paint from snapshot
+  loadLive();           // then upgrade to live
+  tick(); setInterval(tick, 1000);
+  setInterval(loadLive, 120000); // refresh with the site's cron cadence
+}
+boot();
+
+let rt;
+window.addEventListener('resize', () => { clearTimeout(rt); rt = setTimeout(drawYieldCurve, 120); });
+</script>
+</body>
+</html>

← 220feb94 auto-save: 2026-07-24T23:30:18 (3 files) — data/markets.json  ·  back to Rentv  ·  auto-save: 2026-07-25T00:01:48 (4 files) — data/markets.json ce438f64 →