← back to Age Theme Slider
public/index.html
400 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>Theme Slider by Age</title>
<meta name="description" content="A theme slider that morphs any design through 8 age bands — toddler through elder. Drop-in for any CSS-var-based site.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600&family=Nunito:wght@400;700&family=Public+Sans:wght@400;600&family=Space+Grotesk:wght@400;700&family=JetBrains+Mono&family=Source+Serif+Pro:ital,wght@0,400;0,600;1,400&family=Atkinson+Hyperlegible:wght@400;700&display=swap">
<style>
:root {
--bg: #0f1115;
--panel: #181b22;
--fg: #e8eaee;
--muted: #a1a8b3;
--accent: #c9a96e;
--border: #2a2f38;
--radius: 12px;
--gap: 16px;
--body: 15px;
--h: 22px;
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-line-height: 1.5;
--btn-min-height: 44px;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--fg);
font-family: var(--font);
font-size: var(--body);
line-height: var(--font-line-height);
min-height: 100vh;
transition: background 0.25s, color 0.25s, font-family 0.25s, font-size 0.25s;
}
h1, h2, h3 { font-size: var(--h); margin: 0 0 12px; line-height: 1.25; }
h1 { font-size: calc(var(--h) * 1.4); }
p { margin: 0 0 12px; }
button, .btn {
min-height: var(--btn-min-height);
background: var(--accent);
color: var(--bg);
border: 1px solid var(--accent);
border-radius: var(--radius);
padding: 10px 18px;
font-family: inherit;
font-size: inherit;
cursor: pointer;
font-weight: 600;
}
button.ghost { background: transparent; color: var(--fg); border: 1px solid var(--border); }
.panel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--gap);
margin-bottom: var(--gap);
}
/* ─── Slider widget (the actual product) ─────────────────────────── */
.slider-shell {
position: fixed;
left: 0; right: 0; bottom: 0;
z-index: 9999;
background: rgba(15,17,21,0.96);
border-top: 1px solid rgba(255,255,255,0.08);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
padding: 16px 20px 18px;
display: grid;
grid-template-columns: auto auto 1fr auto auto;
grid-template-rows: auto auto;
gap: 8px 16px;
align-items: center;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
line-height: 1.3;
transition: transform 0.25s ease;
}
.slider-shell.collapsed { transform: translateY(calc(100% - 50px)); }
.age-display {
font-size: 28px;
font-weight: 700;
color: var(--accent);
min-width: 92px;
text-align: center;
line-height: 1;
}
.age-display small { display: block; font-size: 11px; font-weight: 500; color: rgba(255,255,255,0.7); letter-spacing: 1.5px; margin-top: 4px; text-transform: uppercase; }
.band-label {
font-size: 13px;
color: rgba(255,255,255,0.75);
text-transform: uppercase;
letter-spacing: 1.5px;
font-weight: 600;
}
.age-slider {
flex: 1;
width: 100%;
accent-color: var(--accent);
height: 6px;
}
.age-ticks {
grid-column: 3;
grid-row: 2;
display: flex;
justify-content: space-between;
font-size: 10px;
color: rgba(255,255,255,0.5);
padding: 0 4px;
pointer-events: none;
}
.age-tagline {
grid-column: 1 / -1;
grid-row: 3;
font-size: 12px;
color: rgba(255,255,255,0.55);
text-align: center;
font-style: italic;
}
.slider-action {
background: transparent;
color: rgba(255,255,255,0.7);
border: 1px solid rgba(255,255,255,0.18);
border-radius: 8px;
padding: 8px 14px;
font-size: 13px;
cursor: pointer;
min-height: 38px;
}
.slider-action:hover { color: #fff; border-color: rgba(255,255,255,0.4); }
.slider-close {
width: 38px; height: 38px;
border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
padding: 0; font-size: 18px; line-height: 1;
}
/* The “handle” strip shown when collapsed, doubles as click-to-reopen */
.slider-handle {
position: absolute;
top: -1px;
left: 50%;
transform: translate(-50%, -100%);
background: rgba(15,17,21,0.96);
color: rgba(255,255,255,0.85);
border: 1px solid rgba(255,255,255,0.08);
border-bottom: none;
border-radius: 12px 12px 0 0;
padding: 6px 16px;
font-size: 12px;
cursor: pointer;
display: none;
}
.slider-shell.collapsed .slider-handle { display: block; }
@media (max-width: 720px) {
.slider-shell { grid-template-columns: auto 1fr auto; }
.age-ticks { display: none; }
.band-label { display: none; }
}
/* ─── Demo content area — visually morphs as the slider moves ────── */
.demo-stage {
min-height: 100vh;
padding: 32px 24px 200px;
max-width: 980px;
margin: 0 auto;
}
.demo-stage header { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; margin-bottom: 28px; }
.demo-stage .lede { font-size: calc(var(--body) + 1px); color: var(--muted); margin-bottom: 24px; }
.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--gap); }
.card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--gap); }
.card .emoji { font-size: 2.2em; display: block; margin-bottom: 8px; }
.card .label { font-weight: 700; }
.card .desc { color: var(--muted); margin-top: 6px; font-size: 0.92em; }
/* ─── 8 age bands — overwrite the CSS vars ───────────────────────── */
/* dual-selector beats body[data-theme="…"] specificity, per validation memo */
:root[data-band="toddler"], :root[data-band="toddler"] body {
--bg: #fff9e6 !important; --panel: #fff !important; --fg: #222 !important;
--accent: #ff3b30 !important; --border: #f4d000 !important; --muted: #555 !important;
--font: Fredoka, "Comic Neue", "Comic Sans MS", system-ui !important;
--body: 24px !important; --h: 48px !important; --radius: 24px !important; --gap: 24px !important;
--btn-min-height: 56px !important;
}
:root[data-band="kid"], :root[data-band="kid"] body {
--bg: #fff !important; --panel: #fef9c3 !important; --fg: #1f2937 !important;
--accent: #3b82f6 !important; --border: #fbbf24 !important; --muted: #4b5563 !important;
--font: Nunito, "Chalkboard SE", system-ui !important;
--body: 18px !important; --h: 30px !important; --radius: 14px !important; --gap: 18px !important;
--btn-min-height: 52px !important;
}
:root[data-band="tween"], :root[data-band="tween"] body {
--bg: #fef3c7 !important; --panel: #fff !important; --fg: #1f2937 !important;
--accent: #4338ca !important; --border: #fbbf24 !important; --muted: #6b7280 !important;
--font: "Public Sans", Avenir, system-ui !important;
--body: 15px !important; --h: 22px !important; --radius: 10px !important; --gap: 14px !important;
}
:root[data-band="teen"], :root[data-band="teen"] body {
--bg: #0a0e27 !important; --panel: #131a3a !important; --fg: #e0e0ff !important;
--accent: #ff2ec1 !important; --border: #2a3666 !important; --muted: #8a8fc7 !important;
--font: "Space Grotesk", system-ui !important;
--body: 16px !important; --h: 22px !important; --radius: 4px !important; --gap: 10px !important;
}
:root[data-band="adult"], :root[data-band="adult"] body {
--bg: #0f1115 !important; --panel: #181b22 !important; --fg: #e8eaee !important;
--accent: #c9a96e !important; --border: #2a2f38 !important; --muted: #a1a8b3 !important;
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
--body: 15px !important; --h: 18px !important; --radius: 12px !important; --gap: 16px !important;
}
:root[data-band="mature"], :root[data-band="mature"] body {
--bg: #faf3e7 !important; --panel: #fff !important; --fg: #1c0e08 !important;
--accent: #6b1d07 !important; --border: #d6c5a8 !important; --muted: #6e5b46 !important;
--font: "Source Serif Pro", Georgia, serif !important;
--body: 15px !important; --h: 22px !important; --radius: 6px !important; --gap: 16px !important;
}
:root[data-band="senior"], :root[data-band="senior"] body {
--bg: #ffffff !important; --panel: #f8f8f8 !important; --fg: #000 !important;
--accent: #0033cc !important; --border: #999 !important; --muted: #333 !important;
--font: "Atkinson Hyperlegible", Verdana, sans-serif !important;
--body: 20px !important; --h: 30px !important; --radius: 10px !important; --gap: 20px !important;
--btn-min-height: 48px !important;
}
:root[data-band="elder"], :root[data-band="elder"] body {
--bg: #000 !important; --panel: #1a1a1a !important; --fg: #ffeb3b !important;
--accent: #ffeb3b !important; --border: #555 !important; --muted: #ccc !important;
--font: "Atkinson Hyperlegible", Verdana, sans-serif !important;
--body: 28px !important; --h: 44px !important; --radius: 14px !important; --gap: 28px !important;
--btn-min-height: 56px !important;
--font-line-height: 1.6 !important;
}
/* Hide all but the simplest card at toddler + elder bands */
:root[data-band="toddler"] .card-grid .card:nth-child(n+4),
:root[data-band="elder"] .card-grid .card:nth-child(n+2) { display: none; }
</style>
<!-- Meta Pixel — INERT until set-fb-pixel.sh is run (does NOT load fbevents.js or hit Meta until flipped) -->
<script>
(function(){
var PIXEL_ID = '{{FACEBOOK_PIXEL_ID}}';
// Inert until flipped — the brace check prevents BOTH the loader IIFE
// AND the init call from running, so fbevents.js is never fetched.
if (PIXEL_ID.indexOf('{{') !== -1) return;
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
document,'script','https://connect.facebook.net/en_US/fbevents.js');
fbq('init', PIXEL_ID);
fbq('track', 'PageView');
})();
</script>
<!-- noscript fallback is added by set-fb-pixel.sh on flip (omitted here to avoid unflipped img phone-home) -->
<!-- End Meta Pixel -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
</head>
<body>
<!-- ─── Demo stage that morphs as the slider moves ─── -->
<main class="demo-stage">
<header>
<div>
<h1>The theme slider by age</h1>
<p class="lede">Drag the slider at the bottom of the screen from 3 to 95. The page morphs through 8 design bands — typography, palette, density, button size, copy detail.</p>
</div>
<button class="btn ghost" id="copyEmbed" title="Copy iframe embed code">Copy embed code</button>
</header>
<section class="panel">
<h2>Current band: <span id="curBand">adult</span></h2>
<p id="curRationale" style="color:var(--muted)">Modern dashboard defaults — dark indigo, system font, 15 px body. Validated 2026-05-12 (v9, APCA Lc + WCAG SC 1.4.12 + WCAG 16 px floor on teen body).</p>
<p style="color:var(--muted);font-size:0.85em">Tokens applied: font <span id="tokFont"></span> · body <span id="tokBody"></span> px · heading <span id="tokH"></span> px · accent <span id="tokAccent"></span></p>
</section>
<h2 style="margin-bottom:16px">Sample content (morphs with the slider)</h2>
<div class="card-grid">
<div class="card"><span class="emoji">📚</span><div class="label">Stories</div><div class="desc">Today's reading list.</div></div>
<div class="card"><span class="emoji">🎨</span><div class="label">Art studio</div><div class="desc">Three new prompts ready.</div></div>
<div class="card"><span class="emoji">🧮</span><div class="label">Math drills</div><div class="desc">5-minute warmups.</div></div>
<div class="card"><span class="emoji">🎬</span><div class="label">Movie reel</div><div class="desc">Watchlist updated.</div></div>
<div class="card"><span class="emoji">📰</span><div class="label">Headlines</div><div class="desc">3 stories curated for you.</div></div>
<div class="card"><span class="emoji">💬</span><div class="label">Messages</div><div class="desc">2 new replies in your threads.</div></div>
</div>
<details style="margin-top:32px;color:var(--muted);font-size:0.9em">
<summary style="cursor:pointer">How to drop this slider into your own site</summary>
<pre style="white-space:pre-wrap;background:var(--panel);border:1px solid var(--border);border-radius:var(--radius);padding:var(--gap);overflow:auto;font-family:'JetBrains Mono',monospace;font-size:12px;line-height:1.6"><code><!-- Drop at the bottom of any page that uses CSS vars (--bg, --fg, --accent, --body, --h, --font) -->
<iframe src="http://127.0.0.1:9717/embed.html" style="position:fixed;left:0;right:0;bottom:0;width:100vw;height:160px;border:0;z-index:9999"></iframe>
<script>
window.addEventListener('message', e => {
if (e.data && e.data.type === 'age-theme:band') {
document.documentElement.dataset.band = e.data.band;
localStorage.setItem('age-pref', e.data.age);
}
});
</script></code></pre>
</details>
</main>
<!-- ─── The actual slider widget ─── -->
<div class="slider-shell" id="sliderShell">
<div class="slider-handle" id="sliderHandle">▲ Open theme slider</div>
<div class="age-display">
<span id="ageNum">30</span>
<small id="bandLabel">Adult</small>
</div>
<div class="band-label" id="bandLabelSide">Adult</div>
<input type="range" class="age-slider" id="ageSlider" min="3" max="95" value="30" step="1">
<div class="age-ticks" aria-hidden="true">
<span>3</span><span>10</span><span>20</span><span>40</span><span>60</span><span>80</span><span>95</span>
</div>
<button class="slider-action" id="resetBtn" title="Reset to default (age 30)">Reset</button>
<button class="slider-action slider-close" id="closeBtn" aria-label="Close slider">×</button>
<div class="age-tagline" id="tagline">Modern dashboard.</div>
</div>
<script>
(async () => {
const $ = (id) => document.getElementById(id);
const bands = await fetch('/api/bands').then(r => r.json()).catch(() => null);
// bandFor: id, label, tagline + the data object from bands.json
function bandFor(age) {
if (!bands || !bands.bands) return { id: 'adult', label: 'Adult', tagline: '' };
const tableAge = (id) => bands.bands.find(b => b.id === id);
let id;
if (age <= 5) id = 'toddler';
else if (age <= 9) id = 'kid';
else if (age <= 12) id = 'tween';
else if (age <= 19) id = 'teen';
else if (age <= 44) id = 'adult';
else if (age <= 59) id = 'mature';
else if (age <= 79) id = 'senior';
else id = 'elder';
const b = tableAge(id) || {};
const taglines = {
toddler: 'Primary colors. One face = good, one face = sad.',
kid: 'Friendly emoji and clear names.',
tween: 'Brighter palette, full info.',
teen: 'Dense, dark, neon.',
adult: 'Modern dashboard.',
mature: 'Serif, calmer pacing.',
senior: 'High contrast, large text.',
elder: 'Yellow on black. Just the essentials.',
};
return { id, label: b.label || id, tagline: taglines[id] || '', data: b };
}
function apply(age) {
const info = bandFor(age);
document.documentElement.dataset.band = info.id;
$('ageNum').textContent = age;
$('bandLabel').textContent = info.label;
$('bandLabelSide').textContent = info.label;
$('tagline').textContent = info.tagline;
$('curBand').textContent = info.label.toLowerCase();
if (info.data && info.data.best) {
$('tokFont').textContent = (info.data.best.font || '').split(',')[0].trim();
$('tokBody').textContent = info.data.best.body;
$('tokH').textContent = info.data.best.h;
$('tokAccent').innerHTML = `<span style="display:inline-block;width:12px;height:12px;border-radius:3px;vertical-align:middle;background:${info.data.best.accent};border:1px solid #888"></span> ${info.data.best.accent}`;
$('curRationale').textContent = info.data.reasoning || '';
}
localStorage.setItem('age-pref', age);
// Post to parent for iframe embedding
if (window.parent !== window) {
window.parent.postMessage({ type: 'age-theme:band', age, band: info.id, label: info.label }, '*');
}
}
const slider = $('ageSlider');
const saved = parseInt(localStorage.getItem('age-pref') || '30', 10);
slider.value = saved;
apply(saved);
slider.addEventListener('input', e => apply(parseInt(e.target.value, 10)));
$('resetBtn').addEventListener('click', () => { slider.value = 30; apply(30); });
const shell = $('sliderShell');
$('closeBtn').addEventListener('click', () => { shell.classList.add('collapsed'); });
$('sliderHandle').addEventListener('click', () => { shell.classList.remove('collapsed'); });
$('copyEmbed').addEventListener('click', async () => {
const code = `<iframe src="${location.origin}/embed.html" style="position:fixed;left:0;right:0;bottom:0;width:100vw;height:160px;border:0;z-index:9999"></iframe>`;
try { await navigator.clipboard.writeText(code); $('copyEmbed').textContent = 'Copied ✓'; setTimeout(() => $('copyEmbed').textContent = 'Copy embed code', 1800); }
catch { alert(code); }
});
})();
</script>
</body>
</html>