← back to Consulting Rentv Com
auto-save: 2026-07-26T08:41:50 (2 files) — server.js public/slideshow.html
aa47540cdd32636a4459a9ecb25d916640449570 · 2026-07-26 08:41:52 -0700 · Steve Abrams
Files touched
A public/slideshow.htmlM server.js
Diff
commit aa47540cdd32636a4459a9ecb25d916640449570
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 26 08:41:52 2026 -0700
auto-save: 2026-07-26T08:41:50 (2 files) — server.js public/slideshow.html
---
public/slideshow.html | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++
server.js | 1 +
2 files changed, 123 insertions(+)
diff --git a/public/slideshow.html b/public/slideshow.html
new file mode 100644
index 0000000..4c9882c
--- /dev/null
+++ b/public/slideshow.html
@@ -0,0 +1,122 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Slideshow · Consulting Deliverable</title>
+<!-- Canonical consulting Slideshow. Source of truth: ~/.claude/skills/consulting/assets/slideshow.html
+ Copied verbatim into each portal's public/slideshow.html; served (gated) at /slideshow.
+ Self-contained, zero deps: fetches the portal's own /portal deliverable and turns each
+ <section id> into a full-screen slide (kicker + headline + subhead). Keyboard/click/dot nav. -->
+<style>
+ :root{--bg:#0c1016;--panel:#12100e;--gold:#c6a765;--ink:#eef1f6;--dim:rgba(238,241,246,.66);--line:rgba(198,167,101,.28)}
+ *{margin:0;padding:0;box-sizing:border-box}
+ html,body{height:100%}
+ body{background:var(--bg);color:var(--ink);font-family:'Jost',system-ui,-apple-system,sans-serif;overflow:hidden}
+ .disp{font-family:'Cormorant Garamond',Georgia,serif}
+ #deck{position:fixed;inset:0}
+ .slide{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;padding:0 clamp(28px,9vw,140px);opacity:0;transform:translateY(24px);transition:opacity .5s ease,transform .5s ease;pointer-events:none}
+ .slide.on{opacity:1;transform:none;pointer-events:auto}
+ .kicker{font-size:clamp(11px,1.4vw,14px);letter-spacing:.36em;text-transform:uppercase;color:var(--gold);margin-bottom:22px}
+ .head{font-family:'Cormorant Garamond',Georgia,serif;font-weight:400;font-size:clamp(34px,6vw,86px);line-height:1.04;max-width:20ch}
+ .head span{color:var(--gold)}
+ .sub{margin-top:26px;max-width:60ch;font-size:clamp(15px,1.9vw,23px);line-height:1.5;color:var(--dim)}
+ .cover .head{font-size:clamp(48px,9vw,120px)}
+ .cover .meta{margin-top:34px;font-size:13px;letter-spacing:.22em;text-transform:uppercase;color:var(--dim)}
+ /* chrome */
+ .bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:rgba(255,255,255,.06);z-index:20}
+ .bar i{display:block;height:100%;background:var(--gold);width:0;transition:width .4s ease}
+ .count{position:fixed;bottom:16px;right:22px;font-size:12px;letter-spacing:.14em;color:var(--dim);z-index:20;font-variant-numeric:tabular-nums}
+ .dots{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);display:flex;gap:7px;z-index:20;max-width:70vw;flex-wrap:wrap;justify-content:center}
+ .dots b{width:7px;height:7px;border-radius:50%;background:rgba(255,255,255,.22);cursor:pointer;transition:.25s}
+ .dots b.on{background:var(--gold);transform:scale(1.35)}
+ .hint{position:fixed;top:16px;left:22px;font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--dim);z-index:20}
+ .exit{position:fixed;top:14px;right:22px;z-index:20;color:var(--gold);text-decoration:none;font-size:12px;letter-spacing:.14em;text-transform:uppercase;border:1px solid var(--line);padding:7px 13px;border-radius:3px}
+ .exit:hover{background:var(--gold);color:var(--bg)}
+ .nav{position:fixed;top:0;bottom:0;width:22%;z-index:15;cursor:pointer}
+ .nav.l{left:0}.nav.r{right:0}
+ .err{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;text-align:center;color:var(--dim);padding:40px}
+ @media(prefers-reduced-motion:reduce){.slide{transition:none}}
+</style>
+<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=Cormorant+Garamond:ital,wght@0,400;0,500;1,400&family=Jost:wght@300;400;500&display=swap" rel="stylesheet">
+</head>
+<body>
+<div class="hint">← → or space · Esc to exit</div>
+<a class="exit" href="/portal">✕ Deliverable</a>
+<div id="deck"><div class="err" id="err">Building the deck…</div></div>
+<div class="nav l" id="navL"></div><div class="nav r" id="navR"></div>
+<div class="dots" id="dots"></div>
+<div class="count" id="count"></div>
+<div class="bar"><i id="prog"></i></div>
+<script>
+(function(){
+ var deck=document.getElementById('deck'), slides=[], idx=0;
+ function esc(s){var d=document.createElement('div');d.textContent=s==null?'':String(s);return d.innerHTML;}
+ function txt(el){return el?el.textContent.replace(/\s+/g,' ').trim():'';}
+
+ fetch('/portal').then(function(r){return r.ok?r.text():Promise.reject(r.status);}).then(function(html){
+ var doc=new DOMParser().parseFromString(html,'text/html');
+ var out=[];
+ // Cover slide from the deliverable's hero (h1 + first hero paragraph) or title.
+ var h1=doc.querySelector('.hero h1, h1');
+ var kick=doc.querySelector('.hero .kick, .kick, .eyebrow');
+ out.push({cover:true, kicker:txt(kick)||'Consulting Deliverable', head:(h1?h1.innerHTML:document.title), sub:txt(doc.querySelector('.hero p'))});
+ // One slide per section.
+ var secs=doc.querySelectorAll('section[id]');
+ if(!secs.length) secs=doc.querySelectorAll('section');
+ secs.forEach(function(s){
+ var k=s.querySelector(':scope > .eyebrow') || s.querySelector('.eyebrow');
+ var h=s.querySelector(':scope > h2, :scope h2, h2, h3');
+ var p=s.querySelector('.lead2, .lead, p');
+ var head=txt(h); if(!head) return;
+ out.push({kicker:txt(k), head:head, sub:txt(p)});
+ });
+ // Closing CTA slide.
+ out.push({cover:true, kicker:'The next step', head:'Pick a direction —<br>and we build it.', sub:'Every section above is one click from execution. Tell us where to start.'});
+ render(out);
+ }).catch(function(e){
+ document.getElementById('err').innerHTML='Could not load the deliverable to build the deck (/portal → '+esc(e)+').<br>Open the <a href="/portal" style="color:#c6a765">full deliverable</a> instead.';
+ });
+
+ function render(items){
+ deck.innerHTML='';
+ items.forEach(function(it,i){
+ var el=document.createElement('div');
+ el.className='slide'+(it.cover?' cover':'');
+ el.innerHTML=(it.kicker?'<div class="kicker">'+esc(it.kicker)+'</div>':'')
+ +'<h1 class="head disp">'+(it.cover?it.head:esc(it.head))+'</h1>'
+ +(it.sub?'<div class="sub">'+esc(it.sub)+'</div>':'')
+ +(it.cover&&i===0?'<div class="meta">Confidential strategy brief · press → to begin</div>':'');
+ deck.appendChild(el);
+ slides.push(el);
+ });
+ var dots=document.getElementById('dots');
+ slides.forEach(function(_,i){var b=document.createElement('b');b.onclick=function(){go(i);};dots.appendChild(b);});
+ go(0);
+ }
+ function go(n){
+ if(!slides.length) return;
+ idx=Math.max(0,Math.min(slides.length-1,n));
+ slides.forEach(function(s,i){s.classList.toggle('on',i===idx);});
+ var db=document.querySelectorAll('.dots b');
+ db.forEach(function(b,i){b.classList.toggle('on',i===idx);});
+ document.getElementById('count').textContent=(idx+1)+' / '+slides.length;
+ document.getElementById('prog').style.width=((idx)/(slides.length-1||1)*100)+'%';
+ }
+ function next(){go(idx+1);} function prev(){go(idx-1);}
+ document.getElementById('navR').onclick=next;
+ document.getElementById('navL').onclick=prev;
+ document.addEventListener('keydown',function(e){
+ if(e.key==='ArrowRight'||e.key===' '||e.key==='PageDown'){e.preventDefault();next();}
+ else if(e.key==='ArrowLeft'||e.key==='PageUp'){e.preventDefault();prev();}
+ else if(e.key==='Home'){go(0);} else if(e.key==='End'){go(slides.length-1);}
+ else if(e.key==='Escape'){location.href='/portal';}
+ });
+ // touch swipe
+ var x0=null;
+ document.addEventListener('touchstart',function(e){x0=e.touches[0].clientX;},{passive:true});
+ document.addEventListener('touchend',function(e){if(x0==null)return;var dx=e.changedTouches[0].clientX-x0;if(Math.abs(dx)>50){dx<0?next():prev();}x0=null;},{passive:true});
+})();
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
index 43fbb30..3eba4a7 100644
--- a/server.js
+++ b/server.js
@@ -79,6 +79,7 @@ app.post('/api/intake', (req, res) => {
// ---- gated routes ----------------------------------------------------------
app.get('/portal', gate, (_req, res) => res.sendFile(path.join(PUB, 'portal.html')));
app.get('/admin', gate, (_req, res) => res.sendFile(path.join(PUB, 'admin', 'index.html')));
+app.get('/slideshow', gate, (_req, res) => res.sendFile(path.join(PUB, 'slideshow.html'))); // deck view of the deliverable
app.get('/api/admin/:bucket', gate, (req, res) => {
if (!BUCKETS.includes(req.params.bucket)) return res.status(404).json({ error: 'unknown bucket' });
res.json(readJSON(req.params.bucket + '.json', []));
← 290685b auto-save: 2026-07-26T07:41:30 (2 files) — public/admin/inde
·
back to Consulting Rentv Com
·
slideshow: deck view of the deliverable at /slideshow (parse 64a8195 →