← back to Consulting Rentv Com
public/slideshow.html
144 lines
<!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():'';}
// Cover heads keep the deliverable's <br>/<span> styling but are whitelist-sanitized:
// never render raw innerHTML from a parsed deliverable (XSS-safe). Only text, <br>, <span>.
function sanitizeHead(el){
var out='', ns=el.childNodes, i;
for(i=0;i<ns.length;i++){
var n=ns[i];
if(n.nodeType===3){ out+=esc(n.nodeValue); }
else if(n.nodeType===1){
var t=n.tagName.toLowerCase();
if(t==='br'){ out+='<br>'; }
else if(t==='span'){ out+='<span>'+esc(n.textContent)+'</span>'; }
else { out+=esc(n.textContent); }
}
}
return out;
}
// The deliverable lives at /portal on template-based portals, or at / on the
// originals (car-wash, fantasea). Try /portal, fall back to /.
var deliverableUrl='/portal';
function load(url){return fetch(url).then(function(r){if(!r.ok)return Promise.reject(r.status);deliverableUrl=url;return r.text();});}
load('/portal').catch(function(){return load('/');}).then(function(html){
var exit=document.querySelector('.exit'); if(exit) exit.href=deliverableUrl;
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?sanitizeHead(h1):esc(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=deliverableUrl;}
});
// 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>