← 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>&lt;!-- Drop at the bottom of any page that uses CSS vars (--bg, --fg, --accent, --body, --h, --font) --&gt;
&lt;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"&gt;&lt;/iframe&gt;
&lt;script&gt;
window.addEventListener('message', e =&gt; {
  if (e.data &amp;&amp; e.data.type === 'age-theme:band') {
    document.documentElement.dataset.band = e.data.band;
    localStorage.setItem('age-pref', e.data.age);
  }
});
&lt;/script&gt;</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>