← back to Rentv 2026
public/blog.html
72 lines
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>RENTV Originals — The Blog</title>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
<style>
:root{--red:#c8102e;--ink:#14171a;--sub:#5b636b;--line:#e4e7ea;--bg:#fff;--wash:#f5f6f8}
*{box-sizing:border-box;margin:0;padding:0}body{font-family:'Inter',system-ui,sans-serif;color:var(--ink);background:var(--bg);line-height:1.5}
a{color:inherit;text-decoration:none}.wrap{max-width:1100px;margin:0 auto;padding:0 20px}
.util{background:var(--ink);color:#cfd4d9;font-size:12px}.util .wrap{display:flex;justify-content:space-between;align-items:center;height:34px}.util a{color:#cfd4d9}
.mast{border-bottom:2px solid var(--ink);padding:18px 0 12px;text-align:center}.logo{font-family:'Playfair Display',serif;font-weight:700;font-size:34px;letter-spacing:-1px}.logo span{color:var(--red)}
.logo small{display:block;font-size:10px;letter-spacing:3px;text-transform:uppercase;color:var(--sub);margin-top:2px}
.hd{padding:26px 0 6px}.hd h1{font-family:'Playfair Display',serif;font-size:32px}.hd p{color:var(--sub);margin-top:4px}
.controls{display:flex;justify-content:flex-end;align-items:center;gap:16px;flex-wrap:wrap;padding:6px 0 0}
.controls label{display:inline-flex;align-items:center;gap:7px;font-size:11px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--sub)}
.controls select{font:600 13px 'Inter';padding:7px 10px;border:1.5px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);cursor:pointer}
.controls select:focus{outline:0;border-color:var(--red)}
.controls input[type=range]{width:104px;accent-color:var(--red);cursor:pointer}
@media(max-width:560px){.densl{display:none}}/* density is a no-op on phones — auto-fill is 1-col there regardless of --min; sort still works */
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--min,300px),1fr));gap:22px;padding:16px 0 60px}
.pc{border:1px solid var(--line);border-radius:8px;overflow:hidden;display:block;transition:.2s}.pc:hover{transform:translateY(-4px);box-shadow:0 16px 40px rgba(0,0,0,.08)}
.pc .img{aspect-ratio:16/9;background:linear-gradient(135deg,#475569,#c8102e)}.pc .img img{width:100%;height:100%;object-fit:cover;display:block}
.pc .b{padding:16px 18px}.pc .chip{font-size:11px;font-weight:800;text-transform:uppercase;color:var(--red)}.pc h3{font-family:'Playfair Display',serif;font-size:20px;margin:6px 0}.pc .dek{font-size:14px;color:var(--sub)}.pc .byl{font-size:12px;color:var(--sub);margin-top:8px}
.empty{color:var(--sub);padding:40px 0}
footer{border-top:1px solid var(--line);color:var(--sub);font-size:12px;padding:26px 0;text-align:center}
</style></head><body>
<div class="util"><div class="wrap"><a href="/">RENTV.com · Commercial Real Estate News</a><a href="/admin/publish">Publish →</a></div></div>
<header class="mast"><div class="wrap"><a class="logo" href="/">REN<span>TV</span>.com<small>RENTV Originals</small></a></div></header>
<div class="wrap"><div class="hd"><h1>RENTV Originals</h1><p>Original reporting and analysis from the RENTV newsroom.</p></div>
<div class="controls">
<label>Sort
<select id="sort">
<option value="newest">Newest</option>
<option value="oldest">Oldest</option>
<option value="title">Title A–Z</option>
<option value="cat">Category</option>
</select>
</label>
<label class="densl">Density <input id="dens" type="range" min="1" max="4" step="1" value="2" aria-label="Grid density — slide right for more, smaller cards"></label>
</div>
<div class="grid" id="grid"><p class="empty">Loading…</p></div></div>
<footer><div class="wrap">RENTV.com · <a href="/" style="color:var(--red)">back to the front page</a></div></footer>
<script>
var esc=function(s){return (s||'').replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];});};
var $=function(id){return document.getElementById(id);};
// Every grid gets sort + density (Steve's standing rule). Posts load client-side in full,
// so sorting is honest — it reorders the whole set, not just a visible page.
var ALL=[],
sortKey=localStorage.getItem('rentv.blog.sort')||'newest',
dens=String(Math.max(1,Math.min(4,parseInt(localStorage.getItem('rentv.blog.dens'),10)||2))),
MINW={'1':'380px','2':'320px','3':'270px','4':'235px'}; // higher slider = smaller min = denser
function applyDens(){ $('grid').style.setProperty('--min', MINW[dens]||'320px'); }
function sortItems(a){ var r=a.slice();
if(sortKey==='oldest') r.sort(function(x,y){return (x.created_at||'').localeCompare(y.created_at||'');});
else if(sortKey==='title') r.sort(function(x,y){return (x.title||'').localeCompare(y.title||'');});
else if(sortKey==='cat') r.sort(function(x,y){return (x.cat||'').localeCompare(y.cat||'')||(y.created_at||'').localeCompare(x.created_at||'');});
else r.sort(function(x,y){return (y.created_at||'').localeCompare(x.created_at||'');}); // newest (default)
return r;
}
function card(p){var img=p.image?'<div class="img"><img src="'+esc(p.image)+'" alt="" onerror="this.parentNode.style.background=\'linear-gradient(135deg,#475569,#c8102e)\';this.remove()"></div>':'<div class="img"></div>';
return '<a class="pc" href="/post/'+p.id+'">'+img+'<div class="b"><span class="chip">'+esc(p.cat)+'</span><h3>'+esc(p.title)+'</h3>'+(p.dek?'<div class="dek">'+esc(p.dek)+'</div>':'')+'<div class="byl">By '+esc(p.author)+' · '+esc((p.created_at||'').slice(0,10))+'</div></div></a>';}
function render(){ var g=$('grid');
if(!ALL.length){g.innerHTML='<p class="empty">No published articles yet. <a href="/admin/publish" style="color:var(--red)">Publish the first →</a></p>';return;}
g.innerHTML=sortItems(ALL).map(card).join('');
}
$('sort').value=sortKey;
$('dens').value=dens; applyDens();
$('sort').onchange=function(e){sortKey=e.target.value;localStorage.setItem('rentv.blog.sort',sortKey);render();};
$('dens').oninput=function(e){dens=e.target.value;localStorage.setItem('rentv.blog.dens',dens);applyDens();};
fetch('/api/posts?status=published').then(function(r){return r.json();}).then(function(d){
ALL=d.items||[]; render();
}).catch(function(){$('grid').innerHTML='<p class="empty">Blog temporarily unavailable.</p>';});
</script></body></html>