

===== FILE: Sam/mem_store.php @ 2025-09-21 04:00:24 =====
<?php
// mem_store.php — JSON facts store + CSVs + edit helpers
declare(strict_types=1);
// hi
function mem_dir(): string {
  $dir = __DIR__ . '/data';
  @mkdir($dir, 0775, true);
  return $dir;
}
function mem_path(): string { return mem_dir() . '/facts.json'; }

function mem_load_obj(): array {
  $path = mem_path();
  if(!is_file($path)){
    $obj = ['rev'=>0,'updated_at'=>gmdate('c'),'facts'=>['environment'=>[],'people'=>[],'self'=>[]]];
    file_put_contents($path, json_encode($obj, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE));
  }
  $raw = (string)file_get_contents($path);
  $j = json_decode($raw, true);
  if(!is_array($j) || !isset($j['facts'])) $j = ['rev'=>0,'updated_at'=>gmdate('c'),'facts'=>['environment'=>[],'people'=>[],'self'=>[]]];
  return $j;
}
function mem_load_facts(): array {
  $j = mem_load_obj();
  return [$j['facts'], (int)($j['rev'] ?? 0)];
}

function mem_normalize(string $bucket, string $txt): string {
  $t = trim($txt);
  // People: title-case names before dashes
  if($bucket === 'people'){
    // e.g. "david bowie — musician" -> "David Bowie — musician"
    if(preg_match('/^\s*([^\—\-:|]+)([\—\-\:\|]\s*)(.+)$/u', $t, $m)){
      $name = mb_convert_case(trim($m[1]), MB_CASE_TITLE, "UTF-8");
      return $name . $m[2] . trim($m[3]);
    }
    // plain name only
    return mb_convert_case($t, MB_CASE_TITLE, "UTF-8");
  }
  // Environment/Self: sentence case
  $t = preg_replace('/\s+/', ' ', $t);
  if($t === '') return $t;
  $t = mb_strtolower($t, "UTF-8");
  $t = mb_strtoupper(mb_substr($t,0,1,"UTF-8"),"UTF-8") . mb_substr($t,1,null,"UTF-8");
  return $t;
}

function mem_write_obj(array $obj): void {
  $obj['rev'] = (int)($obj['rev'] ?? 0) + 1;
  $obj['updated_at'] = gmdate('c');
  file_put_contents(mem_path(), json_encode($obj, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE));
  mem_write_csvs($obj['facts']);
}

function mem_merge_and_save(array $incoming): array {
  $obj = mem_load_obj();
  $facts = $obj['facts'];
  foreach(['environment','people','self'] as $k){
    $cur = (array)($facts[$k] ?? []);
    $add = array_values(array_filter(array_map('trim', (array)($incoming[$k] ?? [])), fn($x)=>$x!==''));
    // normalize + case-insensitive dedupe
    $curLower = array_map(fn($x)=>mb_strtolower($x,'UTF-8'), $cur);
    foreach($add as $a){
      $norm = mem_normalize($k, $a);
      if($norm==='') continue;
      if(!in_array(mb_strtolower($norm,'UTF-8'), $curLower, true)){
        $cur[] = $norm;
        $curLower[] = mb_strtolower($norm,'UTF-8');
      }
    }
    $facts[$k] = array_values($cur);
  }
  $obj['facts'] = $facts;
  mem_write_obj($obj);
  return [$facts, (int)$obj['rev']];
}

// ----- NEW: edit helpers -----
function mem_delete_value(string $bucket, string $value): array {
  $obj = mem_load_obj();
  $arr = (array)($obj['facts'][$bucket] ?? []);
  $needle = mb_strtolower(trim($value),'UTF-8');
  $arr = array_values(array_filter($arr, fn($x)=>mb_strtolower($x,'UTF-8') !== $needle));
  $obj['facts'][$bucket] = $arr;
  mem_write_obj($obj);
  return [$obj['facts'], (int)$obj['rev']];
}

function mem_add_value(string $bucket, string $value): array {
  return mem_merge_and_save([$bucket => [$value]]);
}

function mem_replace_value(string $bucket, string $old, string $new): array {
  $obj = mem_load_obj();
  $arr = (array)($obj['facts'][$bucket] ?? []);
  $needle = mb_strtolower(trim($old),'UTF-8');
  $arr = array_map(function($x) use ($bucket,$needle,$new){
    if(mb_strtolower($x,'UTF-8') === $needle) return mem_normalize($bucket, $new);
    return $x;
  }, $arr);
  // de-dupe after replacement
  $seen = [];
  $arr = array_values(array_filter($arr, function($x) use (&$seen){
    $k = mb_strtolower($x,'UTF-8');
    if(isset($seen[$k])) return false;
    $seen[$k] = true; return true;
  }));
  $obj['facts'][$bucket] = $arr;
  mem_write_obj($obj);
  return [$obj['facts'], (int)$obj['rev']];
}

// ----- CSVs -----
function mem_write_csvs(array $facts): void {
  $dir = mem_dir();
  write_csv_list("$dir/environment.csv", array_values(array_filter((array)($facts['environment'] ?? []), 'strlen')), 'environment_fact');
  write_csv_list("$dir/people.csv", array_values(array_filter((array)($facts['people'] ?? []), 'strlen')), 'person_fact');
  write_csv_list("$dir/self.csv", array_values(array_filter((array)($facts['self'] ?? []), 'strlen')), 'self_fact');
}
function write_csv_list(string $file, array $rows, string $header): void {
  $fh = fopen($file, 'w'); if($fh === false) return;
  fputcsv($fh, [$header]);
  foreach($rows as $r) fputcsv($fh, [$r]);
  fclose($fh);
}


===== FILE: Sam/index.html @ 2025-09-21 04:00:57 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bowie — Class Robot hi</title>
<link rel="icon" type="image/png" href="favicon.png" />
<style>
  :root{
    --bg:#f7f7f4; --panel:#ffffff; --border:#e2e8e4; --ink:#1f2a24; --sub:#5b6a62;
    --accent:#22a06b; --pill:#eaf7f1; --pillText:#1f7b58; --user:#dff3ea; --bot:#fff;
  }
  *{box-sizing:border-box}
  body{margin:0;background:var(--bg);color:var(--ink);
       font-family:system-ui,-apple-system,Segoe UI,Roboto,Inter,Helvetica,Arial,sans-serif}
  header{display:flex;gap:12px;align-items:center;justify-content:space-between;
         padding:14px 18px;background:var(--panel);border-bottom:1px solid var(--border);
         position:sticky;top:0;z-index:2}
  .brand{display:flex;gap:10px;align-items:center}
  .dot{width:10px;height:10px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 3px var(--pill)}
  .title{font-weight:700}
  .actions{display:flex;gap:8px}
  button,.ghost{border:1px solid var(--border);background:#fff;border-radius:10px;
                padding:9px 12px;font-weight:600;cursor:pointer}
  button.primary{background:var(--accent);border-color:var(--accent);color:#fff}
  main{display:grid;grid-template-columns:2fr 1fr;gap:14px;padding:14px}
  .card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:12px}
  .scroll{max-height:62vh;overflow:auto;padding-right:4px}
  .msg{padding:10px 12px;border-radius:10px;margin:10px 0;border:1px solid var(--border)}
  .u{background:var(--user)}
  .b{background:var(--bot)}
  .think{color:#7b8680;font-style:italic;white-space:pre-wrap;margin-top:6px}
  .meta{font-size:12px;color:var(--sub)}
  .row{display:flex;gap:8px}
  textarea{flex:1;border:1px solid var(--border);border-radius:10px;
           padding:10px;resize:vertical;min-height:48px}
  .pill{display:inline-flex;gap:6px;align-items:center;background:var(--pill);color:var(--pillText);
        padding:4px 8px;border-radius:999px;font-size:12px}
  ul{margin:6px 0 12px 20px;padding:0}
  h3{margin:8px 0}
  .muted{color:var(--sub)}
  .small{font-size:12px}
</style>
</head>
<body>
  <header>
    <div class="brand">
      <div class="dot" id="savedDot" title="Journal saved"></div>
      <div class="title">Bowie — Class Robot</div>
      <div class="pill" id="journalPill">journal: <span id="jCount">0</span></div>
    </div>
    <div class="actions">
      <label class="ghost" style="display:flex;align-items:center;gap:6px;padding:8px 10px;user-select:none">
        <input id="autoAsk" type="checkbox" checked style="transform:scale(1.1)"/> Auto-ask
      </label>
      <label class="ghost" style="display:flex;align-items:center;gap:6px;padding:8px 10px;user-select:none">
        <input id="showThink" type="checkbox" checked style="transform:scale(1.1)"/> Show thinking
      </label>
      <button id="endBtn" class="primary">End Chat & Extract Facts</button>
      <button id="clearBtn" class="ghost">Clear Chat</button>
      <label class="ghost" style="display:flex;align-items:center;gap:6px;padding:8px 10px;user-select:none">
        <input id="speakToggle" type="checkbox" checked style="transform:scale(1.1)"/> Speak replies
      </label>
      <label class="ghost" style="display:flex;align-items:center;gap:6px;padding:8px 10px;user-select:none">
        <input id="fxToggle" type="checkbox" checked style="transform:scale(1.1)"/> Droid FX
      </label>
    </div>
  </header>

  <main>
    <section class="card">
      <div class="scroll" id="chat"></div>
      <div class="row">
        <textarea id="input" placeholder="Say something to Bowie… (Shift+Enter for newline)"></textarea>
        <button id="sendBtn" class="primary">Send</button>
      </div>
      <div class="small muted" style="margin-top:6px">
        Press Enter to send, Shift+Enter for new line.
      </div>
    </section>

    <aside class="card">
      <div style="display:flex;align-items:center;justify-content:space-between;gap:8px">
        <h3 style="margin:4px 0">Extracted Facts</h3>
        <span class="pill small" id="factsRev">rev –</span>
      </div>
      <div class="small muted" style="margin-bottom:8px">
        Auto-fills after “End Chat & Extract Facts”.
      </div>
      <div>
        <h4>Environment</h4>
        <ul id="env"></ul>
        <h4>People</h4>
        <ul id="people"></ul>
        <h4>Self</h4>
        <ul id="self"></ul>
      </div>
    </aside>
  </main>

  <!-- Shared audio element for reliable playback & CORS -->
  <audio id="tts" preload="auto" crossorigin="anonymous"></audio>

<script>
const $ = (q, el=document)=>el.querySelector(q);
const chatEl = $('#chat'), inputEl = $('#input'), jCount = $('#jCount');
const sendBtn = $('#sendBtn'), endBtn = $('#endBtn'), clearBtn = $('#clearBtn');
const savedDot = $('#savedDot'), factsRev = $('#factsRev');
const envUl = $('#env'), peopleUl = $('#people'), selfUl = $('#self');
const elTTS = $('#tts');

let coachTarget = null;
let dirty = false;
let sending = false;

/* ====== Slightly higher pitch (kid-like, but subtle) ====== */
const PITCH_SEMITONES = 3; // was 4 — now slightly lower
function semitonesToRate(st){ return Math.pow(2, st/12); } // 12 st per octave

function li(text){
  const li = document.createElement('li');
  li.textContent = text;
  return li;
}
function addMsg(role, content, think=null){
  const div = document.createElement('div');
  div.className = 'msg ' + (role === 'user' ? 'u':'b');
  let html = `<div>${content}</div>`;
  if (think && $('#showThink').checked) html += `<div class="think">${think}</div>`;
  div.innerHTML = html;
  chatEl.appendChild(div);
  chatEl.scrollTop = chatEl.scrollHeight;
}
function setDirty(v){ dirty = v; savedDot.style.opacity = v ? 0.3 : 1; }

async function api(data){
  const form = new FormData();
  Object.entries(data).forEach(([k,v])=>form.append(k, v));
  const r = await fetch('api.php', { method:'POST', body:form });
  const text = await r.text();
  const ct = (r.headers.get('content-type') || '').toLowerCase();
  if (ct.includes('application/json')) {
    try { return JSON.parse(text); }
    catch (e) { return { ok:false, error:'Invalid JSON parse', raw:text, status:r.status }; }
  }
  return { ok:false, error:'Non-JSON response', status:r.status, raw:text.slice(0,1000) };
}

async function ttsRequest(text, voice='Chip-PlayAI'){
  const form = new FormData();
  form.append('action','tts');
  form.append('text', text);
  form.append('voice', voice);
  const r = await fetch('api.php', { method:'POST', body:form });
  const t = await r.text();
  const ct = (r.headers.get('content-type') || '').toLowerCase();
  if (ct.includes('application/json')) {
    try { return JSON.parse(t); }
    catch { return { ok:false, error:'Invalid JSON parse', raw:t, status:r.status }; }
  }
  return { ok:false, error:'Non-JSON response (TTS)', status:r.status, raw:t.slice(0,1000) };
}

async function send(){
  const msg = inputEl.value.trim();
  if(!msg || sending) return;
  sending = true; sendBtn.disabled = true;
  addMsg('user', escapeHTML(msg));
  inputEl.value = '';

  try {
    if (coachTarget){
      const apply = await api({ action:'facts_apply_answer', bucket:coachTarget.bucket, value:coachTarget.value, answer:msg });
      if (apply && apply.ok) renderFacts(apply.facts, apply.rev);
      coachTarget = null;
    }

    const r = await api({action:'chat', message: msg});
    if (r.ok){
      const spoken = (r.spoken && r.spoken.trim()) ? r.spoken : '…';
      const think  = (r.think && r.think.trim()) ? r.think : null;
      addMsg('assistant', escapeHTML(spoken), think ? escapeHTML(think) : null);
      speak(spoken);   // 🔊 TTS here
      jCount.textContent = r.journal_count ?? '-';
      setDirty(true);
    } else {
      addMsg('assistant', `<i class="muted">Error: ${escapeHTML(r.error||'unknown')}</i>`);
    }
  } catch (err){
    addMsg('assistant', `<i class="muted">Network/JS error: ${escapeHTML(String(err))}</i>`);
  } finally {
    sending = false; sendBtn.disabled = false;
  }
}

async function refreshHistory(){
  const r = await api({action:'history'});
  if(r.ok){
    chatEl.innerHTML = '';
    (r.items||[]).forEach(m=>{
      const think = m.role==='assistant' ? extractThink(m.content).think : null;
      const spoken = m.role==='assistant' ? extractThink(m.content).spoken : m.content;
      addMsg(m.role, escapeHTML(spoken), think ? escapeHTML(think) : null);
    });
    jCount.textContent = r.items?.length || 0;
    setDirty(false);
  }
}

function escapeHTML(s){ return s.replace(/[&<>]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c])); }
function extractThink(raw){
  if (!raw) return { spoken: '', think: null };
  const m = raw.match(/<think>([\s\S]*?)<\/think>/i);
  if (m){
    const think  = m[1].trim();
    const spoken = raw.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
    return { spoken, think: think || null };
  }
  let spoken = raw.replace(/<\/?think>/gi, '')
                  .replace(/<think\s*\/?>/gi, '')
                  .replace(/^\s*(?:\[?think\]?|think:)\s*/i, '')
                  .trim();
  return { spoken, think: null };
}

sendBtn.addEventListener('click', send);
inputEl.addEventListener('keydown', e=>{ if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); send(); } });

endBtn.addEventListener('click', async ()=>{
  endBtn.disabled = true;
  const r = await api({action:'extract'});
  if(r.ok){
    renderFacts(r.facts, r.rev);
    await api({action:'close'});
    await refreshHistory();
  } else alert('Extract error: ' + (r.error||'unknown'));
  endBtn.disabled = false;
});
clearBtn.addEventListener('click', async ()=>{ await api({action:'clear'}); await refreshHistory(); });

function renderFacts(f, rev){
  envUl.innerHTML = ''; peopleUl.innerHTML = ''; selfUl.innerHTML = '';
  (f.environment||[]).forEach(x=>envUl.appendChild(li(x)));
  (f.people||[]).forEach(x=>peopleUl.appendChild(li(x)));
  (f.self||[]).forEach(x=>selfUl.appendChild(li(x)));
  factsRev.textContent = 'rev ' + (rev ?? '-');
}

/* ==================== TTS ==================== */
async function ttsRequestSimple(text, voice='Chip-PlayAI'){
  const form = new FormData();
  form.append('action','tts');
  form.append('text', text);
  form.append('voice', voice);
  const r = await fetch('api.php', { method:'POST', body:form });
  const t = await r.text();
  try { return JSON.parse(t); }
  catch { return { ok:false, error:'non-json', raw:t }; }
}

async function speak(text, voice='Chip-PlayAI'){
  const speakToggle = $('#speakToggle');
  if (!speakToggle || !speakToggle.checked) return;
  if (!text || !text.trim()) return;

  const res = await ttsRequestSimple(text, voice);
  if (!res || !res.ok){
    const msg = (res && (res.error || res.message)) || (res && res.detail && (res.detail.error?.message || res.detail.message)) || (typeof res === 'string' ? res : 'unknown');
    addMsg('assistant', `<i class="muted">TTS error: ${escapeHTML(String(msg))}${res?.status ? ` (status ${res.status})` : ''}</i>`);
    return;
  }

  let url = null;
  if (typeof res.src === 'string') url = res.src;
  else if (typeof res.url === 'string') url = res.url;
  else if (typeof res.data_url === 'string') url = res.data_url;
  else if (typeof res.b64 === 'string') {
    const mime = res.mime || 'audio/mpeg';
    url = `data:${mime};base64,${res.b64}`;
  }
  if (!url){ addMsg('assistant', `<i class="muted">TTS error: no audio in response</i>`); return; }

  const fxOn = $('#fxToggle')?.checked;

  try {
    if (fxOn) {
      const resp = await fetch(url);
      if (!resp.ok) throw new Error('audio HTTP ' + resp.status);
      const arr = await resp.arrayBuffer();
      await playRobotFX(arr, { grit: 0, ringDepth: 0 });
    } else {
      elTTS.pause();
      elTTS.currentTime = 0;
      elTTS.src = url;

      // Slight pitch-up via rate
      elTTS.preservesPitch = false;
      elTTS.mozPreservesPitch = false;
      elTTS.webkitPreservesPitch = false;
      elTTS.playbackRate = semitonesToRate(PITCH_SEMITONES);

      await elTTS.play();
    }
  } catch (e){
    console.warn('FX or playback failed, falling back to plain audio', e);
    try {
      elTTS.pause();
      elTTS.currentTime = 0;
      elTTS.src = url;
      elTTS.preservesPitch = false;
      elTTS.playbackRate = semitonesToRate(PITCH_SEMITONES);
      await elTTS.play();
    } catch {}
  }
}

/* ============== FX engine ============== */
let _ctx;
function audioCtx(){ _ctx = _ctx || new (window.AudioContext||window.webkitAudioContext)(); return _ctx; }

// soft-clip curve (unused when grit=0)
function makeDistortion(amount=0.1){
  const n = 1024, curve = new Float32Array(n);
  const k = amount*100;
  for (let i=0;i<n;i++){ const x = i*2/n - 1; curve[i] = (1+k)*x/(1+k*Math.abs(x)); }
  return curve;
}

// tiny “room” IR
function makeImpulse(ctx, secs=0.12){
  const rate = ctx.sampleRate, len = Math.floor(secs*rate);
  const buf = ctx.createBuffer(2, len, rate);
  for (let ch=0; ch<2; ch++){
    const d = buf.getChannelData(ch);
    for (let i=0; i<len; i++){
      d[i] = (Math.random()*2-1) * Math.pow(1 - i/len, 2.2) * 0.6;
    }
  }
  return buf;
}

async function playRobotFX(arrayBuffer, opts = {}) {
  const ctx = audioCtx();
  if (ctx.state === 'suspended') await ctx.resume();

  const cfg = Object.assign({
    // Tone
    hp: 180, lp: 5600,

    // === Chorus (like before): fast & shallow, light mix ===
    chorusRate: 6.0,       // Hz
    chorusDepth: 0.002,    // seconds (very shallow)
    chorusDelay: 0.010,    // seconds
    chorusMix: 0.10,       // subtle width

    // === Phaser (like before): fast, shallow, light mix ===
    phaserRate: 3.5,       // Hz (fast, audible)
    phaserDepth: 0.05,     // shallow sweep (fraction of base)
    phaserBase: 350,       // Hz
    phaserStages: 4,       // fewer stages = less hollow
    phaserFeedback: 0.05,  // keep harmonics
    phaserMix: 0.18,       // subtle

    // No robot tremble
    ringFreq: 80, ringDepth: 0.00,

    // Room & loudness
    grit: 0.00,
    space: 0.05,           // tiny room tail
    reverbMix: 0.12,       // light ambience
    gain: 1.00,
    postGain: 1.12,

    // Loudness helpers
    normalize: true, targetPeak: 0.90,
    comp: { threshold: -18, knee: 20, ratio: 2.2, attack: 0.004, release: 0.22 }
  }, opts);

  // Decode first
  const audioBuf = await ctx.decodeAudioData(arrayBuffer);

  // Source (pitch-up *before* effects)
  const src = ctx.createBufferSource();
  src.buffer = audioBuf;
  src.playbackRate.value = semitonesToRate(PITCH_SEMITONES);

  // Optional normalization
  let srcNode = src;
  if (cfg.normalize) {
    let peak = 0;
    for (let ch = 0; ch < audioBuf.numberOfChannels; ch++) {
      const d = audioBuf.getChannelData(ch);
      for (let i = 0; i < d.length; i++) peak = Math.max(peak, Math.abs(d[i]));
    }
    const mult = peak > 0 ? Math.min(4.0, cfg.targetPeak / peak) : 1.0;
    const norm = ctx.createGain(); norm.gain.value = mult;
    src.connect(norm); srcNode = norm;
  }

  // Filters
  const hp = ctx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value=cfg.hp;
  const lp = ctx.createBiquadFilter(); lp.type='lowpass';  lp.frequency.value=cfg.lp;

  // Chorus
  const chorusDelay = ctx.createDelay(0.05); chorusDelay.delayTime.value = cfg.chorusDelay;
  const chorusLFO = ctx.createOscillator(); chorusLFO.type='sine'; chorusLFO.frequency.value = cfg.chorusRate;
  const chorusLFOAmp = ctx.createGain(); chorusLFOAmp.gain.value = cfg.chorusDepth;
  chorusLFO.connect(chorusLFOAmp).connect(chorusDelay.delayTime);
  const chorusWet = ctx.createGain(); chorusWet.gain.value = cfg.chorusMix;
  const chorusDry = ctx.createGain(); chorusDry.gain.value = 1 - cfg.chorusMix;

  // Phaser
  const phaserWet = ctx.createGain(); phaserWet.gain.value = cfg.phaserMix;
  const phaserDry = ctx.createGain(); phaserDry.gain.value = 1 - cfg.phaserMix;
  const stageCount = Math.max(1, Math.round(cfg.phaserStages));
  const phaserStages = [];
  for (let i=0; i<stageCount; i++){
    const ap = ctx.createBiquadFilter(); ap.type='allpass';
    ap.frequency.value = cfg.phaserBase * (1 + i*0.35);
    phaserStages.push(ap);
  }
  const phLFO = ctx.createOscillator(); phLFO.type='sine'; phLFO.frequency.value = cfg.phaserRate;
  const phLFOAmt = ctx.createGain(); phLFOAmt.gain.value = cfg.phaserDepth * cfg.phaserBase;
  phLFO.connect(phLFOAmt);
  phaserStages.forEach(ap => {
    ap.frequency.setValueAtTime(ap.frequency.value, ctx.currentTime);
    phLFOAmt.connect(ap.frequency);
  });
  const phFeedback = ctx.createGain(); phFeedback.gain.value = cfg.phaserFeedback;

  // Room
  const verb = ctx.createConvolver(); verb.buffer = makeImpulse(ctx, Math.max(0.03, Math.min(0.12, cfg.space)));
  const verbWet = ctx.createGain(); verbWet.gain.value = cfg.reverbMix;
  const verbDry = ctx.createGain(); verbDry.gain.value = 1 - cfg.reverbMix;

  // Dynamics
  const comp = ctx.createDynamicsCompressor();
  comp.threshold.value = cfg.comp.threshold; comp.knee.value=cfg.comp.knee;
  comp.ratio.value = cfg.comp.ratio; comp.attack.value=cfg.comp.attack; comp.release.value=cfg.comp.release;
  const master = ctx.createGain(); master.gain.value = cfg.gain;
  const postGain = ctx.createGain(); postGain.gain.value = cfg.postGain;

  // Routing
  srcNode.connect(hp); hp.connect(lp);

  lp.connect(chorusDry);
  lp.connect(chorusDelay); chorusDelay.connect(chorusWet);
  const chorusSum = ctx.createGain(); chorusDry.connect(chorusSum); chorusWet.connect(chorusSum);

  chorusSum.connect(phaserDry);
  let pIn = ctx.createGain(); chorusSum.connect(pIn);
  let pOut = pIn;
  for (const ap of phaserStages){ pOut.connect(ap); pOut = ap; }
  pOut.connect(phFeedback).connect(pIn);
  pOut.connect(phaserWet);
  const phaserSum = ctx.createGain(); phaserDry.connect(phaserSum); phaserWet.connect(phaserSum);

  phaserSum.connect(verbDry);
  phaserSum.connect(verb); verb.connect(verbWet);
  const roomSum = ctx.createGain(); verbDry.connect(roomSum); verbWet.connect(roomSum);

  roomSum.connect(comp); comp.connect(master); master.connect(postGain); postGain.connect(ctx.destination);

  // Start
  chorusLFO.start(); phLFO.start(); src.start();
  src.onended = ()=>{ try{chorusLFO.stop()}catch{}; try{phLFO.stop()}catch{}; };
}

/* ============== Unlock audio policy & init ============== */
let audioUnlocked = false;
window.addEventListener('click', ()=>{
  if (!audioUnlocked){
    const a = new Audio(); a.muted = true;
    a.play().catch(()=>{}).finally(()=>{ audioUnlocked = true; });
  }
}, { once:true });

async function init(){ await refreshHistory(); }
init();
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-02 09:10:25 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<style>
  :root{
    --bg:#f7f7f4; --panel:#ffffff; --ink:#142019; --sub:#5b6a62;
    --accent:#22a06b; --accent-2:#1b5ad6; --border:#e2e8e4; --muted:#eef2ef;
  }
  *{box-sizing:border-box} html,body{height:100%}
  body{margin:0;background:var(--bg);color:var(--ink);
       font:15px/1.5 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial}
  a{color:var(--accent-2);text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:980px;margin:0 auto;padding:20px}
  header{background:var(--panel);border-bottom:1px solid var(--border)}
  .brand{display:flex;align-items:center;gap:10px}
  .brand img{width:40px;height:40px;border-radius:8px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  /* Hero */
  .hero{display:grid;grid-template-columns:1.15fr .85fr;gap:20px;align-items:center;padding:26px 0}
  .hero h2{font-size:34px;line-height:1.1;margin:0 0 8px}
  .lead{font-size:17px;color:var(--sub);margin:0 0 14px}
  .badges{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0 18px}
  .badge{padding:6px 10px;border:1px solid var(--border);background:var(--muted);
         border-radius:999px;font-size:12px;color:var(--sub)}
  .card{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:16px}
  /* Buttons */
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:700;cursor:pointer;user-select:none;transition:transform .02s ease;
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--accent);color:#fff}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  /* Sections */
  section{padding:22px 0}
  h3{margin:0 0 10px;font-size:20px}
  ul{margin:0;padding-left:18px} li{margin:6px 0}
  .grid{display:grid;gap:16px}
  .grid-2{grid-template-columns:1fr 1fr}
  /* EBP tile */
  .ebp{display:grid;gap:10px}
  .ebp .pill{display:inline-block;background:#effbf5;border:1px solid #c9eedc;
             color:#0b5134;border-radius:999px;padding:6px 10px;font-size:12px}
  /* Table-ish list */
  .simplelist li{list-style:"✔  "; padding-left:6px}
  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .note{font-size:13px;color:var(--sub)}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}
  @media (max-width:920px){.hero{grid-template-columns:1fr}.grid-2{grid-template-columns:1fr}}
</style>
</head>
<body>
<header>
  <div class="wrap">
    <div class="brand">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build | Code | Design</h2>
      <p class="lead">We begin with a streamlined 8-week, small-group program where students design, code, and 3D-print their own desktop chatbot robot — while learning safe, ethical AI API use.</p>
      <div class="badges">
        <span class="badge">Years 4–10 (grouped)</span>
        <span class="badge">3 hrs sessions</span>
        <span class="badge">Classes limited to 5</span>
        <span class="badge">3D Printing</span>
        <span class="badge">AI calls and prompting</span>
      </div>
      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:8px">NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.</p>
    </div>
    <div class="card">
      <img src="/images/kidslearn1.jpg" alt="Student desktop chatbot robot" style="width:100%;height:auto;border-radius:12px;background:#ddd;display:block" />
      <p class="note" style="margin-top:6px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>

  <!-- EVIDENCE-BASED PRACTICE -->
  <section id="ebp">
    <div class="card ebp">
      <h3>Evidence-Based Teaching from registered QLD Teacher</h3>
      <span class="pill">High Engagement</span>
      <span class="pill">High Support</span>
      <span class="pill">Relevance</span>
      <p style="margin:6px 0 0">
        We design sessions using established teaching and learning models that prioritise
        <strong>high engagement</strong> (hands-on builds, visible progress),
        <strong>high support</strong> (clear scaffolds, step-by-step goals, co-regulation),
        and <strong>relevance</strong> (real-world robotics and practical AI that matter to students).
        Every activity connects to planning, problem-solving, and communication — not just code.
      </p>
    </div>
  </section>

  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — kept simple and safe</h3>
      <ul class="simplelist">
        <li>Wire an ESP32, control servos/LEDs, read sensors.</li>
        <li>MicroPython basics with online custom editors</li>
        <li>Call a safe AI API (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Make it yours</h3>
      <ul class="simplelist">
        <li>Design and 3D-print a custom shell and mounts.</li>
        <li>Give your robot a voice and simple “personality”.</li>
        <li>Showcase at the Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

  <!-- SCHEDULE (short) -->
  <section>
    <div class="card">
      <h3>Schedule</h3>
      <ul class="simplelist">
        <li>After-school hours: 2–3 hrs/week for 8 weeks</li>
        <li>Homeschool day group: 3 hrs/week for 8 weeks</li>
        <li>Sunshine Coast venues (or onsite by request)</li>
      </ul>
      <p class="note" style="margin-top:6px">
        We align invoices and notes with NDIS <em>Capacity Building – Improved Learning</em> goals on request.
      </p>
    </div>
  </section>

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="/submit_robotics.php" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:4px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>
    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@mountofolivesfoundation.com">hello@curlywords.com</a><br/>
         </p>
      <p class="note">We’re happy to map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/bench-build.jpg" alt="Students assembling robots" style="width:100%;border-radius:12px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  document.getElementById('y').textContent = new Date().getFullYear();

  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  // Simple preview (no submit)
  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-02 09:11:04 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<style>
  :root{
    --bg:#f7f7f4; --panel:#ffffff; --ink:#142019; --sub:#5b6a62;
    --accent:#22a06b; --accent-2:#1b5ad6; --border:#e2e8e4; --muted:#eef2ef;
  }
  *{box-sizing:border-box} html,body{height:100%}
  body{margin:0;background:var(--bg);color:var(--ink);
       font:15px/1.5 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial}
  a{color:var(--accent-2);text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:980px;margin:0 auto;padding:20px}
  header{background:var(--panel);border-bottom:1px solid var(--border)}
  .brand{display:flex;align-items:center;gap:10px}
  .brand img{width:40px;height:40px;border-radius:8px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  /* Hero */
  .hero{display:grid;grid-template-columns:1.15fr .85fr;gap:20px;align-items:center;padding:26px 0}
  .hero h2{font-size:34px;line-height:1.1;margin:0 0 8px}
  .lead{font-size:17px;color:var(--sub);margin:0 0 14px}
  .badges{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0 18px}
  .badge{padding:6px 10px;border:1px solid var(--border);background:var(--muted);
         border-radius:999px;font-size:12px;color:var(--sub)}
  .card{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:16px}
  /* Buttons */
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:700;cursor:pointer;user-select:none;transition:transform .02s ease;
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--accent);color:#fff}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  /* Sections */
  section{padding:22px 0}
  h3{margin:0 0 10px;font-size:20px}
  ul{margin:0;padding-left:18px} li{margin:6px 0}
  .grid{display:grid;gap:16px}
  .grid-2{grid-template-columns:1fr 1fr}
  /* EBP tile */
  .ebp{display:grid;gap:10px}
  .ebp .pill{display:inline-block;background:#effbf5;border:1px solid #c9eedc;
             color:#0b5134;border-radius:999px;padding:6px 10px;font-size:12px}
  /* Table-ish list */
  .simplelist li{list-style:"✔  "; padding-left:6px}
  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .note{font-size:13px;color:var(--sub)}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}
  @media (max-width:920px){.hero{grid-template-columns:1fr}.grid-2{grid-template-columns:1fr}}
</style>
</head>
<body>
<header>
  <div class="wrap">
    <div class="brand">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build | Code | Design</h2>
      <p class="lead">We begin with a streamlined 8-week, small-group program where students design, code, and 3D-print their own desktop chatbot robot — while learning safe, ethical AI API use.</p>
      <div class="badges">
        <span class="badge">Years 4–10 (grouped)</span>
        <span class="badge">3 hrs sessions</span>
        <span class="badge">Classes limited to 5</span>
        <span class="badge">3D Printing</span>
        <span class="badge">AI calls and prompting</span>
      </div>
      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:8px">NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.</p>
    </div>
    <div class="card">
      <img src="/images/kidslearn1.jpg" alt="Student desktop chatbot robot" style="width:100%;height:auto;border-radius:12px;background:#ddd;display:block" />
      <p class="note" style="margin-top:6px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>

  <!-- EVIDENCE-BASED PRACTICE -->
  <section id="ebp">
    <div class="card ebp">
      <h3>Evidence-Based Teaching from registered QLD Teacher</h3>
      <span class="pill">High Engagement</span>
      <span class="pill">High Support</span>
      <span class="pill">Relevance</span>
      <p style="margin:6px 0 0">
        We design sessions using established teaching and learning models that prioritise
        <strong>high engagement</strong> (hands-on builds, visible progress),
        <strong>high support</strong> (clear scaffolds, step-by-step goals, co-regulation),
        and <strong>relevance</strong> (real-world robotics and practical AI that matter to students).
        Every activity connects to planning, problem-solving, and communication — not just code.
      </p>
    </div>
  </section>

  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — kept simple and safe</h3>
      <ul class="simplelist">
        <li>Wire an ESP32, control servos/LEDs, read sensors.</li>
        <li>MicroPython basics with online custom editors</li>
        <li>Call a safe AI API (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Make it yours</h3>
      <ul class="simplelist">
        <li>Design and 3D-print a custom shell and mounts.</li>
        <li>Give your robot a voice and simple “personality”.</li>
        <li>Showcase at the Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

  <!-- SCHEDULE (short) -->
  <section>
    <div class="card">
      <h3>Schedule</h3>
      <ul class="simplelist">
        <li>After-school hours: 2–3 hrs/week for 8 weeks</li>
        <li>Homeschool day group: 3 hrs/week for 8 weeks</li>
        <li>Sunshine Coast venues (or onsite by request)</li>
      </ul>
      <p class="note" style="margin-top:6px">
        We align invoices and notes with NDIS <em>Capacity Building – Improved Learning</em> goals on request.
      </p>
    </div>
  </section>

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="/submit_robotics.php" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:4px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>
    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@mountofolivesfoundation.com">hello@curlywords.com</a><br/>
         </p>
      <p class="note">We’re happy to map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/kidslearn3.jpg" alt="Students assembling robots" style="width:100%;border-radius:12px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  document.getElementById('y').textContent = new Date().getFullYear();

  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  // Simple preview (no submit)
  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-02 09:12:07 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<style>
  :root{
    --bg:#f7f7f4; --panel:#ffffff; --ink:#142019; --sub:#5b6a62;
    --accent:#22a06b; --accent-2:#1b5ad6; --border:#e2e8e4; --muted:#eef2ef;
  }
  *{box-sizing:border-box} html,body{height:100%}
  body{margin:0;background:var(--bg);color:var(--ink);
       font:15px/1.5 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial}
  a{color:var(--accent-2);text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:980px;margin:0 auto;padding:20px}
  header{background:var(--panel);border-bottom:1px solid var(--border)}
  .brand{display:flex;align-items:center;gap:10px}
  .brand img{width:40px;height:40px;border-radius:8px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  /* Hero */
  .hero{display:grid;grid-template-columns:1.15fr .85fr;gap:20px;align-items:center;padding:26px 0}
  .hero h2{font-size:34px;line-height:1.1;margin:0 0 8px}
  .lead{font-size:17px;color:var(--sub);margin:0 0 14px}
  .badges{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0 18px}
  .badge{padding:6px 10px;border:1px solid var(--border);background:var(--muted);
         border-radius:999px;font-size:12px;color:var(--sub)}
  .card{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:16px}
  /* Buttons */
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:700;cursor:pointer;user-select:none;transition:transform .02s ease;
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--accent);color:#fff}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  /* Sections */
  section{padding:22px 0}
  h3{margin:0 0 10px;font-size:20px}
  ul{margin:0;padding-left:18px} li{margin:6px 0}
  .grid{display:grid;gap:16px}
  .grid-2{grid-template-columns:1fr 1fr}
  /* EBP tile */
  .ebp{display:grid;gap:10px}
  .ebp .pill{display:inline-block;background:#effbf5;border:1px solid #c9eedc;
             color:#0b5134;border-radius:999px;padding:6px 10px;font-size:12px}
  /* Table-ish list */
  .simplelist li{list-style:"✔  "; padding-left:6px}
  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .note{font-size:13px;color:var(--sub)}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}
  @media (max-width:920px){.hero{grid-template-columns:1fr}.grid-2{grid-template-columns:1fr}}
</style>
</head>
<body>
<header>
  <div class="wrap">
    <div class="brand">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build | Code | Design</h2>
      <p class="lead">We begin with a streamlined 8-week, small-group program where students design, code, and 3D-print their own desktop chatbot robot — while learning safe, ethical AI API use.</p>
      <div class="badges">
        <span class="badge">Years 4–10 (grouped)</span>
        <span class="badge">3 hrs sessions</span>
        <span class="badge">Classes limited to 5</span>
        <span class="badge">3D Printing</span>
        <span class="badge">AI calls and prompting</span>
      </div>
      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:8px">NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.</p>
    </div>
    <div class="card">
      <img src="/images/kidslearn1.jpg" alt="Student desktop chatbot robot" style="width:100%;height:auto;border-radius:12px;background:#ddd;display:block" />
      <p class="note" style="margin-top:6px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>

  <!-- EVIDENCE-BASED PRACTICE -->
  <section id="ebp">
    <div class="card ebp">
      <h3>Evidence-Based Teaching from registered QLD Teacher</h3>
      
      <p style="margin:6px 0 0">
        We design sessions using established teaching and learning models that prioritise
        <strong>high engagement</strong> (hands-on builds, visible progress),
        <strong>high support</strong> (clear scaffolds, step-by-step goals, co-regulation),
        and <strong>relevance</strong> (real-world robotics and practical AI that matter to students).
        Every activity connects to planning, problem-solving, and communication — not just code.
      </p>
    </div>
  </section>

  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — kept simple and safe</h3>
      <ul class="simplelist">
        <li>Wire an ESP32, control servos/LEDs, read sensors.</li>
        <li>MicroPython basics with online custom editors</li>
        <li>Call a safe AI API (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Make it yours</h3>
      <ul class="simplelist">
        <li>Design and 3D-print a custom shell and mounts.</li>
        <li>Give your robot a voice and simple “personality”.</li>
        <li>Showcase at the Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

  <!-- SCHEDULE (short) -->
  <section>
    <div class="card">
      <h3>Schedule</h3>
      <ul class="simplelist">
        <li>After-school hours: 2–3 hrs/week for 8 weeks</li>
        <li>Homeschool day group: 3 hrs/week for 8 weeks</li>
        <li>Sunshine Coast venues (or onsite by request)</li>
      </ul>
      <p class="note" style="margin-top:6px">
        We align invoices and notes with NDIS <em>Capacity Building – Improved Learning</em> goals on request.
      </p>
    </div>
  </section>

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="/submit_robotics.php" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:4px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>
    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@mountofolivesfoundation.com">hello@curlywords.com</a><br/>
         </p>
      <p class="note">We’re happy to map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/kidslearn3.jpg" alt="Students assembling robots" style="width:100%;border-radius:12px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  document.getElementById('y').textContent = new Date().getFullYear();

  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  // Simple preview (no submit)
  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-02 09:12:34 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<style>
  :root{
    --bg:#f7f7f4; --panel:#ffffff; --ink:#142019; --sub:#5b6a62;
    --accent:#22a06b; --accent-2:#1b5ad6; --border:#e2e8e4; --muted:#eef2ef;
  }
  *{box-sizing:border-box} html,body{height:100%}
  body{margin:0;background:var(--bg);color:var(--ink);
       font:15px/1.5 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial}
  a{color:var(--accent-2);text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:980px;margin:0 auto;padding:20px}
  header{background:var(--panel);border-bottom:1px solid var(--border)}
  .brand{display:flex;align-items:center;gap:10px}
  .brand img{width:40px;height:40px;border-radius:8px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  /* Hero */
  .hero{display:grid;grid-template-columns:1.15fr .85fr;gap:20px;align-items:center;padding:26px 0}
  .hero h2{font-size:34px;line-height:1.1;margin:0 0 8px}
  .lead{font-size:17px;color:var(--sub);margin:0 0 14px}
  .badges{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0 18px}
  .badge{padding:6px 10px;border:1px solid var(--border);background:var(--muted);
         border-radius:999px;font-size:12px;color:var(--sub)}
  .card{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:16px}
  /* Buttons */
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:700;cursor:pointer;user-select:none;transition:transform .02s ease;
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--accent);color:#fff}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  /* Sections */
  section{padding:22px 0}
  h3{margin:0 0 10px;font-size:20px}
  ul{margin:0;padding-left:18px} li{margin:6px 0}
  .grid{display:grid;gap:16px}
  .grid-2{grid-template-columns:1fr 1fr}
  /* EBP tile */
  .ebp{display:grid;gap:10px}
  .ebp .pill{display:inline-block;background:#effbf5;border:1px solid #c9eedc;
             color:#0b5134;border-radius:999px;padding:6px 10px;font-size:12px}
  /* Table-ish list */
  .simplelist li{list-style:"✔  "; padding-left:6px}
  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .note{font-size:13px;color:var(--sub)}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}
  @media (max-width:920px){.hero{grid-template-columns:1fr}.grid-2{grid-template-columns:1fr}}
</style>
</head>
<body>
<header>
  <div class="wrap">
    <div class="brand">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build | Code | Design</h2>
      <p class="lead">We begin with a streamlined 8-week, small-group program where students design, code, and 3D-print their own desktop chatbot robot — while learning safe, ethical AI API use.</p>
      <div class="badges">
        <span class="badge">Years 4–10 (grouped)</span>
        <span class="badge">3 hrs sessions</span>
        <span class="badge">Classes limited to 5</span>
        <span class="badge">3D Printing</span>
        <span class="badge">AI calls and prompting</span>
      </div>
      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:8px">NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.</p>
    </div>
    <div class="card">
      <img src="/images/kidslearn1.jpg" alt="Student desktop chatbot robot" style="width:100%;height:auto;border-radius:12px;background:#ddd;display:block" />
      <p class="note" style="margin-top:6px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>

  <!-- EVIDENCE-BASED PRACTICE -->
  <section id="ebp">
    <div class="card ebp">
      <h3>Evidence-Based Teaching from registered QLD Teacher</h3>
      
      <p style="margin:6px 0 0">
        We design sessions using established teaching and learning models that prioritise
        <strong>high engagement</strong> (hands-on builds, visible progress),
        <strong>high support</strong> (clear scaffolds, step-by-step goals, co-regulation),
        and <strong>relevance</strong> (real-world robotics and practical AI that matter to students).
        Every activity connects to planning, problem-solving, and communication — not just code.
      </p>
    </div>
  </section>

  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — kept simple and safe</h3>
      <ul class="simplelist">
        <li>Wire an ESP32, control servos/LEDs, read sensors.</li>
        <li>MicroPython basics with online custom editors</li>
        <li>Call a safe AI API (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Make it yours</h3>
      <ul class="simplelist">
        <li>Design and 3D-print a custom shell and mounts.</li>
        <li>Give your robot a voice and simple “personality”.</li>
        <li>Showcase at the Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

  <!-- SCHEDULE (short) -->
  <section>
    <div class="card">
      <h3>Schedule</h3>
      <ul class="simplelist">
        <li>After-school hours: 2–3 hrs/week for 8 weeks</li>
        <li>Homeschool day group: 3 hrs/week for 8 weeks</li>
        <li>Sunshine Coast venues (or onsite by request)</li>
      </ul>
      <p class="note" style="margin-top:6px">
        We align invoices and notes with NDIS <em>Capacity Building – Improved Learning</em> goals on request.
      </p>
    </div>
  </section>

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="/submit_robotics.php" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:4px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>
    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a><br/>
         </p>
      <p class="note">We’re happy to map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/kidslearn3.jpg" alt="Students assembling robots" style="width:100%;border-radius:12px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  document.getElementById('y').textContent = new Date().getFullYear();

  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  // Simple preview (no submit)
  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\\n'));
  });
</script>
</body>
</html>


===== FILE: submit_robotics.php @ 2025-10-02 09:13:50 =====
<?php
// submit_robotics.php with SMTP (PHPMailer)

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;

// If using Composer autoload:
require __DIR__ . '/vendor/autoload.php';
// If not, include PHPMailer class files manually.

$to = "hello@curlywords.com";
$subject = "New Robotics Program Registration";

// Collect POST data safely
$parent     = trim($_POST['parent'] ?? '');
$email      = trim($_POST['email'] ?? '');
$phone      = trim($_POST['phone'] ?? '');
$student    = trim($_POST['student'] ?? '');
$preference = trim($_POST['preference'] ?? '');
$location   = trim($_POST['location'] ?? '');
$funding    = trim($_POST['funding'] ?? '');
$notes      = trim($_POST['notes'] ?? '');

// Build body
$body  = "A new registration has been received for the Robotics + Safe AI Program:\n\n";
$body .= "Parent/Carer: $parent\n";
$body .= "Email: $email\n";
$body .= "Phone: $phone\n";
$body .= "Student: $student\n";
$body .= "Preferred Session: $preference\n";
$body .= "Location: $location\n";
$body .= "Funding: $funding\n";
$body .= "Notes:\n$notes\n";

try {
    $mail = new PHPMailer(true);
    // Server settings
    $mail->isSMTP();
    $mail->Host       = 'mail.curlywords.com';     // ← change to your SMTP host
    $mail->SMTPAuth   = true;
    $mail->Username   = 'no-reply@curlywords.com'; // ← full email address
    $mail->Password   = 'YOUR_PASSWORD_HERE';      // ← mailbox password
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // or PHPMailer::ENCRYPTION_SMTPS
    $mail->Port       = 587; // TLS usually 587, SSL 465

    // Recipients
    $mail->setFrom('no-reply@curlywords.com', 'Curly Words Robotics');
    $mail->addAddress($to);
    $mail->addReplyTo($email ?: 'no-reply@curlywords.com');

    // Content
    $mail->isHTML(false);
    $mail->Subject = $subject;
    $mail->Body    = $body;

    $mail->send();

    // Redirect to thank-you page
    header("Location: thank-you.html");
    exit;

} catch (Exception $e) {
    echo "<p>❌ Message could not be sent. Mailer Error: {$mail->ErrorInfo}</p>";
}
?>


===== FILE: robotics.html @ 2025-10-02 09:16:48 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<style>
  :root{
    --bg:#f7f7f4; --panel:#ffffff; --ink:#142019; --sub:#5b6a62;
    --accent:#22a06b; --accent-2:#1b5ad6; --border:#e2e8e4; --muted:#eef2ef;
  }
  *{box-sizing:border-box} html,body{height:100%}
  body{margin:0;background:var(--bg);color:var(--ink);
       font:15px/1.5 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial}
  a{color:var(--accent-2);text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:980px;margin:0 auto;padding:20px}
  header{background:var(--panel);border-bottom:1px solid var(--border)}
  .brand{display:flex;align-items:center;gap:10px}
  .brand img{width:40px;height:40px;border-radius:8px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  /* Hero */
  .hero{display:grid;grid-template-columns:1.15fr .85fr;gap:20px;align-items:center;padding:26px 0}
  .hero h2{font-size:34px;line-height:1.1;margin:0 0 8px}
  .lead{font-size:17px;color:var(--sub);margin:0 0 14px}
  .badges{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0 18px}
  .badge{padding:6px 10px;border:1px solid var(--border);background:var(--muted);
         border-radius:999px;font-size:12px;color:var(--sub)}
  .card{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:16px}
  /* Buttons */
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:700;cursor:pointer;user-select:none;transition:transform .02s ease;
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--accent);color:#fff}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  /* Sections */
  section{padding:22px 0}
  h3{margin:0 0 10px;font-size:20px}
  ul{margin:0;padding-left:18px} li{margin:6px 0}
  .grid{display:grid;gap:16px}
  .grid-2{grid-template-columns:1fr 1fr}
  /* EBP tile */
  .ebp{display:grid;gap:10px}
  .ebp .pill{display:inline-block;background:#effbf5;border:1px solid #c9eedc;
             color:#0b5134;border-radius:999px;padding:6px 10px;font-size:12px}
  /* Table-ish list */
  .simplelist li{list-style:"✔  "; padding-left:6px}
  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .note{font-size:13px;color:var(--sub)}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}
  @media (max-width:920px){.hero{grid-template-columns:1fr}.grid-2{grid-template-columns:1fr}}
</style>
</head>
<body>
<header>
  <div class="wrap">
    <div class="brand">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build | Code | Design</h2>
      <p class="lead">We begin with a streamlined 8-week, small-group program where students design, code, and 3D-print their own desktop chatbot robot — while learning safe, ethical AI API use.</p>
      <div class="badges">
        <span class="badge">Years 4–10 (grouped)</span>
        <span class="badge">3 hrs sessions</span>
        <span class="badge">Classes limited to 5</span>
        <span class="badge">3D Printing</span>
        <span class="badge">AI calls and prompting</span>
      </div>
      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:8px">NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.</p>
    </div>
    <div class="card">
      <img src="/images/kidslearn1.jpg" alt="Student desktop chatbot robot" style="width:100%;height:auto;border-radius:12px;background:#ddd;display:block" />
      <p class="note" style="margin-top:6px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>

  <!-- EVIDENCE-BASED PRACTICE -->
  <section id="ebp">
    <div class="card ebp">
      <h3>Evidence-Based Teaching from registered QLD Teacher</h3>
      
      <p style="margin:6px 0 0">
        We design sessions using established teaching and learning models that prioritise
        <strong>high engagement</strong> (hands-on builds, visible progress),
        <strong>high support</strong> (clear scaffolds, step-by-step goals, co-regulation),
        and <strong>relevance</strong> (real-world robotics and practical AI that matter to students).
        Every activity connects to planning, problem-solving, and communication — not just code.
      </p>
    </div>
  </section>

  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — kept simple and safe</h3>
      <ul class="simplelist">
        <li>Wire an ESP32, control servos/LEDs, read sensors.</li>
        <li>MicroPython basics with online custom editors</li>
        <li>Call a safe AI API (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Make it yours</h3>
      <ul class="simplelist">
        <li>Design and 3D-print a custom shell and mounts.</li>
        <li>Give your robot a voice and simple “personality”.</li>
        <li>Showcase at the Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

  <!-- SCHEDULE (short) -->
  <section>
    <div class="card">
      <h3>Schedule</h3>
      <ul class="simplelist">
        <li>After-school hours: 2–3 hrs/week for 8 weeks</li>
        <li>Homeschool day group: 3 hrs/week for 8 weeks</li>
        <li>Sunshine Coast venues (or onsite by request)</li>
      </ul>
      <p class="note" style="margin-top:6px">
        We align invoices and notes with NDIS <em>Capacity Building – Improved Learning</em> goals on request.
      </p>
    </div>
  </section>

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="/submit_robotics.php" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:4px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>
    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a><br/>
         </p>
      <p class="note">We’re happy to map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/kidslearn3.jpg" alt="Students assembling robots" style="width:100%;border-radius:12px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  document.getElementById('y').textContent = new Date().getFullYear();

  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  // Simple preview (no submit)
  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\\n'));
  });
</script>
</body>
</html>


===== FILE: submit_robotics.php @ 2025-10-02 09:17:57 =====
<?php
// submit_robotics.php — PHPMailer + SMTP with diagnostics for HTTP 500s

// --- TEMP DIAGNOSTICS (disable once live) ---
ini_set('log_errors', '1');
ini_set('error_log', __DIR__ . '/error_log');
ini_set('display_errors', '1');       // set to '0' once fixed
error_reporting(E_ALL);

// --- Collect POST data safely ---
$parent     = trim($_POST['parent'] ?? '');
$email      = trim($_POST['email'] ?? '');
$phone      = trim($_POST['phone'] ?? '');
$student    = trim($_POST['student'] ?? '');
$preference = trim($_POST['preference'] ?? '');
$location   = trim($_POST['location'] ?? '');
$funding    = trim($_POST['funding'] ?? '');
$notes      = trim($_POST['notes'] ?? '');

// Basic required fields check (defensive)
if (!$parent || !$email || !$phone || !$student || !$preference || !$location || !$funding) {
  http_response_code(400);
  echo "Missing required fields.";
  exit;
}

// --- Email config ---
$to        = "hello@curlywords.com";
$fromEmail = "no-reply@curlywords.com";   // should be a real mailbox on your domain
$fromName  = "Curly Words Robotics";
$subject   = "New Robotics Program Registration";

// --- Build body (plain text) ---
$body  = "A new registration has been received for the Robotics + Safe AI Program:\n\n";
$body .= "Parent/Carer: $parent\n";
$body .= "Email: $email\n";
$body .= "Phone: $phone\n";
$body .= "Student: $student\n";
$body .= "Preferred Session: $preference\n";
$body .= "Location: $location\n";
$body .= "Funding: $funding\n";
$body .= "Notes:\n$notes\n";

// --- PHPMailer bootstrap with robust autoload checks ---
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;

$autoloadErrors = [];
$autoloadOk = false;

// 1) Composer in same folder: /vendor/autoload.php
if (!$autoloadOk && file_exists(__DIR__ . '/vendor/autoload.php')) {
  require __DIR__ . '/vendor/autoload.php';
  $autoloadOk = true;
} else {
  $autoloadErrors[] = "Missing " . __DIR__ . "/vendor/autoload.php";
}

// 2) Composer one level up (common on cPanel app roots)
if (!$autoloadOk && file_exists(dirname(__DIR__) . '/vendor/autoload.php')) {
  require dirname(__DIR__) . '/vendor/autoload.php';
  $autoloadOk = true;
} else {
  $autoloadErrors[] = "Missing " . dirname(__DIR__) . "/vendor/autoload.php";
}

// 3) Manual include (if you uploaded PHPMailer src/ directory yourself)
if (!$autoloadOk) {
  $src = __DIR__ . '/phpmailer/src';
  if (file_exists("$src/PHPMailer.php") && file_exists("$src/Exception.php") && file_exists("$src/SMTP.php")) {
    require "$src/PHPMailer.php";
    require "$src/Exception.php";
    require "$src/SMTP.php";
    $autoloadOk = true;
  } else {
    $autoloadErrors[] = "Missing manual PHPMailer src/ in $src";
  }
}

// If PHPMailer still not available, show a clear error (instead of HTTP 500)
if (!$autoloadOk) {
  http_response_code(500);
  echo "PHPMailer not found. Tried:\n- " . implode("\n- ", $autoloadErrors) . "\n\n".
       "Fix: run 'composer require phpmailer/phpmailer' or upload PHPMailer 'src' folder.";
  exit;
}

try {
  $mail = new PHPMailer(true);

  // --- SMTP SETTINGS (update these to your host) ---
  $mail->isSMTP();
  $mail->Host       = 'mail.curlywords.com';   // ← your SMTP host
  $mail->SMTPAuth   = true;
  $mail->Username   = 'no-reply@curlywords.com'; // ← full mailbox
  $mail->Password   = 'YOUR_PASSWORD_HERE';      // ← real password or app password
  $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;  // or PHPMailer::ENCRYPTION_SMTPS
  $mail->Port       = 587;                            // 587 for STARTTLS, 465 for SMTPS

  // Optional: show SMTP dialogue in error_log (not to screen)
  $mail->SMTPDebug  = 2;        // 0=off, 2=verbose
  $mail->Debugoutput = function($str, $level) {
    error_log("SMTP[$level]: $str");
  };

  // --- HEADERS & RECIPIENTS ---
  $mail->setFrom($fromEmail, $fromName);
  $mail->addAddress($to);
  if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $mail->addReplyTo($email);
  } else {
    $mail->addReplyTo($fromEmail);
  }

  // --- CONTENT ---
  $mail->isHTML(false);
  $mail->Subject = $subject;
  $mail->Body    = $body;

  // --- SEND ---
  if (!$mail->send()) {
    // PHPMailer throws exceptions on ->send() if true; but keep guard:
    http_response_code(500);
    echo "Mailer send failed. See error_log for details.";
    exit;
  }

  // Success → redirect (ensure no output before this)
  header("Location: thank-you.html");
  exit;

} catch (Exception $e) {
  http_response_code(500);
  echo "Mailer exception: " . htmlspecialchars($e->getMessage());
  // Full details will also be in error_log
  exit;
}


===== FILE: submit_robotics.php @ 2025-10-02 09:19:18 =====
<?php
// submit_robotics.php — PHPMailer via manual includes (no Composer)

// --- TEMP DIAGNOSTICS (turn off display_errors once working) ---
ini_set('log_errors', '1');
ini_set('error_log', __DIR__ . '/error_log');
ini_set('display_errors', '1');
error_reporting(E_ALL);

// Collect POST data
$parent     = trim($_POST['parent'] ?? '');
$email      = trim($_POST['email'] ?? '');
$phone      = trim($_POST['phone'] ?? '');
$student    = trim($_POST['student'] ?? '');
$preference = trim($_POST['preference'] ?? '');
$location   = trim($_POST['location'] ?? '');
$funding    = trim($_POST['funding'] ?? '');
$notes      = trim($_POST['notes'] ?? '');

// Basic required fields check
if (!$parent || !$email || !$phone || !$student || !$preference || !$location || !$funding) {
  http_response_code(400);
  echo "Missing required fields.";
  exit;
}

// Recipient & envelope
$to        = "hello@curlywords.com";
$fromEmail = "no-reply@curlywords.com";  // must be a real mailbox on your domain
$fromName  = "Curly Words Robotics";
$subject   = "New Robotics Program Registration";

// Build email body
$body  = "A new registration has been received for the Robotics + Safe AI Program:\n\n";
$body .= "Parent/Carer: $parent\n";
$body .= "Email: $email\n";
$body .= "Phone: $phone\n";
$body .= "Student: $student\n";
$body .= "Preferred Session: $preference\n";
$body .= "Location: $location\n";
$body .= "Funding: $funding\n";
$body .= "Notes:\n$notes\n";

// --- PHPMailer manual includes ---
$src = __DIR__ . '/phpmailer/src';
if (!file_exists("$src/PHPMailer.php") || !file_exists("$src/Exception.php") || !file_exists("$src/SMTP.php")) {
  http_response_code(500);
  echo "PHPMailer files not found in $src. Upload PHPMailer.php, Exception.php, SMTP.php.";
  exit;
}
require "$src/PHPMailer.php";
require "$src/Exception.php";
require "$src/SMTP.php";

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;

try {
  $mail = new PHPMailer(true);

  // SMTP settings — update to your provider details
  $mail->isSMTP();
  $mail->Host       = 'mail.curlywords.com';          // e.g. mail.curlywords.com
  $mail->SMTPAuth   = true;
  $mail->Username   = 'no-reply@curlywords.com';      // full mailbox
  $mail->Password   = 'YOUR_PASSWORD_HERE';           // mailbox/app password
  $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // or PHPMailer::ENCRYPTION_SMTPS
  $mail->Port       = 587;                            // 587 for STARTTLS, 465 for SMTPS

  // Optional SMTP debug to error_log (not to screen)
  $mail->SMTPDebug  = 2; // 0=off; set 0 when live
  $mail->Debugoutput = function($str, $level) { error_log("SMTP[$level]: $str"); };

  // Headers & recipients
  $mail->setFrom($fromEmail, $fromName);
  $mail->addAddress($to);
  if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $mail->addReplyTo($email);
  } else {
    $mail->addReplyTo($fromEmail);
  }

  // Content
  $mail->isHTML(false);
  $mail->Subject = $subject;
  $mail->Body    = $body;

  // Send
  $mail->send();

  // Redirect on success
  header("Location: thank-you.html");
  exit;

} catch (Exception $e) {
  http_response_code(500);
  echo "Mailer exception: " . htmlspecialchars($e->getMessage());
  exit;
}


===== FILE: robotics.html @ 2025-10-02 11:06:10 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<style>
  :root{
    --bg:#f7f7f4; --panel:#ffffff; --ink:#142019; --sub:#5b6a62;
    --accent:#22a06b; --accent-2:#1b5ad6; --border:#e2e8e4; --muted:#eef2ef;
  }
  *{box-sizing:border-box} html,body{height:100%}
  body{margin:0;background:var(--bg);color:var(--ink);
       font:15px/1.5 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial}
  a{color:var(--accent-2);text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:980px;margin:0 auto;padding:20px}
  header{background:var(--panel);border-bottom:1px solid var(--border)}
  .brand{display:flex;align-items:center;gap:10px}
  .brand img{width:40px;height:40px;border-radius:8px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  /* Hero */
  .hero{display:grid;grid-template-columns:1.15fr .85fr;gap:20px;align-items:center;padding:26px 0}
  .hero h2{font-size:34px;line-height:1.1;margin:0 0 8px}
  .lead{font-size:17px;color:var(--sub);margin:0 0 14px}
  .badges{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0 18px}
  .badge{padding:6px 10px;border:1px solid var(--border);background:var(--muted);
         border-radius:999px;font-size:12px;color:var(--sub)}
  .card{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:16px}
  /* Buttons */
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:700;cursor:pointer;user-select:none;transition:transform .02s ease;
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--accent);color:#fff}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  /* Sections */
  section{padding:22px 0}
  h3{margin:0 0 10px;font-size:20px}
  ul{margin:0;padding-left:18px} li{margin:6px 0}
  .grid{display:grid;gap:16px}
  .grid-2{grid-template-columns:1fr 1fr}
  /* EBP tile */
  .ebp{display:grid;gap:10px}
  .ebp .pill{display:inline-block;background:#effbf5;border:1px solid #c9eedc;
             color:#0b5134;border-radius:999px;padding:6px 10px;font-size:12px}
  /* Table-ish list */
  .simplelist li{list-style:"✔  "; padding-left:6px}
  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .note{font-size:13px;color:var(--sub)}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}
  @media (max-width:920px){.hero{grid-template-columns:1fr}.grid-2{grid-template-columns:1fr}}
</style>
</head>
<body>
<header>
  <div class="wrap">
    <div class="brand">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build | Code | Design</h2>
      <p class="lead">We begin with a streamlined 8-week, small-group program where students design, code, and 3D-print their own desktop chatbot robot — while learning safe, ethical AI API use.</p>
      <div class="badges">
        <span class="badge">Years 4–10 (grouped)</span>
        <span class="badge">3 hrs sessions</span>
        <span class="badge">Classes limited to 5</span>
        <span class="badge">3D Printing</span>
        <span class="badge">AI calls and prompting</span>
      </div>
      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:8px">NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.</p>
    </div>
    <div class="card">
      <img src="/images/kidslearn1.jpg" alt="Student desktop chatbot robot" style="width:100%;height:auto;border-radius:12px;background:#ddd;display:block" />
      <p class="note" style="margin-top:6px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>

  <!-- EVIDENCE-BASED PRACTICE -->
  <section id="ebp">
    <div class="card ebp">
      <h3>Evidence-Based Teaching from registered QLD Teacher</h3>
      
      <p style="margin:6px 0 0">
        We design sessions using established teaching and learning models that prioritise
        <strong>high engagement</strong> (hands-on builds, visible progress),
        <strong>high support</strong> (clear scaffolds, step-by-step goals, co-regulation),
        and <strong>relevance</strong> (real-world robotics and practical AI that matter to students).
        Every activity connects to planning, problem-solving, and communication — not just code.
      </p>
    </div>
  </section>

  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — kept simple and safe</h3>
      <ul class="simplelist">
        <li>Wire an ESP32, control servos/LEDs, read sensors.</li>
        <li>MicroPython basics with online custom editors</li>
        <li>Call a safe AI API (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Make it yours</h3>
      <ul class="simplelist">
        <li>Design and 3D-print a custom shell and mounts.</li>
        <li>Give your robot a voice and simple “personality”.</li>
        <li>Showcase at the Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

  <!-- SCHEDULE (short) -->
  <section>
    <div class="card">
      <h3>Schedule</h3>
      <ul class="simplelist">
        <li>After-school hours: 2–3 hrs/week for 8 weeks</li>
        <li>Homeschool day group: 3 hrs/week for 8 weeks</li>
        <li>Sunshine Coast venues (or onsite by request)</li>
      </ul>
      <p class="note" style="margin-top:6px">
        We align invoices and notes with NDIS <em>Capacity Building – Improved Learning</em> goals on request.
      </p>
    </div>
  </section>

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:4px">
         <!--<button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
     --> </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>
    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a><br/>
         </p>
      <p class="note">We’re happy to map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/kidslearn3.jpg" alt="Students assembling robots" style="width:100%;border-radius:12px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  document.getElementById('y').textContent = new Date().getFullYear();

  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  // Simple preview (no submit)
  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\\n'));
  });
</script>
</body>
</html>


===== FILE: Flynn/Flynn/schema.sql @ 2025-10-03 06:51:19 =====
-- Minimal tables
CREATE TABLE api_tokens (
  id INT AUTO_INCREMENT PRIMARY KEY,
  label VARCHAR(100) NOT NULL,
  provider ENUM('groq','openrouter','huggingface','together','fireworks','other') NOT NULL DEFAULT 'groq',
  api_key_cipher TEXT NOT NULL,       -- encrypted (or plaintext fallback)
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE icon_codes (
  id INT AUTO_INCREMENT PRIMARY KEY,
  api_token_id INT NOT NULL,
  code VARCHAR(64) NOT NULL,          -- e.g. SFTCRSFT (ASCII) maps to emojis
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  expires_at TIMESTAMP NULL DEFAULT NULL,
  one_time_use TINYINT(1) NOT NULL DEFAULT 1,
  used_at TIMESTAMP NULL DEFAULT NULL,
  FOREIGN KEY (api_token_id) REFERENCES api_tokens(id) ON DELETE CASCADE,
  UNIQUE KEY (code)
);

CREATE TABLE sessions (
  id INT AUTO_INCREMENT PRIMARY KEY,
  icon_code_id INT NOT NULL,
  session_token CHAR(44) NOT NULL,    -- urlsafe base64
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  expires_at TIMESTAMP NOT NULL,
  FOREIGN KEY (icon_code_id) REFERENCES icon_codes(id) ON DELETE CASCADE,
  UNIQUE KEY (session_token)
);


===== FILE: Flynn/Flynn/config.php @ 2025-10-03 06:51:32 =====
<?php
// config.php
declare(strict_types=1);

$dbHost = 'localhost';
$dbUser = 'YOUR_DB_USER';
$dbPass = 'YOUR_DB_PASS';
$dbName = 'YOUR_DB_NAME';

// Optional: libsodium key for at-rest encryption of provider API keys.
// Generate once: base64_encode(random_bytes(32))
$SODIUM_KEY_B64 = getenv('SODIUM_KEY_B64') ?: ''; // or hardcode for dev only

function pdo(): PDO {
  global $dbHost, $dbUser, $dbPass, $dbName;
  $dsn = "mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4";
  $pdo = new PDO($dsn, $dbUser, $dbPass, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  ]);
  return $pdo;
}

function seal(string $plaintext): string {
  global $SODIUM_KEY_B64;
  if (function_exists('sodium_crypto_secretbox') && $SODIUM_KEY_B64) {
    $key = base64_decode($SODIUM_KEY_B64, true);
    $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
    $cipher = sodium_crypto_secretbox($plaintext, $nonce, $key);
    return 'enc:'.base64_encode($nonce.$cipher);
  }
  return 'raw:'.$plaintext; // fallback (dev)
}

function unseal(string $stored): string {
  global $SODIUM_KEY_B64;
  if (str_starts_with($stored,'enc:') && function_exists('sodium_crypto_secretbox') && $SODIUM_KEY_B64) {
    $buf = base64_decode(substr($stored,4), true);
    $nonce = substr($buf, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
    $cipher = substr($buf, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
    $key = base64_decode($SODIUM_KEY_B64, true);
    $plain = sodium_crypto_secretbox_open($cipher,$nonce,$key);
    if ($plain === false) throw new RuntimeException('Decrypt failed');
    return $plain;
  }
  return str_starts_with($stored,'raw:') ? substr($stored,4) : $stored;
}


===== FILE: Flynn/Flynn/icons.php @ 2025-10-03 06:51:54 =====
<?php
// icons.php - shared utilities for icon codes
declare(strict_types=1);

// Five-icon alphabet — ASCII tags + emoji presentation
const ICON_ALPHA = ['S','F','T','C','R']; // Ship, Flower, Tree, Chicken, Robot
const ICON_EMOJI = [
  'S' => '🚢', 'F' => '🌸', 'T' => '🌳', 'C' => '🐔', 'R' => '🤖'
];

function random_icon_code(int $len = 8): string {
  $out = '';
  for ($i=0; $i<$len; $i++) {
    $out .= ICON_ALPHA[random_int(0, count(ICON_ALPHA)-1)];
  }
  return $out; // e.g. "SFTCRSFT"
}

function code_to_emoji(string $code): string {
  $chars = str_split(strtoupper(preg_replace('/[^SFTCR]/i','',$code)));
  return implode('', array_map(fn($c)=> ICON_EMOJI[$c] ?? '', $chars));
}


===== FILE: Flynn/Flynn/teacher_register.php @ 2025-10-03 06:52:11 =====
<?php
declare(strict_types=1);
require __DIR__.'/config.php';
require __DIR__.'/icons.php';

$pdo = pdo();
$msg = '';

if ($_SERVER['REQUEST_METHOD']==='POST' && isset($_POST['label'],$_POST['provider'],$_POST['api_key'])) {
  $stmt = $pdo->prepare("INSERT INTO api_tokens(label,provider,api_key_cipher) VALUES(?,?,?)");
  $stmt->execute([trim($_POST['label']), $_POST['provider'], seal(trim($_POST['api_key']))]);
  $msg = "Saved provider token for “".htmlspecialchars($_POST['label'])."”.";
}

// Fetch tokens to display + “generate code” button
$tokens = $pdo->query("SELECT id,label,provider,created_at FROM api_tokens ORDER BY id DESC")->fetchAll();
?>
<!doctype html><meta charset="utf-8">
<title>Flynn • Token Registry</title>
<style>
  body{font:14px/1.4 system-ui,Segoe UI,Inter,sans-serif;margin:24px;color:#0f172a}
  input,select,button{font:inherit;padding:8px;border:1px solid #cbd5e1;border-radius:8px}
  form{display:grid;gap:8px;max-width:520px;margin-bottom:24px;background:#fff;padding:16px;border:1px solid #e2e8f0;border-radius:12px}
  .row{display:flex;gap:8px}
  table{border-collapse:collapse;width:100%;max-width:800px}
  th,td{border-bottom:1px solid #e2e8f0;padding:10px}
  .pill{display:inline-block;padding:2px 8px;border-radius:999px;background:#eef2ff}
</style>

<h1>Flynn • Token Registry</h1>
<?php if($msg): ?><p class="pill"><?=htmlspecialchars($msg)?></p><?php endif; ?>

<form method="post">
  <div class="row">
    <input name="label" placeholder="Label (e.g., Groq Classroom)" required>
    <select name="provider">
      <option value="groq">Groq</option>
      <option value="openrouter">OpenRouter</option>
      <option value="huggingface">Hugging Face</option>
      <option value="together">Together AI</option>
      <option value="fireworks">Fireworks</option>
      <option value="other">Other (proxy)</option>
    </select>
  </div>
  <input name="api_key" placeholder="Paste REAL provider API key" required>
  <button>Save Provider Token</button>
</form>

<h2>Saved Provider Tokens</h2>
<table>
  <tr><th>Label</th><th>Provider</th><th>Created</th><th>Share Icon Code</th></tr>
  <?php foreach($tokens as $t): ?>
  <tr>
    <td><?=htmlspecialchars($t['label'])?></td>
    <td><?=htmlspecialchars($t['provider'])?></td>
    <td><?=htmlspecialchars($t['created_at'])?></td>
    <td>
      <form action="generate_code.php" method="post" style="display:inline">
        <input type="hidden" name="api_token_id" value="<?=$t['id']?>">
        <input type="datetime-local" name="expires_at" placeholder="Expiry (optional)">
        <button>Create Icon Code</button>
      </form>
    </td>
  </tr>
  <?php endforeach; ?>
</table>


===== FILE: Flynn/Flynn/generate_code.php @ 2025-10-03 06:52:24 =====
<?php
declare(strict_types=1);
require __DIR__.'/config.php';
require __DIR__.'/icons.php';

if ($_SERVER['REQUEST_METHOD']!=='POST' || empty($_POST['api_token_id'])) {
  http_response_code(400); echo "Bad request"; exit;
}
$pdo = pdo();
$tokenId = (int)$_POST['api_token_id'];
$expiresAt = !empty($_POST['expires_at']) ? date('Y-m-d H:i:s', strtotime($_POST['expires_at'])) : null;

$code = random_icon_code(8); // 8 “digits” base-5 ≈ 1:390625 space
$stmt = $pdo->prepare("INSERT INTO icon_codes(api_token_id,code,expires_at) VALUES(?,?,?)");
$stmt->execute([$tokenId,$code,$expiresAt]);

$emoji = code_to_emoji($code);
?>
<!doctype html><meta charset="utf-8">
<title>Share this Icon Code</title>
<style>
  body{font:16px/1.5 system-ui,Segoe UI,Inter,sans-serif;margin:40px;color:#0f172a;text-align:center}
  .card{display:inline-block;border:1px solid #e2e8f0;border-radius:16px;padding:24px 28px;background:#fff}
  .emoji{font-size:42px;letter-spacing:6px}
  .ascii{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:#334155}
  .hint{color:#64748b}
  .row{display:flex;gap:12px;justify-content:center;margin-top:16px}
  input{font:inherit;padding:8px;border:1px solid #cbd5e1;border-radius:8px;width:340px}
  button{font:inherit;padding:10px 14px;border:1px solid #cbd5e1;border-radius:10px;background:#ecfeff}
</style>

<div class="card">
  <h1>Give students this Icon Code</h1>
  <div class="emoji"><?=$emoji?></div>
  <p class="ascii"><?=$code?></p>
  <p class="hint">Students can click icons or paste the ASCII letters (S F T C R) on the keypad page.</p>

  <div class="row">
    <input readonly value="<?=htmlspecialchars('https://YOURDOMAIN/Flynn/iconauth/student_pad.php')?>">
    <button onclick="navigator.clipboard.writeText('<?=htmlspecialchars('https://YOURDOMAIN/Flynn/iconauth/student_pad.php')?>')">Copy keypad link</button>
  </div>
</div>


===== FILE: Flynn/Flynn/student_pad.php @ 2025-10-03 06:52:35 =====
<?php declare(strict_types=1); require __DIR__.'/icons.php'; ?>
<!doctype html><meta charset="utf-8">
<title>Enter Icon Code</title>
<style>
  :root{--ink:#0f172a;--sub:#475569;--btn:#e2e8f0;--accent:#22a06b}
  body{font:16px/1.4 system-ui,Segoe UI,Inter,sans-serif;margin:24px;color:var(--ink)}
  .pad{display:grid;grid-template-columns:repeat(5,80px);gap:12px;justify-content:center;margin:20px 0}
  .btn{font-size:36px;line-height:60px;width:80px;height:80px;border-radius:16px;border:1px solid #cbd5e1;background:#fff;cursor:pointer}
  .screen{font-size:28px;text-align:center;min-height:48px;margin:10px;color:#111}
  .row{display:flex;gap:8px;justify-content:center}
  input{font:inherit;padding:8px;border:1px solid #cbd5e1;border-radius:8px;width:340px}
  .fine{color:var(--sub);text-align:center}
</style>

<h1 style="text-align:center">Enter your Icon Code</h1>
<div class="screen" id="screen"></div>
<div class="pad" id="pad"></div>
<div class="row">
  <button class="btn" onclick="backspace()">⌫</button>
  <button class="btn" onclick="clearCode()">⟲</button>
  <button class="btn" onclick="submitCode()" title="Exchange for session token">✓</button>
</div>
<p class="fine">Tip: You can also type S, F, T, C, R on the keyboard.</p>
<div class="row"><input id="result" readonly placeholder="Session token will appear here"></div>

<script>
const ICONS = { S:'🚢', F:'🌸', T:'🌳', C:'🐔', R:'🤖' };
const order = ['S','F','T','C','R'];
const pad = document.getElementById('pad');
const screen = document.getElementById('screen');
const result = document.getElementById('result');
let code='';

order.forEach(k=>{
  const b=document.createElement('button');
  b.className='btn'; b.textContent=ICONS[k];
  b.onclick=()=>{ code+=k; render(); };
  pad.appendChild(b);
});

function render(){ screen.textContent = code.split('').map(c=>ICONS[c]||'').join(''); }
function backspace(){ code = code.slice(0,-1); render(); }
function clearCode(){ code=''; render(); }

document.addEventListener('keydown', (e)=>{
  const k = e.key.toUpperCase();
  if (['S','F','T','C','R'].includes(k)){ code+=k; render(); }
  if (e.key==='Backspace') backspace();
  if (e.key==='Enter') submitCode();
});

async function submitCode(){
  if(!code){ alert('Enter your code first'); return; }
  const res = await fetch('exchange.php', {
    method:'POST',
    headers:{'Content-Type':'application/json'},
    body: JSON.stringify({ code })
  });
  const data = await res.json();
  if(!res.ok){ alert(data.error||'Exchange failed'); return; }
  result.value = data.session_token;
}
</script>


===== FILE: robotics.html @ 2025-10-08 03:57:35 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    content:"🤖";position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build • Code • Design</h2>
      <p class="lead">
        A playful 8-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
        NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
      </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>

  <!-- EVIDENCE-BASED PRACTICE -->
  <section id="ebp">
    <div class="card">
      <span class="pill">✅ Evidence-Based • Registered QLD Teacher</span>
      <h3 style="margin-top:10px">High Engagement • High Support • Real-World Relevance</h3>
      <p style="margin:6px 0 0">
        Sessions follow clear scaffolds and goal steps with co-regulation built in.
        Every activity links planning, problem-solving, and communication to practical robotics + AI — not just code.
      </p>
    </div>
  </section>

  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Wire an ESP32, control servos/LEDs, and read sensors.</li>
        <li>MicroPython basics with our kid-friendly web editors.</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Make it yours</h3>
      <ul class="list">
        <li>Design and 3D-print a custom shell and mounts.</li>
        <li>Give your robot a voice and simple “personality”.</li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

  <!-- SCHEDULE -->
  <section>
    <div class="card">
      <h3>Schedule</h3>
      <ul class="list">
        <li>After-school: 2–3 hrs/week for 8 weeks</li>
        <li>Homeschool day group: 3 hrs/week for 8 weeks</li>
        <li>Sunshine Coast venues (or onsite by request)</li>
      </ul>
      <p class="note" style="margin-top:6px">
        Invoices and notes can align with NDIS <em>Capacity Building – Improved Learning</em> goals.
      </p>
    </div>
  </section>

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/robots2.jpg" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:13:53 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • Curly Words</title>
<meta name="description" content="Build a friendly desktop chatbot robot while learning safe AI basics." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; --panel:#ffffff; --ink:#142019; --sub:#5b6a62;
    --accent:#22a06b; --accent-2:#6b7cff; --border:#e2e8e4;
    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 60%, #9b55e5 100%);
  }
  *{box-sizing:border-box}
  html,body{height:100%}
  body{margin:0;background:var(--bg);color:var(--ink);
       font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial}

  a{color:#335aff;text-decoration:none}
  a:hover{text-decoration:underline}
  .wrap{max-width:980px;margin:0 auto;padding:20px}

  header{background:#fff;border-bottom:1px solid var(--border)}
  .brand{display:flex;align-items:center;gap:12px}
  .brand img{width:40px;height:40px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}

  /* HERO */
  .hero{
    margin-top:16px;
    background:linear-gradient(180deg,#ffffff, #f4f6ff);
    border:1px solid var(--border); border-radius:22px; padding:28px;
  }
  .hero-inner{
    display:grid; grid-template-columns:1fr 280px 1fr; gap:18px; align-items:center;
  }
  .hero h2{font-size:38px;line-height:1.05;margin:0 0 10px}
  .hero p{margin:0;color:var(--sub)}
  .hero img{
    width:100%; height:auto; display:block; border-radius:18px;
    box-shadow:0 14px 40px rgba(0,0,0,.08);
    background:#e9eef3
  }
  .chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
  .chip{padding:6px 10px;border-radius:999px;border:1px solid var(--border);background:#fff;font-size:12px;color:var(--sub)}
  .cta{display:flex;gap:10px;margin-top:14px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;padding:12px 16px;
    border-radius:14px;border:1px solid transparent;font-weight:800;cursor:pointer;
    box-shadow:0 8px 24px rgba(0,0,0,.06)
  }
  .btn-primary{background:var(--grad);color:#fff}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}

  /* THREE CARDS */
  .cards{display:grid;grid-template-columns:1fr 1fr 1fr; gap:16px; margin:22px 0}
  .card{background:#fff;border:1px solid var(--border);border-radius:16px;padding:16px}
  .card h3{margin:0 0 8px;font-size:18px}
  .card p{margin:0;color:var(--sub)}

  /* CONTACT */
  .contact{
    background:#fff;border:1px solid var(--border);border-radius:18px;padding:18px;
  }
  .contact h3{margin:0 0 10px}
  form{display:grid;gap:10px}
  input,textarea{width:100%;padding:12px;border:1px solid var(--border);border-radius:12px}
  .row{display:grid;grid-template-columns:1fr 1fr; gap:10px}
  .note{font-size:13px;color:var(--sub)}
  footer{padding:18px 0;color:var(--sub);font-size:13px}

  @media (max-width:980px){
    .hero-inner{grid-template-columns:1fr; text-align:center}
    .cards{grid-template-columns:1fr}
    .row{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap">
    <div class="brand">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div class="hero-inner">
      <div>
        <h2>Build a Chat-Bot Robot</h2>
        <p>8 fun weeks. Small groups. Safe AI. Real skills.</p>
        <div class="chips">
          <span class="chip">Years 4–10</span>
          <span class="chip">Max 5 per class</span>
          <span class="chip">3D printing + MicroPython</span>
        </div>
        <div class="cta">
          <a class="btn btn-primary" href="#contact">Register interest</a>
          <a class="btn btn-secondary" href="#about">How it works</a>
        </div>
      </div>

      <!-- Middle image (your generated bot) -->
      <div>
        <img src="/images/aichatbot1.png" alt="Desktop 3D-printed chatbot robot" />
      </div>

      <div id="about">
        <h2>Simple. Safe. Hands-on.</h2>
        <p>Students assemble a desktop robot, code behaviours, and make filtered AI API calls with keys kept safe.</p>
        <div class="chips">
          <span class="chip">ESP32-C3 Mini</span>
          <span class="chip">Voice + LED face</span>
          <span class="chip">Showcase in Week 8</span>
        </div>
      </div>
    </div>
  </section>

  <!-- 3 quick points -->
  <section class="cards">
    <div class="card">
      <h3>Build</h3>
      <p>Wire sensors & wheels, print parts, and customise the shell.</p>
    </div>
    <div class="card">
      <h3>Code</h3>
      <p>MicroPython basics + safe AI calls with kid-friendly editors.</p>
    </div>
    <div class="card">
      <h3>Show</h3>
      <p>Mini demo day where students present their robot.</p>
    </div>
  </section>

  <!-- Short schedule strip -->
  <section class="card" style="background:linear-gradient(135deg,#ffffff, #f3fff9);">
    <strong>Schedule:</strong> After-school or homeschool groups • 2–3 hrs/week • 8 weeks • Sunshine Coast  
    <span class="note">NDIS (Capacity Building – Improved Learning) friendly.</span>
  </section>

  <!-- CONTACT (simpler form) -->
  <section id="contact" class="contact">
    <h3>Register Your Interest</h3>
    <form id="contactForm" method="post" action="">
      <div class="row">
        <input id="name" name="name" placeholder="Your name" required />
        <input id="email" name="email" type="email" placeholder="Email" required />
      </div>
      <input id="phone" name="phone" type="tel" placeholder="Phone (optional)" />
      <textarea id="message" name="message" rows="3" placeholder="Student year level and best time to contact" required></textarea>
      <button class="btn btn-primary" type="submit" style="width:max-content">Send</button>
      <p class="note" id="msg" role="status" aria-live="polite"></p>
    </form>
  </section>

  <footer>
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation</p>
  </footer>
</main>

<script>
  document.getElementById('y').textContent = new Date().getFullYear();

  // Very light validation message
  const form = document.getElementById('contactForm');
  const msg  = document.getElementById('msg');
  form.addEventListener('submit', (e)=>{
    const need = ['name','email','message'];
    const missing = need.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please fill in your name, email and message.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Thanks! We’ll be in touch.';
      msg.style.color = '#142019';
    }
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:14:21 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    content:"🤖";position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build • Code • Design</h2>
      <p class="lead">
        A playful 8-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
        NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
      </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>

  <!-- EVIDENCE-BASED PRACTICE -->
  <section id="ebp">
    <div class="card">
      <span class="pill">✅ Evidence-Based • Registered QLD Teacher</span>
      <h3 style="margin-top:10px">High Engagement • High Support • Real-World Relevance</h3>
      <p style="margin:6px 0 0">
        Sessions follow clear scaffolds and goal steps with co-regulation built in.
        Every activity links planning, problem-solving, and communication to practical robotics + AI — not just code.
      </p>
    </div>
  </section>

  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Wire an ESP32, control servos/LEDs, and read sensors.</li>
        <li>MicroPython basics with our kid-friendly web editors.</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Make it yours</h3>
      <ul class="list">
        <li>Design and 3D-print a custom shell and mounts.</li>
        <li>Give your robot a voice and simple “personality”.</li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

  <!-- SCHEDULE -->
  <section>
    <div class="card">
      <h3>Schedule</h3>
      <ul class="list">
        <li>After-school: 2–3 hrs/week for 8 weeks</li>
        <li>Homeschool day group: 3 hrs/week for 8 weeks</li>
        <li>Sunshine Coast venues (or onsite by request)</li>
      </ul>
      <p class="note" style="margin-top:6px">
        Invoices and notes can align with NDIS <em>Capacity Building – Improved Learning</em> goals.
      </p>
    </div>
  </section>

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/robots2.jpg" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:15:13 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    content:"🤖";position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build • Code • Design</h2>
      <p class="lead">
        A playful 8-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>

  <!-- EVIDENCE-BASED PRACTICE -->
  <section id="ebp">
    <div class="card">
      <span class="pill">✅ Evidence-Based • Registered QLD Teacher</span>
      <h3 style="margin-top:10px">High Engagement • High Support • Real-World Relevance</h3>
      <p style="margin:6px 0 0">
        Sessions follow clear scaffolds and goal steps with co-regulation built in.
        Every activity links planning, problem-solving, and communication to practical robotics + AI — not just code.
      </p>
    </div>
  </section>

  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Wire an ESP32, control servos/LEDs, and read sensors.</li>
        <li>MicroPython basics with our kid-friendly web editors.</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Make it yours</h3>
      <ul class="list">
        <li>Design and 3D-print a custom shell and mounts.</li>
        <li>Give your robot a voice and simple “personality”.</li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

  <!-- SCHEDULE -->
  <section>
    <div class="card">
      <h3>Schedule</h3>
      <ul class="list">
        <li>After-school: 2–3 hrs/week for 8 weeks</li>
        <li>Homeschool day group: 3 hrs/week for 8 weeks</li>
        <li>Sunshine Coast venues (or onsite by request)</li>
      </ul>
      <p class="note" style="margin-top:6px">
        Invoices and notes can align with NDIS <em>Capacity Building – Improved Learning</em> goals.
      </p>
    </div>
  </section>

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/robots2.jpg" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:16:06 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    content:"🤖";position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build • Code • Design</h2>
      <p class="lead">
        A playful 8-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Wire an ESP32, control servos/LEDs, and read sensors.</li>
        <li>MicroPython basics with our kid-friendly web editors.</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Make it yours</h3>
      <ul class="list">
        <li>Design and 3D-print a custom shell and mounts.</li>
        <li>Give your robot a voice and simple “personality”.</li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/robots2.jpg" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:17:55 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    content:"🤖";position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build • Code • Design</h2>
      <p class="lead">
        A playful 8-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Wire an ESP32, control servos/LEDs, and read sensors.</li>
        <li>MicroPython basics with our kid-friendly web editors.</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        <li>Wednesday 9am - 11:30am</li>
        <li>Wednesday 3pm - 5:30pm</li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/robots2.jpg" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:18:19 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build • Code • Design</h2>
      <p class="lead">
        A playful 8-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Wire an ESP32, control servos/LEDs, and read sensors.</li>
        <li>MicroPython basics with our kid-friendly web editors.</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        <li>Wednesday 9am - 11:30am</li>
        <li>Wednesday 3pm - 5:30pm</li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/robots2.jpg" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:21:05 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build • Code • Design</h2>
      <p class="lead">
        A playful 8-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Wire an ESP32, control servos/LEDs, and read sensors.</li>
        <li>MicroPython basics with our kid-friendly web editors.</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah community hall</a></li>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/robots2.jpg" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:21:30 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build • Code • Design</h2>
      <p class="lead">
        A playful 8-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Wire an ESP32, control servos/LEDs, and read sensors.</li>
        <li>MicroPython basics with our kid-friendly web editors.</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/robots2.jpg" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:23:28 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build • Code • Design</h2>
      <p class="lead">
        A playful 8-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/robots2.jpg" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:24:02 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build • Code • Design</h2>
      <p class="lead">
        A playful 8-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot.jpg" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:24:19 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build • Code • Design</h2>
      <p class="lead">
        A playful 8-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:24:45 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build • Code • Design</h2>
      <p class="lead">
        A playful 8-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot1.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:25:39 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Build • Code • Design</h2>
      <p class="lead">
        A playful 8-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot2.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:26:33 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Learn AI and Robotics</h2>
      <p class="lead">
        A playful 8-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Register Your Interest</h3>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot2.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:31:38 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Learn AI and Robotics</h2>
      <p class="lead">
        A playful 8-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Classes are limited, register now.</h3>
      <p>6 week unit delivered by a registered teacher - $500 per child (includes equipment and parts).</p>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot2.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:31:57 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Learn AI and Robotics</h2>
      <p class="lead">
        A playful 6-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Classes are limited, register now.</h3>
      <p>6 week unit delivered by a registered teacher - $500 per child (includes equipment and parts).</p>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>After-school (2 hrs)</option>
              <option>After-school (3 hrs)</option>
              <option>Homeschool day (3 hrs)</option>
            </select>
          </div>
          <div>
            <label for="location">Location</label>
            <select id="location" name="location" required>
              <option value="">Choose…</option>
              <option>Sunshine Coast (Nambour)</option>
              <option>Sunshine Coast (Maroochydore)</option>
              <option>School site visit</option>
            </select>
          </div>
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>NDIS – Group & Centre-Based (Social & Community)</option>
            <option>Self-funded</option>
            <option>School funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot2.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:35:04 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Learn AI and Robotics</h2>
      <p class="lead">
        A playful 6-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Classes are limited, register now.</h3>
      <p>6 week unit delivered by a registered teacher - $500 per child (includes equipment and parts).</p>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>Morning session (Didillabah Community Hall)</option>
              <option>After-school (Northshore Community Centre)</option>
            </select>
          </div>
          
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>Self-funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot2.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:37:35 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Learn AI and Robotics</h2>
      <p class="lead">
        A playful 6-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <img src="images/didillabah.jpg">
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
         <img src="images/northshore.jpg">
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Classes are limited, register now.</h3>
      <p>6 week unit delivered by a registered teacher - $500 per child (includes equipment and parts).</p>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>Morning session (Didillabah Community Hall)</option>
              <option>After-school (Northshore Community Centre)</option>
            </select>
          </div>
          
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>Self-funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot2.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:38:02 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Learn AI and Robotics</h2>
      <p class="lead">
        A playful 6-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <img src="images/didillabah.jpg" height="60px">
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
         <img src="images/northshore.jpg" height="60px">
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Classes are limited, register now.</h3>
      <p>6 week unit delivered by a registered teacher - $500 per child (includes equipment and parts).</p>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>Morning session (Didillabah Community Hall)</option>
              <option>After-school (Northshore Community Centre)</option>
            </select>
          </div>
          
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>Self-funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot2.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:38:18 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Learn AI and Robotics</h2>
      <p class="lead">
        A playful 6-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <img src="images/didillabah.jpg" height="60px"><br><br>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
         <img src="images/northshore.jpg" height="60px">
        <li>Showcase at a Week-8 mini demo day.</li>
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Classes are limited, register now.</h3>
      <p>6 week unit delivered by a registered teacher - $500 per child (includes equipment and parts).</p>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>Morning session (Didillabah Community Hall)</option>
              <option>After-school (Northshore Community Centre)</option>
            </select>
          </div>
          
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>Self-funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot2.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:38:36 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Learn AI and Robotics</h2>
      <p class="lead">
        A playful 6-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <img src="images/didillabah.jpg" height="60px"><br><br>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
         <img src="images/northshore.jpg" height="60px">
      
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Classes are limited, register now.</h3>
      <p>6 week unit delivered by a registered teacher - $500 per child (includes equipment and parts).</p>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>Morning session (Didillabah Community Hall)</option>
              <option>After-school (Northshore Community Centre)</option>
            </select>
          </div>
          
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>Self-funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot2.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:40:29 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Learn AI and Robotics</h2>
      <p class="lead">
        A playful 6-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
      <p>Students are guided through an engaging and individually led program of hands-on fun, learning the fundamentals of how to call AI models and store webapps online. Students link their chatbot brains with their custom printed bots to make a unique take-home project they can call their own.</p>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <img src="images/didillabah.jpg" height="60px"><br><br>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
         <img src="images/northshore.jpg" height="60px">
      
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Classes are limited, register now.</h3>
      <p>6 week unit delivered by a registered teacher - $500 per child (includes equipment and parts).</p>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>Morning session (Didillabah Community Hall)</option>
              <option>After-school (Northshore Community Centre)</option>
            </select>
          </div>
          
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>Self-funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot2.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:40:59 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Learn AI and Robotics</h2>
      <p class="lead">
        A playful 6-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
      <p style="color:grey">Students are guided through an engaging and individually led program of hands-on fun, learning the fundamentals of how to call AI models and store webapps online. Students link their chatbot brains with their custom printed bots to make a unique take-home project they can call their own.</p>
    </div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <img src="images/didillabah.jpg" height="60px"><br><br>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
         <img src="images/northshore.jpg" height="60px">
      
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Classes are limited, register now.</h3>
      <p>6 week unit delivered by a registered teacher - $500 per child (includes equipment and parts).</p>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>Morning session (Didillabah Community Hall)</option>
              <option>After-school (Northshore Community Centre)</option>
            </select>
          </div>
          
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>Self-funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot2.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-08 04:41:16 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Learn AI and Robotics</h2>
      <p class="lead">
        A playful 6-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
      <p style="color:grey"><em>Students are guided through an engaging and individually led program of hands-on fun, learning the fundamentals of how to call AI models and store webapps online. Students link their chatbot brains with their custom printed bots to make a unique take-home project they can call their own.</p>
    </em></div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <img src="images/didillabah.jpg" height="60px"><br><br>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
         <img src="images/northshore.jpg" height="60px">
      
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Classes are limited, register now.</h3>
      <p>6 week unit delivered by a registered teacher - $500 per child (includes equipment and parts).</p>
      <form action="" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>Morning session (Didillabah Community Hall)</option>
              <option>After-school (Northshore Community Centre)</option>
            </select>
          </div>
          
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>Self-funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form>
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot2.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-09 23:14:37 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Learn AI and Robotics</h2>
      <p class="lead">
        A playful 6-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
      <p style="color:grey"><em>Students are guided through an engaging and individually led program of hands-on fun, learning the fundamentals of how to call AI models and store webapps online. Students link their chatbot brains with their custom printed bots to make a unique take-home project they can call their own.</p>
    </em></div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <img src="images/didillabah.jpg" height="60px"><br><br>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
         <img src="images/northshore.jpg" height="60px">
      
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Classes are limited, register now.</h3>
      <p>6 week unit delivered by a registered teacher - $500 per child (includes equipment and parts).</p>
     
     <h1><a href="https://forms.office.com/r/nECjAEeVQW">Register your interest here!</a></h1>
     
      <!-- ### old contact form
      
      
      
      <form action="submit_robotics.php" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>Morning session (Didillabah Community Hall)</option>
              <option>After-school (Northshore Community Centre)</option>
            </select>
          </div>
          
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>Self-funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form> -->
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:hello@curlywords.com">hello@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot2.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics.html @ 2025-10-09 23:14:55 =====
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robotics + Safe AI • 8-Week Program | Curly Words</title>
<meta name="description" content="Build a desktop chat-bot robot and learn safe AI. Evidence-based teaching with high engagement, high support, and real-world relevance." />
<link rel="icon" type="image/png" href="/images/favicon.png" />

<style>
  :root{
    --bg:#f7f7f4; 
    --panel:#ffffff; 
    --ink:#0f1b17; 
    --sub:#5b6a62;
    --accent:#22a06b;           /* emerald */
    --accent-2:#6b7cff;         /* playful indigo */
    --accent-3:#ffb703;         /* mango */
    --border:#e2e8e4; 
    --muted:#eef2ef;

    --grad: linear-gradient(135deg,#2fb7a3 0%, #6b7cff 50%, #9b55e5 100%);
    --soft-grad: linear-gradient(135deg,#e8fbf6 0%, #eef0ff 50%, #f3eaff 100%);
  }

  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0; 
    background:var(--bg); 
    color:var(--ink);
    font:15px/1.55 system-ui,-apple-system,Segoe UI,Inter,Roboto,Helvetica,Arial
  }
  a{color:#335aff;text-decoration:none} a:hover{text-decoration:underline}
  .wrap{max-width:1060px;margin:0 auto;padding:20px}

  /* Header */
  header{
    position:relative;
    background:var(--panel);
    border-bottom:1px solid var(--border);
  }
  .brand{
    display:flex;align-items:center;gap:12px;padding:8px 0;
  }
  .brand img{width:42px;height:42px;border-radius:10px;object-fit:cover;background:#ddd}
  .brand h1{font-size:18px;margin:0}
  .tag{margin-left:auto;background:#eefbf6;color:#08684e;border:1px solid #c8efe0;
       padding:6px 10px;border-radius:999px;font-size:12px}

  /* Hero (fun) */
  .hero{
    position:relative;
    margin-top:10px;
    display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:center;
    background:var(--soft-grad);
    border:1px solid var(--border);
    border-radius:22px;
    padding:26px;
    overflow:hidden;
  }
  .hero::after{ /* colourful ribbon */
    content:""; position:absolute; inset:-40% -40% auto auto; height:260px; width:260px;
    background:var(--grad); filter:blur(40px); opacity:.35; transform:rotate(20deg);
    border-radius:50%;
  }
  .hero h2{font-size:40px;line-height:1.05;margin:0 0 10px}
  .hero .lead{font-size:17px;color:#2c3832;margin:0 0 16px}
  .badges{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 18px}
  .chip{
    --c: var(--accent);
    padding:7px 11px;border:1px solid color-mix(in srgb,var(--c),#000 8%);
    background: color-mix(in srgb,var(--c),#fff 85%);
    color: #0f3d2f; border-radius:999px; font-size:12px; font-weight:600;
  }
  .chip.yellow{--c: var(--accent-3)}
  .chip.indigo{--c: var(--accent-2)}
  .btnrow{display:flex;flex-wrap:wrap;gap:10px}
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    padding:14px 18px;border-radius:14px;border:1px solid transparent;
    font-weight:800;cursor:pointer;user-select:none;transition:transform .04s ease, box-shadow .2s ease, background .2s ease;
    box-shadow:0 6px 20px rgba(0,0,0,.06);
  }
  .btn:active{transform:translateY(1px)}
  .btn-primary{background:var(--grad);color:#fff;border:0}
  .btn-secondary{background:#fff;border:1px solid var(--border);color:var(--ink)}
  .note{font-size:13px;color:var(--sub)}

  /* Carousel card (right side) */
  .card{
    background:var(--panel);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px;
    box-shadow:0 10px 30px rgba(0,0,0,.05);
  }
  .carousel{
    position:relative;aspect-ratio:4/3;border-radius:14px;overflow:hidden;background:#dfe8ff;
  }
  .carousel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s ease}
  .carousel img.active{opacity:1}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
  .dots button{
    width:8px;height:8px;border-radius:50%;border:0;background:#cfd6ff;cursor:pointer
  }
  .dots button.active{background:#6b7cff}

  /* Sections */
  section{padding:24px 0}
  h3{margin:0 0 12px;font-size:20px}
  .grid{display:grid;gap:18px}
  .grid-2{grid-template-columns:1fr 1fr}

  /* Table-ish list with fun ticks */
  .list li{list-style:"";margin:8px 0;padding-left:26px;position:relative}
  .list li::before{
    position:absolute;left:0;top:-2px;font-size:18px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.05));
  }

  /* Pill header in EBP */
  .pill{
    display:inline-flex;align-items:center;gap:6px;
    background:#fff;border:1px dashed #bfe6d3;color:#0b5134;
    border-radius:999px;padding:6px 10px;font-size:12px
  }

  /* Form */
  form{display:grid;gap:10px}
  input,select,textarea{
    width:100%;padding:12px;border:1px solid var(--border);border-radius:12px;background:#fff
  }
  label{font-weight:600;font-size:14px}
  .footer{padding:18px 0;border-top:1px solid var(--border);color:var(--sub);font-size:13px}

  /* Responsive */
  @media (max-width:980px){
    .hero{grid-template-columns:1fr}
    .grid-2{grid-template-columns:1fr}
  }
</style>
</head>
<body>
<header>
  <div class="wrap brand">
    <div style="display:flex;align-items:center;gap:12px">
      <img src="/images/logo1.png" alt="Curly Words logo">
      <h1>Curly Words • Robotics + AI</h1>
    </div>
    <span class="tag">Neuro-affirming • Sunshine Coast</span>
  </div>
</header>

<main class="wrap">
  <!-- HERO -->
  <section class="hero">
    <div>
      <h2>Learn AI and Robotics</h2>
      <p class="lead">
        A playful 6-week, small-group program where students design, code, and 3D-print a desktop chatbot robot —
        while learning safe, ethical AI API use.
      </p>

      <div class="badges">
        <span class="chip">Years 4–10 (grouped)</span>
        <span class="chip indigo">3-hour sessions</span>
        <span class="chip yellow">Max 5 per class</span>
        <span class="chip">3D Printing</span>
        <span class="chip indigo">AI calls & prompting</span>
      </div>

      <div class="btnrow">
        <a class="btn btn-primary" href="#register">Register your interest</a>
        <a class="btn btn-secondary" href="#what-we-do">What we do</a>
      </div>
      <p class="note" style="margin-top:10px">
       <strong> NDIS (Capacity Building – Improved Learning) friendly. Homeschool & after-school options available.
     </strong> </p>
    </div>

    <!-- Fun image carousel -->
    <div class="card">
      <div class="carousel" id="carousel">
        <img src="/images/robots1.jpg" alt="Students with desktop chatbot robots" class="active">
        <img src="/images/robots2.jpg" alt="Building and wiring ESP32 boards">
        <img src="/images/robots3.jpg" alt="3D printed shells and personality">
      </div>
      <div class="dots" id="dots" aria-label="carousel controls"></div>
      <p class="note" style="margin-top:8px">Students customise their robot’s look and behaviour.</p>
    </div>
  </section>



  <!-- WHAT WE DO -->
  <section id="what-we-do" class="grid grid-2">
    <div class="card">
      <h3>Robotics & AI — simple and safe</h3>
      <ul class="list">
        <li>Learn robotics and app building.</li>
        <li>No need to know how to code!</li>
        <li>Make safe AI API calls (keys protected, filters on).</li>
      </ul>
      <p style="color:grey"><em>Students are guided through an engaging and individually led program of hands-on fun, learning the fundamentals of how to call AI models and store webapps online. Students link their chatbot brains with their custom printed bots to make a unique take-home project they can call their own.</p>
    </em></div>
    <div class="card">
      <h3>Class Times</h3>
      <ul class="list">
        
        <li>Wednesday 9am - 11:30am - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIJCAEQABgNGIAEMhgIAhAuGA0YrwEYxwEYgAQYjgUYmAUYngUyCQgDEC4YDRiABDIVCAQQLhgNGK8BGMcBGIAEGI4FGJgFMgkIBRAAGA0YgAQyCQgGEAAYDRiABDIJCAcQABgNGIAEMhIICBAuGA0YrwEYxwEYgAQYjgUyCQgJEAAYDRiABNIBCDMwMTdqMGo3qAIAsAIA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=Ked4J3KFd5NrMXOLrdsUt0IN&daddr=658+Diddillibah+Rd,+Diddillibah+QLD+4559">Didillabah Community Hall</a></li>
        <img src="images/didillabah.jpg" height="60px"><br><br>
        <li>Wednesday 3pm - 5:30pm - <a href="https://www.google.com/maps?rlz=1C1ONGR_en-GBAU1170AU1173&gs_lcrp=EgZjaHJvbWUyCAgAEEUYJxg5MgYIARBFGEAyFQgCEC4YChivARjHARjJAxiABBiOBTIJCAMQABgKGIAEMgkIBBAAGAoYgAQyCQgFEAAYChiABDIJCAYQABgKGIAEMgYIBxBFGDzSAQg0NDcxajBqN6gCALACAA&um=1&ie=UTF-8&fb=1&gl=au&sa=X&geocode=KW18tOy1dpNrMTCm6-h0WqLa&daddr=701+David+Low+Way,+Mudjimba+QLD+4564">Northshore Community Centre</a></li>
         <img src="images/northshore.jpg" height="60px">
      
      </ul>
    </div>
  </section>

 

  <!-- REGISTER -->
  <section id="register" class="grid grid-2">
    <div class="card">
      <h3>Classes are limited, register now.</h3>
      <p>6 week unit delivered by a registered teacher - $500 per child (includes equipment and parts).</p>
     
     <h1><a href="https://forms.office.com/r/nECjAEeVQW">Register your interest here!</a></h1>
     
      <!-- ### old contact form
      
      
      
      <form action="submit_robotics.php" method="post" id="regForm" novalidate>
        <div>
          <label for="parent">Parent/Carer Name</label>
          <input id="parent" name="parent" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="email">Email</label>
            <input id="email" name="email" type="email" required />
          </div>
          <div>
            <label for="phone">Phone</label>
            <input id="phone" name="phone" type="tel" required />
          </div>
        </div>
        <div>
          <label for="student">Student Name & Year Level</label>
          <input id="student" name="student" required />
        </div>
        <div class="grid grid-2">
          <div>
            <label for="preference">Preferred Session</label>
            <select id="preference" name="preference" required>
              <option value="">Choose…</option>
              <option>Morning session (Didillabah Community Hall)</option>
              <option>After-school (Northshore Community Centre)</option>
            </select>
          </div>
          
        </div>
        <div>
          <label for="funding">Funding</label>
          <select id="funding" name="funding" required>
            <option value="">Choose…</option>
            <option>NDIS – Capacity Building (Improved Learning)</option>
            <option>Self-funded</option>
          </select>
        </div>
        <div>
          <label for="notes">Anything we should know? (interests, supports, goals)</label>
          <textarea id="notes" name="notes" rows="4" placeholder="E.g., loves Minecraft, prefers low sensory space, goal: improve planning"></textarea>
        </div>
        <div class="btnrow" style="margin-top:6px">
          <button class="btn btn-primary" type="submit">Send</button>
          <button class="btn btn-secondary" type="button" id="preview">Preview</button>
        </div>
        <p class="note" id="msg" role="status" aria-live="polite"></p>
      </form> -->
    </div>

    <div class="card">
      <h3>Contact</h3>
      <p>Email: <a href="mailto:admin@curlywords.com">admin@curlywords.com</a></p>
      <p class="note">We can map the program to your child’s NDIS goals and provide progress notes.</p>
      <img src="/images/aichatbot2.png" alt="Students assembling robots" style="width:100%;border-radius:14px;background:#ddd" />
    </div>
  </section>

  <section class="footer">
    <p>&copy; <span id="y"></span> Curly Words • Mount of Olives Foundation. Neuro-affirming education and support.</p>
  </section>
</main>

<script>
  // Year
  document.getElementById('y').textContent = new Date().getFullYear();

  // Carousel (simple, accessible)
  const imgs = Array.from(document.querySelectorAll('#carousel img'));
  const dotsWrap = document.getElementById('dots');
  let i = 0, timer;

  function show(idx){
    imgs.forEach((im,j)=>im.classList.toggle('active', j===idx));
    [...dotsWrap.children].forEach((b,j)=>b.classList.toggle('active', j===idx));
    i = idx;
  }
  imgs.forEach((_, j)=>{
    const b = document.createElement('button');
    b.setAttribute('aria-label', 'Go to slide ' + (j+1));
    b.addEventListener('click', ()=>{ show(j); restart(); });
    dotsWrap.appendChild(b);
  });
  show(0);

  function tick(){
    i = (i+1) % imgs.length;
    show(i);
  }
  function restart(){
    clearInterval(timer);
    timer = setInterval(tick, 4200);
  }
  restart();

  // Form validation + preview
  const form = document.getElementById('regForm');
  const msg  = document.getElementById('msg');

  form.addEventListener('submit', (e)=>{
    const required = ['parent','email','phone','student','preference','location','funding'];
    const missing = required.filter(id => !document.getElementById(id).value.trim());
    if(missing.length){
      e.preventDefault();
      msg.textContent = 'Please complete all required fields.';
      msg.style.color = '#b42318';
    }else{
      msg.textContent = 'Submitting…';
      msg.style.color = '#142019';
    }
  });

  document.getElementById('preview').addEventListener('click', ()=>{
    const data = Object.fromEntries(new FormData(form).entries());
    alert('Preview:\n' + Object.entries(data).map(([k,v])=>`${k}: ${v}`).join('\n'));
  });
</script>
</body>
</html>


===== FILE: robotics/gamemaker.html @ 2025-10-11 22:15:31 =====
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>Droid Dojo - IDE</title>

  <!-- CodeMirror -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.13/lib/codemirror.min.css">
  <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.13/lib/codemirror.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.13/mode/clike/clike.min.js"></script>

  <style>
    :root{
      --bg:#fdfcf9; --ink:#222; --bar:#d69e3b; --muted:#4a3f35;
      --panel:#f4f1ee; --border:#ddd; --console:#111; --consoleText:#0f0;
    }
    *{box-sizing:border-box}
    html,body{height:100%}
    body{
      margin:0; background:var(--bg); color:var(--ink);
      font-family:system-ui,"Segoe UI",Arial,sans-serif;
      display:flex; flex-direction:column;
    }
    #topbar{
      background:var(--bar); color:var(--muted);
      padding:10px 16px; border-bottom:1px solid #c3b299;
      height:12%;
      display:flex; align-items:center; justify-content:space-between;
      font-weight:600;
    }
    .btn-primary {
  background:#22a06b;   /* emerald green */
  color:white;
  border:1px solid #1c7e55;
  font-weight:700;
  box-shadow:0 2px 4px rgba(0,0,0,0.2);
  display:inline-flex; align-items:center; justify-content:center;
      padding:10px 12px; border-radius:8px; border:1px solid #c3b299;
       cursor:pointer; font-weight:600;
}
.btn-primary:hover {
  background:#1c7e55;
}

    #main{flex:1; display:flex; min-height:0}
    #sidebar{
      width:340px; background:var(--panel); border-right:1px solid var(--border);
      padding:12px; overflow:auto; font-size:14px;
    }
    #editorContainer{flex:1; display:flex; flex-direction:column; min-width:0}
    #codeArea{flex:1; min-height:300px}
    .CodeMirror{height:100%; font-size:14px}
    #bottomBar{
      height:140px; background:var(--console); color:var(--consoleText);
      font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
      padding:10px; overflow:auto; border-top:1px solid #444; white-space:pre-wrap;
    }
    h3{margin:10px 0 6px}
    .section{background:#fff; border:1px solid var(--border); border-radius:8px; padding:10px; margin-bottom:10px}
    label{display:block; font-weight:600; margin:8px 0 4px}
    select, textarea, input[type="text"]{
      width:100%; padding:8px; border:1px solid var(--border); border-radius:6px; background:#fff
    }
    .row{display:grid; grid-template-columns:1fr 1fr; gap:8px}
    .btn{
      display:inline-flex; align-items:center; justify-content:center;
      padding:10px 12px; border-radius:8px; border:1px solid #c3b299;
      background:#ffe9c8; color:#5b4b3f; cursor:pointer; font-weight:600;
    }
    .btn:active{transform:translateY(1px)}
    .toolbar{display:flex; gap:8px; align-items:center}
    .hint{color:#555; font-size:12px}
  </style>
</head>
<body>

  <div id="topbar">
    <div><img src="images/title.png" height="70px"> <span style="padding:10px; font-weight:700">Write words, get code...</span></div>
    <div class="toolbar">
      <button id="saveBtn" class="btn">Save</button>
      <button id="loadBtn" class="btn">Load</button>
      <button id="connectBtn" class="btn">Connect</button>
      <button id="sendBtn" class="btn-primary">Send</button>

      <input type="file" id="fileInput" style="display:none" />
    </div>
  </div>

  <div id="main">
    <!-- LEFT: AI Helper -->
    <aside id="sidebar">
      <h3>AI Code Helper</h3>

      <div class="section">
        <label for="projectGoal">Project goal</label>
        <textarea id="projectGoal" rows="4" placeholder="e.g., Drive forward until an obstacle is detected, then stop and beep."></textarea>
      </div>

      <div class="section">
        <h4 style="margin:0 0 6px">Sensors</h4>
        <label for="sensorType">Sensor type</label>
        <select id="sensorType">
          <option value="">— choose a sensor —</option>
          <option value="button">Button (digital)</option>
          <option value="ldr">LDR / Photoresistor (ADC)</option>
          <option value="dht11">DHT11 (temperature/humidity)</option>
          <option value="ultrasonic">HC-SR04 Ultrasonic (Trig/Echo)</option>
          <option value="irrecv">IR Receiver (digital)</option>
          <option value="i2c">I²C device (SDA/SCL)</option>
        </select>

        <div id="sensorPinsArea" style="margin-top:8px"></div>
      </div>

      <div class="section">
        <h4 style="margin:0 0 6px">Outputs</h4>
        <label for="outputType">Output type</label>
        <select id="outputType">
          <option value="">— choose an output —</option>
          <option value="led">LED (digital)</option>
          <option value="buzzer">Buzzer (PWM)</option>
          <option value="servo">Servo (PWM)</option>
          <option value="motor">DC Motor (H-bridge IN1/IN2)</option>
          <option value="neopixel">NeoPixel strip (1-wire)</option>
        </select>

        <div id="outputPinsArea" style="margin-top:8px"></div>
      </div>

      <div class="section">
        <button id="askAI" class="btn" style="width:100%">Ask AI for Arduino Code</button>
        <div class="hint" style="margin-top:6px">
          Your <strong>CODE</strong> will be generated to the right.
        </div>
      </div>
    </aside>

    <!-- RIGHT: Editor + Console -->
    <div id="editorContainer">
      <div id="codeArea"></div>
      <div id="bottomBar">[ Serial output will appear here ]</div>
    </div>
  </div>

  <script>
  // --------------------- Pin Helpers ---------------------
  // Common, safe-to-use GPIOs for ESP32-S2 Mini (digital/PWM capable in most libs)
  const ALL_PINS = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,21,33,34,35,36,37,38,39,40];
  const ADC_PINS = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,21,33,34,35,36,37,38,39,40]; // S2 ADC1/2 lines; keep broad for simplicity
  const PWM_PINS = ALL_PINS; // ESP32-S2 can attach PWM to most GPIOs via LEDC
  const I2C_DEFAULT = { SDA: 3, SCL: 4 }; // You can change these; ESP32 lets you remap

  function pinSelect(id, label, pins = ALL_PINS, value = "") {
    const opts = pins.map(p => `<option value="${p}" ${value==p?'selected':''}>GPIO${p}</option>`).join("");
    return `
      <label for="${id}">${label}</label>
      <select id="${id}"><option value="">— select —</option>${opts}</select>
    `;
  }

  // Build dynamic pin UIs based on selection
  const sensorTypeEl = document.getElementById('sensorType');
  const sensorPinsArea = document.getElementById('sensorPinsArea');
  const outputTypeEl = document.getElementById('outputType');
  const outputPinsArea = document.getElementById('outputPinsArea');

  sensorTypeEl.addEventListener('change', () => {
    const t = sensorTypeEl.value;
    if (!t) { sensorPinsArea.innerHTML = ""; return; }
    if (t === 'button' || t === 'irrecv' || t === 'dht11') {
      sensorPinsArea.innerHTML = pinSelect('sensorPin1', 'Sensor pin', ALL_PINS);
    } else if (t === 'ldr') {
      sensorPinsArea.innerHTML = pinSelect('sensorPin1', 'ADC pin', ADC_PINS);
    } else if (t === 'ultrasonic') {
      sensorPinsArea.innerHTML =
        pinSelect('sensorTrig', 'Trig pin', ALL_PINS) +
        pinSelect('sensorEcho', 'Echo pin', ALL_PINS);
    } else if (t === 'i2c') {
      sensorPinsArea.innerHTML = `
        ${pinSelect('sensorSDA', 'SDA pin', ALL_PINS, I2C_DEFAULT.SDA)}
        ${pinSelect('sensorSCL', 'SCL pin', ALL_PINS, I2C_DEFAULT.SCL)}
      `;
    }
  });

  outputTypeEl.addEventListener('change', () => {
    const t = outputTypeEl.value;
    if (!t) { outputPinsArea.innerHTML = ""; return; }
    if (t === 'led' || t === 'buzzer' || t === 'servo' || t === 'neopixel') {
      const pins = (t === 'buzzer' || t === 'servo') ? PWM_PINS : ALL_PINS;
      outputPinsArea.innerHTML = pinSelect('outputPin1', 'Output pin', pins);
    } else if (t === 'motor') {
      outputPinsArea.innerHTML =
        pinSelect('motorIn1', 'Motor IN1', ALL_PINS) +
        pinSelect('motorIn2', 'Motor IN2', ALL_PINS);
    }
  });

  // --------------------- Editor ---------------------
  let editor, port, writer, reader;

  window.addEventListener('DOMContentLoaded', () => {
    editor = CodeMirror(document.getElementById('codeArea'), {
      value: `// Arduino code will appear here (ESP32-S2 Mini)\n\n`,
      lineNumbers: true,
      mode: 'text/x-c++src',
      lineWrapping: true
    });

    document.getElementById('saveBtn').addEventListener('click', () => {
      const code = editor.getValue();
      const blob = new Blob([code], { type: 'text/plain' });
      const a = document.createElement('a');
      a.download = 'robot-arduino.cpp';
      a.href = URL.createObjectURL(blob);
      a.click();
    });

    document.getElementById('loadBtn').addEventListener('click', () => {
      document.getElementById('fileInput').click();
    });

    document.getElementById('fileInput').addEventListener('change', e => {
      const f = e.target.files[0]; if (!f) return;
      const r = new FileReader();
      r.onload = ev => editor.setValue(ev.target.result);
      r.readAsText(f);
    });

    document.getElementById('connectBtn').addEventListener('click', connectSerial);
    document.getElementById('sendBtn').addEventListener('click', sendCode);
    document.getElementById('askAI').addEventListener('click', askAIForHelp);
  });

  // --------------------- AI Helper (via ai.php -> Groq llama-3.3-70b-versatile) ---------------------
  async function askAIForHelp() {
    const goal = (document.getElementById('projectGoal').value || '').trim();

    // Collect sensor selection
    const sensorType = sensorTypeEl.value;
    const sensor = { type: sensorType };
    if (sensorType === 'button' || sensorType === 'irrecv' || sensorType === 'dht11' || sensorType === 'ldr') {
      sensor.pin1 = document.getElementById('sensorPin1')?.value || "";
    } else if (sensorType === 'ultrasonic') {
      sensor.trig = document.getElementById('sensorTrig')?.value || "";
      sensor.echo = document.getElementById('sensorEcho')?.value || "";
    } else if (sensorType === 'i2c') {
      sensor.sda = document.getElementById('sensorSDA')?.value || "";
      sensor.scl = document.getElementById('sensorSCL')?.value || "";
    }

    // Collect output selection
    const outputType = outputTypeEl.value;
    const output = { type: outputType };
    if (outputType === 'led' || outputType === 'buzzer' || outputType === 'servo' || outputType === 'neopixel') {
      output.pin1 = document.getElementById('outputPin1')?.value || "";
    } else if (outputType === 'motor') {
      output.in1 = document.getElementById('motorIn1')?.value || "";
      output.in2 = document.getElementById('motorIn2')?.value || "";
    }

    const prompt = `You are a friendly Arduino mentor for school students using an ESP32-S2 Mini.

Hardware:
- Sensor: ${JSON.stringify(sensor)}
- Output: ${JSON.stringify(output)}
- Goal: ${goal || "(not specified)"}

Write COMPLETE Arduino C++ code (for the Arduino-ESP32 core) that:
- Uses correct ESP32 pin numbers (GPIO) according to the selections.
- Includes clear comments explaining each step for beginners.
- Avoids blocking delays where practical; use millis() where helpful.
- If the sensor/output requires a library (e.g., DHT, NeoPixel, Servo), include the correct #include and a short setup snippet.
- Use default I²C pins from the user's selection if I²C is chosen (Wire.begin(SDA, SCL)).

Return ONLY one code block starting with \`\`\`cpp and ending with \`\`\`. Do not add any extra prose outside the code block.`;

    try {
      const res = await fetch('ai.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          // ai.php will inject the Groq key and forward to: llama-3.3-70b-versatile
          messages: [{ role: 'user', content: prompt }]
        })
      });
      if (!res.ok) {
        const t = await res.text();
        editor.setValue(`[Error ${res.status}] ${t}`);
        return;
      }
      const data = await res.json();
      let text = data.choices?.[0]?.message?.content || '';

      // Strip code fences if present
      if (text.startsWith('```')) {
        const lines = text.split('\n');
        lines.shift();
        if (lines[lines.length - 1].trim() === '```') lines.pop();
        text = lines.join('\n');
      }
      editor.setValue(text || '// No response');
      logOutput('[ AI: Arduino code generated ]');
    } catch (e) {
      editor.setValue(`// Network error: ${e.message}`);
    }
  }

  // --------------------- Serial (optional for your workflow) ---------------------
  async function connectSerial() {
    try {
      if (!('serial' in navigator)) {
        alert('Web Serial API requires Chrome/Edge over HTTPS.');
        return;
      }
      port = await navigator.serial.requestPort();
      await port.open({ baudRate: 115200 });

      writer = port.writable.getWriter();

      const decoder = new TextDecoderStream();
      port.readable.pipeTo(decoder.writable);
      reader = decoder.readable.getReader();

      logOutput('[ Connected to device ]');
      readLoop();
    } catch (err) {
      alert('Error connecting: ' + err);
    }
  }

  async function readLoop() {
    try {
      while (true) {
        const { value, done } = await reader.read();
        if (done) break;
        if (value) logOutput(value);
      }
    } catch (_) {}
  }

  async function sendCode() {
    if (!writer) return alert('Not connected');
    const code = editor.getValue();
    const enc = new TextEncoder();
    try {
      await writer.write(enc.encode(code + '\r\n'));
      logOutput('[ Code sent ]');
    } catch (err) {
      logOutput('[ Upload error ] ' + err.message);
    }
  }

  function logOutput(t) {
    const el = document.getElementById('bottomBar');
    el.textContent += (t + '\n');
    el.scrollTop = el.scrollHeight;
  }
  </script>
</body>
</html>


===== FILE: robotics/gamemaker.php @ 2025-10-12 02:59:49 =====
import React, { useRef, useState } from "react";

export default function GameMakerIDE() {
  const [title, setTitle] = useState("My Vintage Game");
  const [desc, setDesc] = useState("Flappy-style with mountains and a header splash.");
  const [game, setGame] = useState("flappy");
  const [generatedIno, setGeneratedIno] = useState("");
  const [imagePreview, setImagePreview] = useState<string | null>(null);
  const [testLog, setTestLog] = useState<string>("");
  const canvasRef = useRef<HTMLCanvasElement | null>(null);

  // ---------------- Local image generation (menu header preview) ----------------
  function drawLocalTitleCardToCanvas(text: string) {
    const W = 160, H = 128;
    let c = canvasRef.current; if (!c) return;
    c.width = W; c.height = H;
    const ctx = c.getContext("2d"); if (!ctx) return;

    // Sky gradient
    const grad = ctx.createLinearGradient(0, 0, 0, H);
    grad.addColorStop(0, "#2278C5"); grad.addColorStop(1, "#93CCEA");
    ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H);

    // Stylized mountains
    ctx.fillStyle = "#5A6E82";
    for (let x = 0; x < W; x++) {
      const yTop = 80 - Math.sin((x + 10) * 0.11) * 8;
      ctx.fillRect(x, yTop, 1, H - yTop);
    }
    ctx.fillStyle = "#465764";
    for (let x = 0; x < W; x++) {
      const yTop = 88 - Math.sin((x + 20) * 0.13) * 10;
      ctx.fillRect(x, yTop, 1, H - yTop);
    }

    // Title text
    ctx.fillStyle = "#ffffff"; ctx.textAlign = "center";
    ctx.font = "bold 18px system-ui";
    ctx.fillText(text || "Game Title", W / 2, 28);
    ctx.font = "12px system-ui";
    ctx.fillText("Press button to start", W / 2, H - 10);

    setImagePreview(c.toDataURL("image/png"));
  }

  // ---------------- Code generation stub ----------------
  function handleGenerateIno() {
    setGeneratedIno(`// ${title}\n// ${desc}\n// Game type: ${game}`);
  }

  function generateImageOnly() {
    drawLocalTitleCardToCanvas(title);
  }

  function download(filename: string, text: string) {
    const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a"); a.href = url; a.download = filename; a.click();
    URL.revokeObjectURL(url);
  }

  // ---------------- Robust copy helper with sandbox-safe fallbacks ----------------
  type CopyResult = { method: "clipboard" | "exec" | "select"; success: boolean; message?: string };

  // Pure, testable decision function that simulates environments.
  function copyTextSmart(text: string, env?: { canClipboard?: boolean; isSecure?: boolean; allowExec?: boolean }): CopyResult {
    const canClipboard = env?.canClipboard ?? (typeof navigator !== "undefined" && !!navigator.clipboard);
    const isSecure = env?.isSecure ?? (typeof window !== "undefined" && window.isSecureContext);
    const allowExec = env?.allowExec ?? (typeof document !== "undefined" && typeof document.execCommand === "function");

    // 1) Prefer Clipboard API when available + secure contexts
    if (canClipboard && isSecure) {
      return { method: "clipboard", success: true };
    }
    // 2) Fallback to execCommand('copy') if allowed
    if (allowExec) {
      return { method: "exec", success: true };
    }
    // 3) Last resort: auto-select for user manual copy (Ctrl/Cmd+C)
    return { method: "select", success: false, message: "Clipboard blocked by permissions policy" };
  }

  // Actual copy bound to a user gesture (button click). Wraps the decision logic above.
  async function copyAll(text: string) {
    // Decide method based on real environment
    const decision = copyTextSmart(text);

    try {
      if (decision.method === "clipboard") {
        // Might still throw NotAllowedError in sandboxed iframes; we catch below
        await navigator.clipboard.writeText(text);
        alert("Copied to clipboard!");
        return;
      }

      if (decision.method === "exec") {
        const ta = document.createElement("textarea");
        ta.value = text;
        ta.setAttribute("readonly", "");
        ta.style.position = "fixed";  // prevent scroll jump
        ta.style.opacity = "0";
        document.body.appendChild(ta);
        ta.select();
        ta.setSelectionRange(0, ta.value.length);
        const ok = document.execCommand("copy");
        document.body.removeChild(ta);
        if (ok) { alert("Copied to clipboard!"); return; }
        // if exec failed, fall through to select-only
      }
    } catch (err: any) {
      // Intentionally ignore to fall back to selection/manual copy
    }

    // Manual selection fallback — opens a modal-ish selectable area
    const ta = document.createElement("textarea");
    ta.value = text;
    ta.style.width = "1px";
    ta.style.height = "1px";
    ta.style.opacity = "0";
    document.body.appendChild(ta);
    ta.focus();
    ta.select();
    ta.setSelectionRange(0, ta.value.length);
    alert("Couldn't access the clipboard here. Text has been selected — press Ctrl/Cmd+C to copy.");
    // keep the selection briefly then remove
    setTimeout(() => { document.body.removeChild(ta); }, 1500);
  }

  // ---------------- Lightweight tests (do not depend on real clipboard) ----------------
  function runCopyTests() {
    const cases: Array<{name: string, env: Parameters<typeof copyTextSmart>[1], expect: CopyResult["method"]}> = [
      { name: "Secure + clipboard", env: { canClipboard: true, isSecure: true, allowExec: true }, expect: "clipboard" },
      { name: "No clipboard but exec", env: { canClipboard: false, isSecure: false, allowExec: true }, expect: "exec" },
      { name: "Sandboxed no perms", env: { canClipboard: false, isSecure: false, allowExec: false }, expect: "select" },
    ];

    const results = cases.map(t => {
      const r = copyTextSmart("hello", t.env);
      return `${t.name}: ${r.method === t.expect ? "✅" : "❌"} (got ${r.method}, expected ${t.expect})`;
    });
    setTestLog(results.join("\n"));
  }

  // ---------------- UI ----------------
  return (
    <div className="min-h-screen bg-[tan] text-slate-900 font-sans">
      <div className="max-w-5xl mx-auto p-6">
        <h1 className="text-4xl font-extrabold text-center mb-2">🎮 Game Maker IDE</h1>
        <p className="text-center text-slate-700 mb-8">Create fun mini-games for your ESP32! Choose a game, name it, describe it, and watch your idea come to life.</p>

        <div className="bg-amber-100 p-6 rounded-3xl shadow-lg border border-amber-300">
          <div className="flex flex-col md:flex-row gap-6">
            <div className="flex-1 space-y-4">
              <label className="block">
                <span className="text-md font-semibold">Game Title</span>
                <input value={title} onChange={e=>setTitle(e.target.value)} className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg" placeholder="My Awesome Game" />
              </label>

              <label className="block">
                <span className="text-md font-semibold">Game Description</span>
                <textarea value={desc} onChange={e=>setDesc(e.target.value)} rows={4} className="mt-2 w-full rounded-2xl bg-white border border-amber-300 px-3 py-3 text-md" placeholder="Describe your game idea..." />
              </label>

              <label className="block">
                <span className="text-md font-semibold">Pick Game Type</span>
                <select value={game} onChange={e=>setGame(e.target.value)} className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg">
                  <option value="flappy">Flappy Bird</option>
                  <option value="snake">Snake</option>
                  <option value="pong">Pong</option>
                </select>
              </label>

              <div className="flex flex-wrap justify-center gap-4 mt-6">
                <button onClick={handleGenerateIno} className="bg-orange-500 hover:bg-orange-400 text-white font-bold px-5 py-2 rounded-2xl shadow">Generate Code</button>
                <button onClick={generateImageOnly} className="bg-yellow-500 hover:bg-yellow-400 text-white font-bold px-5 py-2 rounded-2xl shadow">Generate Image</button>
                <button onClick={()=>download(`${title.replace(/[^A-Za-z0-9_]+/g,'_')}.ino`, generatedIno || "// generate first")} className="bg-teal-500 hover:bg-teal-400 text-white font-bold px-5 py-2 rounded-2xl shadow">Download .ino</button>
                <button onClick={runCopyTests} className="bg-zinc-600 hover:bg-zinc-500 text-white font-bold px-5 py-2 rounded-2xl shadow">Run Copy Tests</button>
              </div>

              {testLog && (
                <pre className="bg-amber-50 border border-amber-300 rounded-xl p-3 text-xs whitespace-pre-wrap text-slate-800">{testLog}</pre>
              )}
            </div>

            <div className="flex-1 space-y-4">
              <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50 text-center">
                <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                  <span>Menu Header Preview</span>
                  <button onClick={()=>copyAll(imagePreview || "No image available.")} className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg">Copy All</button>
                </div>
                <div className="aspect-[5/4] w-full border-2 border-amber-300 rounded-xl grid place-items-center bg-amber-200">
                  <canvas ref={canvasRef} className="rounded" />
                  {!imagePreview && <div className="text-slate-500 text-sm">Click 'Generate Image'</div>}
                </div>
              </div>

              <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50">
                <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                  <span>Arduino Code</span>
                  <button onClick={()=>copyAll(generatedIno)} className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg">Copy All</button>
                </div>
                <textarea value={generatedIno} onChange={e=>setGeneratedIno(e.target.value)} rows={10} className="w-full rounded-xl bg-white border border-amber-300 p-3 font-mono text-xs" placeholder="Click 'Generate Code'" />
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}


===== FILE: robotics/gamemaker.html @ 2025-10-12 03:05:05 =====
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Game Maker IDE</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body class="bg-[tan] text-slate-900 font-sans">
  <div id="root"></div>

  <script type="text/babel" data-presets="react,typescript">
function GameMakerIDE() {
  const [title, setTitle] = React.useState("My Vintage Game");
  const [desc, setDesc] = React.useState("Flappy-style with mountains and a header splash.");
  const [game, setGame] = React.useState("flappy");
  const [generatedIno, setGeneratedIno] = React.useState("");
  const [imagePreview, setImagePreview] = React.useState(null);
  const canvasRef = React.useRef(null);

  function drawLocalTitleCardToCanvas(text) {
    const W = 160, H = 128;
    let c = canvasRef.current; if (!c) return;
    c.width = W; c.height = H;
    const ctx = c.getContext("2d"); if (!ctx) return;
    const grad = ctx.createLinearGradient(0, 0, 0, H);
    grad.addColorStop(0, "#2278C5"); grad.addColorStop(1, "#93CCEA");
    ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = "#5A6E82";
    for (let x = 0; x < W; x++) { const yTop = 80 - Math.sin((x + 10) * 0.11) * 8; ctx.fillRect(x, yTop, 1, H - yTop); }
    ctx.fillStyle = "#465764";
    for (let x = 0; x < W; x++) { const yTop = 88 - Math.sin((x + 20) * 0.13) * 10; ctx.fillRect(x, yTop, 1, H - yTop); }
    ctx.fillStyle = "#ffffff"; ctx.textAlign = "center";
    ctx.font = "bold 18px system-ui";
    ctx.fillText(text || "Game Title", W / 2, 28);
    ctx.font = "12px system-ui";
    ctx.fillText("Press button to start", W / 2, H - 10);
    setImagePreview(c.toDataURL("image/png"));
  }

  function handleGenerateIno() {
    setGeneratedIno(`// ${title}\n// ${desc}\n// Game type: ${game}`);
  }

  function generateImageOnly() {
    drawLocalTitleCardToCanvas(title);
  }

  function download(filename, text) {
    const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a"); a.href = url; a.download = filename; a.click();
    URL.revokeObjectURL(url);
  }

  async function copyAll(text) {
    try {
      await navigator.clipboard.writeText(text);
      alert("Copied to clipboard!");
    } catch {
      const ta = document.createElement("textarea");
      ta.value = text;
      document.body.appendChild(ta);
      ta.select();
      document.execCommand("copy");
      document.body.removeChild(ta);
      alert("Copied using fallback!");
    }
  }

  return (
    <div className="min-h-screen bg-[tan] text-slate-900 font-sans">
      <div className="max-w-5xl mx-auto p-6">
        <h1 className="text-4xl font-extrabold text-center mb-2">🎮 Game Maker IDE</h1>
        <p className="text-center text-slate-700 mb-8">Create fun mini-games for your ESP32! Choose a game, name it, describe it, and watch your idea come to life.</p>

        <div className="bg-amber-100 p-6 rounded-3xl shadow-lg border border-amber-300">
          <div className="flex flex-col md:flex-row gap-6">
            <div className="flex-1 space-y-4">
              <label className="block">
                <span className="text-md font-semibold">Game Title</span>
                <input value={title} onChange={e=>setTitle(e.target.value)} className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg" placeholder="My Awesome Game" />
              </label>

              <label className="block">
                <span className="text-md font-semibold">Game Description</span>
                <textarea value={desc} onChange={e=>setDesc(e.target.value)} rows={4} className="mt-2 w-full rounded-2xl bg-white border border-amber-300 px-3 py-3 text-md" placeholder="Describe your game idea..." />
              </label>

              <label className="block">
                <span className="text-md font-semibold">Pick Game Type</span>
                <select value={game} onChange={e=>setGame(e.target.value)} className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg">
                  <option value="flappy">Flappy Bird</option>
                  <option value="snake">Snake</option>
                  <option value="pong">Pong</option>
                </select>
              </label>

              <div className="flex flex-wrap justify-center gap-4 mt-6">
                <button onClick={handleGenerateIno} className="bg-orange-500 hover:bg-orange-400 text-white font-bold px-5 py-2 rounded-2xl shadow">Generate Code</button>
                <button onClick={generateImageOnly} className="bg-yellow-500 hover:bg-yellow-400 text-white font-bold px-5 py-2 rounded-2xl shadow">Generate Image</button>
                <button onClick={()=>download(`${title.replace(/[^A-Za-z0-9_]+/g,'_')}.ino`, generatedIno || "// generate first")} className="bg-teal-500 hover:bg-teal-400 text-white font-bold px-5 py-2 rounded-2xl shadow">Download .ino</button>
              </div>
            </div>

            <div className="flex-1 space-y-4">
              <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50 text-center">
                <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                  <span>Menu Header Preview</span>
                  <button onClick={()=>copyAll(imagePreview || "No image available.")} className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg">Copy All</button>
                </div>
                <div className="aspect-[5/4] w-full border-2 border-amber-300 rounded-xl grid place-items-center bg-amber-200">
                  <canvas ref={canvasRef} className="rounded" />
                  {!imagePreview && <div className="text-slate-500 text-sm">Click 'Generate Image'</div>}
                </div>
              </div>

              <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50">
                <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                  <span>Arduino Code</span>
                  <button onClick={()=>copyAll(generatedIno)} className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg">Copy All</button>
                </div>
                <textarea value={generatedIno} onChange={e=>setGeneratedIno(e.target.value)} rows={10} className="w-full rounded-xl bg-white border border-amber-300 p-3 font-mono text-xs" placeholder="Click 'Generate Code'" />
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<GameMakerIDE />);
  </script>
</body>
</html>


===== FILE: robotics/config/config.php @ 2025-10-12 03:11:02 =====
<?php
// config.php — centralised configuration for Game Maker IDE
// DO NOT expose this file publicly. Keep it outside the webroot if possible.

// === GROQ API KEYS ===
// These keys are used for secure backend calls (image + text generation)
// Your frontend will never see these keys directly.

$GROQ_TEXT_API_KEY = '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';   // used for generating code from description
$GROQ_IMAGE_API_KEY = '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';  // used for generating game menu/header images

// === OTHER SETTINGS ===
$MODEL_TEXT = 'groq-llama-3.1-70b';   // example text model name
$MODEL_IMAGE = 'groq-vision-image';   // example image model (replace if needed)

// === PATHS ===
// Where to store and read working code examples
$CODE_REFERENCE_FILE = __DIR__ . '/code_reference.md'; // markdown file containing working code templates
?>


===== FILE: robotics/gamemaker.html @ 2025-10-12 23:34:13 =====
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Game Maker IDE</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body class="bg-[tan] text-slate-900 font-sans">
  <div id="root"></div>

  <script type="text/babel" data-presets="react,typescript">
  function GameMakerIDE() {
  // ...your existing state
  const [title, setTitle] = React.useState("My Vintage Game");
  const [desc, setDesc]  = React.useState("Flappy-style with mountains and a header splash.");
  const [game, setGame]  = React.useState("flappy");
  const [generatedIno, setGeneratedIno] = React.useState("");
  const [imagePreview, setImagePreview] = React.useState(null);
  const [showSettings, setShowSettings] = React.useState(false);
  const [isLoading, setIsLoading] = React.useState(false);

  // Settings (persisted locally)
  const [apiEndpoint, setApiEndpoint] = React.useState(
    localStorage.getItem("gm_apiEndpoint") || "/api/generate.php"
  );
  const [apiToken, setApiToken] = React.useState(  // optional if your proxy uses server-side key
    localStorage.getItem("gm_apiToken") || ""
  );
  const [apiModel, setApiModel] = React.useState(
    localStorage.getItem("gm_apiModel") || "gpt-4o-mini" // or whatever your backend supports
  );

  const canvasRef = React.useRef(null);

  function saveSettings() {
    localStorage.setItem("gm_apiEndpoint", apiEndpoint);
    localStorage.setItem("gm_apiToken", apiToken);
    localStorage.setItem("gm_apiModel", apiModel);
    setShowSettings(false);
  }

  async function aiGenerate() {
    // Make sure we have a current image; if not, create one locally first.
    if (!imagePreview) drawLocalTitleCardToCanvas(title);

    // Grab canvas as data URL (PNG)
    const c = canvasRef.current;
    const pngDataUrl = c ? c.toDataURL("image/png") : null;

    const payload = {
      model: apiModel,                 // your backend can ignore/change this
      title,
      description: desc,
      game_type: game,                 // "flappy" | "snake" | "pong"
      image_data_url: pngDataUrl       // base64 PNG Data URL
    };

    setIsLoading(true);
    try {
      const res = await fetch(apiEndpoint, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...(apiToken ? { Authorization: `Bearer ${apiToken}` } : {})
        },
        body: JSON.stringify(payload)
      });

      if (!res.ok) {
        const t = await res.text();
        throw new Error(`AI error: ${res.status} ${t}`);
      }

      const data = await res.json();
      // Expecting { ino: "...", png_data_url?: "data:image/png;base64,..." }
      if (data.ino) setGeneratedIno(data.ino);
      if (data.png_data_url) setImagePreview(data.png_data_url);
      alert("AI generation complete!");
    } catch (err) {
      console.error(err);
      alert(err.message || "AI request failed.");
    } finally {
      setIsLoading(false);
    }
  }

function GameMakerIDE() {
  const [title, setTitle] = React.useState("My Vintage Game");
  const [desc, setDesc] = React.useState("Flappy-style with mountains and a header splash.");
  const [game, setGame] = React.useState("flappy");
  const [generatedIno, setGeneratedIno] = React.useState("");
  const [imagePreview, setImagePreview] = React.useState(null);
  const canvasRef = React.useRef(null);

  function drawLocalTitleCardToCanvas(text) {
    const W = 160, H = 128;
    let c = canvasRef.current; if (!c) return;
    c.width = W; c.height = H;
    const ctx = c.getContext("2d"); if (!ctx) return;
    const grad = ctx.createLinearGradient(0, 0, 0, H);
    grad.addColorStop(0, "#2278C5"); grad.addColorStop(1, "#93CCEA");
    ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = "#5A6E82";
    for (let x = 0; x < W; x++) { const yTop = 80 - Math.sin((x + 10) * 0.11) * 8; ctx.fillRect(x, yTop, 1, H - yTop); }
    ctx.fillStyle = "#465764";
    for (let x = 0; x < W; x++) { const yTop = 88 - Math.sin((x + 20) * 0.13) * 10; ctx.fillRect(x, yTop, 1, H - yTop); }
    ctx.fillStyle = "#ffffff"; ctx.textAlign = "center";
    ctx.font = "bold 18px system-ui";
    ctx.fillText(text || "Game Title", W / 2, 28);
    ctx.font = "12px system-ui";
    ctx.fillText("Press button to start", W / 2, H - 10);
    setImagePreview(c.toDataURL("image/png"));
  }

  function handleGenerateIno() {
    setGeneratedIno(`// ${title}\n// ${desc}\n// Game type: ${game}`);
  }

  function generateImageOnly() {
    drawLocalTitleCardToCanvas(title);
  }

  function download(filename, text) {
    const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a"); a.href = url; a.download = filename; a.click();
    URL.revokeObjectURL(url);
  }

  async function copyAll(text) {
    try {
      await navigator.clipboard.writeText(text);
      alert("Copied to clipboard!");
    } catch {
      const ta = document.createElement("textarea");
      ta.value = text;
      document.body.appendChild(ta);
      ta.select();
      document.execCommand("copy");
      document.body.removeChild(ta);
      alert("Copied using fallback!");
    }
  }

  return (
    <div className="min-h-screen bg-[tan] text-slate-900 font-sans">
      <div className="max-w-5xl mx-auto p-6">
        <h1 className="text-4xl font-extrabold text-center mb-2">🎮 Game Maker IDE</h1>
        <p className="text-center text-slate-700 mb-8">Create fun mini-games for your ESP32! Choose a game, name it, describe it, and watch your idea come to life.</p>

        <div className="bg-amber-100 p-6 rounded-3xl shadow-lg border border-amber-300">
          <div className="flex flex-col md:flex-row gap-6">
            <div className="flex-1 space-y-4">
              <label className="block">
                <span className="text-md font-semibold">Game Title</span>
                <input value={title} onChange={e=>setTitle(e.target.value)} className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg" placeholder="My Awesome Game" />
              </label>

              <label className="block">
                <span className="text-md font-semibold">Game Description</span>
                <textarea value={desc} onChange={e=>setDesc(e.target.value)} rows={4} className="mt-2 w-full rounded-2xl bg-white border border-amber-300 px-3 py-3 text-md" placeholder="Describe your game idea..." />
              </label>

              <label className="block">
                <span className="text-md font-semibold">Pick Game Type</span>
                <select value={game} onChange={e=>setGame(e.target.value)} className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg">
                  <option value="flappy">Flappy Bird</option>
                  <option value="snake">Snake</option>
                  <option value="pong">Pong</option>
                </select>
              </label>

                <div className="flex flex-wrap justify-center gap-4 mt-6">
    <button onClick={handleGenerateIno} className="bg-orange-500 hover:bg-orange-400 text-white font-bold px-5 py-2 rounded-2xl shadow">
      Generate Code (Local)
    </button>
    <button onClick={generateImageOnly} className="bg-yellow-500 hover:bg-yellow-400 text-white font-bold px-5 py-2 rounded-2xl shadow">
      Generate Image (Local)
    </button>
    <button onClick={aiGenerate} disabled={isLoading} className="bg-emerald-600 hover:bg-emerald-500 disabled:opacity-60 text-white font-bold px-5 py-2 rounded-2xl shadow">
      {isLoading ? "Working…" : "AI Generate Code + Image"}
    </button>
    <button onClick={()=>download(`${title.replace(/[^A-Za-z0-9_]+/g,'_')}.ino`, generatedIno || "// generate first")} className="bg-teal-500 hover:bg-teal-400 text-white font-bold px-5 py-2 rounded-2xl shadow">
      Download .ino
    </button>
    <button onClick={()=>setShowSettings(true)} className="bg-amber-700 hover:bg-amber-600 text-white font-bold px-4 py-2 rounded-2xl shadow">
      ⚙︎ Settings
    </button>
  </div>

            </div>

            <div className="flex-1 space-y-4">
              <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50 text-center">
                <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                  <span>Menu Header Preview</span>
                  <button onClick={()=>copyAll(imagePreview || "No image available.")} className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg">Copy All</button>
                </div>
                <div className="aspect-[5/4] w-full border-2 border-amber-300 rounded-xl grid place-items-center bg-amber-200">
                  <canvas ref={canvasRef} className="rounded" />
                  {!imagePreview && <div className="text-slate-500 text-sm">Click 'Generate Image'</div>}
                </div>
              </div>

              <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50">
                <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                  <span>Arduino Code</span>
                  <button onClick={()=>copyAll(generatedIno)} className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg">Copy All</button>
                </div>
                <textarea value={generatedIno} onChange={e=>setGeneratedIno(e.target.value)} rows={10} className="w-full rounded-xl bg-white border border-amber-300 p-3 font-mono text-xs" placeholder="Click 'Generate Code'" />
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<GameMakerIDE />);
  </script>
</body>
</html>


===== FILE: robotics/gamemaker.html @ 2025-10-12 23:34:53 =====
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Game Maker IDE</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body class="bg-[tan] text-slate-900 font-sans">
  <div id="root"></div>

  <script type="text/babel" data-presets="react,typescript">
  function GameMakerIDE() {
  // ...your existing state
  const [title, setTitle] = React.useState("My Vintage Game");
  const [desc, setDesc]  = React.useState("Flappy-style with mountains and a header splash.");
  const [game, setGame]  = React.useState("flappy");
  const [generatedIno, setGeneratedIno] = React.useState("");
  const [imagePreview, setImagePreview] = React.useState(null);
  const [showSettings, setShowSettings] = React.useState(false);
  const [isLoading, setIsLoading] = React.useState(false);

  // Settings (persisted locally)
  const [apiEndpoint, setApiEndpoint] = React.useState(
    localStorage.getItem("gm_apiEndpoint") || "/api/generate.php"
  );
  const [apiToken, setApiToken] = React.useState(  // optional if your proxy uses server-side key
    localStorage.getItem("gm_apiToken") || ""
  );
  const [apiModel, setApiModel] = React.useState(
    localStorage.getItem("gm_apiModel") || "gpt-4o-mini" // or whatever your backend supports
  );

  const canvasRef = React.useRef(null);

  function saveSettings() {
    localStorage.setItem("gm_apiEndpoint", apiEndpoint);
    localStorage.setItem("gm_apiToken", apiToken);
    localStorage.setItem("gm_apiModel", apiModel);
    setShowSettings(false);
  }

  async function aiGenerate() {
    // Make sure we have a current image; if not, create one locally first.
    if (!imagePreview) drawLocalTitleCardToCanvas(title);

    // Grab canvas as data URL (PNG)
    const c = canvasRef.current;
    const pngDataUrl = c ? c.toDataURL("image/png") : null;

    const payload = {
      model: apiModel,                 // your backend can ignore/change this
      title,
      description: desc,
      game_type: game,                 // "flappy" | "snake" | "pong"
      image_data_url: pngDataUrl       // base64 PNG Data URL
    };

    setIsLoading(true);
    try {
      const res = await fetch(apiEndpoint, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...(apiToken ? { Authorization: `Bearer ${apiToken}` } : {})
        },
        body: JSON.stringify(payload)
      });

      if (!res.ok) {
        const t = await res.text();
        throw new Error(`AI error: ${res.status} ${t}`);
      }

      const data = await res.json();
      // Expecting { ino: "...", png_data_url?: "data:image/png;base64,..." }
      if (data.ino) setGeneratedIno(data.ino);
      if (data.png_data_url) setImagePreview(data.png_data_url);
      alert("AI generation complete!");
    } catch (err) {
      console.error(err);
      alert(err.message || "AI request failed.");
    } finally {
      setIsLoading(false);
    }
  }

function GameMakerIDE() {
  const [title, setTitle] = React.useState("My Vintage Game");
  const [desc, setDesc] = React.useState("Flappy-style with mountains and a header splash.");
  const [game, setGame] = React.useState("flappy");
  const [generatedIno, setGeneratedIno] = React.useState("");
  const [imagePreview, setImagePreview] = React.useState(null);
  const canvasRef = React.useRef(null);

  function drawLocalTitleCardToCanvas(text) {
    const W = 160, H = 128;
    let c = canvasRef.current; if (!c) return;
    c.width = W; c.height = H;
    const ctx = c.getContext("2d"); if (!ctx) return;
    const grad = ctx.createLinearGradient(0, 0, 0, H);
    grad.addColorStop(0, "#2278C5"); grad.addColorStop(1, "#93CCEA");
    ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = "#5A6E82";
    for (let x = 0; x < W; x++) { const yTop = 80 - Math.sin((x + 10) * 0.11) * 8; ctx.fillRect(x, yTop, 1, H - yTop); }
    ctx.fillStyle = "#465764";
    for (let x = 0; x < W; x++) { const yTop = 88 - Math.sin((x + 20) * 0.13) * 10; ctx.fillRect(x, yTop, 1, H - yTop); }
    ctx.fillStyle = "#ffffff"; ctx.textAlign = "center";
    ctx.font = "bold 18px system-ui";
    ctx.fillText(text || "Game Title", W / 2, 28);
    ctx.font = "12px system-ui";
    ctx.fillText("Press button to start", W / 2, H - 10);
    setImagePreview(c.toDataURL("image/png"));
  }

  function handleGenerateIno() {
    setGeneratedIno(`// ${title}\n// ${desc}\n// Game type: ${game}`);
  }

  function generateImageOnly() {
    drawLocalTitleCardToCanvas(title);
  }

  function download(filename, text) {
    const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a"); a.href = url; a.download = filename; a.click();
    URL.revokeObjectURL(url);
  }

  async function copyAll(text) {
    try {
      await navigator.clipboard.writeText(text);
      alert("Copied to clipboard!");
    } catch {
      const ta = document.createElement("textarea");
      ta.value = text;
      document.body.appendChild(ta);
      ta.select();
      document.execCommand("copy");
      document.body.removeChild(ta);
      alert("Copied using fallback!");
    }
  }

  return (
    <div className="min-h-screen bg-[tan] text-slate-900 font-sans">
      <div className="max-w-5xl mx-auto p-6">
        <h1 className="text-4xl font-extrabold text-center mb-2">🎮 Game Maker IDE</h1>
        <p className="text-center text-slate-700 mb-8">Create fun mini-games for your ESP32! Choose a game, name it, describe it, and watch your idea come to life.</p>

        <div className="bg-amber-100 p-6 rounded-3xl shadow-lg border border-amber-300">
          <div className="flex flex-col md:flex-row gap-6">
            <div className="flex-1 space-y-4">
              <label className="block">
                <span className="text-md font-semibold">Game Title</span>
                <input value={title} onChange={e=>setTitle(e.target.value)} className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg" placeholder="My Awesome Game" />
              </label>

              <label className="block">
                <span className="text-md font-semibold">Game Description</span>
                <textarea value={desc} onChange={e=>setDesc(e.target.value)} rows={4} className="mt-2 w-full rounded-2xl bg-white border border-amber-300 px-3 py-3 text-md" placeholder="Describe your game idea..." />
              </label>

              <label className="block">
                <span className="text-md font-semibold">Pick Game Type</span>
                <select value={game} onChange={e=>setGame(e.target.value)} className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg">
                  <option value="flappy">Flappy Bird</option>
                  <option value="snake">Snake</option>
                  <option value="pong">Pong</option>
                </select>
              </label>

                <div className="flex flex-wrap justify-center gap-4 mt-6">
    <button onClick={handleGenerateIno} className="bg-orange-500 hover:bg-orange-400 text-white font-bold px-5 py-2 rounded-2xl shadow">
      Generate Code (Local)
    </button>
    <button onClick={generateImageOnly} className="bg-yellow-500 hover:bg-yellow-400 text-white font-bold px-5 py-2 rounded-2xl shadow">
      Generate Image (Local)
    </button>
    <button onClick={aiGenerate} disabled={isLoading} className="bg-emerald-600 hover:bg-emerald-500 disabled:opacity-60 text-white font-bold px-5 py-2 rounded-2xl shadow">
      {isLoading ? "Working…" : "AI Generate Code + Image"}
    </button>
    <button onClick={()=>download(`${title.replace(/[^A-Za-z0-9_]+/g,'_')}.ino`, generatedIno || "// generate first")} className="bg-teal-500 hover:bg-teal-400 text-white font-bold px-5 py-2 rounded-2xl shadow">
      Download .ino
    </button>
    <button onClick={()=>setShowSettings(true)} className="bg-amber-700 hover:bg-amber-600 text-white font-bold px-4 py-2 rounded-2xl shadow">
      ⚙︎ Settings
    </button>
  </div>

            </div>

{showSettings && (
  <div className="fixed inset-0 bg-black/40 grid place-items-center z-50">
    <div className="bg-white w-[92vw] max-w-lg rounded-2xl p-6 shadow-xl border">
      <h2 className="text-xl font-bold mb-4">AI Settings</h2>

      <label className="block mb-3">
        <span className="text-sm font-semibold">API Endpoint (recommended: your proxy)</span>
        <input value={apiEndpoint} onChange={e=>setApiEndpoint(e.target.value)} className="mt-1 w-full rounded-xl bg-white border px-3 py-2" placeholder="/api/generate.php" />
      </label>

      <label className="block mb-3">
        <span className="text-sm font-semibold">Auth Token (optional if proxy handles keys)</span>
        <input value={apiToken} onChange={e=>setApiToken(e.target.value)} className="mt-1 w-full rounded-xl bg-white border px-3 py-2" placeholder="Leave blank if proxy stores provider key" />
      </label>

      <label className="block mb-6">
        <span className="text-sm font-semibold">Model (hint for backend)</span>
        <input value={apiModel} onChange={e=>setApiModel(e.target.value)} className="mt-1 w-full rounded-xl bg-white border px-3 py-2" placeholder="gpt-4o-mini / claude-3.5-sonnet / etc." />
      </label>

      <div className="flex justify-end gap-3">
        <button onClick={()=>setShowSettings(false)} className="px-4 py-2 rounded-xl border">Cancel</button>
        <button onClick={saveSettings} className="px-4 py-2 rounded-xl bg-amber-700 text-white">Save</button>
      </div>
    </div>
  </div>
)}



            <div className="flex-1 space-y-4">
              <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50 text-center">
                <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                  <span>Menu Header Preview</span>
                  <button onClick={()=>copyAll(imagePreview || "No image available.")} className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg">Copy All</button>
                </div>
                <div className="aspect-[5/4] w-full border-2 border-amber-300 rounded-xl grid place-items-center bg-amber-200">
                  <canvas ref={canvasRef} className="rounded" />
                  {!imagePreview && <div className="text-slate-500 text-sm">Click 'Generate Image'</div>}
                </div>
              </div>

              <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50">
                <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                  <span>Arduino Code</span>
                  <button onClick={()=>copyAll(generatedIno)} className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg">Copy All</button>
                </div>
                <textarea value={generatedIno} onChange={e=>setGeneratedIno(e.target.value)} rows={10} className="w-full rounded-xl bg-white border border-amber-300 p-3 font-mono text-xs" placeholder="Click 'Generate Code'" />
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<GameMakerIDE />);
  </script>
</body>
</html>


===== FILE: robotics/api/config.php @ 2025-10-12 23:36:10 =====
<?php
// api/config.php
declare(strict_types=1);
const PROVIDER_API_KEY = 'PUT_YOUR_SECRET_KEY_HERE';
const PROVIDER_CHAT_URL = 'https://YOUR_PROVIDER_ENDPOINT_HERE';


===== FILE: robotics/gamemaker.html @ 2025-10-13 03:02:32 =====
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Game Maker IDE</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body class="bg-[tan] text-slate-900 font-sans">
  <div id="root"></div>

  <script type="text/babel" data-presets="react,typescript">
function GameMakerIDE() {
  const [title, setTitle] = React.useState("My Vintage Game");
  const [desc, setDesc] = React.useState("Flappy-style with mountains and a header splash.");
  const [game, setGame] = React.useState("flappy");
  const [generatedIno, setGeneratedIno] = React.useState("");
  const [imagePreview, setImagePreview] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const canvasRef = React.useRef(null);

  // --- Local (secure) image generator: no keys in browser
  function drawLocalTitleCardToCanvas(text) {
    const W = 160, H = 128;
    let c = canvasRef.current; if (!c) return;
    c.width = W; c.height = H;
    const ctx = c.getContext("2d"); if (!ctx) return;

    // background
    const grad = ctx.createLinearGradient(0, 0, 0, H);
    grad.addColorStop(0, "#2278C5"); grad.addColorStop(1, "#93CCEA");
    ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H);

    // mountains
    ctx.fillStyle = "#5A6E82";
    for (let x = 0; x < W; x++) {
      const yTop = 80 - Math.sin((x + 10) * 0.11) * 8;
      ctx.fillRect(x, yTop, 1, H - yTop);
    }
    ctx.fillStyle = "#465764";
    for (let x = 0; x < W; x++) {
      const yTop = 88 - Math.sin((x + 20) * 0.13) * 10;
      ctx.fillRect(x, yTop, 1, H - yTop);
    }

    // title
    ctx.fillStyle = "#ffffff"; ctx.textAlign = "center";
    ctx.font = "bold 18px system-ui";
    ctx.fillText(text || "Game Title", W / 2, 28);
    ctx.font = "12px system-ui";
    ctx.fillText("Press button to start", W / 2, H - 10);

    setImagePreview(c.toDataURL("image/png"));
  }

  // --- Backend code generation via Groq (uses server key)
  async function generateCodeWithGroq() {
    setBusy(true);
    try {
      const res = await fetch("api/generate.php", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          action: "text",
          title,
          description: desc,
          gameType: game
        })
      });
      const out = await res.json();
      if (!out.ok) throw new Error(out.error || "Generation failed");
      setGeneratedIno(out.code || "");
    } catch (e) {
      alert("Code generation failed: " + e.message);
    } finally {
      setBusy(false);
    }
  }

  function generateImageOnly() {
    drawLocalTitleCardToCanvas(title);
  }

  function download(filename, text) {
    const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a"); a.href = url; a.download = filename; a.click();
    URL.revokeObjectURL(url);
  }

  async function copyAll(text) {
    try {
      await navigator.clipboard.writeText(text);
      alert("Copied to clipboard!");
    } catch {
      const ta = document.createElement("textarea");
      ta.value = text;
      document.body.appendChild(ta);
      ta.select();
      document.execCommand("copy");
      document.body.removeChild(ta);
      alert("Copied using fallback!");
    }
  }

  return (
    <div className="min-h-screen bg-[tan] text-slate-900 font-sans">
      <div className="max-w-5xl mx-auto p-6">
        <h1 className="text-4xl font-extrabold text-center mb-2">🎮 Game Maker IDE</h1>
        <p className="text-center text-slate-700 mb-8">Create fun mini-games for your ESP32! Choose a game, name it, describe it, and watch your idea come to life.</p>

        <div className="bg-amber-100 p-6 rounded-3xl shadow-lg border border-amber-300">
          <div className="flex flex-col md:flex-row gap-6">
            <div className="flex-1 space-y-4">
              <label className="block">
                <span className="text-md font-semibold">Game Title</span>
                <input value={title} onChange={e=>setTitle(e.target.value)} className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg" placeholder="My Awesome Game" />
              </label>

              <label className="block">
                <span className="text-md font-semibold">Game Description</span>
                <textarea value={desc} onChange={e=>setDesc(e.target.value)} rows={4} className="mt-2 w-full rounded-2xl bg-white border border-amber-300 px-3 py-3 text-md" placeholder="Describe your game idea..." />
              </label>

              <label className="block">
                <span className="text-md font-semibold">Pick Game Type</span>
                <select value={game} onChange={e=>setGame(e.target.value)} className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg">
                  <option value="flappy">Flappy Bird</option>
                  <option value="snake">Snake</option>
                  <option value="pong">Pong</option>
                </select>
              </label>

              <div className="flex flex-wrap justify-center gap-4 mt-6">
                <button onClick={generateCodeWithGroq} disabled={busy} className="bg-orange-500 hover:bg-orange-400 disabled:opacity-50 text-white font-bold px-5 py-2 rounded-2xl shadow">Generate Code (Groq)</button>
                <button onClick={generateImageOnly} className="bg-yellow-500 hover:bg-yellow-400 text-white font-bold px-5 py-2 rounded-2xl shadow">Generate Image (Local)</button>
                <button onClick={()=>download(`${title.replace(/[^A-Za-z0-9_]+/g,'_')}.ino`, generatedIno || "// generate first")} className="bg-teal-500 hover:bg-teal-400 text-white font-bold px-5 py-2 rounded-2xl shadow">Download .ino</button>
              </div>
            </div>

            <div className="flex-1 space-y-4">
              <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50 text-center">
                <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                  <span>Menu Header Preview</span>
                  <button onClick={()=>copyAll(imagePreview || "No image available.")} className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg">Copy All</button>
                </div>
                <div className="aspect-[5/4] w-full border-2 border-amber-300 rounded-xl grid place-items-center bg-amber-200">
                  <canvas ref={canvasRef} className="rounded" />
                  {!imagePreview && <div className="text-slate-500 text-sm">Click 'Generate Image (Local)'</div>}
                </div>
              </div>

              <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50">
                <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                  <span>Arduino Code</span>
                  <button onClick={()=>copyAll(generatedIno)} className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg">Copy All</button>
                </div>
                <textarea value={generatedIno} onChange={e=>setGeneratedIno(e.target.value)} rows={10} className="w-full rounded-xl bg-white border border-amber-300 p-3 font-mono text-xs" placeholder="Click 'Generate Code (Groq)'" />
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<GameMakerIDE />);
  </script>
</body>
</html>


===== FILE: Tools/config.php @ 2025-10-15 06:08:53 =====
// ==========================
// config.php
// ==========================
<?php
return [
    'db' => [
        'host' => 'localhost',
        'user' => 'webtool_user',
        'pass' => 'yourpassword',
        'name' => 'webtool_db'
    ],
    'storage_path' => __DIR__ . '/storage/projects/',
    'archive_path' => __DIR__ . '/storage/archives/',
    'log_path' => __DIR__ . '/storage/logs/',
];
?>


===== FILE: Tools/db/migrations/001_create_tables.sql @ 2025-10-15 06:09:08 =====

// ==========================
// db/migrations/001_create_tables.sql
// ==========================
CREATE DATABASE IF NOT EXISTS webtool_db;
USE webtool_db;

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE projects (
    id INT AUTO_INCREMENT PRIMARY KEY,
    slug VARCHAR(100) UNIQUE NOT NULL,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    owner_id INT,
    architect_file VARCHAR(255),
    current_process_file VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY(owner_id) REFERENCES users(id)
);

CREATE TABLE audit_logs (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT,
    action VARCHAR(255),
    user_id INT,
    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY(project_id) REFERENCES projects(id),
    FOREIGN KEY(user_id) REFERENCES users(id)
);



===== FILE: Tools/app/utils/file_io.php @ 2025-10-15 06:09:25 =====

// ==========================
// app/utils/file_io.php
// ==========================
<?php
function readFileSafe($path) {
    if (!file_exists($path)) return null;
    return file_get_contents($path);
}

function writeFileSafe($path, $content) {
    file_put_contents($path, $content);
}

function sanitizeSlug($slug) {
    return preg_replace('/[^a-zA-Z0-9_\-]/', '_', strtolower($slug));
}
?>



===== FILE: Tools/app/utils/diff.php @ 2025-10-15 06:09:33 =====

// ==========================
// app/utils/diff.php
// ==========================
<?php
function diffFiles($oldContent, $newContent) {
    // simple line-based diff
    $oldLines = explode("\n", $oldContent);
    $newLines = explode("\n", $newContent);
    return array_diff($newLines, $oldLines);
}
?>


===== FILE: Tools/app/analyzers/project_analyzer.php @ 2025-10-15 06:09:47 =====

// ==========================
// app/analyzers/project_analyzer.php
// ==========================
<?php
function analyzeProject($projectPath) {
    $files = [];
    $dir = new RecursiveDirectoryIterator($projectPath);
    $iterator = new RecursiveIteratorIterator($dir);
    foreach($iterator as $file) {
        if ($file->isFile()) {
            $files[] = $file->getPathname();
        }
    }
    return $files;
}
?>


===== FILE: Tools/app/architects/architect_generator.php @ 2025-10-15 06:09:59 =====

// ==========================
// app/architects/architect_generator.php
// ==========================
<?php
function generateArchitectFile($projectName, $files) {
    $architect = [
        'project' => $projectName,
        'steps' => []
    ];
    foreach($files as $f) {
        $architect['steps'][] = [
            'file' => $f,
            'action' => 'review'
        ];
    }
    return json_encode($architect, JSON_PRETTY_PRINT);
}
?>



===== FILE: Tools/app/processors/current_process.php @ 2025-10-15 06:10:12 =====

// ==========================
// app/processors/current_process.php
// ==========================
<?php
function buildCurrentProcess($files) {
    $process = [];
    foreach ($files as $file) {
        $process[$file] = [
            'dependencies' => [], // TODO: parse actual dependencies
            'last_modified' => filemtime($file)
        ];
    }
    return json_encode($process, JSON_PRETTY_PRINT);
}
?>



===== FILE: Tools/app/controllers/project_controller.php @ 2025-10-15 06:10:21 =====

// ==========================
// app/controllers/project_controller.php
// ==========================
<?php
$config = include __DIR__ . '/../../config.php';
$mysqli = new mysqli(
    $config['db']['host'],
    $config['db']['user'],
    $config['db']['pass'],
    $config['db']['name']
);

function createProject($name, $ownerId) {
    global $mysqli, $config;
    $slug = sanitizeSlug($name);
    $projectPath = $config['storage_path'] . $slug;
    if (!file_exists($projectPath)) mkdir($projectPath, 0777, true);
    $mysqli->query("INSERT INTO projects (slug, name, owner_id) VALUES ('$slug','$name','$ownerId')");
    return $projectPath;
}
?>


===== FILE: Tools/public/index.php @ 2025-10-15 06:10:47 =====

// ==========================
// public/index.php
// ==========================
<?php
// basic admin interface
echo "<h1>Web Tool Engine Admin</h1>";
echo "<a href='create_project.php'>Create New Project</a>";
?>



===== FILE: Tools/scripts/create_project.php @ 2025-10-15 06:10:58 =====

// ==========================
// scripts/create_project.php
// ==========================
<?php
include '../app/controllers/project_controller.php';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = $_POST['project_name'];
    $ownerId = $_POST['owner_id'];
    $path = createProject($name, $ownerId);
    echo "Project created at $path";
} else {
    echo '<form method="POST">
        Project Name: <input type="text" name="project_name"><br>
        Owner ID: <input type="text" name="owner_id"><br>
        <input type="submit">
    </form>';
}
?>


===== FILE: Tools/README.md @ 2025-10-15 06:11:10 =====

// ==========================
// README.md
// ==========================
# Web Tool Engine
A server-side tool to create and manage web-based projects (HTML, PHP, JS, SQL, CSS). 

**Structure**
- `app/` - core code for analyzing, architecting, and processing projects
- `storage/projects` - user projects
- `db/migrations` - SQL schema
- `public/` - web UI for admin
- `scripts/` - CLI helpers

**Usage**
1. Run the migration SQL.
2. Configure `config.php`.
3. Access `public/index.php` in browser.
4. Use forms or CLI scripts to create and analyze projects.


===== FILE: Tools/config.php @ 2025-10-15 06:17:52 =====
<?php
return [
    'db' => [
        'host' => 'localhost',
        'user' => 'webtool_user',
        'pass' => 'yourpassword',
        'name' => 'webtool_db'
    ],
    'storage_path' => __DIR__ . '/storage/projects/',
    'archive_path' => __DIR__ . '/storage/archives/',
    'log_path' => __DIR__ . '/storage/logs/',
    'ai' => [
        'provider' => 'Groq',          // or 'anthropic', etc.
        'api_key' => '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',
        'model' => 'openai/gpt-oss-20b',         // or whichever model
        'max_tokens' => 1500,
        'temperature' => 0.7
    ]
];


===== FILE: Tools/file_crawler.php @ 2025-10-15 06:18:40 =====
<?php
/**
 * file_crawler.php
 * 
 * Recursively collects the contents of every file in the project
 * and writes them to `running_files.txt` in the same directory.
 */

define('OUTPUT_FILE', __DIR__ . '/running_files.txt');

/**
 * Recursively scans a directory and returns a flat array of file paths.
 */
function getAllFiles($dir) {
    $files = [];
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)
    );

    foreach ($iterator as $file) {
        if ($file->isFile()) {
            $files[] = $file->getPathname();
        }
    }

    return $files;
}

/**
 * Escapes delimiters for readability in the running_files.txt
 */
function formatFileHeader($filePath) {
    return "\n// ==========================\n// FILE: $filePath\n// ==========================\n";
}

/**
 * Writes all file contents to the output file
 */
function writeRunningFiles($rootDir) {
    $allFiles = getAllFiles($rootDir);
    $output = '';

    foreach ($allFiles as $file) {
        // Skip the running_files.txt itself to prevent recursion
        if (realpath($file) === realpath(OUTPUT_FILE)) continue;

        $content = file_get_contents($file);
        $output .= formatFileHeader($file);
        $output .= $content . "\n";
    }

    file_put_contents(OUTPUT_FILE, $output);
    echo "running_files.txt created/updated successfully with " . count($allFiles) . " files.\n";
}

// Run the script
writeRunningFiles(__DIR__);


===== FILE: Tools/public/index.php @ 2025-10-15 07:00:08 =====
mm

===== FILE: Tools2/webtool/templates/title_manager.txt @ 2025-10-15 07:44:03 =====
You are TITLE_MANAGER. Read the user's request and produce a short, unique title (3–6 words) that best summarizes the project, suitable for a folder name. Output only the title on a single line. If there are ambiguous terms, prefer clarity and brevity.

===== FILE: Tools2/webtool/templates/project_manager_prompt.txt @ 2025-10-15 07:44:27 =====
You are PROJECT_MANAGER. Inputs: user request, chosen title.
Task: produce a JSON called priorities.json listing up to 7 prioritized items.
Each item: { "id":1, "name":"Create UI", "desc":"...", "priority":1..5, "estimate_hours":int, "risk":"low|medium|high" }
Also include an overall "scope_summary" one-paragraph.
Output only valid JSON.

===== FILE: Tools2/webtool/templates/structural_architect.txt @ 2025-10-15 07:44:47 =====
You are STRUCTURAL_ARCHITECT. Input: priorities.json and user request.
Produce structure.json with keys:
{
  "files": [
    { "path":"index.html", "role":"frontend entry", "components":["search input","project list"] },
    { "path":"api/orchestrator.php", "role":"orchestration endpoint" }
  ],
  "components": [...]
}
Output only JSON.

===== FILE: Tools2/webtool/templates/connection_architect.txt @ 2025-10-15 07:45:17 =====
You are CONNECTION_ARCHITECT. Input: structure.json.
Produce connections.txt — a line-delimited shorthand mapping items, syntax:
<file>,<symbol><id>,<content brief>,<->,<target file>
Symbols:
! = element/component (eg !input, !button)
? = parent link
> = emits / writes to file
; = end-of-line
Example:
index.html, !search_input, placeholder 'Describe tool', > api/orchestrator.php;
Write each mapping on its own line. Keep mappings compact.

===== FILE: Tools2/webtool/templates/first_revision_manager.txt @ 2025-10-15 07:45:38 =====
You are REVISION_MANAGER. Input: structure.json + connections.txt.
Task: Validate connections are consistent (every file referenced exists in structure.json). Output a markdown report: summary, problems[], suggestions[].

===== FILE: Tools2/webtool/templates/coder_prompt.txt @ 2025-10-15 07:46:01 =====
You are CODER. Input: connections.txt and structure.json and user request.
Produce file contents for each file in structure.json. For HTML/CSS/JS include readable, commented code. For PHP endpoints, include safe placeholder AI call function call_ai($prompt) and do NOT embed secrets. Output as a tar-like structure using triple-backticks markers:

--- FILE: index.html ---
<html>...</html>
--- END FILE ---

Only output these blocks.

===== FILE: Tools2/webtool/templates/coder_review_prompt.txt @ 2025-10-15 07:46:26 =====
You are CODE_REVIEWER. Inputs: generated code files.
Check for: missing security practices (sanitization), missing CSRF tokens, obvious XSS, missing Content-Type, non-sanitized file operations, and return a small fix-list and suggested code edits.
Output JSON { "issues": [...], "edits": { "file":"patch" } }

===== FILE: Tools2/webtool/api/orchestrator.php @ 2025-10-15 07:47:17 =====
<?php
// api/orchestrator.php
declare(strict_types=1);
require __DIR__ . '/ai_client.php';
require __DIR__ . '/parse_connections.php';
require __DIR__ . '/scaffold.php';

function safe_slug($s) {
    $s = preg_replace('/[^a-z0-9 _-]/i', '', $s);
    $s = trim($s);
    $s = str_replace(' ', '-', strtolower($s));
    return substr($s,0,60);
}

// Read user input
$body = file_get_contents('php://input');
$data = json_decode($body, true) ?? ['request' => trim($body)];

$request = $data['request'] ?? '';
if (!$request) {
    http_response_code(400); echo json_encode(['error'=>'empty request']); exit;
}

$id = bin2hex(random_bytes(6));
$runtime = __DIR__ . '/../runtime/';
@mkdir("$runtime/requests", 0770, true);
file_put_contents("$runtime/requests/{$id}.txt", $request);

// Step A: TITLE_MANAGER
$title_prompt = file_get_contents(__DIR__.'/../templates/title_manager.txt') . "\n\nUser request:\n".$request;
$title = call_ai($title_prompt); // returns text
$title = trim(explode("\n", $title)[0]);
$slug = safe_slug($title);
$project_dir = __DIR__ . "/../runtime/projects/{$slug}";
if (!is_dir($project_dir)) { mkdir($project_dir, 0755, true); }

// Save title
file_put_contents("$runtime/intermediate/{$id}_title.txt", $title);

// Step B: PROJECT_MANAGER produce priorities.json
$pm_prompt_template = file_get_contents(__DIR__.'/../templates/project_manager_prompt.txt');
$pm_prompt = $pm_prompt_template . "\n\nTitle:\n{$title}\n\nUser request:\n{$request}";
$priorities_json = call_ai($pm_prompt); // should return JSON
file_put_contents("$runtime/intermediate/{$id}_priorities.json", $priorities_json);

// Step C: STRUCTURE
$struct_prompt = file_get_contents(__DIR__.'/../templates/structural_architect.txt') . "\n\nInput priorities:\n".$priorities_json;
$structure_json = call_ai($struct_prompt);
file_put_contents("$runtime/intermediate/{$id}_structure.json", $structure_json);

// Step D: CONNECTIONS
$conn_prompt = file_get_contents(__DIR__.'/../templates/connection_architect.txt') . "\n\nStructure:\n".$structure_json;
$connections = call_ai($conn_prompt);
file_put_contents("$runtime/intermediate/{$id}_connections.txt", $connections);

// Step E: REVISION
$rev_prompt = file_get_contents(__DIR__.'/../templates/first_revision_manager.txt') . "\n\nStructure:\n".$structure_json . "\nConnections:\n".$connections;
$revision = call_ai($rev_prompt);
file_put_contents("$runtime/intermediate/{$id}_revision.md", $revision);

// Step F: CODER
$coder_prompt = file_get_contents(__DIR__.'/../templates/coder_prompt.txt') . "\n\nStructure:\n".$structure_json . "\nConnections:\n".$connections . "\nUser request:\n".$request;
$code_package = call_ai($coder_prompt);
file_put_contents("$runtime/intermediate/{$id}_code.txt", $code_package);

// Optionally parse the code package into files:
$files = scaffold_from_package($code_package, $project_dir);
file_put_contents("$runtime/intermediate/{$id}_manifest.json", json_encode(array_keys($files), JSON_PRETTY_PRINT));

// Step G: REVIEW
$review_prompt = file_get_contents(__DIR__.'/../templates/coder_review_prompt.txt') . "\n\nFiles:\n".implode("\n", array_map(function($f){return file_get_contents($f);}, $files));
$review = call_ai($review_prompt);
file_put_contents("$runtime/intermediate/{$id}_review.json", $review);

// Finish: return project details
echo json_encode([
  'id'=>$id,
  'title'=>$title,
  'slug'=>$slug,
  'project_url'=>"/runtime/projects/{$slug}/",
  'manifest'=>array_keys($files)
]);

===== FILE: Tools2/webtool/api/parse_connections.php @ 2025-10-15 07:47:40 =====
index.html, !search_input, placeholder 'Describe tool', > api/orchestrator.php;

===== FILE: Tools2/webtool/api/parse_connections.php @ 2025-10-15 07:48:58 =====
<?php
function parse_connections_text($text) {
    $lines = preg_split('/\r?\n/', $text);
    $out = [];
    foreach ($lines as $line) {
        $line = trim($line);
        if (!$line) continue;
        // remove trailing semicolon
        $line = rtrim($line, ';');
        $parts = array_map('trim', explode(',', $line));
        $file = array_shift($parts);
        $entry = ['file'=>$file, 'tokens'=>[]];
        foreach ($parts as $p) {
            $entry['tokens'][] = $p;
        }
        $out[] = $entry;
    }
    return $out;
}

===== FILE: Tools2/webtool/api/scaffold.php @ 2025-10-15 07:49:17 =====
<?php
function scaffold_from_package($package_text, $project_dir) {
    // Expect blocks like:
    // --- FILE: path/to/file.ext ---
    // ...content...
    // --- END FILE ---
    preg_match_all('/--- FILE: (.+?) ---\s*(.*?)\s*--- END FILE ---/s', $package_text, $m);
    $files = [];
    for ($i=0;$i<count($m[0]);$i++) {
        $path = trim($m[1][$i]);
        $content = $m[2][$i];
        $full = rtrim($project_dir,'/') . '/' . $path;
        $dir = dirname($full);
        if (!is_dir($dir)) mkdir($dir, 0755, true);
        file_put_contents($full, $content);
        $files[$path] = $full;
    }
    return $files;
}

===== FILE: Tools2/.env @ 2025-10-15 07:53:19 =====

# .env
AI_API_KEY=your_api_key_here
AI_API_ENDPOINT=https://api.openai.com/v1/chat/completions

# Optional defaults
AI_MODEL=gpt-4o-mini
LOG_LEVEL=debug

===== FILE: Tools2/webtool/.env @ 2025-10-15 07:53:49 =====

# .env
AI_API_KEY=your_api_key_here
AI_API_ENDPOINT=https://api.openai.com/v1/chat/completions

# Optional defaults
AI_MODEL=gpt-4o-mini
LOG_LEVEL=debug

===== FILE: Tools2/webtool/.env @ 2025-10-15 07:59:43 =====
// Not editable (binary or large). Size: 160 bytes
# .env
AI_API_KEY=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
AI_API_ENDPOINT=https://api.openai.com/v1/chat/completions

# Optional defaults
AI_MODEL=gpt-4o-mini
LOG_LEVEL=debug


===== FILE: Tools2/webtool/.env @ 2025-10-15 08:02:04 =====
// Not editable (binary or large). Size: 160 bytes
# .env
AI_API_KEY=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
AI_API_ENDPOINT=https://api.groq.com/openai/v1/chat/completions

# Optional defaults
AI_MODEL=openai/gpt-oss-20b
LOG_LEVEL=debug


===== FILE: Tools2/.env @ 2025-10-15 08:59:36 =====
// Not editable (binary or large). Size: 160 bytes
a

===== FILE: Tools2/webtool/.env @ 2025-10-15 08:59:59 =====
// Not editable (binary or large). Size: 263 bytes
a

===== FILE: Tools2/webtool/ui/index.html @ 2025-10-15 21:28:10 =====
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Tool Helper</title>
<link rel="stylesheet" href="app.css">
</head>
<body>
  <div class="container">
    <h1>Web Tool Helper</h1>
    <input id="userRequest" type="text" placeholder="Describe the tool you want to build..." autofocus>
    <button id="submitBtn">Create Tool</button>
    <div id="progress"></div>
    <div id="thinking-box" style="border:1px solid #ccc;padding:10px;height:200px;overflow:auto;">
  Thinking...
</div>

    <div id="result"></div>
    <hr>
    <h2>Existing Projects</h2>
    <ul id="projectList"></ul>
  </div>

  <script src="app.js">const evtSource = new EventSource(`/api/orchestrator_stream.php?id=${projectId}`);
evtSource.onmessage = e => {
  const data = JSON.parse(e.data);
  document.getElementById('thinking-box').innerText += `\n[${data.file}]: ${data.content}`;
};
</script>
</body>
</html>

===== FILE: Tools2/webtool/api/orchestrator_stream.php @ 2025-10-15 21:33:43 =====
<?php
// orchestrator_stream.php
// Server-Sent Events streamer that watches runtime/intermediate for a given id
declare(strict_types=1);

if (php_sapi_name() === 'cli') {
    echo "Run from a webserver.";
    exit;
}

$id = $_GET['id'] ?? '';
if (!$id) {
    http_response_code(400);
    echo json_encode(['error'=>'Missing id']);
    exit;
}

$runtime = realpath(__DIR__ . '/../runtime') ?: (__DIR__ . '/../runtime');
$intermediate = $runtime . '/intermediate';
$project_root = $runtime . '/projects';

header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');

function sse_send($event, $data) {
    echo "event: {$event}\n";
    // encode data as JSON
    $json = json_encode($data);
    // split long data lines safely
    $lines = preg_split("/\r\n|\n|\r/", $json);
    foreach ($lines as $line) {
        echo "data: {$line}\n";
    }
    echo "\n";
    @ob_flush();
    @flush();
}

// track files already emitted
$seen = [];

// timeout after some minutes to avoid forever loop
$start = time();
$timeout_seconds = 60 * 5; // 5 minutes

// helper to list intermediate files for id
function list_intermediate($intermediate, $id) {
    $pattern = $intermediate . '/' . $id . '_*';
    $found = glob($pattern);
    if (!$found) return [];
    // sort by file mtime so steps appear roughly ordered
    usort($found, function($a,$b){ return filemtime($a) <=> filemtime($b); });
    return $found;
}

while (true) {
    // exit on timeout
    if (time() - $start > $timeout_seconds) {
        sse_send('error', ['msg'=>'Stream timeout']);
        break;
    }

    // check for intermediate outputs
    $files = list_intermediate($intermediate, $id);
    foreach ($files as $f) {
        if (isset($seen[$f])) continue;
        $seen[$f] = true;
        $basename = basename($f);
        $content = @file_get_contents($f);
        if ($content === false) $content = '';
        sse_send('progress', ['file'=>$basename, 'content'=>$content]);
    }

    // if there's a manifest file (scaffold success) -> send done and manifest
    $manifestFile = "{$intermediate}/{$id}_manifest.json";
    if (file_exists($manifestFile)) {
        $manifest = @file_get_contents($manifestFile);
        $manifest_parsed = @json_decode($manifest, true);
        sse_send('done', [
            'manifest' => $manifest_parsed ?: [],
            'project_slug' => null // try to find slug from title file
        ]);
        // attempt to detect slug: use title and slugify
        $titleFile = "{$intermediate}/{$id}_title.txt";
        if (file_exists($titleFile)) {
            $title = trim(@file_get_contents($titleFile));
            // naive slugify
            $slug = strtolower(preg_replace('/[^a-z0-9\s-]+/','', $title));
            $slug = preg_replace('/\s+/', '-', trim($slug));
            $slug = preg_replace('/-+/', '-', $slug);
            if ($slug === '') $slug = 'project-'.$id;
            sse_send('project', ['slug'=>$slug]);
        }
        break;
    }

    // If code_package had Error: AI call failed or Error: API key missing, emit error file if not yet emitted
    $errorFile = "{$intermediate}/{$id}_code.txt";
    if (file_exists($errorFile)) {
        $err = @file_get_contents($errorFile);
        if (strpos($err, 'Error:') === 0) {
            sse_send('error', ['msg'=>'Code generation error', 'detail'=>$err]);
            break;
        }
    }

    usleep(300000); // 0.3s sleep
}

sse_send('end', ['msg'=>'stream closed']);
exit;


===== FILE: Tools2/webtool/ui/app.js @ 2025-10-15 21:34:13 =====
document.getElementById('submitBtn').addEventListener('click', sendRequest);

async function sendRequest() {
  const input = document.getElementById('userRequest');
  const requestText = input.value.trim();
  const progress = document.getElementById('progress');
  const result = document.getElementById('result');
  progress.textContent = '';
  result.textContent = '';

  if (!requestText) return alert("Please describe the tool you want to build.");

  progress.textContent = "🧠 Starting...\n";

  // POST to orchestrator (creates id + starts writing intermediate files)
  const resp = await fetch('../api/orchestrator.php', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ request: requestText })
  });

  if (!resp.ok) {
    progress.textContent += "❌ Server error: " + resp.status;
    return;
  }

  const data = await resp.json();
  if (data.error) {
    progress.textContent += "❌ " + data.error;
    return;
  }

  const id = data.id;
  progress.textContent += `📌 Request id: ${id}\n`;
  // open SSE to listen for progress
  const sseUrl = `../api/orchestrator_stream.php?id=${encodeURIComponent(id)}`;
  const evt = new EventSource(sseUrl);

  evt.addEventListener('progress', (e) => {
    const payload = JSON.parse(e.data);
    // append nicely
    progress.textContent += `\n[${payload.file}]\n${payload.content}\n`;
    progress.scrollTop = progress.scrollHeight;
  });

  evt.addEventListener('error', (e) => {
    try {
      const payload = JSON.parse(e.data);
      progress.textContent += `\n[ERROR] ${payload.msg || JSON.stringify(payload)}\n`;
    } catch (err) {
      progress.textContent += '\n[ERROR] stream error\n';
    }
    evt.close();
  });

  evt.addEventListener('done', (e) => {
    const payload = JSON.parse(e.data);
    progress.textContent += `\n✅ Done. Manifest: ${JSON.stringify(payload.manifest || [])}\n`;
    // Next we expect a 'project' event with slug, or we will open editor with id
  });

  evt.addEventListener('project', (e) => {
    const payload = JSON.parse(e.data);
    const slug = payload.slug || id;
    progress.textContent += `\n🔗 Opening editor for ${slug}\n`;
    // Give the user a moment then open editor page
    setTimeout(() => {
      window.location.href = `editor.html?slug=${encodeURIComponent(slug)}&id=${encodeURIComponent(id)}`;
    }, 600);
    evt.close();
  });

  evt.addEventListener('end', () => {
    // ensure closed
    try { evt.close(); } catch (err) {}
  });
}


===== FILE: Tools2/webtool/editor.html @ 2025-10-15 21:34:41 =====
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Project Editor</title>
  <link rel="stylesheet" href="editor.css">
</head>
<body>
  <div class="topbar">
    <a href="index.html">← Back</a>
    <h1 id="title">Project Editor</h1>
  </div>
  <div class="split">
    <aside id="file-list" class="pane left">
      <h3>Files</h3>
      <ul></ul>
    </aside>
    <main class="pane right">
      <div id="file-path"></div>
      <pre id="code"><code></code></pre>
    </main>
  </div>

  <script src="editor.js"></script>
</body>
</html>


===== FILE: Tools2/webtool/editor.css @ 2025-10-15 21:34:59 =====
:root { --gap: 12px; --bg:#f6f7f9; --pane:#fff; }
body { margin:0; font-family: "Segoe UI", Arial, sans-serif; background:var(--bg); color:#222; }
.topbar { display:flex; align-items:center; gap:1rem; padding:12px 16px; background:#fff; box-shadow:0 1px 2px rgba(0,0,0,0.06); }
.split { display:flex; height: calc(100vh - 64px); gap:var(--gap); padding:12px; box-sizing:border-box; }
.pane { background:var(--pane); border-radius:8px; padding:12px; box-shadow: 0 2px 6px rgba(0,0,0,0.04); overflow:auto; }
.left { width:280px; }
.right { flex:1; }
#file-list ul { list-style:none; padding:0; margin:0; }
#file-list li { padding:8px; border-radius:6px; cursor:pointer; }
#file-list li:hover { background:#f0f4f8; }
#file-path { font-family: monospace; padding:6px 0; color:#555; }
pre#code { white-space: pre-wrap; word-break:break-word; background:#0b1020; color:#e6eef8; padding:12px; border-radius:6px; height:100%; overflow:auto; }


===== FILE: Tools2/webtool/editor.js @ 2025-10-15 21:35:17 =====
// editor.js
const params = new URLSearchParams(window.location.search);
const slug = params.get('slug');
const id = params.get('id');

document.getElementById('title').textContent = `Project Editor — ${slug || id}`;

async function listFiles() {
  const res = await fetch(`../api/list_project_files.php?slug=${encodeURIComponent(slug)}&id=${encodeURIComponent(id)}`);
  const data = await res.json();
  if (data.error) {
    document.querySelector('#file-list ul').innerHTML = `<li>${data.error}</li>`;
    return;
  }
  const ul = document.querySelector('#file-list ul');
  ul.innerHTML = '';
  (data.files || []).forEach(f => {
    const li = document.createElement('li');
    li.textContent = f;
    li.addEventListener('click', () => loadFile(f));
    ul.appendChild(li);
  });
}

async function loadFile(path) {
  document.getElementById('file-path').textContent = path;
  const res = await fetch(`../api/get_project_file.php?slug=${encodeURIComponent(slug)}&path=${encodeURIComponent(path)}`);
  const data = await res.json();
  if (data.error) {
    document.querySelector('#code code').textContent = data.error;
    return;
  }
  document.querySelector('#code code').textContent = data.content || '';
}

// initial
listFiles();


===== FILE: Tools2/webtool/api/list_protect_files.hp @ 2025-10-15 21:36:00 =====
<?php
// list_project_files.php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');

$slug = $_GET['slug'] ?? '';
$id = $_GET['id'] ?? '';

if (!$slug && !$id) {
    echo json_encode(['error'=>'Missing slug or id']);
    exit;
}

$runtime = realpath(__DIR__ . '/../runtime') ?: (__DIR__ . '/../runtime');
$projects = $runtime . '/projects';

// prefer slug; if empty try to detect slug from id manifest title
if (!$slug && $id) {
    // try manifest to find files' paths / get title-based slug
    $intermediate = $runtime . '/intermediate';
    $titleFile = "{$intermediate}/{$id}_title.txt";
    if (file_exists($titleFile)) {
        $title = trim(file_get_contents($titleFile));
        $slug = strtolower(preg_replace('/[^a-z0-9\s-]+/','', $title));
        $slug = preg_replace('/\s+/', '-', trim($slug));
        if ($slug === '') $slug = 'project-'.$id;
    }
}

$project_dir = rtrim($projects, '/') . '/' . $slug;
if (!is_dir($project_dir)) {
    echo json_encode(['error'=>"Project folder not found: {$project_dir}"]);
    exit;
}

$files = [];
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($project_dir, RecursiveDirectoryIterator::SKIP_DOTS));
foreach ($it as $file) {
    if ($file->isFile()) {
        // path relative to project root
        $rel = substr($file->getPathname(), strlen($project_dir)+1);
        $files[] = $rel;
    }
}

echo json_encode(['files'=>$files]);
exit;


===== FILE: Tools2/webtool/api/get_protect_files.php @ 2025-10-15 21:36:29 =====
<?php
// get_project_file.php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');

$slug = $_GET['slug'] ?? '';
$path = $_GET['path'] ?? '';

if (!$slug || !$path) {
    echo json_encode(['error'=>'Missing slug or path']);
    exit;
}

$runtime = realpath(__DIR__ . '/../runtime') ?: (__DIR__ . '/../runtime');
$projects = $runtime . '/projects';
$project_dir = rtrim($projects, '/') . '/' . $slug;
$full = $project_dir . '/' . $path;

if (!file_exists($full) || !is_file($full)) {
    echo json_encode(['error'=>'File not found: ' . $path]);
    exit;
}

$content = file_get_contents($full);
echo json_encode(['content' => $content]);
exit;


===== FILE: Tools2/webtool/api/list_protect_files.php @ 2025-10-15 21:37:00 =====
<?php
// list_project_files.php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');

$slug = $_GET['slug'] ?? '';
$id = $_GET['id'] ?? '';

if (!$slug && !$id) {
    echo json_encode(['error'=>'Missing slug or id']);
    exit;
}

$runtime = realpath(__DIR__ . '/../runtime') ?: (__DIR__ . '/../runtime');
$projects = $runtime . '/projects';

// prefer slug; if empty try to detect slug from id manifest title
if (!$slug && $id) {
    // try manifest to find files' paths / get title-based slug
    $intermediate = $runtime . '/intermediate';
    $titleFile = "{$intermediate}/{$id}_title.txt";
    if (file_exists($titleFile)) {
        $title = trim(file_get_contents($titleFile));
        $slug = strtolower(preg_replace('/[^a-z0-9\s-]+/','', $title));
        $slug = preg_replace('/\s+/', '-', trim($slug));
        if ($slug === '') $slug = 'project-'.$id;
    }
}

$project_dir = rtrim($projects, '/') . '/' . $slug;
if (!is_dir($project_dir)) {
    echo json_encode(['error'=>"Project folder not found: {$project_dir}"]);
    exit;
}

$files = [];
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($project_dir, RecursiveDirectoryIterator::SKIP_DOTS));
foreach ($it as $file) {
    if ($file->isFile()) {
        // path relative to project root
        $rel = substr($file->getPathname(), strlen($project_dir)+1);
        $files[] = $rel;
    }
}

echo json_encode(['files'=>$files]);
exit;


===== FILE: Tools2/webtool/api/orchestrator_stream.php @ 2025-10-15 21:39:08 =====
<?php
// orchestrator_stream.php (improved)
declare(strict_types=1);

$id = $_GET['id'] ?? '';
if (!$id) {
    http_response_code(400);
    echo "Missing id";
    exit;
}

$runtime = realpath(__DIR__ . '/../runtime') ?: (__DIR__ . '/../runtime');
$intermediate = $runtime . '/intermediate';

header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');

function sse_send($event, $data) {
    echo "event: {$event}\n";
    $json = json_encode($data, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
    $lines = preg_split("/\r\n|\n|\r/", $json);
    foreach ($lines as $line) {
        echo "data: {$line}\n";
    }
    echo "\n";
    @ob_flush();
    @flush();
}

function safe_slug_from_title($title) {
    $slug = mb_strtolower($title, 'UTF-8');
    $slug = preg_replace('/[^a-z0-9\s-]/u','', $slug);
    $slug = preg_replace('/\s+/', '-', trim($slug));
    $slug = preg_replace('/-+/', '-', $slug);
    if ($slug === '') $slug = 'project-'.bin2hex(random_bytes(3));
    return mb_substr($slug, 0, 60, 'UTF-8');
}

$seen = [];
$start = time();
$timeout_seconds = 60 * 5;

while (true) {
    if (time() - $start > $timeout_seconds) {
        sse_send('error', ['msg'=>'Stream timeout']);
        break;
    }

    // Find intermediate files for this id, sorted by mtime
    $pattern = $intermediate . '/' . $id . '_*';
    $found = glob($pattern) ?: [];
    usort($found, function($a,$b){ return filemtime($a) <=> filemtime($b); });

    foreach ($found as $file) {
        if (isset($seen[$file])) continue;
        $seen[$file] = true;
        $basename = basename($file);
        $content = @file_get_contents($file);
        if ($content === false) $content = '';

        // Emit specialized events for known filenames
        if (strpos($basename, '_structure.json') !== false) {
            // try to decode JSON for pretty display
            $json = @json_decode($content, true);
            sse_send('structure', ['raw' => $content, 'json' => $json]);
            continue;
        }

        if (strpos($basename, '_connections.txt') !== false) {
            sse_send('connections', ['raw' => $content]);
            continue;
        }

        if (strpos($basename, '_code_package.txt') !== false || strpos($basename, '_code.txt') !== false) {
            sse_send('code', ['raw' => $content]);
            continue;
        }

        if (strpos($basename, '_priorities.json') !== false) {
            $json = @json_decode($content, true);
            sse_send('priorities', ['raw' => $content, 'json' => $json]);
            continue;
        }

        // generic progress for other files
        sse_send('progress', ['file'=>$basename, 'content'=>$content]);
    }

    // If manifest exists -> send manifest + project slug (from title)
    $manifestFile = "{$intermediate}/{$id}_manifest.json";
    if (file_exists($manifestFile)) {
        $manifestRaw = @file_get_contents($manifestFile);
        $manifest = @json_decode($manifestRaw, true) ?: [];
        sse_send('manifest', ['files' => $manifest]);

        // compute slug from title file (if available)
        $titleFile = "{$intermediate}/{$id}_title.txt";
        $slug = null;
        if (file_exists($titleFile)) {
            $title = trim(@file_get_contents($titleFile));
            $slug = safe_slug_from_title($title);
        } else {
            $slug = 'project-'.$id;
        }

        sse_send('project', ['slug' => $slug, 'manifest' => $manifest]);
        break;
    }

    // Error handling: if code file has "Error:" prefix, emit an error
    $codeFile = "{$intermediate}/{$id}_code.txt";
    if (file_exists($codeFile) && strpos(@file_get_contents($codeFile), 'Error:') === 0) {
        sse_send('error', ['msg'=>'Code generation error', 'detail'=>@file_get_contents($codeFile)]);
        break;
    }

    usleep(300000);
}

sse_send('end', ['msg'=>'stream closed']);
exit;


===== FILE: Tools2/webtool/ui/app.js @ 2025-10-15 21:39:40 =====
// after successful POST and getting `data` with id:
const id = data.id;
progress.textContent += `📌 Request id: ${id}\n\n`;

// open SSE
const sseUrl = `../api/orchestrator_stream.php?id=${encodeURIComponent(id)}`;
const evt = new EventSource(sseUrl);

function prettyPrintJSON(obj) {
  try { return JSON.stringify(obj, null, 2); } catch { return String(obj); }
}

evt.addEventListener('structure', (e) => {
  const payload = JSON.parse(e.data);
  progress.textContent += '\n--- STRUCTURE ---\n';
  if (payload.json) {
    progress.textContent += prettyPrintJSON(payload.json) + '\n';
  } else {
    progress.textContent += payload.raw + '\n';
  }
  progress.scrollTop = progress.scrollHeight;
});

evt.addEventListener('connections', (e) => {
  const payload = JSON.parse(e.data);
  progress.textContent += '\n--- CONNECTIONS ---\n' + payload.raw + '\n';
  progress.scrollTop = progress.scrollHeight;
});

evt.addEventListener('priorities', (e) => {
  const payload = JSON.parse(e.data);
  progress.textContent += '\n--- PRIORITIES ---\n';
  if (payload.json) progress.textContent += prettyPrintJSON(payload.json) + '\n';
  else progress.textContent += payload.raw + '\n';
  progress.scrollTop = progress.scrollHeight;
});

evt.addEventListener('code', (e) => {
  const payload = JSON.parse(e.data);
  progress.textContent += '\n--- CODE PACKAGE ---\n' + payload.raw + '\n';
  progress.scrollTop = progress.scrollHeight;
});

evt.addEventListener('manifest', (e) => {
  const payload = JSON.parse(e.data);
  progress.textContent += '\n--- MANIFEST ---\n' + JSON.stringify(payload.files || [], null, 2) + '\n';
  progress.scrollTop = progress.scrollHeight;
});

evt.addEventListener('project', (e) => {
  const payload = JSON.parse(e.data);
  const slug = payload.slug || id;
  progress.textContent += `\n🔗 Opening editor for ${slug}\n`;
  setTimeout(() => {
    window.location.href = `editor.html?slug=${encodeURIComponent(slug)}&id=${encodeURIComponent(id)}`;
  }, 400);
  evt.close();
});

evt.addEventListener('error', (e) => {
  let payload;
  try { payload = JSON.parse(e.data); } catch { payload = {msg: e.data}; }
  progress.textContent += '\n[ERROR] ' + (payload.msg || JSON.stringify(payload)) + '\n';
  evt.close();
});

evt.addEventListener('end', () => {
  try { evt.close(); } catch {}
});


===== FILE: webide/assets/app.js @ 2025-10-18 07:12:42 =====
(async function () {
  // ---------- DOM helpers ----------
  const $ = (q, el = document) => el.querySelector(q),
        $$ = (qAll, el = document) => Array.from(el.querySelectorAll(qAll));

  // ---------- API helper (POST FormData) ----------
  const api = async (params) => {
    const form = new FormData();
    Object.entries(params).forEach(([k, v]) => {
      if (Array.isArray(v)) v.forEach((x) => form.append(k + "[]", x));
      else form.append(k, v);
    });
    const r = await fetch("api.php", { method: "POST", body: form });
    return r.json();
  };

  // ---------- State ----------
  const state = {
    currentRel: "",
    ace: null,
    cfg: null,
    dirty: false,
    saving: false,
    scopeRel: "" // '' = workspace root; otherwise e.g. 'admin/pages'
  };

  // ---------- Init ----------
  async function init() {
    state.cfg = await api({ action: "config" });

    // restore scope
    state.scopeRel = localStorage.getItem("mini_ide_scope") || "";

    bindUI();
    await refreshTree();

    // Editor: ACE if available, fallback to <textarea>
    if (state.cfg.ace && window.ace) {
      state.ace = ace.edit("editor");
      state.ace.setTheme("ace/theme/textmate");
      state.ace.session.setUseSoftTabs(true);
      state.ace.session.setTabSize(2);
      state.ace.setOption("fontSize", "13px");
      $("#ta").classList.add("hidden");
      state.ace.session.on("change", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    } else {
      $("#editor").classList.add("hidden");
      $("#ta").addEventListener("input", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    }

    // Load a sensible default (index.*) if present
    const guess = $$("#tree .file").find((n) =>
      /^(index\.(php|html?|md|txt))$/i.test(n.dataset.rel || "")
    );
    if (guess) openFile(guess.dataset.rel);
  }
  
  (function(){
    // Slide In Panel - by CodyHouse.co
	var panelTriggers = document.getElementsByClassName('js-cd-panel-trigger');
	if( panelTriggers.length > 0 ) {
		for(var i = 0; i < panelTriggers.length; i++) {
			(function(i){
				var panelClass = 'js-cd-panel-'+panelTriggers[i].getAttribute('data-panel'),
					panel = document.getElementsByClassName(panelClass)[0];
					
				// open panel when clicking on trigger btn
				panelTriggers[i].addEventListener('mouseover', function(event){
					event.preventDefault();
					addClass(panel, 'cd-panel--is-visible');
				});
				//close panel when clicking on 'x' or outside the panel
				panel.addEventListener('mouseout', function(event){
					if( hasClass(event.target, 'js-cd-close') || hasClass(event.target, panelClass)) {
						event.preventDefault();
						removeClass(panel, 'cd-panel--is-visible');
					}
				});
			})(i);
		}
	}
	
	//class manipulations - needed if classList is not supported
	//https://jaketrent.com/post/addremove-classes-raw-javascript/
	function hasClass(el, className) {
	  	if (el.classList) return el.classList.contains(className);
	  	else return !!el.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)'));
	}
	function addClass(el, className) {
	 	if (el.classList) el.classList.add(className);
	 	else if (!hasClass(el, className)) el.className += " " + className;
	}
	function removeClass(el, className) {
	  	if (el.classList) el.classList.remove(className);
	  	else if (hasClass(el, className)) {
	    	var reg = new RegExp('(\\s|^)' + className + '(\\s|$)');
	    	el.className=el.className.replace(reg, ' ');
	  	}
	}
})();

  // ---------- UI wiring ----------
  function bindUI() {
    $("#refreshTree")?.addEventListener("click", refreshTree);
    $("#btnCreateFile")?.addEventListener("click", () => createFromInput(false));
    $("#btnCreateDir")?.addEventListener("click", () => createFromInput(true));
    $("#btnBulk")?.addEventListener("click", bulkCreate);
    $("#btnSave")?.addEventListener("click", saveCurrent);
    $("#btnRename")?.addEventListener("click", renameCurrent);
    $("#btnDelete")?.addEventListener("click", deleteCurrent);
    $("#btnScanLinks")?.addEventListener("click", scanLinks);
    $("#scopeReset")?.addEventListener("click", () => setScope(""));

   

    // Keyboard shortcuts (capture)
    window.addEventListener(
      "keydown",
      (e) => {
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
          e.preventDefault(); e.stopPropagation();
          saveCurrent();
        }
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
          e.preventDefault(); toggleSidebar();
        }
      },
      true
    );

    // Preview toggle
    $("#togglePreview")?.addEventListener("change", syncPreviewVisibility);

    // Sidebar toggle button
  //  $("#btnToggleSidebar")?.addEventListener("click", toggleSidebar);
    
  //  $("#btnToggleSidebar")?.addEventListener("mouseover", toggleSidebar);
   // $("#btnToggleSidebar")?.addEventListener("mouseout", toggleSidebar);
    
(() => {
  const filesEdge = document.getElementById('files-edge');
  const filesPanel = document.getElementById('files-panel');
  const fileChev = document.getElementById('filechev');
  const filesPin = document.getElementById('files-pin'); // your pin button inside header

  if (!filesEdge || !filesPanel || !fileChev) return;

  // state
  let panelPinned = localStorage.getItem('mini_ide_panel_pinned') === '1';

  // helpers
  function setCollapsed(collapsed) {
    filesPanel.classList.toggle('expanded', !collapsed);
    filesPanel.classList.toggle('collapsed', collapsed);
    filesPanel.setAttribute('aria-hidden', collapsed ? 'true' : 'false');

    // body class controls chevron visibility
    document.body.classList.toggle('files-panel-collapsed', collapsed);
  }

  // initial
  setCollapsed(!panelPinned);

  // Hover open while not pinned
  filesEdge.addEventListener('mouseenter', () => {
    if (!panelPinned) setCollapsed(false);
  });
  // Also open when hovering the panel itself so user can move cursor inside
  filesPanel.addEventListener('mouseenter', () => {
    if (!panelPinned) setCollapsed(false);
  });

  // Close panel when leaving edge + panel area if not pinned
  // We close only when mouse leaves both the edge and the panel.
  let leaveTimer = null;
  function scheduleMaybeCollapse() {
    if (panelPinned) return;
    clearTimeout(leaveTimer);
    // slight delay to avoid jitter when moving pointer
    leaveTimer = setTimeout(() => {
      const overEdge = filesEdge.matches(':hover');
      const overPanel = filesPanel.matches(':hover');
      if (!overEdge && !overPanel) setCollapsed(true);
    }, 180);
  }
  filesEdge.addEventListener('mouseleave', scheduleMaybeCollapse);
  filesPanel.addEventListener('mouseleave', scheduleMaybeCollapse);

  // Pin toggle
  if (filesPin) {
    filesPin.addEventListener('click', (ev) => {
      ev.stopPropagation();
      panelPinned = !panelPinned;
      localStorage.setItem('mini_ide_panel_pinned', panelPinned ? '1' : '0');
      setCollapsed(!panelPinned);
      // update pin icon (optional)
      filesPin.textContent = panelPinned ? '📌' : '📍';
    });
  }

  // Chev click opens the panel (and does not pin)
  fileChev.addEventListener('click', (ev) => {
    ev.stopPropagation();
    panelPinned = false;
    localStorage.setItem('mini_ide_panel_pinned', '0');
    setCollapsed(false);
  });

  // If you want keyboard toggle (Ctrl/Cmd+B)
  window.addEventListener('keydown', (ev) => {
    if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === 'b') {
      ev.preventDefault();
      panelPinned = !panelPinned;
      localStorage.setItem('mini_ide_panel_pinned', panelPinned ? '1' : '0');
      setCollapsed(!panelPinned);
    }
  });

})();

    // Restore sidebar state
    if (localStorage.getItem("mini_ide_sidebar_hidden") === "0") {
      document.body.classList.add("sidebar-hidden");
    }
  }

  function toggleSidebar() {
    const hidden = document.body.classList.toggle("sidebar-hidden");
    const chev = document.getElementById("filechev");
    localStorage.setItem("mini_ide_sidebar_hidden", hidden ? "1" : "0");
  }

  function syncPreviewVisibility() {
    const on = $("#togglePreview")?.checked;
    if (on == null) return;
    $("#preview").style.display = on ? "block" : "none";
    document.querySelector(".editorWrap").style.gridTemplateColumns = on ? "1fr 40%" : "1fr";
  }

  // ---------- Save status / dirty UI ----------
  function setSaveBusy(on) {
    // big overlay
    const overlay = $("#pageLoader");
    if (overlay) overlay.style.display = on ? "flex" : "none";

    // header spinner
    const loader = $("#saveLoader");
    if (loader) loader.style.visibility = on ? "visible" : "hidden";

    const dot = $("#saveStatusDot");
    if (dot && on) dot.classList.remove("ok", "err", "dirty");
  }
  function setSaveDirty(on) {
    const btn = $("#btnSave");
    if (btn) btn.style.borderColor = on ? "#c9e8dc" : "var(--border)";
  }
  function markSaveOK() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("err", "dirty");
    dot.classList.add("ok");
    setTimeout(() => dot.classList.remove("ok"), 900);
  }
  function markSaveErr() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("ok", "dirty");
    dot.classList.add("err");
  }
  function updateDirtyUI() {
    // header asterisk
    const cp = $("#currentPath");
    if (cp) cp.textContent = (state.currentRel || "—") + (state.dirty ? " *" : "");

    // tree asterisk (on current file only)
    $$("#tree .file .name").forEach((n) => n.classList.remove("dirty"));
    if (state.currentRel) {
      const n = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"] .name`);
      if (n && state.dirty) n.classList.add("dirty");
    }

    // status dot amber while dirty
    const dot = $("#saveStatusDot");
    if (dot) {
      dot.classList.remove("ok", "err");
      dot.classList.toggle("dirty", !!state.dirty);
    }
  }

  // ---------- Scope helpers ----------
  function setScope(rel) {
    state.scopeRel = (rel || "").replace(/^\/+|\/+$/g, "");
    localStorage.setItem("mini_ide_scope", state.scopeRel);
    refreshTree();
  }

function renderScopeCrumbs(){
  // toolbar area
  const wrapTop = document.getElementById('scopeCrumbs');
  // footer area inside the panel
  const wrapFooter = document.getElementById('scopeCrumbsFooter');

  function buildCrumbsInto(wrap, idForWrap){
    if (!wrap) return;
    wrap.innerHTML = "";
    const mk = (txt, rel, isCurrent=false) => {
      const s = document.createElement("span");
      s.className = "crumb" + (isCurrent ? " current" : "");
      s.textContent = txt;
      s.style.cursor = isCurrent ? "default" : "pointer";
      s.onclick = () => !isCurrent && setScope(rel);
      if (isCurrent) s.title = "Current folder";
      return s;
    };

    const parts = (state.scopeRel || "").split("/").filter(Boolean);
    wrap.appendChild(mk("root", "", parts.length === 0));
    let acc = "";
    parts.forEach((p, i) => {
      const sep = document.createElement("span");
      sep.className = "sep"; sep.textContent = "/";
      wrap.appendChild(sep);
      acc = acc ? acc + "/" + p : p;
      wrap.appendChild(mk(p, acc, i === parts.length - 1));
    });

    // Add a small "up to parent" button for convenience (on the footer only)
    if (idForWrap === 'footer') {
      const upBtn = document.createElement('button');
      upBtn.textContent = '↥ Up';
      upBtn.title = 'Go up one level';
      upBtn.style.marginLeft = '8px';
      upBtn.onclick = () => {
        if (!state.scopeRel) return setScope(""); // already root
        const parts = state.scopeRel.split("/").filter(Boolean);
        parts.pop();
        setScope(parts.join("/"));
      };
      wrap.appendChild(upBtn);
    }
  }

  buildCrumbsInto(wrapTop, 'top');
  buildCrumbsInto(wrapFooter, 'footer');

  // ensure the Reset button in footer is wired
  const resetBtn = document.getElementById('scopeResetFooter') || document.getElementById('scopeReset');
  if (resetBtn) {
    resetBtn.onclick = () => setScope("");
  }
}


  // ---------- Tree (server-scoped, collapsible) ----------
  function toNested(items) {
    const root = { name: "", rel: "", dirs: {}, files: [] };
    const entries = items
      .map((it) => ({ type: it.type, rel: (it.rel || "").replace(/^\/+/, "") }))
      .filter((e) => e.rel);

    // ensure nodes
    entries.forEach((e) => {
      const parts = e.rel.split("/");
      if (e.type === "dir") {
        let cur = root;
        for (const part of parts) {
          cur.dirs[part] = cur.dirs[part] || {
            name: part,
            rel: (cur.rel ? cur.rel + "/" : "") + part,
            dirs: {},
            files: []
          };
          cur = cur.dirs[part];
        }
      }
    });

    // place files
    entries.filter((e) => e.type === "file").forEach((e) => {
      const parts = e.rel.split("/");
      const name = parts.pop();
      let cur = root;
      for (const part of parts) {
        cur.dirs[part] = cur.dirs[part] || {
          name: part,
          rel: (cur.rel ? cur.rel + "/" : "") + part,
          dirs: {},
          files: []
        };
        cur = cur.dirs[part];
      }
      cur.files.push({ name, rel: e.rel });
    });

    // sort
    (function sort(n) {
      Object.values(n.dirs).forEach(sort);
      n.files.sort((a, b) => a.name.localeCompare(b.name));
    })(root);

    return root;
  }

  function renderTree(rootNode, container) {
    const openState = JSON.parse(localStorage.getItem("mini_ide_open_dirs") || "{}");
    const setOpen = (rel, open) => {
      if (open) openState[rel || "/"] = 1;
      else delete openState[rel || "/"];
      localStorage.setItem("mini_ide_open_dirs", JSON.stringify(openState));
    };

    container.innerHTML = "";

    const mkDir = (node) => {
      const det = document.createElement("details");
      if (openState[node.rel || "/"]) det.open = true;

      const sum = document.createElement("summary");
      sum.innerHTML = `
        <span class="chev">›</span>
        <span class="label name">📁 ${node.name || "root"}</span>
        <button class="focus" type="button" title="Focus here" data-rel="${node.rel}">📌 Focus</button>
      `;
      det.appendChild(sum);

      // node.rel from server is already full root-relative path
      const dirRel = (node.rel || "");

      // focus controls
      sum.ondblclick = (e) => { e.preventDefault(); setScope(dirRel); };
      sum.querySelector(".focus").onclick = (e) => { e.stopPropagation(); setScope(dirRel); };

      det.addEventListener("toggle", () => setOpen(node.rel, det.open));

      // child dirs
      const dirNames = Object.keys(node.dirs).sort((a, b) => a.localeCompare(b));
      dirNames.forEach((name) => det.appendChild(mkDir(node.dirs[name])));

      // files: use rel from server as-is (already full)
      node.files.forEach((f) => {
        const div = document.createElement("div");
        div.className = "file";
        div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
        div.dataset.rel = f.rel;
        div.onclick = () => openFile(f.rel);
        det.appendChild(div);
      });

      return det;
    };

    // top-level dirs
    const dirNames = Object.keys(rootNode.dirs).sort((a, b) => a.localeCompare(b));
    dirNames.forEach((name) => container.appendChild(mkDir(rootNode.dirs[name])));

    // top-level files: use rel as-is
    rootNode.files.forEach((f) => {
      const div = document.createElement("div");
      div.className = "file";
      div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
      div.dataset.rel = f.rel;
      div.onclick = () => openFile(f.rel);
      container.appendChild(div);
    });
  }

  async function refreshTree() {
    try {
      const res = await api({ action: "tree", rel: state.scopeRel });
      if (!res || res.ok !== true || !Array.isArray(res.items)) {
        const msg = res && res.error ? res.error : "Tree request failed";
        alert("Mini IDE error: " + msg);
        console.error("Tree response:", res);
        return;
      }
      const nested = toNested(res.items);
      renderTree(nested, $("#tree"));
      renderScopeCrumbs();

      // re-highlight current file if visible
      if (state.currentRel) {
        $$("#tree .file").forEach((n) => n.classList.remove("active"));
        const hit = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"]`);
        if (hit) hit.classList.add("active");
        updateDirtyUI();
      }
    } catch (e) {
      alert("Mini IDE fetch error (tree): " + e);
      console.error(e);
    }
  }

  // ---------- File ops ----------
  async function openFile(rel) {
    const res = await api({ action: "read", rel });
    if (!res.ok) return alert(res.error || "Failed to read");
    state.currentRel = rel;

    // highlight selected
    $$("#tree .file").forEach((n) => n.classList.remove("active"));
    const hit = $(`#tree .file[data-rel="${CSS.escape(rel)}"]`);
    if (hit) hit.classList.add("active");

    if (res.file.editable) {
      setEditorContent(res.file.content || "");
    } else {
      setEditorContent("// Not editable (binary or large). Size: " + res.file.size + " bytes\n");
    }
    state.dirty = false;
    setSaveDirty(false);
    updateDirtyUI();
    refreshPreview();
  }

  function getEditorContent() {
    if (state.ace) return state.ace.getValue();
    return $("#ta").value;
  }

  function setEditorContent(txt) {
    if (state.ace) {
      state.ace.setValue(txt ?? "", -1);
      // Set mode from extension
      const ext = (state.currentRel.split(".").pop() || "").toLowerCase();
      const map = {
        js: "javascript",
        css: "css",
        php: "php",
        html: "html",
        htm: "html",
        json: "json",
        md: "markdown",
        py: "python",
        yml: "yaml",
        yaml: "yaml",
        c: "c_cpp",
        cpp: "c_cpp",
        h: "c_cpp",
        sql: "sql",
        csv: "text",
        txt: "text"
      };
      state.ace.session.setMode("ace/mode/" + (map[ext] || "text"));
    } else {
      $("#ta").classList.remove("hidden");
      $("#ta").value = txt ?? "";
    }
  }

  async function saveCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (state.saving) return; // prevent double-taps
    state.saving = true;
    setSaveBusy(true);

    try {
      const content = getEditorContent();
      const res = await api({ action: "save", rel: state.currentRel, content });
      if (!res.ok) {
        markSaveErr();
        throw new Error(res.error || "Save failed");
      }

      // Server verification
      let verified = !!res.verify_ok;

      // mtime wait (shared hosts can be slow)
      const targetMtime = res.mtime || 0;
      const okStat = await waitForStat(state.currentRel, targetMtime, 8, 150);
      if (!okStat) console.warn("Proceeding without stat confirmation");

      // Optional client-side readback if server said not verified
      if (!verified) {
        try {
          const rb = await api({ action: "read", rel: state.currentRel });
          if (rb.ok && rb.file && typeof rb.file.content === "string") {
            verified = (rb.file.content === content);
          }
        } catch {}
      }

      if (!verified) {
        markSaveErr();
        alert("Save did not verify. The file contents on disk did not match what was sent.");
        return; // do not mark OK or clear dirty
      }

      state.dirty = false;
      setSaveDirty(false);
      updateDirtyUI();   // clear asterisks + amber dot
      markSaveOK();      // green blip
      refreshPreview(true);

    } catch (e) {
      console.error(e);
      alert("Save error: " + e.message);
      markSaveErr();     // red
    } finally {
      state.saving = false;
      setSaveBusy(false);
    }
  }

  // poll stat until mtime >= target
  async function waitForStat(rel, target, tries = 8, delay = 150) {
    for (let i = 0; i < tries; i++) {
      try {
        const s = await api({ action: "stat", rel });
        if (s.ok && (s.mtime || 0) >= target) return true;
      } catch {}
      await new Promise((r) => setTimeout(r, delay));
    }
    return false;
  }

  async function renameCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    const to = prompt("Rename to (path):", state.currentRel);
    if (!to || to === state.currentRel) return;
    const res = await api({ action: "rename", from: state.currentRel, to });
    if (!res.ok) return alert(res.error || "Rename failed");
    state.currentRel = to;
    $("#currentPath").textContent = to;
    await refreshTree();
    refreshPreview();
  }

  async function deleteCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (!confirm("Delete " + state.currentRel + " ?")) return;
    const res = await api({ action: "delete", rel: state.currentRel });
    if (!res.ok) return alert(res.error || "Delete failed");
    state.currentRel = "";
    $("#currentPath").textContent = "—";
    if (state.ace) state.ace.setValue("");
    else $("#ta").value = "";
    await refreshTree();
    refreshPreview();
  }

  async function createFromInput(isDir) {
    const raw = $("#newPath").value.trim();
    if (!raw) return;

    const relPath = state.scopeRel
      ? (state.scopeRel + "/" + raw).replace(/\/+/g, "/").replace(/^\/|\/$/g, "")
      : raw;

    const res = await api({ action: "create", rel: relPath, type: isDir ? "dir" : "file" });
    if (!res.ok) return alert(res.error || "Create failed");
    $("#newPath").value = "";
    await refreshTree();
    if (!isDir) openFile(relPath);
  }

  async function bulkCreate() {
    const lines = $("#bulkLines").value;
    if (!lines.trim()) return;

    // Prefix each line's path with scope (keep labels 'dir:'/'file:' intact)
    const scoped = lines.split("\n").map((t) => {
      const ln = t.trim();
      if (!ln) return ln;
      const m = ln.match(/^(dir:|file:)?\s*(.*)$/i);
      if (!m) return ln;
      const label = (m[1] || "").toLowerCase();
      let p = m[2].trim();
      if (p && state.scopeRel && !p.startsWith("/")) {
        p = (state.scopeRel + "/" + p).replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
      }
      return (label ? label + " " : "") + p;
    }).join("\n");

    const res = await api({ action: "bulk", lines: scoped });
    if (!res.ok) return alert(res.error || "Bulk failed");
    await refreshTree();
    alert("Bulk operations complete.");
  }

  // ---------- Preview ----------
  function refreshPreview(/* force = false */) {
    const on = $("#togglePreview")?.checked;
    if (!on) return;

    // Prefer current file; otherwise fall back to index.html (safer default)
    let rel = state.currentRel || "index.html";
    if (!/\.(php|html?|md|txt|css|js)$/i.test(rel)) rel = "index.html";

    const base = (state.cfg && state.cfg.workspace_url) || "/";
    const url = (base.endsWith("/") ? base : base + "/") + rel;

    const ifr = $("#preview");
    ifr.src = url + (url.includes("?") ? "&" : "?") + "_t=" + Date.now(); // cache-bust
  }

  // ---------- Link scan ----------
  async function scanLinks() {
    const res = await api({ action: "links" });
    if (!res.ok) return alert(res.error || "Scan failed");
    const box = $("#links");
    box.innerHTML = "";
    if (!res.edges.length) {
      box.textContent = "No references found.";
      return;
    }
    res.edges.forEach((e) => {
      const div = document.createElement("div");
      div.className = "edge";
      div.textContent = `${e.from}  →  ${e.to}`;
      box.appendChild(div);
    });
  }

  // ---------- Go ----------
  init();
})();


===== FILE: webide/index.php @ 2025-10-18 07:14:04 =====
<?php
// index.php
declare(strict_types=1);
require_once __DIR__ . '/_inc/helpers.php';
ensure_auth();
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initital-scale=1" />
<title>Mini Web IDE</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
<link rel="stylesheet" href="assets/style.css" />
<link rel="icon" type="png" href="cool.png"/>
<?php
require __DIR__.'/_inc/config.php';
if ($USE_ACE_CDN): ?>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.9/ace.js" crossorigin="anonymous"></script>
<?php endif; ?>
</head>
<body>
    <div id="saveStatusDot" title="save status"></div>

<!-- thin left hover edge (visible when panel is collapsed) -->
<div id="files-edge" aria-hidden="false"></div>

<!-- sliding white files panel (single source of truth) -->
<aside id="files-panel" class="collapsed" role="navigation" aria-hidden="true">
  <div id="files-header">
    <button id="files-pin" title="Pin panel" aria-pressed="false">📍</button>
    <span class="files-title">Files</span>
    <button id="refreshTree" title="Refresh">↻</button>
    <button id="open-designer" title="Open Connections Designer">🧭</button>
  </div>

  <!-- file tree area (populated by app.js via `action: "tree"`) -->
  <div id="tree" class="tree" role="tree" aria-label="Files"></div>

  <!-- footer: scope crumbs, create controls, link scan -->
  <div id="files-footer">
    <div class="scopebar-footer">
      <span>📂</span>
      <span id="scopeCrumbsFooter"></span>
      <button id="scopeResetFooter" title="Back to workspace root">Reset</button>
    </div>

    <div class="createBox-footer">
      <input id="newPath" placeholder="path/to/file.ext or folder/" />
      <div class="row">
        <button data-kind="file" id="btnCreateFile">+ File</button>
        <button data-kind="dir" id="btnCreateDir">+ Folder</button>
      </div>
      <details>
        <summary>Bulk create</summary>
        <textarea id="bulkLines" rows="4" placeholder="dir: assets/
file: assets/app.js
file: index.php"></textarea>
        <button id="btnBulk">Run Bulk</button>
      </details>
    </div>

    <div class="linksBox-footer">
      <button id="btnScanLinks">Scan links</button>
      <div id="links"></div>
    </div>
  </div>
</aside>

<!-- chevron shown when panel is collapsed (must be outside panel) -->
<div id="js-cd-panel-trigger" title="Open files" role="button" aria-hidden="true">⮞</div>

<!-- MAIN APP LAYOUT -->
<div class="layout">
  <main class="main">
    <header class="mainbar">
      <div class="path">
        <span id="currentPath">—</span>
      </div>
      <div class="actions">
        
        <button id="btnSave">Save (Ctrl/Cmd+S)</button>
        <button id="btnRename">Rename</button>
        <button id="btnDelete" class="danger">Delete</button>
        <label class="previewToggle"><input type="checkbox" id="togglePreview" checked> Preview</label>
      </div>
    </header>

    <section class="editorWrap">
      <div id="editor" aria-label="Code editor"></div>
      <textarea id="ta" class="hidden" aria-hidden="true"></textarea>
      <iframe id="preview" title="Preview"></iframe>
    </section>
  </main>
</div>

<script>window.MINI_IDE = { base: '' };</script>

<!-- Panel behaviour script: hover edge opens panel, pin locks it, chevron shows when collapsed -->
<script>
(function () {
  const filesEdge = document.getElementById('files-edge');
  const filesPanel = document.getElementById('files-panel');
  const fileChev = document.getElementById('filechev');
  const filesPin = document.getElementById('files-pin');
  const toggleSidebarBtn = document.getElementById('btnToggleSidebar');

  if (!filesEdge || !filesPanel || !fileChev) return;

  // read pin state
  let panelPinned = localStorage.getItem('mini_ide_panel_pinned') === '1';

  function setCollapsed(collapsed) {
    filesPanel.classList.toggle('expanded', !collapsed);
    filesPanel.classList.toggle('collapsed', collapsed);
    filesPanel.setAttribute('aria-hidden', collapsed ? 'true' : 'false');

    // show/hide chevron (chev visible when collapsed)
    fileChev.setAttribute('aria-hidden', collapsed ? 'false' : 'true');
    fileChev.style.display = collapsed ? 'flex' : 'none';

    // update pin aria
    if (filesPin) filesPin.setAttribute('aria-pressed', !!panelPinned);
  }

  // initialize
  setCollapsed(!panelPinned);

  // Hover to open
  filesEdge.addEventListener('mouseenter', () => {
    if (!panelPinned) setCollapsed(false);
  });
  filesPanel.addEventListener('mouseenter', () => {
    if (!panelPinned) setCollapsed(false);
  });

  // Schedule collapse when leaving both edge and panel
  let leaveTimer = null;
  function scheduleMaybeCollapse() {
    if (panelPinned) return;
    clearTimeout(leaveTimer);
    leaveTimer = setTimeout(() => {
      const overEdge = filesEdge.matches(':hover');
      const overPanel = filesPanel.matches(':hover');
      if (!overEdge && !overPanel) setCollapsed(true);
    }, 160);
  }
  filesEdge.addEventListener('mouseleave', scheduleMaybeCollapse);
  filesPanel.addEventListener('mouseleave', scheduleMaybeCollapse);

  // Pin toggle
  if (filesPin) {
    filesPin.addEventListener('click', (ev) => {
      ev.stopPropagation();
      panelPinned = !panelPinned;
      localStorage.setItem('mini_ide_panel_pinned', panelPinned ? '1' : '0');
      setCollapsed(!panelPinned);
      filesPin.textContent = panelPinned ? '📌' : '📍';
      filesPin.setAttribute('aria-pressed', panelPinned ? 'true' : 'false');
    });
  }

  // Chev click opens (does not pin)
  fileChev.addEventListener('click', (ev) => {
    ev.stopPropagation();
    panelPinned = false;
    localStorage.setItem('mini_ide_panel_pinned', '0');
    setCollapsed(false);
  });

  // Toggle button in header toggles pin/visibility (Ctrl/Cmd+B also handled in app.js)
  if (toggleSidebarBtn) toggleSidebarBtn.addEventListener('click', () => {
    // If visible and not pinned -> collapse; if pinned -> unpin or collapse
    panelPinned = !panelPinned;
    localStorage.setItem('mini_ide_panel_pinned', panelPinned ? '1' : '0');
    setCollapsed(!panelPinned);
  });

  // keyboard shortcut: Ctrl/Cmd+B
  window.addEventListener('keydown', (ev) => {
    if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === 'b') {
      ev.preventDefault();
      panelPinned = !panelPinned;
      localStorage.setItem('mini_ide_panel_pinned', panelPinned ? '1' : '0');
      setCollapsed(!panelPinned);
    }
  });
})();
</script>

<!-- main app script (tree population, file ops, editor hooks) -->
<script src="assets/app.js"></script>

<!-- Big centered loader overlay -->
<div id="pageLoader" aria-hidden="true">
  <div class="spinner" role="status" title="Saving…"></div>
</div>

</body>
</html>


===== FILE: webide/assets/style.css @ 2025-10-18 07:15:29 =====
/* -------------------------------- 

Primary style

-------------------------------- */
*, *::after, *::before {
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
}

html {
  font-size: 62.5%;
}

body {
  font-size: 1.6rem;
  font-family: "Droid Sans", sans-serif;
  color: #dbe2e9;
  background: #424f5c;
}

a {
  color: #89ba2c;
  text-decoration: none;
}

/* -------------------------------- 

Slide In Panel - by CodyHouse.co

-------------------------------- */
.cd-main-content {
  text-align: center;
}

.cd-main-content h1 {
  font-size: 2rem;
  color: #64788c;
  padding: 4em 0;
}

.cd-btn {
  position: relative;
  display: inline-block;
  padding: 1em 2em;
  background-color: #89ba2c;
  color: #ffffff;
  border-radius: 50em;
  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 0 5px rgba(0, 0, 0, 0.1);
          box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 0 5px rgba(0, 0, 0, 0.1);
  -webkit-transition: -webkit-box-shadow 0.2s;
  transition: -webkit-box-shadow 0.2s;
  transition: box-shadow 0.2s;
  transition: box-shadow 0.2s, -webkit-box-shadow 0.2s;
}

.cd-btn:hover {
  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 0 20px rgba(0, 0, 0, 0.3);
          box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 0 20px rgba(0, 0, 0, 0.3);
}

@media only screen and (min-width: 1170px) {
  .cd-main-content h1 {
    font-size: 3.2rem;
  }
}

.cd-panel {
  position: fixed;
  top: 0;
  left: 0;
  height: 100%;
  width: 100%;
  visibility: hidden;
  -webkit-transition: visibility 0s 0.6s;
  transition: visibility 0s 0.6s;
}

.cd-panel::after {
  /* overlay layer */
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: transparent;
  cursor: pointer;
  -webkit-transition: background 0.3s 0.3s;
  transition: background 0.3s 0.3s;
}

.cd-panel.cd-panel--is-visible {
  visibility: visible;
  -webkit-transition: visibility 0s 0s;
  transition: visibility 0s 0s;
}

.cd-panel.cd-panel--is-visible::after {
  background: rgba(0, 0, 0, 0.6);
  -webkit-transition: background 0.3s 0s;
  transition: background 0.3s 0s;
}

.cd-panel__header {
  position: fixed;
  width: 90%;
  height: 50px;
  line-height: 50px;
  background: rgba(255, 255, 255, 0.96);
  z-index: 2;
  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.08);
          box-shadow: 0 1px 1px rgba(0, 0, 0, 0.08);
  -webkit-transition: -webkit-transform 0.3s 0s;
  transition: -webkit-transform 0.3s 0s;
  transition: transform 0.3s 0s;
  transition: transform 0.3s 0s, -webkit-transform 0.3s 0s;
  -webkit-transform: translateY(-50px);
      -ms-transform: translateY(-50px);
          transform: translateY(-50px);
}

.cd-panel__header h1 {
  color: #89ba2c;
  padding-left: 5%;
}

.cd-panel--from-right .cd-panel__header {
  right: 0;
}

.cd-panel--from-left .cd-panel__header {
  left: 0;
}

.cd-panel--is-visible .cd-panel__header {
  -webkit-transition: -webkit-transform 0.3s 0.3s;
  transition: -webkit-transform 0.3s 0.3s;
  transition: transform 0.3s 0.3s;
  transition: transform 0.3s 0.3s, -webkit-transform 0.3s 0.3s;
  -webkit-transform: translateY(0px);
      -ms-transform: translateY(0px);
          transform: translateY(0px);
}

@media only screen and (min-width: 768px) {
  .cd-panel__header {
    width: 70%;
  }
}

@media only screen and (min-width: 1170px) {
  .cd-panel__header {
    width: 50%;
  }
}

.cd-panel__close {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: 60px;
  /* image replacement */
  display: inline-block;
  overflow: hidden;
  text-indent: 100%;
  white-space: nowrap;
}

.cd-panel__close::before, .cd-panel__close::after {
  /* close icon created in CSS */
  content: '';
  position: absolute;
  top: 22px;
  left: 20px;
  height: 3px;
  width: 20px;
  background-color: #424f5c;
  /* this fixes a bug where pseudo elements are slighty off position */
  -webkit-backface-visibility: hidden;
          backface-visibility: hidden;
}

.cd-panel__close::before {
  -webkit-transform: rotate(45deg);
      -ms-transform: rotate(45deg);
          transform: rotate(45deg);
}

.cd-panel__close::after {
  -webkit-transform: rotate(-45deg);
      -ms-transform: rotate(-45deg);
          transform: rotate(-45deg);
}

.cd-panel__close:hover {
  background-color: #424f5c;
}

.cd-panel__close:hover::before, .cd-panel__close:hover::after {
  background-color: #ffffff;
  -webkit-transition: -webkit-transform 0.3s;
  transition: -webkit-transform 0.3s;
  transition: transform 0.3s;
  transition: transform 0.3s, -webkit-transform 0.3s;
}

.cd-panel__close:hover::before {
  -webkit-transform: rotate(220deg);
      -ms-transform: rotate(220deg);
          transform: rotate(220deg);
}

.cd-panel__close:hover::after {
  -webkit-transform: rotate(135deg);
      -ms-transform: rotate(135deg);
          transform: rotate(135deg);
}

.cd-panel--is-visible .cd-panel__close::before {
  -webkit-animation: cd-close-1 0.6s 0.3s;
          animation: cd-close-1 0.6s 0.3s;
}

.cd-panel--is-visible .cd-panel__close::after {
  -webkit-animation: cd-close-2 0.6s 0.3s;
          animation: cd-close-2 0.6s 0.3s;
}

@-webkit-keyframes cd-close-1 {
  0%, 50% {
    -webkit-transform: rotate(0deg);
            transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(45deg);
            transform: rotate(45deg);
  }
}

@keyframes cd-close-1 {
  0%, 50% {
    -webkit-transform: rotate(0deg);
            transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(45deg);
            transform: rotate(45deg);
  }
}

@-webkit-keyframes cd-close-2 {
  0%, 50% {
    -webkit-transform: rotate(0deg);
            transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(-45deg);
            transform: rotate(-45deg);
  }
}

@keyframes cd-close-2 {
  0%, 50% {
    -webkit-transform: rotate(0deg);
            transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(-45deg);
            transform: rotate(-45deg);
  }
}

.cd-panel__container {
  position: fixed;
  width: 90%;
  height: 100%;
  top: 0;
  background: #dbe2e9;
  z-index: 1;
  -webkit-transition: -webkit-transform 0.3s 0.3s;
  transition: -webkit-transform 0.3s 0.3s;
  transition: transform 0.3s 0.3s;
  transition: transform 0.3s 0.3s, -webkit-transform 0.3s 0.3s;
}

.cd-panel--from-right .cd-panel__container {
  right: 0;
  -webkit-transform: translate3d(100%, 0, 0);
          transform: translate3d(100%, 0, 0);
}

.cd-panel--from-left .cd-panel__container {
  left: 0;
  -webkit-transform: translate3d(-100%, 0, 0);
          transform: translate3d(-100%, 0, 0);
}

.cd-panel--is-visible .cd-panel__container {
  -webkit-transform: translate3d(0, 0, 0);
          transform: translate3d(0, 0, 0);
  -webkit-transition-delay: 0s;
          transition-delay: 0s;
}

@media only screen and (min-width: 768px) {
  .cd-panel__container {
    width: 70%;
  }
}

@media only screen and (min-width: 1170px) {
  .cd-panel__container {
    width: 50%;
  }
}

.cd-panel__content {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  padding: 70px 5%;
  overflow: auto;
  /* smooth scrolling on touch devices */
  -webkit-overflow-scrolling: touch;
}

.cd-panel__content p {
  font-size: 1.4rem;
  color: #424f5c;
  line-height: 1.4;
  margin: 2em 0;
}

.cd-panel__content p:first-of-type {
  margin-top: 0;
}

@media only screen and (min-width: 768px) {
  .cd-panel__content p {
    font-size: 1.6rem;
    line-height: 1.6;
  }
}


===== FILE: robotics/gamemaker.html @ 2025-10-18 10:22:25 =====
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Game Maker IDE</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body class="bg-[tan] text-slate-900 font-sans">
  <div id="root"></div>

  <script type="text/babel" data-presets="react,typescript">
    function GameMakerIDE() {
      const [title, setTitle] = React.useState("My Vintage Game");
      const [desc, setDesc] = React.useState("Flappy-style with mountains and a header splash.");
      const [game, setGame] = React.useState("flappy");
      const [generatedIno, setGeneratedIno] = React.useState("");
      const [imagePreview, setImagePreview] = React.useState(null);
      const [busy, setBusy] = React.useState(false);
      const canvasRef = React.useRef(null);

      async function aiGenerateImage() {
        setBusy(true);
        try {
          const res = await fetch('api/generate_image_groq_spec.php', {
            method: 'POST',
            headers: {'Content-Type':'application/json'},
            body: JSON.stringify({
              title,
              description: desc,
              game_type: game,
              target_width: 160,
              target_height: 128,
              grid_w: 20,
              grid_h: 16,
              palette_size: 5
            })
          });
          const out = await res.json();
          if (out.error) throw new Error(out.error || 'No image returned');
          if (out.png_data_url) {
            setImagePreview(out.png_data_url);
            const img = new Image();
            img.onload = () => {
              const c = canvasRef.current;
              if (!c) return;
              c.width = img.width;
              c.height = img.height;
              c.getContext('2d').drawImage(img, 0, 0);
            };
            img.src = out.png_data_url;
          }
        } catch (e) {
          alert('Image generation failed: ' + e.message);
          console.error(e);
        } finally {
          setBusy(false);
        }
      }

      function drawLocalTitleCardToCanvas(text) {
        const W = 160, H = 128;
        let c = canvasRef.current;
        if (!c) return;
        c.width = W; c.height = H;
        const ctx = c.getContext("2d");
        if (!ctx) return;

        const grad = ctx.createLinearGradient(0, 0, 0, H);
        grad.addColorStop(0, "#2278C5");
        grad.addColorStop(1, "#93CCEA");
        ctx.fillStyle = grad;
        ctx.fillRect(0, 0, W, H);

        ctx.fillStyle = "#5A6E82";
        for (let x = 0; x < W; x++) {
          const yTop = 80 - Math.sin((x + 10) * 0.11) * 8;
          ctx.fillRect(x, yTop, 1, H - yTop);
        }

        ctx.fillStyle = "#465764";
        for (let x = 0; x < W; x++) {
          const yTop = 88 - Math.sin((x + 20) * 0.13) * 10;
          ctx.fillRect(x, yTop, 1, H - yTop);
        }

        ctx.fillStyle = "#fff";
        ctx.textAlign = "center";
        ctx.font = "bold 18px system-ui";
        ctx.fillText(text || "Game Title", W / 2, 28);
        ctx.font = "12px system-ui";
        ctx.fillText("Press button to start", W / 2, H - 10);

        setImagePreview(c.toDataURL("image/png"));
      }

      async function generateCodeWithGroq() {
        setBusy(true);
        try {
          const imgData = canvasRef.current ? canvasRef.current.toDataURL("image/png") : null;
          const res = await fetch("api/generate.php", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
              title,
              description: desc,
              game_type: game,
              image_data_url: imgData
            })
          });
          const out = await res.json();

          if (out.error) throw new Error(out.error);
          if (out.ino) setGeneratedIno(out.ino);
          if (out.png_data_url) setImagePreview(out.png_data_url);
        } catch (e) {
          alert("Code generation failed: " + e.message);
          console.error(e);
        } finally {
          setBusy(false);
        }
      }

      function generateImageOnly() {
        drawLocalTitleCardToCanvas(title);
      }

      function download(filename, text) {
        const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = filename;
        a.click();
        URL.revokeObjectURL(url);
      }

      async function copyAll(text) {
        try {
          await navigator.clipboard.writeText(text);
          alert("Copied to clipboard!");
        } catch {
          const ta = document.createElement("textarea");
          ta.value = text;
          document.body.appendChild(ta);
          ta.select();
          document.execCommand("copy");
          document.body.removeChild(ta);
          alert("Copied using fallback!");
        }
      }

      return (
        <div className="min-h-screen bg-[tan] text-slate-900 font-sans">
          <div className="max-w-5xl mx-auto p-6">
            <img src="idetitle.png" style="max-height:100px"/>
            <p className="text-center text-slate-700 mb-8">
              Create fun mini-games for your ESP32! Choose a game, name it, describe it, and let AI write the Arduino code.
            </p>

            <div className="bg-amber-100 p-6 rounded-3xl shadow-lg border border-amber-300">
              <div className="flex flex-col md:flex-row gap-6">
                {/* Left side form */}
                <div className="flex-1 space-y-4">
                  <label className="block">
                    <span className="text-md font-semibold">Game Title</span>
                    <input
                      value={title}
                      onChange={e => setTitle(e.target.value)}
                      className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg"
                      placeholder="My Awesome Game"
                    />
                  </label>

                  <label className="block">
                    <span className="text-md font-semibold">Game Description</span>
                    <textarea
                      value={desc}
                      onChange={e => setDesc(e.target.value)}
                      rows={4}
                      className="mt-2 w-full rounded-2xl bg-white border border-amber-300 px-3 py-3 text-md"
                      placeholder="Describe your game idea..."
                    />
                  </label>

                  <label className="block">
                    <span className="text-md font-semibold">Pick Game Type</span>
                    <select
                      value={game}
                      onChange={e => setGame(e.target.value)}
                      className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg"
                    >
                      <option value="flappy">Flappy Bird</option>
                      <option value="snake">Snake</option>
                      <option value="pong">Pong</option>
                    </select>
                  </label>

                  <div className="flex flex-wrap justify-center gap-4 mt-6">
                    <div className="flex flex-wrap gap-3 justify-center mt-4">
                      <button
                        onClick={generateCodeWithGroq}
                        disabled={busy}
                        className="bg-orange-500 hover:bg-orange-400 disabled:opacity-50 text-white font-bold px-5 py-2 rounded-2xl shadow"
                      >
                        {busy ? "Generating..." : "Generate Code (Groq)"}
                      </button>

                      <button
                        onClick={generateImageOnly}
                        className="bg-yellow-500 hover:bg-yellow-400 text-white font-bold px-5 py-2 rounded-2xl shadow"
                      >
                        Generate Image (Local)
                      </button>

                      <button
                        onClick={aiGenerateImage}
                        className="bg-indigo-600 hover:bg-indigo-500 text-white font-bold px-5 py-2 rounded-2xl shadow"
                      >
                        AI Generate Image (Groq)
                      </button>

                      <button
                        onClick={() => download(`${title.replace(/[^A-Za-z0-9_]+/g, "_")}.ino`, generatedIno || "// generate first")}
                        className="bg-teal-500 hover:bg-teal-400 text-white font-bold px-5 py-2 rounded-2xl shadow"
                      >
                        Download .ino
                      </button>
                    </div>
                  </div>
                </div>

                {/* Right side preview/code */}
                <div className="flex-1 space-y-4">
                  <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50 text-center">
                    <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                      <span>Menu Header Preview</span>
                      <button
                        onClick={() => copyAll(imagePreview || "No image available.")}
                        className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg"
                      >
                        Copy All
                      </button>
                    </div>
                    <div className="aspect-[5/4] w-full border-2 border-amber-300 rounded-xl grid place-items-center bg-amber-200">
                      <canvas ref={canvasRef} className="rounded" />
                      {!imagePreview && <div className="text-slate-500 text-sm">Click 'Generate Image (Local)'</div>}
                      {imagePreview && (
                        <img src={imagePreview} alt="Preview" className="rounded w-full h-full object-contain" />
                      )}
                    </div>
                  </div>

                  <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50">
                    <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                      <span>Arduino Code</span>
                      <button
                        onClick={() => copyAll(generatedIno)}
                        className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg"
                      >
                        Copy All
                      </button>
                    </div>
                    <textarea
                      value={generatedIno}
                      onChange={e => setGeneratedIno(e.target.value)}
                      rows={10}
                      className="w-full rounded-xl bg-white border border-amber-300 p-3 font-mono text-xs"
                      placeholder="Click 'Generate Code (Groq)'"
                    />
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      );
    }

    const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(<GameMakerIDE />);
  </script>
</body>
</html>


===== FILE: robotics/gamemaker.html @ 2025-10-18 10:23:15 =====
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Game Maker IDE</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body class="bg-[tan] text-slate-900 font-sans">
  <div id="root"></div>

  <script type="text/babel" data-presets="react,typescript">
    function GameMakerIDE() {
      const [title, setTitle] = React.useState("My Vintage Game");
      const [desc, setDesc] = React.useState("Flappy-style with mountains and a header splash.");
      const [game, setGame] = React.useState("flappy");
      const [generatedIno, setGeneratedIno] = React.useState("");
      const [imagePreview, setImagePreview] = React.useState(null);
      const [busy, setBusy] = React.useState(false);
      const canvasRef = React.useRef(null);

      async function aiGenerateImage() {
        setBusy(true);
        try {
          const res = await fetch('api/generate_image_groq_spec.php', {
            method: 'POST',
            headers: {'Content-Type':'application/json'},
            body: JSON.stringify({
              title,
              description: desc,
              game_type: game,
              target_width: 160,
              target_height: 128,
              grid_w: 20,
              grid_h: 16,
              palette_size: 5
            })
          });
          const out = await res.json();
          if (out.error) throw new Error(out.error || 'No image returned');
          if (out.png_data_url) {
            setImagePreview(out.png_data_url);
            const img = new Image();
            img.onload = () => {
              const c = canvasRef.current;
              if (!c) return;
              c.width = img.width;
              c.height = img.height;
              c.getContext('2d').drawImage(img, 0, 0);
            };
            img.src = out.png_data_url;
          }
        } catch (e) {
          alert('Image generation failed: ' + e.message);
          console.error(e);
        } finally {
          setBusy(false);
        }
      }

      function drawLocalTitleCardToCanvas(text) {
        const W = 160, H = 128;
        let c = canvasRef.current;
        if (!c) return;
        c.width = W; c.height = H;
        const ctx = c.getContext("2d");
        if (!ctx) return;

        const grad = ctx.createLinearGradient(0, 0, 0, H);
        grad.addColorStop(0, "#2278C5");
        grad.addColorStop(1, "#93CCEA");
        ctx.fillStyle = grad;
        ctx.fillRect(0, 0, W, H);

        ctx.fillStyle = "#5A6E82";
        for (let x = 0; x < W; x++) {
          const yTop = 80 - Math.sin((x + 10) * 0.11) * 8;
          ctx.fillRect(x, yTop, 1, H - yTop);
        }

        ctx.fillStyle = "#465764";
        for (let x = 0; x < W; x++) {
          const yTop = 88 - Math.sin((x + 20) * 0.13) * 10;
          ctx.fillRect(x, yTop, 1, H - yTop);
        }

        ctx.fillStyle = "#fff";
        ctx.textAlign = "center";
        ctx.font = "bold 18px system-ui";
        ctx.fillText(text || "Game Title", W / 2, 28);
        ctx.font = "12px system-ui";
        ctx.fillText("Press button to start", W / 2, H - 10);

        setImagePreview(c.toDataURL("image/png"));
      }

      async function generateCodeWithGroq() {
        setBusy(true);
        try {
          const imgData = canvasRef.current ? canvasRef.current.toDataURL("image/png") : null;
          const res = await fetch("api/generate.php", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
              title,
              description: desc,
              game_type: game,
              image_data_url: imgData
            })
          });
          const out = await res.json();

          if (out.error) throw new Error(out.error);
          if (out.ino) setGeneratedIno(out.ino);
          if (out.png_data_url) setImagePreview(out.png_data_url);
        } catch (e) {
          alert("Code generation failed: " + e.message);
          console.error(e);
        } finally {
          setBusy(false);
        }
      }

      function generateImageOnly() {
        drawLocalTitleCardToCanvas(title);
      }

      function download(filename, text) {
        const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = filename;
        a.click();
        URL.revokeObjectURL(url);
      }

      async function copyAll(text) {
        try {
          await navigator.clipboard.writeText(text);
          alert("Copied to clipboard!");
        } catch {
          const ta = document.createElement("textarea");
          ta.value = text;
          document.body.appendChild(ta);
          ta.select();
          document.execCommand("copy");
          document.body.removeChild(ta);
          alert("Copied using fallback!");
        }
      }

      return (
        <div className="min-h-screen bg-[tan] text-slate-900 font-sans">
          <div className="max-w-5xl mx-auto p-6">
            <img src="idetitle.png"/>
            <p className="text-center text-slate-700 mb-8">
              Create fun mini-games for your ESP32! Choose a game, name it, describe it, and let AI write the Arduino code.
            </p>

            <div className="bg-amber-100 p-6 rounded-3xl shadow-lg border border-amber-300">
              <div className="flex flex-col md:flex-row gap-6">
                {/* Left side form */}
                <div className="flex-1 space-y-4">
                  <label className="block">
                    <span className="text-md font-semibold">Game Title</span>
                    <input
                      value={title}
                      onChange={e => setTitle(e.target.value)}
                      className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg"
                      placeholder="My Awesome Game"
                    />
                  </label>

                  <label className="block">
                    <span className="text-md font-semibold">Game Description</span>
                    <textarea
                      value={desc}
                      onChange={e => setDesc(e.target.value)}
                      rows={4}
                      className="mt-2 w-full rounded-2xl bg-white border border-amber-300 px-3 py-3 text-md"
                      placeholder="Describe your game idea..."
                    />
                  </label>

                  <label className="block">
                    <span className="text-md font-semibold">Pick Game Type</span>
                    <select
                      value={game}
                      onChange={e => setGame(e.target.value)}
                      className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg"
                    >
                      <option value="flappy">Flappy Bird</option>
                      <option value="snake">Snake</option>
                      <option value="pong">Pong</option>
                    </select>
                  </label>

                  <div className="flex flex-wrap justify-center gap-4 mt-6">
                    <div className="flex flex-wrap gap-3 justify-center mt-4">
                      <button
                        onClick={generateCodeWithGroq}
                        disabled={busy}
                        className="bg-orange-500 hover:bg-orange-400 disabled:opacity-50 text-white font-bold px-5 py-2 rounded-2xl shadow"
                      >
                        {busy ? "Generating..." : "Generate Code (Groq)"}
                      </button>

                      <button
                        onClick={generateImageOnly}
                        className="bg-yellow-500 hover:bg-yellow-400 text-white font-bold px-5 py-2 rounded-2xl shadow"
                      >
                        Generate Image (Local)
                      </button>

                      <button
                        onClick={aiGenerateImage}
                        className="bg-indigo-600 hover:bg-indigo-500 text-white font-bold px-5 py-2 rounded-2xl shadow"
                      >
                        AI Generate Image (Groq)
                      </button>

                      <button
                        onClick={() => download(`${title.replace(/[^A-Za-z0-9_]+/g, "_")}.ino`, generatedIno || "// generate first")}
                        className="bg-teal-500 hover:bg-teal-400 text-white font-bold px-5 py-2 rounded-2xl shadow"
                      >
                        Download .ino
                      </button>
                    </div>
                  </div>
                </div>

                {/* Right side preview/code */}
                <div className="flex-1 space-y-4">
                  <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50 text-center">
                    <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                      <span>Menu Header Preview</span>
                      <button
                        onClick={() => copyAll(imagePreview || "No image available.")}
                        className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg"
                      >
                        Copy All
                      </button>
                    </div>
                    <div className="aspect-[5/4] w-full border-2 border-amber-300 rounded-xl grid place-items-center bg-amber-200">
                      <canvas ref={canvasRef} className="rounded" />
                      {!imagePreview && <div className="text-slate-500 text-sm">Click 'Generate Image (Local)'</div>}
                      {imagePreview && (
                        <img src={imagePreview} alt="Preview" className="rounded w-full h-full object-contain" />
                      )}
                    </div>
                  </div>

                  <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50">
                    <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                      <span>Arduino Code</span>
                      <button
                        onClick={() => copyAll(generatedIno)}
                        className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg"
                      >
                        Copy All
                      </button>
                    </div>
                    <textarea
                      value={generatedIno}
                      onChange={e => setGeneratedIno(e.target.value)}
                      rows={10}
                      className="w-full rounded-xl bg-white border border-amber-300 p-3 font-mono text-xs"
                      placeholder="Click 'Generate Code (Groq)'"
                    />
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      );
    }

    const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(<GameMakerIDE />);
  </script>
</body>
</html>


===== FILE: engine2/test_runner_basic.js @ 2025-10-18 22:36:39 =====
// runner/index.js
import fs from 'fs/promises';
import path from 'path';
import dotenv from 'dotenv';
import fetch from 'node-fetch';
dotenv.config();

const ROOT = path.resolve('..'); // run from runner/ directory
const AGENTS_DIR = path.join(ROOT, 'agents');
const RESP_DIR = path.join(ROOT, 'responses');
const SEQ_FILE = path.join(ROOT, 'sequence.json');
const ORIGINAL_FILE = path.join(ROOT, 'original_user_request.txt');

await fs.mkdir(RESP_DIR, { recursive: true });

function parseAgentFile(content) {
  // split header and prompt
  const match = content.split('---');
  if (match.length >= 3) {
    const header = match[1].trim();
    const body = match.slice(2).join('---').trim();
    const meta = {};
    header.split(/\r?\n/).forEach(line => {
      const m = line.match(/^([\w_-]+)\s*:\s*(.*)$/);
      if (m) meta[m[1]] = JSON.parse(m[2]) || m[2];
    });
    return { meta, prompt: body };
  } else {
    // fallback: no metadata
    return { meta: {}, prompt: content.trim() };
  }
}

// Simple LLM call wrapper — replace with your provider
async function callLLM(prompt) {
  // If you want to integrate OpenAI, replace with actual API call.
  // This example uses a very tiny mock behaviour if no API keys are present:
  if (!process.env.OPENAI_API_KEY) {
    // mock: echo the prompt's first 200 chars for fast local testing
    return `MOCK_RESPONSE: ${prompt.slice(0, 200)}`;
  }

  // Example OpenAI REST call (adjust to the provider / model):
  const url = 'https://api.openai.com/v1/chat/completions';
  const body = {
    model: process.env.OPENAI_MODEL || 'gpt-4o-mini', // change as needed
    messages: [{ role: 'user', content: prompt }],
    max_tokens: 800
  };

  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
    },
    body: JSON.stringify(body)
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`LLM error ${res.status}: ${text}`);
  }
  const data = await res.json();
  // adapt depending on API shape:
  const content = data.choices?.[0]?.message?.content ?? JSON.stringify(data);
  return content;
}

async function loadAgentsFromSequence() {
  const seqRaw = await fs.readFile(SEQ_FILE, 'utf8');
  const seq = JSON.parse(seqRaw);
  const agents = [];
  for (const fname of seq) {
    const p = path.join(AGENTS_DIR, fname);
    const raw = await fs.readFile(p, 'utf8');
    const parsed = parseAgentFile(raw);
    parsed.filename = fname;
    // ensure meta fields exist
    parsed.meta = parsed.meta || {};
    agents.push(parsed);
  }
  return agents;
}

async function readResponse(name) {
  const p = path.join(RESP_DIR, name);
  try {
    return await fs.readFile(p, 'utf8');
  } catch {
    return null;
  }
}

async function writeResponse(filename, content) {
  const p = path.join(RESP_DIR, filename);
  await fs.writeFile(p, content, 'utf8');
}

async function runPipeline() {
  const agents = await loadAgentsFromSequence();
  const original = await fs.readFile(ORIGINAL_FILE, 'utf8');

  // helper to resolve input for each agent
  async function resolveInput(input_from) {
    if (!input_from) return original;
    if (typeof input_from === 'string') {
      if (input_from === 'original_user_request') return original;
      // if references previous agent number, try to read response
      const f = `${String(input_from).padStart(2,'0')}.json`;
      const candidate = await readResponse(f);
      if (candidate) return candidate;
      // fallback: if it's a filename present in responses/
      const raw = await readResponse(input_from);
      if (raw) return raw;
      // else fallback
      return original;
    }
    if (Array.isArray(input_from)) {
      // concatenate
      const parts = [];
      for (const item of input_from) {
        parts.push(await resolveInput(item));
      }
      return parts.join('\n\n----\n\n');
    }
    return original;
  }

  for (let idx = 0; idx < agents.length; idx++) {
    const agent = agents[idx];
    const agentId = agent.meta.id ?? (idx+1);
    const fname = agent.filename;
    console.log(`\n==== Running agent ${agent.meta.name ?? fname} (file ${fname}) ====`);
    const inputSource = agent.meta.input_from ?? 'previous';
    // support "previous" to mean last response produced
    let inputText;
    if (inputSource === 'previous') {
      // previous means the last response file (if exists) else original
      if (idx === 0) inputText = original;
      else {
        // previous agent filename
        const prevAgent = agents[idx-1];
        const prevFilename = `${String(prevAgent.meta.id ?? (idx)).padStart(2,'0')}.json`;
        inputText = await readResponse(prevFilename) ?? original;
      }
    } else {
      inputText = await resolveInput(inputSource);
    }

    // craft prompt for LLM: include agent prompt + input
    const fullPrompt = `--- AGENT: ${agent.meta.name ?? fname} ---\n${agent.prompt}\n\n--- INPUT ---\n${inputText}\n\n--- END ---`;
    try {
      const reply = await callLLM(fullPrompt);
      // name response file by agent id (padded) and extension json or txt
      const ext = (agent.meta.output_as === 'json') ? 'json' : 'txt';
      const respFilename = `${String(agentId).padStart(2,'0')}.${ext}`;
      await writeResponse(respFilename, reply);
      console.log(`Wrote response to responses/${respFilename}`);
    } catch (err) {
      console.error(`Agent ${fname} failed:`, err);
      const errFilename = `${String(agentId).padStart(2,'0')}.error.txt`;
      await writeResponse(errFilename, `ERROR: ${err.message}`);
      // continue to next agent (don't abort)
    }
  }

  console.log('\nPipeline finished.');
}

runPipeline().catch(err => {
  console.error('Fatal runner error:', err);
  process.exit(1);
});

===== FILE: generate_engine/generate_engine.php @ 2025-10-18 22:41:54 =====
<?php
/**
 * generate_modular_engine.php
 *
 * Creates a starter project tree for "modular-engine".
 *
 * Usage (CLI recommended):
 *   php generate_modular_engine.php --target=modular-engine
 *   php generate_modular_engine.php --target=modular-engine --force
 *
 * Notes:
 *  - By default the script will NOT overwrite existing files.
 *  - To overwrite files pass --force.
 *  - Run from a directory where you want the modular-engine folder created.
 */

error_reporting(E_ALL);
ini_set('display_errors', 1);

$options = getopt('', ['target::', 'force']);
$target = $options['target'] ?? 'modular-engine';
$force = isset($options['force']);

$root = rtrim($target, '/');

// Helper create directory
function ensure_dir($path) {
    if (!is_dir($path)) {
        if (!mkdir($path, 0755, true)) {
            throw new Exception("Failed to create directory: $path");
        }
    }
}

// Map: relative path => content
$files = [
    // Root
    "$root/README.md" => "# Modular Engine\n\nStarter repo for a text-file driven LLM pipeline + visual designer.\n\nRun `php generate_modular_engine.php --target=modular-engine` to recreate.\n",
    "$root/.gitignore" => <<<GIT
node_modules/
.env
responses/
logs/
GIT
    ,
    "$root/original_user_request.txt" => "Design a reusable modular pipeline engine for my projects — make it text-file driven, visually reorderable, and sequentially executed. Include example agents for system, research and synthesis.\n",
    "$root/sequence.json" => json_encode(["01-system.txt","02-research.txt","03-synthesise.txt"], JSON_PRETTY_PRINT),

    // agents
    "$root/agents/01-system.txt" => <<<TXT
---
id: 01
name: SystemPrime
input_from: original_user_request
output_as: json
---
You are SystemPrime. Your job is to:
- Trim, normalise and surface the user's real intent.
- If user intent is ambiguous, list likely assumptions.
Return only JSON:
{
  "cleaned_request": "<one-line cleaned request>",
  "assumptions": ["..."]
}
TXT
    ,
    "$root/agents/02-research.txt" => <<<TXT
---
id: 02
name: Researcher
input_from: 01
output_as: json
---
You are Researcher. Input is a JSON object with keys cleaned_request and assumptions.
1) Expand the cleaned_request into 3 research tasks and for each task list:
   - data_sources_to_check (short list)
   - critical_questions
Output a JSON array like:
[
  {"task":"...", "data_sources":["..."], "questions":["..."]},
  ...
]
TXT
    ,
    "$root/agents/03-synthesise.txt" => <<<TXT
---
id: 03
name: Synth
input_from: ["01","02"]
output_as: text
---
You are Synth. Inputs are the outputs of previous agents. Create a short plan (bulleted) combining the research tasks and the cleaned request. Output plain text.
TXT
    ,
    "$root/agents/04-evaluator.txt" => <<<TXT
---
id: 04
name: Evaluator
input_from: 03
output_as: json
---
You are Evaluator. Given the plan from Synth, produce a short evaluation: risks, missing info, confidence_score (0-100).
Return JSON:
{
  "confidence_score": 80,
  "risks": ["..."],
  "missing": ["..."]
}
TXT
    ,
    "$root/agents/05-formatter.txt" => <<<TXT
---
id: 05
name: Formatter
input_from: 04
output_as: text
---
You are Formatter. Format the plan + evaluation into a short human-readable deliverable (2-6 bullet points).
Output plain text.
TXT
    ,

    // responses (pre-populate empty placeholders)
    "$root/responses/.gitkeep" => "",
    "$root/responses/01.json" => "",

    // runner
    "$root/runner/package.json" => json_encode([
        "name"=>"modular-engine-runner",
        "version"=>"1.0.0",
        "type"=>"module",
        "scripts"=>["start"=>"node index.js"]
    ], JSON_PRETTY_PRINT),
    "$root/runner/index.js" => <<<JS
// runner/index.js
import fs from 'fs/promises';
import path from 'path';
import dotenv from 'dotenv';
import fetch from 'node-fetch';
dotenv.config();

const ROOT = path.resolve('..');
const AGENTS_DIR = path.join(ROOT, 'agents');
const RESP_DIR = path.join(ROOT, 'responses');
const SEQ_FILE = path.join(ROOT, 'sequence.json');
const ORIGINAL_FILE = path.join(ROOT, 'original_user_request.txt');

await fs.mkdir(RESP_DIR, { recursive: true });

function parseAgentFile(content) {
  const match = content.split('---');
  if (match.length >= 3) {
    const header = match[1].trim();
    const body = match.slice(2).join('---').trim();
    const meta = {};
    header.split(/\r?\n/).forEach(line => {
      const m = line.match(/^([\w_-]+)\s*:\s*(.*)$/);
      if (m) {
        let key = m[1];
        let val = m[2].trim();
        try { // try JSON decode if possible
          meta[key] = JSON.parse(val);
        } catch {
          meta[key] = val;
        }
      }
    });
    return { meta, prompt: body };
  } else {
    return { meta: {}, prompt: content.trim() };
  }
}

async function callLLM(prompt) {
  if (!process.env.OPENAI_API_KEY) {
    return `MOCK_RESPONSE: (no OPENAI_API_KEY set)\\n${prompt.slice(0,100)}`;
  }
  const url = 'https://api.openai.com/v1/chat/completions';
  const body = {
    model: process.env.OPENAI_MODEL || 'gpt-4o-mini',
    messages: [{ role: 'user', content: prompt }],
    max_tokens: 800
  };
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
    },
    body: JSON.stringify(body)
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(\`LLM error \${res.status}: \${text}\`);
  }
  const data = await res.json();
  const content = data.choices?.[0]?.message?.content ?? JSON.stringify(data);
  return content;
}

async function loadAgentsFromSequence() {
  const seqRaw = await fs.readFile(SEQ_FILE, 'utf8');
  const seq = JSON.parse(seqRaw);
  const agents = [];
  for (const fname of seq) {
    const p = path.join(AGENTS_DIR, fname);
    const raw = await fs.readFile(p, 'utf8');
    const parsed = parseAgentFile(raw);
    parsed.filename = fname;
    parsed.meta = parsed.meta || {};
    agents.push(parsed);
  }
  return agents;
}

async function readResponse(name) {
  const p = path.join(RESP_DIR, name);
  try { return await fs.readFile(p, 'utf8'); } catch { return null; }
}
async function writeResponse(filename, content) {
  const p = path.join(RESP_DIR, filename);
  await fs.writeFile(p, content, 'utf8');
}

async function runPipeline() {
  const agents = await loadAgentsFromSequence();
  const original = await fs.readFile(ORIGINAL_FILE, 'utf8');

  async function resolveInput(input_from) {
    if (!input_from) return original;
    if (typeof input_from === 'string') {
      if (input_from === 'original_user_request') return original;
      const f = \`\${String(input_from).padStart(2,'0')}.json\`;
      const candidate = await readResponse(f);
      if (candidate) return candidate;
      const raw = await readResponse(input_from);
      if (raw) return raw;
      return original;
    }
    if (Array.isArray(input_from)) {
      const parts = [];
      for (const item of input_from) { parts.push(await resolveInput(item)); }
      return parts.join('\\n\\n----\\n\\n');
    }
    return original;
  }

  for (let idx = 0; idx < agents.length; idx++) {
    const agent = agents[idx];
    const agentId = agent.meta.id ?? (idx+1);
    const fname = agent.filename;
    console.log(`\\n==== Running agent \${agent.meta.name ?? fname} (file \${fname}) ====`);
    const inputSource = agent.meta.input_from ?? 'previous';
    let inputText;
    if (inputSource === 'previous') {
      if (idx === 0) inputText = original;
      else {
        const prevAgent = agents[idx-1];
        const prevFilename = \`\${String(prevAgent.meta.id ?? (idx)).padStart(2,'0')}.json\`;
        inputText = await readResponse(prevFilename) ?? original;
      }
    } else {
      inputText = await resolveInput(inputSource);
    }

    const fullPrompt = `--- AGENT: \${agent.meta.name ?? fname} ---\\n\${agent.prompt}\\n\\n--- INPUT ---\\n\${inputText}\\n\\n--- END ---`;
    try {
      const reply = await callLLM(fullPrompt);
      const ext = (agent.meta.output_as === 'json') ? 'json' : 'txt';
      const respFilename = `${String(agentId).padStart(2,'0')}.${ext}`;
      await writeResponse(respFilename, reply);
      console.log(`Wrote response to responses/\${respFilename}`);
    } catch (err) {
      console.error(`Agent \${fname} failed:`, err);
      const errFilename = `${String(agentId).padStart(2,'0')}.error.txt`;
      await writeResponse(errFilename, `ERROR: ${err.message}`);
    }
  }

  console.log('\\nPipeline finished.');
}

runPipeline().catch(err => {
  console.error('Fatal runner error:', err);
  process.exit(1);
});
JS
    ,
    "$root/runner/.env" => "OPENAI_API_KEY=\nOPENAI_MODEL=\n",
    "$root/runner/README.md" => "# Runner\n\nNode.js runner for modular-engine. Set env in .env, then `npm install` and `node index.js`.\n",

    // runner lib
    "$root/runner/lib/llm.js" => <<<JS
// llm.js - placeholder for provider adapters
export async function callLLM(prompt, opts = {}) {
  throw new Error('Replace this file with a provider-specific adapter (OpenAI, Anthropic, etc.)');
}
JS
    ,
    "$root/runner/lib/parser.js" => <<<JS
export function parseAgentFile(content) {
  const match = content.split('---');
  if (match.length >= 3) {
    const header = match[1].trim();
    const body = match.slice(2).join('---').trim();
    const meta = {};
    header.split(/\\r?\\n/).forEach(line => {
      const m = line.match(/^([\\w_-]+)\\s*:\\s*(.*)$/);
      if (m) meta[m[1]] = m[2];
    });
    return { meta, prompt: body };
  } else {
    return { meta: {}, prompt: content.trim() };
  }
}
JS
    ,
    "$root/runner/lib/utils.js" => <<<JS
export function nowIso(){ return new Date().toISOString(); }
export function hashString(s){ return require('crypto').createHash('sha1').update(s).digest('hex'); }
JS
    ,
    "$root/runner/lib/validator.js" => <<<JS
export function validateJsonOutput(text){
  try { JSON.parse(text); return true; } catch { return false; }
}
JS
    ,
    "$root/runner/lib/cache.js" => <<<JS
const fs = require('fs');
const path = require('path');
export function cacheKeyFor(prompt){ return require('crypto').createHash('md5').update(prompt).digest('hex'); }
export function readCache(dir, key){ try { return fs.readFileSync(path.join(dir, key), 'utf8'); } catch { return null; } }
export function writeCache(dir, key, content){ fs.mkdirSync(dir, { recursive:true }); fs.writeFileSync(path.join(dir, key), content, 'utf8'); }
JS
    ,

    // designer
    "$root/designer/designer.html" => <<<HTML
<!doctype html>
<html>
<head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/></head>
<body>
<h1>Modular Engine Designer (static)</h1>
<p>This is a static client. Run the server in designer/server.js for API.</p>
<!-- Minimal client omitted for brevity - replace with the earlier provided designer.html if desired -->
</body>
</html>
HTML
    ,
    "$root/designer/server.js" => <<<JS
import express from 'express';
import fs from 'fs/promises';
import path from 'path';
const app = express();
const ROOT = path.resolve('.');
const AGENTS = path.join(ROOT, 'agents');
const SEQ = path.join(ROOT, 'sequence.json');

app.use(express.static(path.join(ROOT,'designer')));
app.use(express.json());

app.get('/api/agents', async (req,res) => {
  try {
    const files = await fs.readdir(AGENTS);
    const filtered = files.filter(f => f.endsWith('.txt')).sort();
    res.json(filtered);
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

app.post('/api/save-sequence', async (req,res) => {
  const seq = req.body;
  if (!Array.isArray(seq)) return res.status(400).send('Bad payload');
  await fs.writeFile(SEQ, JSON.stringify(seq, null, 2), 'utf8');
  res.send('Saved sequence.json');
});

app.listen(3030, ()=> console.log('Designer server running on http://localhost:3030'));
JS
    ,
    "$root/designer/README.md" => "# Designer\n\nRun `node server.js` and open the static page. This provides `/api/agents` and `/api/save-sequence`.\n",

    // designer assets
    "$root/designer/assets/style.css" => "/* add designer styles here */\n",
    "$root/designer/assets/app.js" => "// designer app.js placeholder\n",
    "$root/designer/assets/icons.svg" => "<!-- icons -->\n",
    "$root/designer/examples/sequence_example.json" => json_encode(["01-system.txt","02-research.txt","03-synthesise.txt"], JSON_PRETTY_PRINT),

    // docs
    "$root/docs/architecture.md" => "# Architecture\n\nOverview of the modular-engine architecture.\n",
    "$root/docs/agent-format.md" => "# Agent Format\n\nEach agent file contains a YAML-like header and a prompt body.\n",
    "$root/docs/runner-pipeline.md" => "# Runner Pipeline\n\nRunner reads sequence.json, executes agents sequentially, writes responses.\n",
    "$root/docs/visual-designer.md" => "# Visual Designer\n\nDesigner serves a static UI to reorder agents.\n",
    "$root/docs/roadmap.md" => "# Roadmap\n\n- Add branching\n- Add caching\n- Add plugin system\n",

    // tools
    "$root/tools/generate-agent-template.js" => <<<JS
#!/usr/bin/env node
// Simple generator for a new agent file
import fs from 'fs';
const args = process.argv.slice(2);
if (!args[0]) { console.error('Usage: generate-agent-template.js <id-name> <description>'); process.exit(1); }
const idname = args[0];
const desc = args[1] || 'New agent';
const fname = `../agents/\${idname}.txt`;
const template = `---
id: ${idname.split('-')[0] || idname}
name: ${idname}
input_from: previous
output_as: text
---
You are ${idname}. ${desc}
`;
fs.writeFileSync(fname, template);
console.log('Wrote', fname);
JS
    ,
    "$root/tools/reset-responses.sh" => <<<SH
#!/bin/bash
set -e
TARGET="../responses"
echo "Removing files in \$TARGET"
rm -rf "\$TARGET"/*
mkdir -p "\$TARGET"
echo "Done"
SH
    ,
    "$root/tools/validate-agents.js" => <<<JS
#!/usr/bin/env node
// lightweight check: ensure agents have header block
import fs from 'fs/promises';
import path from 'path';
const AGENTS = path.resolve('../agents');
(async()=>{
  const list = await fs.readdir(AGENTS);
  for (const f of list) {
    const txt = await fs.readFile(path.join(AGENTS,f),'utf8');
    if (!txt.includes('---')) console.warn('Agent missing header:', f);
  }
})();
JS
    ,
    "$root/tools/run-pipeline.sh" => <<<SH
#!/bin/bash
# wrapper from project root to runner
pushd runner
node index.js
popd
SH
    ,

    // logs
    "$root/logs/pipeline.log" => "",
    "$root/logs/errors.log" => "",

    // optional extensions
    "$root/schemas/agent_meta_schema.json" => json_encode([
        "type"=>"object",
        "properties"=>[
            "id"=>["type"=>"string"],
            "name"=>["type"=>"string"],
            "input_from"=>["type"=>"string"],
            "output_as"=>["type"=>"string"]
        ]
    ], JSON_PRETTY_PRINT),
    "$root/plugins/plugin-websearch.js" => "// plugin placeholder for websearch\n",
    "$root/tests/test_runner_basic.js" => "// test placeholder\n",
    "$root/data/cache_index.json" => json_encode(new stdClass(), JSON_PRETTY_PRINT)
];

// Ensure all directories exist and write files
$created = [];
$skipped = [];
$errors = [];

foreach ($files as $path => $content) {
    $dir = dirname($path);
    try {
        ensure_dir($dir);
    } catch (Exception $e) {
        $errors[] = "Failed to create dir for $path: " . $e->getMessage();
        continue;
    }

    if (file_exists($path) && !$force) {
        $skipped[] = $path;
        continue;
    }

    try {
        file_put_contents($path, $content);
        // set executable bit for some scripts
        if (preg_match('#/tools/.*\.sh$#', $path) || preg_match('#/tools/.*\.js$#', $path)) {
            @chmod($path, 0755);
        }
        $created[] = $path;
    } catch (Exception $e) {
        $errors[] = "Failed to write $path: " . $e->getMessage();
    }
}

// Summary
echo "Target root: $root\n";
echo "Force mode: " . ($force ? "YES" : "NO") . "\n\n";
echo "Created files: " . count($created) . "\n";
foreach ($created as $c) { echo "  + $c\n"; }
echo "\nSkipped (already existed): " . count($skipped) . "\n";
foreach ($skipped as $s) { echo "  - $s\n"; }
if ($errors) {
    echo "\nErrors: " . count($errors) . "\n";
    foreach ($errors as $err) { echo "  ! $err\n"; }
} else {
    echo "\nNo errors.\n";
}

echo "\nDone. Next steps:\n";
echo "  cd $root/runner && npm install\n";
echo "  (optionally) cd $root/designer && npm install express && node server.js\n";
echo "\n";

===== FILE: generate_engine/generate_engine.php @ 2025-10-18 22:44:26 =====


===== FILE: generate_engine/generate_engine.php @ 2025-10-18 22:45:01 =====
<?php
/**
 * generate_engine.php
 *
 * Creates a starter project tree for "modular-engine".
 *
 * Usage (CLI recommended):
 *   php generate_engine.php --target=modular-engine
 *   php generate_engine.php --target=modular-engine --force
 *
 * Notes:
 *  - By default the script will NOT overwrite existing files.
 *  - To overwrite files pass --force.
 *  - Run from a directory where you want the modular-engine folder created.
 */

error_reporting(E_ALL);
ini_set('display_errors', 1);

/* parse options */
$options = getopt('', ['target::', 'force']);
$target = $options['target'] ?? 'modular-engine';
$force = isset($options['force']);

$root = rtrim($target, '/');

/* helper to ensure directory exists */
function ensure_dir($path) {
    if (!is_dir($path)) {
        if (!mkdir($path, 0755, true)) {
            throw new Exception("Failed to create directory: $path");
        }
    }
}

/* files map: relative path => content (using nowdocs to avoid interpolation) */
$files = [
    /* Root */
    "$root/README.md" => <<<'MD'
# Modular Engine

Starter repo for a text-file driven LLM pipeline + visual designer.

Run `php generate_engine.php --target=modular-engine` to recreate.

This project scaffolds:
- agents/ (text prompts)
- responses/ (model outputs)
- runner/ (Node.js execution)
- designer/ (visual reorder UI)
- docs/, tools/, logs/
MD
    ,
    "$root/.gitignore" => <<<'GI'
node_modules/
.env
responses/
logs/
GI
    ,
    "$root/original_user_request.txt" => <<<'TXT'
Design a reusable modular pipeline engine for my projects — make it text-file driven, visually reorderable, and sequentially executed. Include example agents for system, research and synthesis.
TXT
    ,
    "$root/sequence.json" => json_encode(["01-system.txt","02-research.txt","03-synthesise.txt"], JSON_PRETTY_PRINT),

    /* agents */
    "$root/agents/01-system.txt" => <<<'TXT'
---
id: 01
name: SystemPrime
input_from: original_user_request
output_as: json
---
You are SystemPrime. Your job is to:
- Trim, normalise and surface the user's real intent.
- If user intent is ambiguous, list likely assumptions.
Return only JSON:
{
  "cleaned_request": "<one-line cleaned request>",
  "assumptions": ["..."]
}
TXT
    ,
    "$root/agents/02-research.txt" => <<<'TXT'
---
id: 02
name: Researcher
input_from: 01
output_as: json
---
You are Researcher. Input is a JSON object with keys cleaned_request and assumptions.
1) Expand the cleaned_request into 3 research tasks and for each task list:
   - data_sources_to_check (short list)
   - critical_questions
Output a JSON array like:
[
  {"task":"...", "data_sources":["..."], "questions":["..."]},
  ...
]
TXT
    ,
    "$root/agents/03-synthesise.txt" => <<<'TXT'
---
id: 03
name: Synth
input_from: ["01","02"]
output_as: text
---
You are Synth. Inputs are the outputs of previous agents. Create a short plan (bulleted) combining the research tasks and the cleaned request. Output plain text.
TXT
    ,
    "$root/agents/04-evaluator.txt" => <<<'TXT'
---
id: 04
name: Evaluator
input_from: 03
output_as: json
---
You are Evaluator. Given the plan from Synth, produce a short evaluation: risks, missing info, confidence_score (0-100).
Return JSON:
{
  "confidence_score": 80,
  "risks": ["..."],
  "missing": ["..."]
}
TXT
    ,
    "$root/agents/05-formatter.txt" => <<<'TXT'
---
id: 05
name: Formatter
input_from: 04
output_as: text
---
You are Formatter. Format the plan + evaluation into a short human-readable deliverable (2-6 bullet points).
Output plain text.
TXT
    ,

    /* responses placeholders */
    "$root/responses/.gitkeep" => "",
    "$root/responses/01.json" => "",

    /* runner */
    "$root/runner/package.json" => json_encode([
        "name" => "modular-engine-runner",
        "version" => "1.0.0",
        "type" => "module",
        "scripts" => (object)["start" => "node index.js"],
        "dependencies" => (object)["node-fetch" => "^3.4.0", "dotenv" => "^16.0.0"]
    ], JSON_PRETTY_PRINT),
    "$root/runner/index.js" => <<<'JS'
// runner/index.js
import fs from 'fs/promises';
import path from 'path';
import dotenv from 'dotenv';
import fetch from 'node-fetch';
dotenv.config();

const ROOT = path.resolve('..');
const AGENTS_DIR = path.join(ROOT, 'agents');
const RESP_DIR = path.join(ROOT, 'responses');
const SEQ_FILE = path.join(ROOT, 'sequence.json');
const ORIGINAL_FILE = path.join(ROOT, 'original_user_request.txt');

await fs.mkdir(RESP_DIR, { recursive: true });

function parseAgentFile(content) {
  const match = content.split('---');
  if (match.length >= 3) {
    const header = match[1].trim();
    const body = match.slice(2).join('---').trim();
    const meta = {};
    header.split(/\r?\n/).forEach(line => {
      const m = line.match(/^([\w_-]+)\s*:\s*(.*)$/);
      if (m) {
        let key = m[1];
        let val = m[2].trim();
        try { // try JSON decode if possible
          meta[key] = JSON.parse(val);
        } catch {
          meta[key] = val;
        }
      }
    });
    return { meta, prompt: body };
  } else {
    return { meta: {}, prompt: content.trim() };
  }
}

async function callLLM(prompt) {
  if (!process.env.OPENAI_API_KEY) {
    return `MOCK_RESPONSE: (no OPENAI_API_KEY set)\n${prompt.slice(0,100)}`;
  }
  const url = 'https://api.openai.com/v1/chat/completions';
  const body = {
    model: process.env.OPENAI_MODEL || 'gpt-4o-mini',
    messages: [{ role: 'user', content: prompt }],
    max_tokens: 800
  };
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
    },
    body: JSON.stringify(body)
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`LLM error ${res.status}: ${text}`);
  }
  const data = await res.json();
  const content = data.choices?.[0]?.message?.content ?? JSON.stringify(data);
  return content;
}

async function loadAgentsFromSequence() {
  const seqRaw = await fs.readFile(SEQ_FILE, 'utf8');
  const seq = JSON.parse(seqRaw);
  const agents = [];
  for (const fname of seq) {
    const p = path.join(AGENTS_DIR, fname);
    const raw = await fs.readFile(p, 'utf8');
    const parsed = parseAgentFile(raw);
    parsed.filename = fname;
    parsed.meta = parsed.meta || {};
    agents.push(parsed);
  }
  return agents;
}

async function readResponse(name) {
  const p = path.join(RESP_DIR, name);
  try { return await fs.readFile(p, 'utf8'); } catch { return null; }
}
async function writeResponse(filename, content) {
  const p = path.join(RESP_DIR, filename);
  await fs.writeFile(p, content, 'utf8');
}

async function runPipeline() {
  const agents = await loadAgentsFromSequence();
  const original = await fs.readFile(ORIGINAL_FILE, 'utf8');

  async function resolveInput(input_from) {
    if (!input_from) return original;
    if (typeof input_from === 'string') {
      if (input_from === 'original_user_request') return original;
      const f = `${String(input_from).padStart(2,'0')}.json`;
      const candidate = await readResponse(f);
      if (candidate) return candidate;
      const raw = await readResponse(input_from);
      if (raw) return raw;
      return original;
    }
    if (Array.isArray(input_from)) {
      const parts = [];
      for (const item of input_from) { parts.push(await resolveInput(item)); }
      return parts.join('\n\n----\n\n');
    }
    return original;
  }

  for (let idx = 0; idx < agents.length; idx++) {
    const agent = agents[idx];
    const agentId = agent.meta.id ?? (idx+1);
    const fname = agent.filename;
    console.log(`\n==== Running agent ${agent.meta.name ?? fname} (file ${fname}) ====`);
    const inputSource = agent.meta.input_from ?? 'previous';
    let inputText;
    if (inputSource === 'previous') {
      if (idx === 0) inputText = original;
      else {
        const prevAgent = agents[idx-1];
        const prevFilename = `${String(prevAgent.meta.id ?? (idx)).padStart(2,'0')}.json`;
        inputText = await readResponse(prevFilename) ?? original;
      }
    } else {
      inputText = await resolveInput(inputSource);
    }

    const fullPrompt = `--- AGENT: ${agent.meta.name ?? fname} ---\n${agent.prompt}\n\n--- INPUT ---\n${inputText}\n\n--- END ---`;
    try {
      const reply = await callLLM(fullPrompt);
      const ext = (agent.meta.output_as === 'json') ? 'json' : 'txt';
      const respFilename = `${String(agentId).padStart(2,'0')}.${ext}`;
      await writeResponse(respFilename, reply);
      console.log(`Wrote response to responses/${respFilename}`);
    } catch (err) {
      console.error(`Agent ${fname} failed:`, err);
      const errFilename = `${String(agentId).padStart(2,'0')}.error.txt`;
      await writeResponse(errFilename, `ERROR: ${err.message}`);
    }
  }

  console.log('\nPipeline finished.');
}

runPipeline().catch(err => {
  console.error('Fatal runner error:', err);
  process.exit(1);
});
JS
    ,
    "$root/runner/.env" => <<<'ENV'
OPENAI_API_KEY=
OPENAI_MODEL=
ENV
    ,
    "$root/runner/README.md" => <<<'MD'
# Runner

Node.js runner for modular-engine. Set env in .env, then:

```bash
cd runner
npm install
node index.js

===== FILE: generate_engine/generate_engine.php @ 2025-10-18 22:48:50 =====


===== FILE: generate_engine/generate_engine.php @ 2025-10-18 22:49:37 =====
<?php
/**
 * generate_engine.php
 *
 * Safe, robust scaffolder for the "modular-engine" project.
 *
 * Usage:
 *   php generate_engine.php --target=modular-engine
 *   php generate_engine.php --target=modular-engine --force
 *
 * Notes:
 *  - Will NOT overwrite files unless --force is provided.
 *  - Uses nowdocs (no variable interpolation) for all large template strings.
 */

error_reporting(E_ALL);
ini_set('display_errors', 1);

/* parse CLI options */
$options = getopt('', ['target::', 'force']);
$target = $options['target'] ?? 'modular-engine';
$force = isset($options['force']);

$root = rtrim($target, '/');

/* helper */
function ensure_dir($path) {
    if (!is_dir($path)) {
        if (!mkdir($path, 0755, true)) {
            throw new Exception("Failed to create directory: $path");
        }
    }
}

/* file map (relative path => content) */
/* Keep content simple and use nowdocs (<<<'LABEL') so PHP will not interpolate or try to parse JS */
$files = [

    /* root files */
    "$root/README.md" => <<<'MD'
# Modular Engine

Starter repository for a text-file driven LLM pipeline + visual designer.

Created by generate_engine.php
MD
    ,

    "$root/.gitignore" => <<<'GI'
node_modules/
.env
responses/
logs/
GI
    ,

    "$root/original_user_request.txt" => <<<'TXT'
Design a reusable modular pipeline engine for my projects — make it text-file driven, visually reorderable, and sequentially executed. Include example agents for system, research and synthesis.
TXT
    ,

    "$root/sequence.json" => <<<'J'
[
  "01-system.txt",
  "02-research.txt",
  "03-synthesise.txt"
]
J
    ,

    /* agents */
    "$root/agents/01-system.txt" => <<<'A01'
---
id: 01
name: SystemPrime
input_from: original_user_request
output_as: json
---
You are SystemPrime. Your job is to:
- Trim, normalise and surface the user's real intent.
- If user intent is ambiguous, list likely assumptions.
Return only JSON:
{
  "cleaned_request": "<one-line cleaned request>",
  "assumptions": ["..."]
}
A01
    ,

    "$root/agents/02-research.txt" => <<<'A02'
---
id: 02
name: Researcher
input_from: 01
output_as: json
---
You are Researcher. Input is a JSON object with keys cleaned_request and assumptions.
1) Expand the cleaned_request into 3 research tasks and for each task list:
   - data_sources_to_check (short list)
   - critical_questions
Output a JSON array like:
[
  {"task":"...", "data_sources":["..."], "questions":["..."]},
  ...
]
A02
    ,

    "$root/agents/03-synthesise.txt" => <<<'A03'
---
id: 03
name: Synth
input_from: ["01","02"]
output_as: text
---
You are Synth. Inputs are the outputs of previous agents. Create a short plan (bulleted) combining the research tasks and the cleaned request. Output plain text.
A03
    ,

    "$root/agents/04-evaluator.txt" => <<<'A04'
---
id: 04
name: Evaluator
input_from: 03
output_as: json
---
You are Evaluator. Given the plan from Synth, produce a short evaluation: risks, missing info, confidence_score (0-100).
Return JSON:
{
  "confidence_score": 80,
  "risks": ["..."],
  "missing": ["..."]
}
A04
    ,

    "$root/agents/05-formatter.txt" => <<<'A05'
---
id: 05
name: Formatter
input_from: 04
output_as: text
---
You are Formatter. Format the plan + evaluation into a short human-readable deliverable (2-6 bullet points).
Output plain text.
A05
    ,

    /* responses folder placeholders */
    "$root/responses/.gitkeep" => "",
    "$root/responses/01.json" => "",

    /* runner files (package.json as static text) */
    "$root/runner/package.json" => <<<'PJ'
{
  "name": "modular-engine-runner",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "node-fetch": "^3.4.0",
    "dotenv": "^16.0.0",
    "express": "^4.18.2"
  }
}
PJ
    ,

    /* runner index.js - keep the JS valid and include placeholder LLM call */
    "$root/runner/index.js" => <<<'RJ'
// runner/index.js
import fs from 'fs/promises';
import path from 'path';
import dotenv from 'dotenv';
import fetch from 'node-fetch';
dotenv.config();

const ROOT = path.resolve('..');
const AGENTS_DIR = path.join(ROOT, 'agents');
const RESP_DIR = path.join(ROOT, 'responses');
const SEQ_FILE = path.join(ROOT, 'sequence.json');
const ORIGINAL_FILE = path.join(ROOT, 'original_user_request.txt');

await fs.mkdir(RESP_DIR, { recursive: true });

function parseAgentFile(content) {
  const parts = content.split('---');
  if (parts.length >= 3) {
    const header = parts[1].trim();
    const body = parts.slice(2).join('---').trim();
    const meta = {};
    header.split(/\r?\n/).forEach(line => {
      const m = line.match(/^([\w_-]+)\s*:\s*(.*)$/);
      if (m) {
        const key = m[1];
        let val = m[2].trim();
        try { meta[key] = JSON.parse(val); } catch { meta[key] = val; }
      }
    });
    return { meta, prompt: body };
  }
  return { meta: {}, prompt: content.trim() };
}

async function callLLM(prompt) {
  // simple mock if no API key
  if (!process.env.OPENAI_API_KEY) {
    return "MOCK_RESPONSE: (no OPENAI_API_KEY set)\n" + prompt.slice(0, 300);
  }
  const url = 'https://api.openai.com/v1/chat/completions';
  const body = {
    model: process.env.OPENAI_MODEL || 'gpt-4o-mini',
    messages: [{ role: 'user', content: prompt }],
    max_tokens: 800
  };
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
    },
    body: JSON.stringify(body)
  });
  if (!res.ok) {
    const txt = await res.text();
    throw new Error(`LLM error ${res.status}: ${txt}`);
  }
  const data = await res.json();
  return data.choices?.[0]?.message?.content ?? JSON.stringify(data);
}

async function loadAgentsFromSequence() {
  const seqRaw = await fs.readFile(SEQ_FILE, 'utf8');
  const seq = JSON.parse(seqRaw);
  const agents = [];
  for (const fname of seq) {
    const p = path.join(AGENTS_DIR, fname);
    const raw = await fs.readFile(p, 'utf8');
    const parsed = parseAgentFile(raw);
    parsed.filename = fname;
    parsed.meta = parsed.meta || {};
    agents.push(parsed);
  }
  return agents;
}

async function readResponse(name) {
  const p = path.join(RESP_DIR, name);
  try { return await fs.readFile(p, 'utf8'); } catch { return null; }
}

async function writeResponse(filename, content) {
  const p = path.join(RESP_DIR, filename);
  await fs.writeFile(p, content, 'utf8');
}

async function runPipeline() {
  const agents = await loadAgentsFromSequence();
  const original = await fs.readFile(ORIGINAL_FILE, 'utf8');

  async function resolveInput(input_from) {
    if (!input_from) return original;
    if (typeof input_from === 'string') {
      if (input_from === 'original_user_request') return original;
      const f = `${String(input_from).padStart(2,'0')}.json`;
      const candidate = await readResponse(f);
      if (candidate) return candidate;
      const raw = await readResponse(input_from);
      if (raw) return raw;
      return original;
    }
    if (Array.isArray(input_from)) {
      const parts = [];
      for (const item of input_from) { parts.push(await resolveInput(item)); }
      return parts.join('\n\n----\n\n');
    }
    return original;
  }

  for (let idx = 0; idx < agents.length; idx++) {
    const agent = agents[idx];
    const agentId = agent.meta.id ?? (idx+1);
    const fname = agent.filename;
    console.log(`\n==== Running agent ${agent.meta.name ?? fname} (file ${fname}) ====`);
    const inputSource = agent.meta.input_from ?? 'previous';
    let inputText;
    if (inputSource === 'previous') {
      if (idx === 0) inputText = original;
      else {
        const prevAgent = agents[idx-1];
        const prevFilename = `${String(prevAgent.meta.id ?? (idx)).padStart(2,'0')}.json`;
        inputText = await readResponse(prevFilename) ?? original;
      }
    } else {
      inputText = await resolveInput(inputSource);
    }

    const fullPrompt = `--- AGENT: ${agent.meta.name ?? fname} ---\n${agent.prompt}\n\n--- INPUT ---\n${inputText}\n\n--- END ---`;
    try {
      const reply = await callLLM(fullPrompt);
      const ext = (agent.meta.output_as === 'json') ? 'json' : 'txt';
      const respFilename = `${String(agentId).padStart(2,'0')}.${ext}`;
      await writeResponse(respFilename, reply);
      console.log(`Wrote response to responses/${respFilename}`);
    } catch (err) {
      console.error(`Agent ${fname} failed:`, err);
      const errFilename = `${String(agentId).padStart(2,'0')}.error.txt`;
      await writeResponse(errFilename, `ERROR: ${err.message}`);
    }
  }

  console.log('\nPipeline finished.');
}

runPipeline().catch(err => {
  console.error('Fatal runner error:', err);
  process.exit(1);
});
RJ
    ,

    "$root/runner/.env" => <<<'ENV'
OPENAI_API_KEY=
OPENAI_MODEL=
ENV
    ,

    "$root/runner/README.md" => <<<'RMD'
# Runner

Node.js runner for modular-engine. Set env in .env, then:

cd runner
npm install
node index.js
RMD
    ,

    /* designer */
    "$root/designer/designer.html" => <<<'DHTML'
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Modular Engine Designer</title>
<link rel="stylesheet" href="/designer/assets/style.css">
</head>
<body>
<h1>Engine Visual Designer</h1>
<p>Drag to reorder agents. Save writes <code>sequence.json</code> on the server.</p>

<ul id="agent-list"></ul>

<div id="controls">
  <button id="save">Save sequence</button>
  <button id="refresh">Refresh agents</button>
  <span id="status" style="margin-left:12px;color:#333"></span>
</div>

<script src="/designer/assets/app.js"></script>
</body>
</html>
DHTML
    ,

    "$root/designer/server.js" => <<<'DSRV'
import express from 'express';
import fs from 'fs/promises';
import path from 'path';
const app = express();
const ROOT = path.resolve('.');
const AGENTS = path.join(ROOT, 'agents');
const SEQ = path.join(ROOT, 'sequence.json');

app.use(express.static(path.join(ROOT,'designer')));
app.use(express.json());

app.get('/api/agents', async (req,res) => {
  try {
    const files = await fs.readdir(AGENTS);
    const filtered = files.filter(f => f.endsWith('.txt')).sort();
    res.json(filtered);
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

app.post('/api/save-sequence', async (req,res) => {
  const seq = req.body;
  if (!Array.isArray(seq)) return res.status(400).send('Bad payload');
  await fs.writeFile(SEQ, JSON.stringify(seq, null, 2), 'utf8');
  res.send('Saved sequence.json');
});

app.listen(3030, ()=> console.log('Designer server running on http://localhost:3030'));
DSRV
    ,

    "$root/designer/assets/style.css" => <<<'CSS'
body{font-family:system-ui,Segoe UI,Roboto,Arial;padding:18px;background:#f7f7f7}
h1{margin:0 0 12px}
#agent-list{list-style:none;padding:0;max-width:720px}
li{background:#fff;padding:12px;margin:8px 0;border:1px solid #ddd;cursor:grab;display:flex;justify-content:space-between;align-items:center}
.meta{font-size:12px;color:#666}
CSS
    ,

    "$root/designer/assets/app.js" => <<<'DJS'
// Minimal designer client-side logic
async function fetchAgents(){
  const res = await fetch('/api/agents');
  return res.json();
}
function makeItem(name){
  const li = document.createElement('li');
  li.draggable = true;
  li.dataset.name = name;
  li.textContent = name;
  li.addEventListener('dragstart', onDragStart);
  li.addEventListener('dragover', onDragOver);
  li.addEventListener('drop', onDrop);
  return li;
}
let dragSrc = null;
function onDragStart(e){ dragSrc = this; e.dataTransfer.effectAllowed = 'move'; }
function onDragOver(e){ e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }
function onDrop(e){
  e.preventDefault();
  if (dragSrc === this) return;
  const ul = this.parentNode;
  const nodes = Array.from(ul.children);
  const from = nodes.indexOf(dragSrc);
  const to = nodes.indexOf(this);
  if (from < to) { ul.insertBefore(dragSrc, this.nextSibling); } else { ul.insertBefore(dragSrc, this); }
}
async function loadList(){
  const names = await fetchAgents();
  const ul = document.getElementById('agent-list');
  ul.innerHTML = '';
  names.forEach(n => ul.appendChild(makeItem(n)));
}
document.getElementById('save').onclick = async () => {
  const ul = document.getElementById('agent-list');
  const seq = Array.from(ul.children).map(li => li.dataset.name);
  const res = await fetch('/api/save-sequence', {
    method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify(seq)
  });
  const data = await res.text();
  document.getElementById('status').textContent = data;
};
document.getElementById('refresh').onclick = loadList;
loadList().catch(e => document.getElementById('status').textContent = 'Load failed: ' + e);
DJS
    ,

    "$root/designer/assets/icons.svg" => <<<'SVG'
<!-- icons placeholder -->
<svg xmlns="http://www.w3.org/2000/svg"></svg>
SVG
    ,

    /* docs */
    "$root/docs/architecture.md" => <<<'MD'
# Architecture

Overview of the modular-engine architecture.
MD
    ,

    "$root/docs/agent-format.md" => <<<'MD'
# Agent Format

Each agent file contains a YAML-like header and a prompt body.
MD
    ,

    "$root/docs/runner-pipeline.md" => <<<'MD'
# Runner Pipeline

Runner reads sequence.json, executes agents sequentially, writes responses.
MD
    ,

    "$root/docs/visual-designer.md" => <<<'MD'
# Visual Designer

Designer serves a static UI to reorder agents.
MD
    ,

    "$root/docs/roadmap.md" => <<<'MD'
# Roadmap

- Add branching
- Add caching
- Add plugin system
MD
    ,

    /* tools */
    "$root/tools/generate-agent-template.js" => <<<'TGA'
#!/usr/bin/env node
// Simple generator for a new agent file
import fs from 'fs';
const args = process.argv.slice(2);
if (!args[0]) { console.error('Usage: generate-agent-template.js <id-name> <description>'); process.exit(1); }
const idname = args[0];
const desc = args[1] || 'New agent';
const fname = `../agents/${idname}.txt`;
const template = `---
id: ${idname.split('-')[0] || idname}
name: ${idname}
input_from: previous
output_as: text
---
You are ${idname}. ${desc}
`;
fs.writeFileSync(fname, template);
console.log('Wrote', fname);
TGA
    ,

    "$root/tools/reset-responses.sh" => <<<'RSH'
#!/bin/bash
set -e
TARGET="../responses"
echo "Removing files in $TARGET"
rm -rf "$TARGET"/*
mkdir -p "$TARGET"
echo "Done"
RSH
    ,

    "$root/tools/validate-agents.js" => <<<'TVA'
#!/usr/bin/env node
// lightweight check: ensure agents have header block
import fs from 'fs/promises';
import path from 'path';
const AGENTS = path.resolve('../agents');
(async()=>{
  const list = await fs.readdir(AGENTS);
  for (const f of list) {
    const txt = await fs.readFile(path.join(AGENTS,f),'utf8');
    if (!txt.includes('---')) console.warn('Agent missing header:', f);
  }
})();
TVA
    ,

    "$root/tools/run-pipeline.sh" => <<<'TRP'
#!/bin/bash
# wrapper from project root to runner
pushd runner
node index.js
popd
TRP
    ,

    /* logs */
    "$root/logs/pipeline.log" => "",
    "$root/logs/errors.log" => "",

    /* optional scaffolds */
    "$root/schemas/agent_meta_schema.json" => <<<'SC'
{
  "type": "object",
  "properties": {
    "id": { "type": "string" },
    "name": { "type": "string" },
    "input_from": { "type": "string" },
    "output_as": { "type": "string" }
  }
}
SC
    ,

    "$root/plugins/plugin-websearch.js" => <<<'PW'
/* plugin placeholder for websearch */
PW
    ,

    "$root/tests/test_runner_basic.js" => <<<'TT'
/* test placeholder */
TT
    ,

    "$root/data/cache_index.json" => <<<'CI'
{}
CI
    ,
];

/* create directories and write files */
$created = [];
$skipped = [];
$errors = [];

foreach ($files as $path => $content) {
    $dir = dirname($path);
    try {
        ensure_dir($dir);
    } catch (Exception $e) {
        $errors[] = "Failed to create dir for $path: " . $e->getMessage();
        continue;
    }

    if (file_exists($path) && !$force) {
        $skipped[] = $path;
        continue;
    }

    try {
        file_put_contents($path, $content);
        /* if a script, attempt to chmod +x */
        if (preg_match('#/tools/.*\.(sh|js)$#', $path) || preg_match('#/designer/server.js$#', $path)) {
            @chmod($path, 0755);
        }
        $created[] = $path;
    } catch (Exception $e) {
        $errors[] = "Failed to write $path: " . $e->getMessage();
    }
}

/* summary output */
echo "Target root: $root\n";
echo "Force mode: " . ($force ? "YES" : "NO") . "\n\n";
echo "Created files: " . count($created) . "\n";
foreach ($created as $c) { echo "  + $c\n"; }
echo "\nSkipped (already existed): " . count($skipped) . "\n";
foreach ($skipped as $s) { echo "  - $s\n"; }
if ($errors) {
    echo "\nErrors: " . count($errors) . "\n";
    foreach ($errors as $err) { echo "  ! $err\n"; }
} else {
    echo "\nNo errors.\n";
}

echo "\nDone. Next steps:\n";
echo "  cd $root/runner && npm install\n";
echo "  (optionally) cd $root/designer && npm install && node server.js\n";
echo "\n";

===== FILE: generate_engine/generate_engine.php @ 2025-10-18 22:55:00 =====


===== FILE: generate_engine/generate_engine.php @ 2025-10-18 22:56:07 =====


===== FILE: robotics/gamemaker.html @ 2025-10-19 03:10:58 =====
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Game Maker IDE</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body class="bg-[tan] text-slate-900 font-sans">
  <div id="root"></div>f

  <script type="text/babel" data-presets="react,typescript">
    function GameMakerIDE() {
      const [title, setTitle] = React.useState("My Vintage Game");
      const [desc, setDesc] = React.useState("Flappy-style with mountains and a header splash.");
      const [game, setGame] = React.useState("flappy");
      const [generatedIno, setGeneratedIno] = React.useState("");
      const [imagePreview, setImagePreview] = React.useState(null);
      const [busy, setBusy] = React.useState(false);
      const canvasRef = React.useRef(null);

      async function aiGenerateImage() {
        setBusy(true);
        try {
          const res = await fetch('api/generate_image_groq_spec.php', {
            method: 'POST',
            headers: {'Content-Type':'application/json'},
            body: JSON.stringify({
              title,
              description: desc,
              game_type: game,
              target_width: 160,
              target_height: 128,
              grid_w: 20,
              grid_h: 16,
              palette_size: 5
            })
          });
          const out = await res.json();
          if (out.error) throw new Error(out.error || 'No image returned');
          if (out.png_data_url) {
            setImagePreview(out.png_data_url);
            const img = new Image();
            img.onload = () => {
              const c = canvasRef.current;
              if (!c) return;
              c.width = img.width;
              c.height = img.height;
              c.getContext('2d').drawImage(img, 0, 0);
            };
            img.src = out.png_data_url;
          }
        } catch (e) {
          alert('Image generation failed: ' + e.message);
          console.error(e);
        } finally {
          setBusy(false);
        }
      }

      function drawLocalTitleCardToCanvas(text) {
        const W = 160, H = 128;
        let c = canvasRef.current;
        if (!c) return;
        c.width = W; c.height = H;
        const ctx = c.getContext("2d");
        if (!ctx) return;

        const grad = ctx.createLinearGradient(0, 0, 0, H);
        grad.addColorStop(0, "#2278C5");
        grad.addColorStop(1, "#93CCEA");
        ctx.fillStyle = grad;
        ctx.fillRect(0, 0, W, H);

        ctx.fillStyle = "#5A6E82";
        for (let x = 0; x < W; x++) {
          const yTop = 80 - Math.sin((x + 10) * 0.11) * 8;
          ctx.fillRect(x, yTop, 1, H - yTop);
        }

        ctx.fillStyle = "#465764";
        for (let x = 0; x < W; x++) {
          const yTop = 88 - Math.sin((x + 20) * 0.13) * 10;
          ctx.fillRect(x, yTop, 1, H - yTop);
        }

        ctx.fillStyle = "#fff";
        ctx.textAlign = "center";
        ctx.font = "bold 18px system-ui";
        ctx.fillText(text || "Game Title", W / 2, 28);
        ctx.font = "12px system-ui";
        ctx.fillText("Press button to start", W / 2, H - 10);

        setImagePreview(c.toDataURL("image/png"));
      }

      async function generateCodeWithGroq() {
        setBusy(true);
        try {
          const imgData = canvasRef.current ? canvasRef.current.toDataURL("image/png") : null;
          const res = await fetch("api/generate.php", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
              title,
              description: desc,
              game_type: game,
              image_data_url: imgData
            })
          });
          const out = await res.json();

          if (out.error) throw new Error(out.error);
          if (out.ino) setGeneratedIno(out.ino);
          if (out.png_data_url) setImagePreview(out.png_data_url);
        } catch (e) {
          alert("Code generation failed: " + e.message);
          console.error(e);
        } finally {
          setBusy(false);
        }
      }

      function generateImageOnly() {
        drawLocalTitleCardToCanvas(title);
      }

      function download(filename, text) {
        const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = filename;
        a.click();
        URL.revokeObjectURL(url);
      }

      async function copyAll(text) {
        try {
          await navigator.clipboard.writeText(text);
          alert("Copied to clipboard!");
        } catch {
          const ta = document.createElement("textarea");
          ta.value = text;
          document.body.appendChild(ta);
          ta.select();
          document.execCommand("copy");
          document.body.removeChild(ta);
          alert("Copied using fallback!");
        }
      }

      return (
        <div className="min-h-screen bg-[tan] text-slate-900 font-sans">
          <div className="max-w-5xl mx-auto p-6">
            <img src="idetitle.png"/>
            <p className="text-center text-slate-700 mb-8">
              Create fun mini-games for your ESP32! Choose a game, name it, describe it, and let AI write the Arduino code.
            </p>

            <div className="bg-amber-100 p-6 rounded-3xl shadow-lg border border-amber-300">
              <div className="flex flex-col md:flex-row gap-6">
                {/* Left side form */}
                <div className="flex-1 space-y-4">
                  <label className="block">
                    <span className="text-md font-semibold">Game Title</span>
                    <input
                      value={title}
                      onChange={e => setTitle(e.target.value)}
                      className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg"
                      placeholder="My Awesome Game"
                    />
                  </label>

                  <label className="block">
                    <span className="text-md font-semibold">Game Description</span>
                    <textarea
                      value={desc}
                      onChange={e => setDesc(e.target.value)}
                      rows={4}
                      className="mt-2 w-full rounded-2xl bg-white border border-amber-300 px-3 py-3 text-md"
                      placeholder="Describe your game idea..."
                    />
                  </label>

                  <label className="block">
                    <span className="text-md font-semibold">Pick Game Type</span>
                    <select
                      value={game}
                      onChange={e => setGame(e.target.value)}
                      className="mt-1 w-full rounded-2xl bg-white border border-amber-300 px-3 py-2 text-lg"
                    >
                      <option value="flappy">Flappy Bird</option>
                      <option value="snake">Snake</option>
                      <option value="pong">Pong</option>
                    </select>
                  </label>

                  <div className="flex flex-wrap justify-center gap-4 mt-6">
                    <div className="flex flex-wrap gap-3 justify-center mt-4">
                      <button
                        onClick={generateCodeWithGroq}
                        disabled={busy}
                        className="bg-orange-500 hover:bg-orange-400 disabled:opacity-50 text-white font-bold px-5 py-2 rounded-2xl shadow"
                      >
                        {busy ? "Generating..." : "Generate Code (Groq)"}
                      </button>

                      <button
                        onClick={generateImageOnly}
                        className="bg-yellow-500 hover:bg-yellow-400 text-white font-bold px-5 py-2 rounded-2xl shadow"
                      >
                        Generate Image (Local)
                      </button>

                      <button
                        onClick={aiGenerateImage}
                        className="bg-indigo-600 hover:bg-indigo-500 text-white font-bold px-5 py-2 rounded-2xl shadow"
                      >
                        AI Generate Image (Groq)
                      </button>

                      <button
                        onClick={() => download(`${title.replace(/[^A-Za-z0-9_]+/g, "_")}.ino`, generatedIno || "// generate first")}
                        className="bg-teal-500 hover:bg-teal-400 text-white font-bold px-5 py-2 rounded-2xl shadow"
                      >
                        Download .ino
                      </button>
                    </div>
                  </div>
                </div>

                {/* Right side preview/code */}
                <div className="flex-1 space-y-4">
                  <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50 text-center">
                    <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                      <span>Menu Header Preview</span>
                      <button
                        onClick={() => copyAll(imagePreview || "No image available.")}
                        className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg"
                      >
                        Copy All
                      </button>
                    </div>
                    <div className="aspect-[5/4] w-full border-2 border-amber-300 rounded-xl grid place-items-center bg-amber-200">
                      <canvas ref={canvasRef} className="rounded" />
                      {!imagePreview && <div className="text-slate-500 text-sm">Click 'Generate Image (Local)'</div>}
                      {imagePreview && (
                        <img src={imagePreview} alt="Preview" className="rounded w-full h-full object-contain" />
                      )}
                    </div>
                  </div>

                  <div className="rounded-3xl border-4 border-amber-300 p-3 bg-amber-50">
                    <div className="font-semibold text-lg mb-2 flex justify-between items-center">
                      <span>Arduino Code</span>
                      <button
                        onClick={() => copyAll(generatedIno)}
                        className="text-xs bg-amber-300 hover:bg-amber-200 px-2 py-1 rounded-lg"
                      >
                        Copy All
                      </button>
                    </div>
                    <textarea
                      value={generatedIno}
                      onChange={e => setGeneratedIno(e.target.value)}
                      rows={10}
                      className="w-full rounded-xl bg-white border border-amber-300 p-3 font-mono text-xs"
                      placeholder="Click 'Generate Code (Groq)'"
                    />
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      );
    }

    const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(<GameMakerIDE />);
  </script>
</body>
</html>


===== FILE: webide/index.php @ 2025-10-19 23:07:22 =====
<?php
// index.php
declare(strict_types=1);
require_once __DIR__ . '/_inc/helpers.php';
ensure_auth();
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initital-scale=1" />
<title>Mini Web IDE</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
<link rel="stylesheet" href="assets/style.css" />
<link rel="icon" type="png" href="cool.png"/>
<?php
require __DIR__.'/_inc/config.php';
if ($USE_ACE_CDN): ?>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.9/ace.js" crossorigin="anonymous"></script>

<?php endif; ?>
</head>
<body>
    <div id="saveStatusDot" title="save status"></div>

<div class="layout">
  <aside class="sidebar">
    <div class="topbar">
      <strong>Files</strong>
      <button id="refreshTree" title="Refresh">↻</button>
    </div>
    <div class="tree-controls">
        <label>Folder: 
    <input id="folderFilter" placeholder="(leave blank for all)" />
  </label><br><br>
  <input id="treeFilter" placeholder="Filter files…" />
  <button id="btnExpandAll" title="Expand all">＋</button>
  <button id="btnCollapseAll" title="Collapse all">－</button>
</div>
<div class="scopebar">
  <span>📂</span>
  <span id="scopeCrumbs"></span>
  <button id="scopeReset" title="Back to workspace root">Reset</button>
</div>

    <div id="tree" class="tree"></div>

    <div class="createBox">
      <h4>Create</h4>
      <input id="newPath" placeholder="path/to/file.ext or folder/" />
      <div class="row">
        <button data-kind="file" id="btnCreateFile">+ File</button>
        <button data-kind="dir"  id="btnCreateDir">+ Folder</button>
      </div>
      <details>
        <summary>Bulk create</summary>
        <p class="hint">One per line. Prefix “dir:” for folders, otherwise a file is created.</p>
        <textarea id="bulkLines" rows="6" placeholder="dir: assets/
file: assets/app.js
file: index.php"></textarea>
        <button id="btnBulk">Run Bulk</button>
      </details>
    </div>

    
  </aside>

  <main class="main">
    <header class="mainbar">
  <div class="path">
    <span id="currentPath">—</span>
  </div>
  <div class="actions">
    <button id="btnToggleSidebar" title="Hide/Show file panel">☰ Files</button>
    <button id="btnSave">Save (Ctrl/Cmd+S)</button>
    <button id="btnRename">Rename</button>
    <button id="btnDelete" class="danger">Delete</button>
    <label class="previewToggle"><input type="checkbox" id="togglePreview" checked> Preview</label>
  </div>
</header>


    <section class="editorWrap">
      <div id="editor"></div>
      <textarea id="ta" class="hidden"></textarea>
      <iframe id="preview" title="Preview"></iframe>
    </section>
  </main>
    <!-- right-hand slideout for Link map -->
  <aside class="linkmap-panel" aria-hidden="true">
    <div class="topbar">
      <strong>Link map</strong>
      <button id="refreshLinks" title="Refresh">↻</button>
    </div>

    <div class="linkmap-controls" style="padding:10px 12px; border-bottom:1px solid var(--border);">
      <button id="btnScanLinks">Scan</button>
      <button id="btnCloseLinks" style="margin-left:8px">Close</button>
    </div>

    <div id="linksPanel" class="linksContent" style="padding:10px 12px; overflow:auto; flex:1;"></div>
  </aside>

  <!-- floating right tab to reveal linkmap -->
  <button id="btnToggleLinks" title="Show link map">☰ Links</button>

</div>

<script>window.MINI_IDE = { base: '' };</script>
<script src="../webide/assets/app.js"></script>
<!-- Big centered loader overlay -->
<div id="pageLoader" aria-hidden="true">
  <div class="spinner" role="status" title="Saving…"></div>
</div>

</body>
</html>


===== FILE: webide/assets/style.css @ 2025-10-19 23:07:59 =====
:root{
  --bg:#f7f7f4;
  --panel:#fff;
  --border:#dfe6e2;
  --ink:#1f2a24;
  --sub:#57645d;
  --accent:#22a06b;
  --danger:#d93b3b;
}

*{box-sizing:border-box}
html,body{height:100%}
body{
  margin:0;
  background:var(--bg);
  color:var(--ink);
  font:14px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;
}

/* Layout */
.layout{
  display:grid;
  grid-template-columns:1fr; /* main content always full width */
  height:100vh;
  position:relative; /* for absolute sidebar */
  z-index:0;
}

/* Bold current scope */
.scopebar .crumb.current{ font-weight:700; color: var(--ink); }

/* Centered page loader */
#pageLoader{
  position: fixed; inset:0; display:none;
  align-items:center; justify-content:center;
  background:rgba(255,255,255,.65);
  z-index:500;
}
#pageLoader .spinner{
  width:64px; height:64px;
  border:8px solid #cfd8d3;
  border-top-color:var(--accent);
  border-radius:50%;
  animation:spin .9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg) } }

/* Sidebar */
.sidebar {
  position:absolute; /* slides over main content */
  top:0; left:0;
  width:280px;
  height:100%;
  background: var(--panel);
  border-right:1px solid var(--border);
  display:flex;
  flex-direction:column;
  padding-right:6px;
  transform: translateX(-100%);
  transition: transform 0.25s ease-in-out;
  z-index:100; /* above main content */
}

body.sidebar-visible .sidebar {
  transform: translateX(0);
}

/* Main content */
.main{
  display:flex;
  flex-direction:column;
}

.mainbar{
  background:var(--panel);
  border-bottom:1px solid var(--border);
  padding:8px 12px;
  display:flex;
  align-items:center;
  justify-content:space-between;
}
.mainbar .path{
  min-width:0;
  overflow:hidden;
  white-space:nowrap;
  text-overflow:ellipsis;
  display:flex;
  align-items:center;
  gap:8px;
}
.actions button{
  margin-left:6px;
  padding:6px 12px;
  border:1px solid var(--border);
  background:#fff;
  border-radius:8px;
  cursor:pointer;
}

/* Floating full-height purple toggle tab */
#btnToggleSidebar {
  position: fixed;
  left:0;
  top:0;
  height:100vh;
  width:42px;
  display:flex;
  align-items:center;
  justify-content:center;
  padding:6px;
  background: rgba(128, 0, 128, 0.25);
  border:none;
  border-right:1px solid rgba(0,0,0,0.06);
  z-index:110; /* above sidebar */
  cursor:pointer;
  color:white;
  font-weight:700;
  writing-mode: vertical-rl;
  text-orientation: mixed;
  border-radius:0;
  box-shadow:0 4px 10px rgba(0,0,0,0.08);
}

/* Ensure main header button styling doesn't override toggle tab */
.actions #btnToggleSidebar { margin-left:0; transform:none; }

/* Other styling */
.actions .danger{border-color:#f3c6c6; color:#8b1a1a; background:#fff5f5}
.previewToggle{margin-left:10px; color:var(--sub); font-size:13px}

.topbar{
  display:flex; align-items:center; justify-content:space-between;
  padding:10px 12px; border-bottom:1px solid var(--border)
}
#refreshTree{border:1px solid var(--border); background:#fff; padding:4px 8px; border-radius:6px; cursor:pointer}

/* Scope bar */
.scopebar{
  display:flex; align-items:center; gap:8px; padding:8px 10px; border-bottom:1px solid var(--border);
  font-size:13px; color:var(--sub); overflow:auto; white-space:nowrap;
}
.scopebar .crumb{color:var(--ink); cursor:pointer; padding:2px 4px; border-radius:6px}
.scopebar .crumb:hover{background:#f3f5f4}
.scopebar .sep{opacity:.5; padding:0 2px}
.scopebar button{margin-left:auto; border:1px solid var(--border); background:#fff; border-radius:8px; padding:4px 8px; cursor:pointer}

/* Tree */
.tree{padding:8px 8px 16px; overflow:auto; flex:1; font-size:14px}
.tree details{margin:2px 0}
.tree summary{list-style:none; cursor:pointer; padding:4px 6px; border-radius:6px; display:flex; align-items:center; gap:6px}
.tree summary::-webkit-details-marker{display:none}
.tree summary:hover{background:#f3f5f4}
.tree .chev{display:inline-block; width:1em; text-align:center; transition:transform .15s}
.tree details[open] > summary .chev{transform:rotate(90deg)}
.tree .label{user-select:none; flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis}
.tree .focus{opacity:0; transition:opacity .12s; border:1px solid var(--border); background:#fff; border-radius:6px; padding:2px 6px; cursor:pointer; font-size:12px}
.tree summary:hover .focus{opacity:1}
.tree .file{padding:4px 6px; border-radius:6px; cursor:pointer; display:flex; align-items:center; gap:6px; white-space:nowrap}
.tree .file:hover{background:#f3f5f4}
.tree .file.active{background:#eef7f3}
.tree .name{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis}
.tree .file .name.dirty::after{content:" *"; margin-left:2px; color:#f5b301; font-weight:600}

/* Create + links */
.createBox{border-top:1px solid var(--border); padding:10px 12px}
.createBox input, .createBox textarea{
  width:100%; border:1px solid var(--border); border-radius:8px; padding:8px; margin:6px 0; font:inherit; background:#fff;
}
.createBox .row{display:flex; gap:6px}
.createBox button{padding:6px 10px; border:1px solid var(--border); border-radius:8px; background:#fff; cursor:pointer}

.linksBox{border-top:1px solid var(--border); padding:10px 12px}
.linksBox button{margin-top:6px; padding:6px 10px; border:1px solid var(--border); border-radius:8px; background:#fff; cursor:pointer}
#links{max-height:180px; overflow:auto; font-family:ui-monospace, Menlo, Consolas, monospace}
#links .edge{padding:2px 0; border-bottom:1px dashed #eaeaea}

/* Editor + preview */
.editorWrap{display:grid; grid-template-columns:1fr 40%; height:100%}
#editor,#ta{height:calc(100vh - 56px); width:100%; border:none; outline:none; margin:0}
#ta{padding:12px; font:13px/1.5 ui-monospace, Menlo, Consolas, monospace}
.hidden{display:none}
#preview{height:calc(100vh - 56px); width:100%; border-left:1px solid var(--border)}
@media (max-width:1000px){ .editorWrap{grid-template-columns:1fr} #preview{display:none} }

/* Save/loader + status dot */
#saveLoader{display:inline-block; margin-left:8px; width:16px; height:16px; border:3px solid #bbb; border-top-color:var(--accent); border-radius:50%; animation:spin .8s linear infinite; visibility:hidden; vertical-align:middle}
@keyframes spin{to{transform:rotate(360deg)}}
#saveStatusDot{position:fixed; right:10px; top:10px; width:10px; height:10px; border-radius:50%; background:#bbb; box-shadow:0 0 0 2px #fff,0 0 0 3px rgba(0,0,0,.05); z-index:5}
#saveStatusDot.ok{background:#22a06b} 
#saveStatusDot.err{background:#d93b3b} 
#saveStatusDot.dirty{background:#f5b301} 

/* Right-hand linkmap panel (mirror of .sidebar) */
.linkmap-panel {
  position: absolute;
  top: 0;
  right: 0;
  width: 320px;
  height: 100%;
  background: var(--panel);
  border-left: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  padding-left: 6px;
  transform: translateX(100%); /* hidden by default */
  transition: transform 0.25s ease-in-out;
  z-index: 100; /* above main content */
}

/* shown state */
body.linkmap-visible .linkmap-panel {
  transform: translateX(0);
}

/* floating full-height right toggle tab */
#btnToggleLinks {
  position: fixed;
  right: 0;
  top: 0;
  height: 100vh;
  width: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 6px;
  background: rgba(0, 102, 153, 0.18);
  border: none;
  border-left: 1px solid rgba(0,0,0,0.06);
  z-index: 110; /* above linkmap */
  cursor: pointer;
  color: white;
  font-weight: 700;
  writing-mode: vertical-rl;
  text-orientation: mixed;
  border-radius: 0;
  box-shadow: 0 4px 10px rgba(0,0,0,0.08);
}

/* small adjustments for the link panel content */
.linksContent { max-height: calc(100vh - 88px); font-family: ui-monospace, Menlo, Consolas, monospace; }
.linksContent .edge{ padding:6px 0; border-bottom:1px dashed #eaeaea; white-space:nowrap; overflow:auto; }


/* Sidebar hidden state */
body.sidebar-hidden .sidebar { transform: translateX(-100%); }


===== FILE: webide/assets/app.js @ 2025-10-19 23:08:32 =====
(async function () {
  // ---------- DOM helpers ----------
  const $ = (q, el = document) => el.querySelector(q),
        $$ = (qAll, el = document) => Array.from(el.querySelectorAll(qAll));

  // ---------- API helper (POST FormData) ----------
  const api = async (params) => {
    const form = new FormData();
    Object.entries(params).forEach(([k, v]) => {
      if (Array.isArray(v)) v.forEach((x) => form.append(k + "[]", x));
      else form.append(k, v);
    });
    const r = await fetch("api.php", { method: "POST", body: form });
    return r.json();
  };

  // ---------- State ----------
  const state = {
    currentRel: "",
    ace: null,
    cfg: null,
    dirty: false,
    saving: false,
    scopeRel: "" // '' = workspace root; otherwise e.g. 'admin/pages'
  };

  // ---------- Init ----------
  async function init() {
    state.cfg = await api({ action: "config" });

    // restore scope
    state.scopeRel = localStorage.getItem("mini_ide_scope") || "";

    bindUI();
    await refreshTree();

    // Editor: ACE if available, fallback to <textarea>
    if (state.cfg.ace && window.ace) {
      state.ace = ace.edit("editor");
      state.ace.setTheme("ace/theme/textmate");
      state.ace.session.setUseSoftTabs(true);
      state.ace.session.setTabSize(2);
      state.ace.setOption("fontSize", "13px");
      $("#ta").classList.add("hidden");
      state.ace.session.on("change", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    } else {
      $("#editor").classList.add("hidden");
      $("#ta").addEventListener("input", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    }

    // Load a sensible default (index.*) if present
    const guess = $$("#tree .file").find((n) =>
      /^(index\.(php|html?|md|txt))$/i.test(n.dataset.rel || "")
    );
    if (guess) openFile(guess.dataset.rel);
  }

  // ---------- UI wiring ----------
  function bindUI() {
    $("#refreshTree")?.addEventListener("click", refreshTree);
    $("#btnCreateFile")?.addEventListener("click", () => createFromInput(false));
    $("#btnCreateDir")?.addEventListener("click", () => createFromInput(true));
    $("#btnBulk")?.addEventListener("click", bulkCreate);
    $("#btnSave")?.addEventListener("click", saveCurrent);
    $("#btnRename")?.addEventListener("click", renameCurrent);
    $("#btnDelete")?.addEventListener("click", deleteCurrent);
    $("#btnScanLinks")?.addEventListener("click", scanLinks);
    $("#scopeReset")?.addEventListener("click", () => setScope(""));
    
        // existing
    $("#btnScanLinks")?.addEventListener("click", scanLinks);

    // right-panel linkmap controls
    $("#btnToggleLinks")?.addEventListener("click", toggleLinkmap);
    $("#refreshLinks")?.addEventListener("click", scanLinks);
    $("#btnCloseLinks")?.addEventListener("click", () => {
      document.body.classList.remove("linkmap-visible");
      localStorage.setItem("mini_ide_linkmap_hidden", "1");
    });

    
        // ---------- Floating tab reveal behaviour ----------
    // Keeps a temporary reveal state so we don't overwrite user's stored choice.
    // set default to hidden on first visit
if (localStorage.getItem("mini_ide_sidebar_hidden") === null) {
  localStorage.setItem("mini_ide_sidebar_hidden", "1");
}

// Restore sidebar state (existing code)
if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
  document.body.classList.add("sidebar-hidden");
}


    (function setupFloatingTabReveal() {
      const tab = $("#btnToggleSidebar");
      const sidebarEl = document.querySelector(".sidebar");
      if (!tab || !sidebarEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden"); // "1" if user had hidden

      // When pointer enters the floating tab: reveal the sidebar and hide the tab itself.
      tab.addEventListener("mouseenter", (e) => {
        // remember stored preference so we can restore it later
        prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden");
        document.body.classList.add("sidebar-visible");
        tempReveal = true;

        // reveal sidebar visually (do NOT change stored preference)
        document.body.classList.remove("sidebar-hidden");
        // hide the floating tab while user is interacting with sidebar
        tab.style.display = "none";
      });

      // When pointer leaves the sidebar area, hide the sidebar again if it was shown by the tab
      sidebarEl.addEventListener("mouseleave", (e) => {
        // Only auto-hide when this reveal was triggered by the tab (avoid interfering with manual toggle)
       // toggleSidebar();
       document.body.classList.remove("sidebar-visible");
        if (!tempReveal) return;

        // restore to user's stored preference
        if (prevStoredHidden === "1") {
          // user had explicitly hidden -> re-hide sidebar
          document.body.classList.add("sidebar-hidden");
        } else {
          // user had it visible by default — keep visible (or you can re-hide; sticking with restore)
          document.body.classList.remove("sidebar-hidden");
        }

        // show the floating tab again
        tab.style.display = "";
        tempReveal = false;
      });

      // Extra: if user moves quickly from the tab into the sidebar, keep it shown.
      // Also handle pointerout from tab to sidebar — do nothing because mouseover already removed tab.
      // If user clicks the tab (existing click handler remains), we still toggle as before.
    })();


    // Keyboard shortcuts (capture)
    window.addEventListener(
      "keydown",
      (e) => {
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
          e.preventDefault(); e.stopPropagation();
          saveCurrent();
        }
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
          e.preventDefault(); toggleSidebar();
        }
      },
      true
    );

    // Preview toggle
    $("#togglePreview")?.addEventListener("change", syncPreviewVisibility);

    // Sidebar toggle button
    $("#btnToggleSidebar")?.addEventListener("click", toggleSidebar);

    // Restore sidebar state
    if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
      document.body.classList.add("sidebar-hidden");
    }
  }

  function toggleSidebar() {
    const hidden = document.body.classList.toggle("sidebar-hidden");
    localStorage.setItem("mini_ide_sidebar_hidden", hidden ? "1" : "0");
  }

  function syncPreviewVisibility() {
    const on = $("#togglePreview")?.checked;
    if (on == null) return;
    $("#preview").style.display = on ? "block" : "none";
    document.querySelector(".editorWrap").style.gridTemplateColumns = on ? "1fr 40%" : "1fr";
  }

  // ---------- Save status / dirty UI ----------
  function setSaveBusy(on) {
    // big overlay
    const overlay = $("#pageLoader");
    if (overlay) overlay.style.display = on ? "flex" : "none";

    // header spinner
    const loader = $("#saveLoader");
    if (loader) loader.style.visibility = on ? "visible" : "hidden";

    const dot = $("#saveStatusDot");
    if (dot && on) dot.classList.remove("ok", "err", "dirty");
  }
  function setSaveDirty(on) {
    const btn = $("#btnSave");
    if (btn) btn.style.borderColor = on ? "#c9e8dc" : "var(--border)";
  }
  function markSaveOK() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("err", "dirty");
    dot.classList.add("ok");
    setTimeout(() => dot.classList.remove("ok"), 900);
  }
  function markSaveErr() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("ok", "dirty");
    dot.classList.add("err");
  }
  function updateDirtyUI() {
    // header asterisk
    const cp = $("#currentPath");
    if (cp) cp.textContent = (state.currentRel || "—") + (state.dirty ? " *" : "");

    // tree asterisk (on current file only)
    $$("#tree .file .name").forEach((n) => n.classList.remove("dirty"));
    if (state.currentRel) {
      const n = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"] .name`);
      if (n && state.dirty) n.classList.add("dirty");
    }

    // status dot amber while dirty
    const dot = $("#saveStatusDot");
    if (dot) {
      dot.classList.remove("ok", "err");
      dot.classList.toggle("dirty", !!state.dirty);
    }
  }

  // ---------- Scope helpers ----------
  function setScope(rel) {
    state.scopeRel = (rel || "").replace(/^\/+|\/+$/g, "");
    localStorage.setItem("mini_ide_scope", state.scopeRel);
    refreshTree();
  }

  function renderScopeCrumbs(){
    const wrap = $("#scopeCrumbs"); if (!wrap) return;
    wrap.innerHTML = "";
    const mk = (txt, rel, isCurrent=false) => {
      const s = document.createElement("span");
      s.className = "crumb" + (isCurrent ? " current" : "");
      s.textContent = txt;
      s.onclick = () => !isCurrent && setScope(rel);
      if (isCurrent) s.title = "Current folder";
      return s;
    };
    const parts = (state.scopeRel || "").split("/").filter(Boolean);
    wrap.appendChild(mk("root", "", parts.length === 0));
    let acc = "";
    parts.forEach((p, i) => {
      const sep = document.createElement("span");
      sep.className = "sep"; sep.textContent = "/";
      wrap.appendChild(sep);
      acc = acc ? acc + "/" + p : p;
      wrap.appendChild(mk(p, acc, i === parts.length - 1));
    });
  }

  // ---------- Tree (server-scoped, collapsible) ----------
  function toNested(items) {
    const root = { name: "", rel: "", dirs: {}, files: [] };
    const entries = items
      .map((it) => ({ type: it.type, rel: (it.rel || "").replace(/^\/+/, "") }))
      .filter((e) => e.rel);

    // ensure nodes
    entries.forEach((e) => {
      const parts = e.rel.split("/");
      if (e.type === "dir") {
        let cur = root;
        for (const part of parts) {
          cur.dirs[part] = cur.dirs[part] || {
            name: part,
            rel: (cur.rel ? cur.rel + "/" : "") + part,
            dirs: {},
            files: []
          };
          cur = cur.dirs[part];
        }
      }
    });

    // place files
    entries.filter((e) => e.type === "file").forEach((e) => {
      const parts = e.rel.split("/");
      const name = parts.pop();
      let cur = root;
      for (const part of parts) {
        cur.dirs[part] = cur.dirs[part] || {
          name: part,
          rel: (cur.rel ? cur.rel + "/" : "") + part,
          dirs: {},
          files: []
        };
        cur = cur.dirs[part];
      }
      cur.files.push({ name, rel: e.rel });
    });

    // sort
    (function sort(n) {
      Object.values(n.dirs).forEach(sort);
      n.files.sort((a, b) => a.name.localeCompare(b.name));
    })(root);

    return root;
  }

  function renderTree(rootNode, container) {
    const openState = JSON.parse(localStorage.getItem("mini_ide_open_dirs") || "{}");
    const setOpen = (rel, open) => {
      if (open) openState[rel || "/"] = 1;
      else delete openState[rel || "/"];
      localStorage.setItem("mini_ide_open_dirs", JSON.stringify(openState));
    };

    container.innerHTML = "";

    const mkDir = (node) => {
      const det = document.createElement("details");
      if (openState[node.rel || "/"]) det.open = true;

      const sum = document.createElement("summary");
      sum.innerHTML = `
        <span class="chev">›</span>
        <span class="label name">📁 ${node.name || "root"}</span>
        <button class="focus" type="button" title="Focus here" data-rel="${node.rel}">📌 Focus</button>
      `;
      det.appendChild(sum);

      // node.rel from server is already full root-relative path
      const dirRel = (node.rel || "");

      // focus controls
      sum.ondblclick = (e) => { e.preventDefault(); setScope(dirRel); };
      sum.querySelector(".focus").onclick = (e) => { e.stopPropagation(); setScope(dirRel); };

      det.addEventListener("toggle", () => setOpen(node.rel, det.open));

      // child dirs
      const dirNames = Object.keys(node.dirs).sort((a, b) => a.localeCompare(b));
      dirNames.forEach((name) => det.appendChild(mkDir(node.dirs[name])));

      // files: use rel from server as-is (already full)
      node.files.forEach((f) => {
        const div = document.createElement("div");
        div.className = "file";
        div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
        div.dataset.rel = f.rel;
        div.onclick = () => openFile(f.rel);
        det.appendChild(div);
      });

      return det;
    };

    // top-level dirs
    const dirNames = Object.keys(rootNode.dirs).sort((a, b) => a.localeCompare(b));
    dirNames.forEach((name) => container.appendChild(mkDir(rootNode.dirs[name])));

    // top-level files: use rel as-is
    rootNode.files.forEach((f) => {
      const div = document.createElement("div");
      div.className = "file";
      div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
      div.dataset.rel = f.rel;
      div.onclick = () => openFile(f.rel);
      container.appendChild(div);
    });
  }

  async function refreshTree() {
    try {
      const res = await api({ action: "tree", rel: state.scopeRel });
      if (!res || res.ok !== true || !Array.isArray(res.items)) {
        const msg = res && res.error ? res.error : "Tree request failed";
        alert("Mini IDE error: " + msg);
        console.error("Tree response:", res);
        return;
      }
      const nested = toNested(res.items);
      renderTree(nested, $("#tree"));
      renderScopeCrumbs();

      // re-highlight current file if visible
      if (state.currentRel) {
        $$("#tree .file").forEach((n) => n.classList.remove("active"));
        const hit = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"]`);
        if (hit) hit.classList.add("active");
        updateDirtyUI();
      }
    } catch (e) {
      alert("Mini IDE fetch error (tree): " + e);
      console.error(e);
    }
  }

  // ---------- File ops ----------
  async function openFile(rel) {
    const res = await api({ action: "read", rel });
    if (!res.ok) return alert(res.error || "Failed to read");
    state.currentRel = rel;

    // highlight selected
    $$("#tree .file").forEach((n) => n.classList.remove("active"));
    const hit = $(`#tree .file[data-rel="${CSS.escape(rel)}"]`);
    if (hit) hit.classList.add("active");

    if (res.file.editable) {
      setEditorContent(res.file.content || "");
    } else {
      setEditorContent("// Not editable (binary or large). Size: " + res.file.size + " bytes\n");
    }
    state.dirty = false;
    setSaveDirty(false);
    updateDirtyUI();
    refreshPreview();
  }

  function getEditorContent() {
    if (state.ace) return state.ace.getValue();
    return $("#ta").value;
  }

  function setEditorContent(txt) {
    if (state.ace) {
      state.ace.setValue(txt ?? "", -1);
      // Set mode from extension
      const ext = (state.currentRel.split(".").pop() || "").toLowerCase();
      const map = {
        js: "javascript",
        css: "css",
        php: "php",
        html: "html",
        htm: "html",
        json: "json",
        md: "markdown",
        py: "python",
        yml: "yaml",
        yaml: "yaml",
        c: "c_cpp",
        cpp: "c_cpp",
        h: "c_cpp",
        sql: "sql",
        csv: "text",
        txt: "text"
      };
      state.ace.session.setMode("ace/mode/" + (map[ext] || "text"));
    } else {
      $("#ta").classList.remove("hidden");
      $("#ta").value = txt ?? "";
    }
  }

  async function saveCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (state.saving) return; // prevent double-taps
    state.saving = true;
    setSaveBusy(true);

    try {
      const content = getEditorContent();
      const res = await api({ action: "save", rel: state.currentRel, content });
      if (!res.ok) {
        markSaveErr();
        throw new Error(res.error || "Save failed");
      }

      // Server verification
      let verified = !!res.verify_ok;

      // mtime wait (shared hosts can be slow)
      const targetMtime = res.mtime || 0;
      const okStat = await waitForStat(state.currentRel, targetMtime, 8, 150);
      if (!okStat) console.warn("Proceeding without stat confirmation");

      // Optional client-side readback if server said not verified
      if (!verified) {
        try {
          const rb = await api({ action: "read", rel: state.currentRel });
          if (rb.ok && rb.file && typeof rb.file.content === "string") {
            verified = (rb.file.content === content);
          }
        } catch {}
      }

      if (!verified) {
        markSaveErr();
        alert("Save did not verify. The file contents on disk did not match what was sent.");
        return; // do not mark OK or clear dirty
      }

      state.dirty = false;
      setSaveDirty(false);
      updateDirtyUI();   // clear asterisks + amber dot
      markSaveOK();      // green blip
      refreshPreview(true);

    } catch (e) {
      console.error(e);
      alert("Save error: " + e.message);
      markSaveErr();     // red
    } finally {
      state.saving = false;
      setSaveBusy(false);
    }
  }

  // poll stat until mtime >= target
  async function waitForStat(rel, target, tries = 8, delay = 150) {
    for (let i = 0; i < tries; i++) {
      try {
        const s = await api({ action: "stat", rel });
        if (s.ok && (s.mtime || 0) >= target) return true;
      } catch {}
      await new Promise((r) => setTimeout(r, delay));
    }
    return false;
  }

  async function renameCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    const to = prompt("Rename to (path):", state.currentRel);
    if (!to || to === state.currentRel) return;
    const res = await api({ action: "rename", from: state.currentRel, to });
    if (!res.ok) return alert(res.error || "Rename failed");
    state.currentRel = to;
    $("#currentPath").textContent = to;
    await refreshTree();
    refreshPreview();
  }

  async function deleteCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (!confirm("Delete " + state.currentRel + " ?")) return;
    const res = await api({ action: "delete", rel: state.currentRel });
    if (!res.ok) return alert(res.error || "Delete failed");
    state.currentRel = "";
    $("#currentPath").textContent = "—";
    if (state.ace) state.ace.setValue("");
    else $("#ta").value = "";
    await refreshTree();
    refreshPreview();
  }

  async function createFromInput(isDir) {
    const raw = $("#newPath").value.trim();
    if (!raw) return;

    const relPath = state.scopeRel
      ? (state.scopeRel + "/" + raw).replace(/\/+/g, "/").replace(/^\/|\/$/g, "")
      : raw;

    const res = await api({ action: "create", rel: relPath, type: isDir ? "dir" : "file" });
    if (!res.ok) return alert(res.error || "Create failed");
    $("#newPath").value = "";
    await refreshTree();
    if (!isDir) openFile(relPath);
  }

  async function bulkCreate() {
    const lines = $("#bulkLines").value;
    if (!lines.trim()) return;

    // Prefix each line's path with scope (keep labels 'dir:'/'file:' intact)
    const scoped = lines.split("\n").map((t) => {
      const ln = t.trim();
      if (!ln) return ln;
      const m = ln.match(/^(dir:|file:)?\s*(.*)$/i);
      if (!m) return ln;
      const label = (m[1] || "").toLowerCase();
      let p = m[2].trim();
      if (p && state.scopeRel && !p.startsWith("/")) {
        p = (state.scopeRel + "/" + p).replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
      }
      return (label ? label + " " : "") + p;
    }).join("\n");

    const res = await api({ action: "bulk", lines: scoped });
    if (!res.ok) return alert(res.error || "Bulk failed");
    await refreshTree();
    alert("Bulk operations complete.");
  }

  // ---------- Preview ----------
  function refreshPreview(/* force = false */) {
    const on = $("#togglePreview")?.checked;
    if (!on) return;

    // Prefer current file; otherwise fall back to index.html (safer default)
    let rel = state.currentRel || "index.html";
    if (!/\.(php|html?|md|txt|css|js)$/i.test(rel)) rel = "index.html";

    const base = (state.cfg && state.cfg.workspace_url) || "/";
    const url = (base.endsWith("/") ? base : base + "/") + rel;

    const ifr = $("#preview");
    ifr.src = url + (url.includes("?") ? "&" : "?") + "_t=" + Date.now(); // cache-bust
  }

  // ---------- Link scan ----------
  async function scanLinks() {
    const res = await api({ action: "links" });
    if (!res.ok) return alert(res.error || "Scan failed");
    const box = $("#links");
    box.innerHTML = "";
    if (!res.edges.length) {
      box.textContent = "No references found.";
      return;
    }
    res.edges.forEach((e) => {
      const div = document.createElement("div");
      div.className = "edge";
      div.textContent = `${e.from}  →  ${e.to}`;
      box.appendChild(div);
    });
  }

  // ---------- Go ----------
  init();
})();


===== FILE: webide/assets/app.js @ 2025-10-19 23:08:57 =====
(async function () {
  // ---------- DOM helpers ----------
  const $ = (q, el = document) => el.querySelector(q),
        $$ = (qAll, el = document) => Array.from(el.querySelectorAll(qAll));

  // ---------- API helper (POST FormData) ----------
  const api = async (params) => {
    const form = new FormData();
    Object.entries(params).forEach(([k, v]) => {
      if (Array.isArray(v)) v.forEach((x) => form.append(k + "[]", x));
      else form.append(k, v);
    });
    const r = await fetch("api.php", { method: "POST", body: form });
    return r.json();
  };

  // ---------- State ----------
  const state = {
    currentRel: "",
    ace: null,
    cfg: null,
    dirty: false,
    saving: false,
    scopeRel: "" // '' = workspace root; otherwise e.g. 'admin/pages'
  };

  // ---------- Init ----------
  async function init() {
    state.cfg = await api({ action: "config" });

    // restore scope
    state.scopeRel = localStorage.getItem("mini_ide_scope") || "";

    bindUI();
    await refreshTree();

    // Editor: ACE if available, fallback to <textarea>
    if (state.cfg.ace && window.ace) {
      state.ace = ace.edit("editor");
      state.ace.setTheme("ace/theme/textmate");
      state.ace.session.setUseSoftTabs(true);
      state.ace.session.setTabSize(2);
      state.ace.setOption("fontSize", "13px");
      $("#ta").classList.add("hidden");
      state.ace.session.on("change", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    } else {
      $("#editor").classList.add("hidden");
      $("#ta").addEventListener("input", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    }

    // Load a sensible default (index.*) if present
    const guess = $$("#tree .file").find((n) =>
      /^(index\.(php|html?|md|txt))$/i.test(n.dataset.rel || "")
    );
    if (guess) openFile(guess.dataset.rel);
  }

  // ---------- UI wiring ----------
  function bindUI() {
    $("#refreshTree")?.addEventListener("click", refreshTree);
    $("#btnCreateFile")?.addEventListener("click", () => createFromInput(false));
    $("#btnCreateDir")?.addEventListener("click", () => createFromInput(true));
    $("#btnBulk")?.addEventListener("click", bulkCreate);
    $("#btnSave")?.addEventListener("click", saveCurrent);
    $("#btnRename")?.addEventListener("click", renameCurrent);
    $("#btnDelete")?.addEventListener("click", deleteCurrent);
    $("#btnScanLinks")?.addEventListener("click", scanLinks);
    $("#scopeReset")?.addEventListener("click", () => setScope(""));
    
        // existing
    $("#btnScanLinks")?.addEventListener("click", scanLinks);

    // right-panel linkmap controls
    $("#btnToggleLinks")?.addEventListener("click", toggleLinkmap);
    $("#refreshLinks")?.addEventListener("click", scanLinks);
    $("#btnCloseLinks")?.addEventListener("click", () => {
      document.body.classList.remove("linkmap-visible");
      localStorage.setItem("mini_ide_linkmap_hidden", "1");
    });

    
        // ---------- Floating tab reveal behaviour ----------
    // Keeps a temporary reveal state so we don't overwrite user's stored choice.
    // set default to hidden on first visit
if (localStorage.getItem("mini_ide_sidebar_hidden") === null) {
  localStorage.setItem("mini_ide_sidebar_hidden", "1");
}

// Restore sidebar state (existing code)
if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
  document.body.classList.add("sidebar-hidden");
}


    (function setupFloatingTabReveal() {
      const tab = $("#btnToggleSidebar");
      const sidebarEl = document.querySelector(".sidebar");
      if (!tab || !sidebarEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden"); // "1" if user had hidden

      // When pointer enters the floating tab: reveal the sidebar and hide the tab itself.
      tab.addEventListener("mouseenter", (e) => {
        // remember stored preference so we can restore it later
        prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden");
        document.body.classList.add("sidebar-visible");
        tempReveal = true;

        // reveal sidebar visually (do NOT change stored preference)
        document.body.classList.remove("sidebar-hidden");
        // hide the floating tab while user is interacting with sidebar
        tab.style.display = "none";
      });

      // When pointer leaves the sidebar area, hide the sidebar again if it was shown by the tab
      sidebarEl.addEventListener("mouseleave", (e) => {
        // Only auto-hide when this reveal was triggered by the tab (avoid interfering with manual toggle)
       // toggleSidebar();
       document.body.classList.remove("sidebar-visible");
        if (!tempReveal) return;

        // restore to user's stored preference
        if (prevStoredHidden === "1") {
          // user had explicitly hidden -> re-hide sidebar
          document.body.classList.add("sidebar-hidden");
        } else {
          // user had it visible by default — keep visible (or you can re-hide; sticking with restore)
          document.body.classList.remove("sidebar-hidden");
        }

        // show the floating tab again
        tab.style.display = "";
        tempReveal = false;
      });

      // Extra: if user moves quickly from the tab into the sidebar, keep it shown.
      // Also handle pointerout from tab to sidebar — do nothing because mouseover already removed tab.
      // If user clicks the tab (existing click handler remains), we still toggle as before.
    })();


    // Keyboard shortcuts (capture)
    window.addEventListener(
      "keydown",
      (e) => {
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
          e.preventDefault(); e.stopPropagation();
          saveCurrent();
        }
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
          e.preventDefault(); toggleSidebar();
        }
      },
      true
    );

    // Preview toggle
    $("#togglePreview")?.addEventListener("change", syncPreviewVisibility);

    // Sidebar toggle button
    $("#btnToggleSidebar")?.addEventListener("click", toggleSidebar);

    // Restore sidebar state
    if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
      document.body.classList.add("sidebar-hidden");
    }
  }

  function toggleSidebar() {
    const hidden = document.body.classList.toggle("sidebar-hidden");
    localStorage.setItem("mini_ide_sidebar_hidden", hidden ? "1" : "0");
  }
  
    function toggleLinkmap() {
    const hidden = document.body.classList.toggle("linkmap-visible");
    // store the inverse meaning: "hidden" = 1
    localStorage.setItem("mini_ide_linkmap_hidden", hidden ? "0" : "1");
  }


  function syncPreviewVisibility() {
    const on = $("#togglePreview")?.checked;
    if (on == null) return;
    $("#preview").style.display = on ? "block" : "none";
    document.querySelector(".editorWrap").style.gridTemplateColumns = on ? "1fr 40%" : "1fr";
  }

  // ---------- Save status / dirty UI ----------
  function setSaveBusy(on) {
    // big overlay
    const overlay = $("#pageLoader");
    if (overlay) overlay.style.display = on ? "flex" : "none";

    // header spinner
    const loader = $("#saveLoader");
    if (loader) loader.style.visibility = on ? "visible" : "hidden";

    const dot = $("#saveStatusDot");
    if (dot && on) dot.classList.remove("ok", "err", "dirty");
  }
  function setSaveDirty(on) {
    const btn = $("#btnSave");
    if (btn) btn.style.borderColor = on ? "#c9e8dc" : "var(--border)";
  }
  function markSaveOK() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("err", "dirty");
    dot.classList.add("ok");
    setTimeout(() => dot.classList.remove("ok"), 900);
  }
  function markSaveErr() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("ok", "dirty");
    dot.classList.add("err");
  }
  function updateDirtyUI() {
    // header asterisk
    const cp = $("#currentPath");
    if (cp) cp.textContent = (state.currentRel || "—") + (state.dirty ? " *" : "");

    // tree asterisk (on current file only)
    $$("#tree .file .name").forEach((n) => n.classList.remove("dirty"));
    if (state.currentRel) {
      const n = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"] .name`);
      if (n && state.dirty) n.classList.add("dirty");
    }

    // status dot amber while dirty
    const dot = $("#saveStatusDot");
    if (dot) {
      dot.classList.remove("ok", "err");
      dot.classList.toggle("dirty", !!state.dirty);
    }
  }

  // ---------- Scope helpers ----------
  function setScope(rel) {
    state.scopeRel = (rel || "").replace(/^\/+|\/+$/g, "");
    localStorage.setItem("mini_ide_scope", state.scopeRel);
    refreshTree();
  }

  function renderScopeCrumbs(){
    const wrap = $("#scopeCrumbs"); if (!wrap) return;
    wrap.innerHTML = "";
    const mk = (txt, rel, isCurrent=false) => {
      const s = document.createElement("span");
      s.className = "crumb" + (isCurrent ? " current" : "");
      s.textContent = txt;
      s.onclick = () => !isCurrent && setScope(rel);
      if (isCurrent) s.title = "Current folder";
      return s;
    };
    const parts = (state.scopeRel || "").split("/").filter(Boolean);
    wrap.appendChild(mk("root", "", parts.length === 0));
    let acc = "";
    parts.forEach((p, i) => {
      const sep = document.createElement("span");
      sep.className = "sep"; sep.textContent = "/";
      wrap.appendChild(sep);
      acc = acc ? acc + "/" + p : p;
      wrap.appendChild(mk(p, acc, i === parts.length - 1));
    });
  }

  // ---------- Tree (server-scoped, collapsible) ----------
  function toNested(items) {
    const root = { name: "", rel: "", dirs: {}, files: [] };
    const entries = items
      .map((it) => ({ type: it.type, rel: (it.rel || "").replace(/^\/+/, "") }))
      .filter((e) => e.rel);

    // ensure nodes
    entries.forEach((e) => {
      const parts = e.rel.split("/");
      if (e.type === "dir") {
        let cur = root;
        for (const part of parts) {
          cur.dirs[part] = cur.dirs[part] || {
            name: part,
            rel: (cur.rel ? cur.rel + "/" : "") + part,
            dirs: {},
            files: []
          };
          cur = cur.dirs[part];
        }
      }
    });

    // place files
    entries.filter((e) => e.type === "file").forEach((e) => {
      const parts = e.rel.split("/");
      const name = parts.pop();
      let cur = root;
      for (const part of parts) {
        cur.dirs[part] = cur.dirs[part] || {
          name: part,
          rel: (cur.rel ? cur.rel + "/" : "") + part,
          dirs: {},
          files: []
        };
        cur = cur.dirs[part];
      }
      cur.files.push({ name, rel: e.rel });
    });

    // sort
    (function sort(n) {
      Object.values(n.dirs).forEach(sort);
      n.files.sort((a, b) => a.name.localeCompare(b.name));
    })(root);

    return root;
  }

  function renderTree(rootNode, container) {
    const openState = JSON.parse(localStorage.getItem("mini_ide_open_dirs") || "{}");
    const setOpen = (rel, open) => {
      if (open) openState[rel || "/"] = 1;
      else delete openState[rel || "/"];
      localStorage.setItem("mini_ide_open_dirs", JSON.stringify(openState));
    };

    container.innerHTML = "";

    const mkDir = (node) => {
      const det = document.createElement("details");
      if (openState[node.rel || "/"]) det.open = true;

      const sum = document.createElement("summary");
      sum.innerHTML = `
        <span class="chev">›</span>
        <span class="label name">📁 ${node.name || "root"}</span>
        <button class="focus" type="button" title="Focus here" data-rel="${node.rel}">📌 Focus</button>
      `;
      det.appendChild(sum);

      // node.rel from server is already full root-relative path
      const dirRel = (node.rel || "");

      // focus controls
      sum.ondblclick = (e) => { e.preventDefault(); setScope(dirRel); };
      sum.querySelector(".focus").onclick = (e) => { e.stopPropagation(); setScope(dirRel); };

      det.addEventListener("toggle", () => setOpen(node.rel, det.open));

      // child dirs
      const dirNames = Object.keys(node.dirs).sort((a, b) => a.localeCompare(b));
      dirNames.forEach((name) => det.appendChild(mkDir(node.dirs[name])));

      // files: use rel from server as-is (already full)
      node.files.forEach((f) => {
        const div = document.createElement("div");
        div.className = "file";
        div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
        div.dataset.rel = f.rel;
        div.onclick = () => openFile(f.rel);
        det.appendChild(div);
      });

      return det;
    };

    // top-level dirs
    const dirNames = Object.keys(rootNode.dirs).sort((a, b) => a.localeCompare(b));
    dirNames.forEach((name) => container.appendChild(mkDir(rootNode.dirs[name])));

    // top-level files: use rel as-is
    rootNode.files.forEach((f) => {
      const div = document.createElement("div");
      div.className = "file";
      div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
      div.dataset.rel = f.rel;
      div.onclick = () => openFile(f.rel);
      container.appendChild(div);
    });
  }

  async function refreshTree() {
    try {
      const res = await api({ action: "tree", rel: state.scopeRel });
      if (!res || res.ok !== true || !Array.isArray(res.items)) {
        const msg = res && res.error ? res.error : "Tree request failed";
        alert("Mini IDE error: " + msg);
        console.error("Tree response:", res);
        return;
      }
      const nested = toNested(res.items);
      renderTree(nested, $("#tree"));
      renderScopeCrumbs();

      // re-highlight current file if visible
      if (state.currentRel) {
        $$("#tree .file").forEach((n) => n.classList.remove("active"));
        const hit = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"]`);
        if (hit) hit.classList.add("active");
        updateDirtyUI();
      }
    } catch (e) {
      alert("Mini IDE fetch error (tree): " + e);
      console.error(e);
    }
  }

  // ---------- File ops ----------
  async function openFile(rel) {
    const res = await api({ action: "read", rel });
    if (!res.ok) return alert(res.error || "Failed to read");
    state.currentRel = rel;

    // highlight selected
    $$("#tree .file").forEach((n) => n.classList.remove("active"));
    const hit = $(`#tree .file[data-rel="${CSS.escape(rel)}"]`);
    if (hit) hit.classList.add("active");

    if (res.file.editable) {
      setEditorContent(res.file.content || "");
    } else {
      setEditorContent("// Not editable (binary or large). Size: " + res.file.size + " bytes\n");
    }
    state.dirty = false;
    setSaveDirty(false);
    updateDirtyUI();
    refreshPreview();
  }

  function getEditorContent() {
    if (state.ace) return state.ace.getValue();
    return $("#ta").value;
  }

  function setEditorContent(txt) {
    if (state.ace) {
      state.ace.setValue(txt ?? "", -1);
      // Set mode from extension
      const ext = (state.currentRel.split(".").pop() || "").toLowerCase();
      const map = {
        js: "javascript",
        css: "css",
        php: "php",
        html: "html",
        htm: "html",
        json: "json",
        md: "markdown",
        py: "python",
        yml: "yaml",
        yaml: "yaml",
        c: "c_cpp",
        cpp: "c_cpp",
        h: "c_cpp",
        sql: "sql",
        csv: "text",
        txt: "text"
      };
      state.ace.session.setMode("ace/mode/" + (map[ext] || "text"));
    } else {
      $("#ta").classList.remove("hidden");
      $("#ta").value = txt ?? "";
    }
  }

  async function saveCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (state.saving) return; // prevent double-taps
    state.saving = true;
    setSaveBusy(true);

    try {
      const content = getEditorContent();
      const res = await api({ action: "save", rel: state.currentRel, content });
      if (!res.ok) {
        markSaveErr();
        throw new Error(res.error || "Save failed");
      }

      // Server verification
      let verified = !!res.verify_ok;

      // mtime wait (shared hosts can be slow)
      const targetMtime = res.mtime || 0;
      const okStat = await waitForStat(state.currentRel, targetMtime, 8, 150);
      if (!okStat) console.warn("Proceeding without stat confirmation");

      // Optional client-side readback if server said not verified
      if (!verified) {
        try {
          const rb = await api({ action: "read", rel: state.currentRel });
          if (rb.ok && rb.file && typeof rb.file.content === "string") {
            verified = (rb.file.content === content);
          }
        } catch {}
      }

      if (!verified) {
        markSaveErr();
        alert("Save did not verify. The file contents on disk did not match what was sent.");
        return; // do not mark OK or clear dirty
      }

      state.dirty = false;
      setSaveDirty(false);
      updateDirtyUI();   // clear asterisks + amber dot
      markSaveOK();      // green blip
      refreshPreview(true);

    } catch (e) {
      console.error(e);
      alert("Save error: " + e.message);
      markSaveErr();     // red
    } finally {
      state.saving = false;
      setSaveBusy(false);
    }
  }

  // poll stat until mtime >= target
  async function waitForStat(rel, target, tries = 8, delay = 150) {
    for (let i = 0; i < tries; i++) {
      try {
        const s = await api({ action: "stat", rel });
        if (s.ok && (s.mtime || 0) >= target) return true;
      } catch {}
      await new Promise((r) => setTimeout(r, delay));
    }
    return false;
  }

  async function renameCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    const to = prompt("Rename to (path):", state.currentRel);
    if (!to || to === state.currentRel) return;
    const res = await api({ action: "rename", from: state.currentRel, to });
    if (!res.ok) return alert(res.error || "Rename failed");
    state.currentRel = to;
    $("#currentPath").textContent = to;
    await refreshTree();
    refreshPreview();
  }

  async function deleteCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (!confirm("Delete " + state.currentRel + " ?")) return;
    const res = await api({ action: "delete", rel: state.currentRel });
    if (!res.ok) return alert(res.error || "Delete failed");
    state.currentRel = "";
    $("#currentPath").textContent = "—";
    if (state.ace) state.ace.setValue("");
    else $("#ta").value = "";
    await refreshTree();
    refreshPreview();
  }

  async function createFromInput(isDir) {
    const raw = $("#newPath").value.trim();
    if (!raw) return;

    const relPath = state.scopeRel
      ? (state.scopeRel + "/" + raw).replace(/\/+/g, "/").replace(/^\/|\/$/g, "")
      : raw;

    const res = await api({ action: "create", rel: relPath, type: isDir ? "dir" : "file" });
    if (!res.ok) return alert(res.error || "Create failed");
    $("#newPath").value = "";
    await refreshTree();
    if (!isDir) openFile(relPath);
  }

  async function bulkCreate() {
    const lines = $("#bulkLines").value;
    if (!lines.trim()) return;

    // Prefix each line's path with scope (keep labels 'dir:'/'file:' intact)
    const scoped = lines.split("\n").map((t) => {
      const ln = t.trim();
      if (!ln) return ln;
      const m = ln.match(/^(dir:|file:)?\s*(.*)$/i);
      if (!m) return ln;
      const label = (m[1] || "").toLowerCase();
      let p = m[2].trim();
      if (p && state.scopeRel && !p.startsWith("/")) {
        p = (state.scopeRel + "/" + p).replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
      }
      return (label ? label + " " : "") + p;
    }).join("\n");

    const res = await api({ action: "bulk", lines: scoped });
    if (!res.ok) return alert(res.error || "Bulk failed");
    await refreshTree();
    alert("Bulk operations complete.");
  }

  // ---------- Preview ----------
  function refreshPreview(/* force = false */) {
    const on = $("#togglePreview")?.checked;
    if (!on) return;

    // Prefer current file; otherwise fall back to index.html (safer default)
    let rel = state.currentRel || "index.html";
    if (!/\.(php|html?|md|txt|css|js)$/i.test(rel)) rel = "index.html";

    const base = (state.cfg && state.cfg.workspace_url) || "/";
    const url = (base.endsWith("/") ? base : base + "/") + rel;

    const ifr = $("#preview");
    ifr.src = url + (url.includes("?") ? "&" : "?") + "_t=" + Date.now(); // cache-bust
  }

  // ---------- Link scan ----------
  async function scanLinks() {
    const res = await api({ action: "links" });
    if (!res.ok) return alert(res.error || "Scan failed");
    const box = $("#links");
    box.innerHTML = "";
    if (!res.edges.length) {
      box.textContent = "No references found.";
      return;
    }
    res.edges.forEach((e) => {
      const div = document.createElement("div");
      div.className = "edge";
      div.textContent = `${e.from}  →  ${e.to}`;
      box.appendChild(div);
    });
  }

  // ---------- Go ----------
  init();
})();


===== FILE: webide/assets/app.js @ 2025-10-19 23:09:28 =====
(async function () {
  // ---------- DOM helpers ----------
  const $ = (q, el = document) => el.querySelector(q),
        $$ = (qAll, el = document) => Array.from(el.querySelectorAll(qAll));

  // ---------- API helper (POST FormData) ----------
  const api = async (params) => {
    const form = new FormData();
    Object.entries(params).forEach(([k, v]) => {
      if (Array.isArray(v)) v.forEach((x) => form.append(k + "[]", x));
      else form.append(k, v);
    });
    const r = await fetch("api.php", { method: "POST", body: form });
    return r.json();
  };

  // ---------- State ----------
  const state = {
    currentRel: "",
    ace: null,
    cfg: null,
    dirty: false,
    saving: false,
    scopeRel: "" // '' = workspace root; otherwise e.g. 'admin/pages'
  };

  // ---------- Init ----------
  async function init() {
    state.cfg = await api({ action: "config" });

    // restore scope
    state.scopeRel = localStorage.getItem("mini_ide_scope") || "";

    bindUI();
    await refreshTree();

    // Editor: ACE if available, fallback to <textarea>
    if (state.cfg.ace && window.ace) {
      state.ace = ace.edit("editor");
      state.ace.setTheme("ace/theme/textmate");
      state.ace.session.setUseSoftTabs(true);
      state.ace.session.setTabSize(2);
      state.ace.setOption("fontSize", "13px");
      $("#ta").classList.add("hidden");
      state.ace.session.on("change", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    } else {
      $("#editor").classList.add("hidden");
      $("#ta").addEventListener("input", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    }

    // Load a sensible default (index.*) if present
    const guess = $$("#tree .file").find((n) =>
      /^(index\.(php|html?|md|txt))$/i.test(n.dataset.rel || "")
    );
    if (guess) openFile(guess.dataset.rel);
  }

  // ---------- UI wiring ----------
  function bindUI() {
    $("#refreshTree")?.addEventListener("click", refreshTree);
    $("#btnCreateFile")?.addEventListener("click", () => createFromInput(false));
    $("#btnCreateDir")?.addEventListener("click", () => createFromInput(true));
    $("#btnBulk")?.addEventListener("click", bulkCreate);
    $("#btnSave")?.addEventListener("click", saveCurrent);
    $("#btnRename")?.addEventListener("click", renameCurrent);
    $("#btnDelete")?.addEventListener("click", deleteCurrent);
    $("#btnScanLinks")?.addEventListener("click", scanLinks);
    $("#scopeReset")?.addEventListener("click", () => setScope(""));
    
        // existing
    $("#btnScanLinks")?.addEventListener("click", scanLinks);

    // right-panel linkmap controls
    $("#btnToggleLinks")?.addEventListener("click", toggleLinkmap);
    $("#refreshLinks")?.addEventListener("click", scanLinks);
    $("#btnCloseLinks")?.addEventListener("click", () => {
      document.body.classList.remove("linkmap-visible");
      localStorage.setItem("mini_ide_linkmap_hidden", "1");
    });

    
        // ---------- Floating tab reveal behaviour ----------
    // Keeps a temporary reveal state so we don't overwrite user's stored choice.
    // set default to hidden on first visit
if (localStorage.getItem("mini_ide_sidebar_hidden") === null) {
  localStorage.setItem("mini_ide_sidebar_hidden", "1");
}

// Restore sidebar state (existing code)
if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
  document.body.classList.add("sidebar-hidden");
}


    (function setupFloatingTabReveal() {
      const tab = $("#btnToggleSidebar");
      const sidebarEl = document.querySelector(".sidebar");
      if (!tab || !sidebarEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden"); // "1" if user had hidden

      // When pointer enters the floating tab: reveal the sidebar and hide the tab itself.
      tab.addEventListener("mouseenter", (e) => {
        // remember stored preference so we can restore it later
        prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden");
        document.body.classList.add("sidebar-visible");
        tempReveal = true;

        // reveal sidebar visually (do NOT change stored preference)
        document.body.classList.remove("sidebar-hidden");
        // hide the floating tab while user is interacting with sidebar
        tab.style.display = "none";
      });

      // When pointer leaves the sidebar area, hide the sidebar again if it was shown by the tab
      sidebarEl.addEventListener("mouseleave", (e) => {
        // Only auto-hide when this reveal was triggered by the tab (avoid interfering with manual toggle)
       // toggleSidebar();
       document.body.classList.remove("sidebar-visible");
        if (!tempReveal) return;

        // restore to user's stored preference
        if (prevStoredHidden === "1") {
          // user had explicitly hidden -> re-hide sidebar
          document.body.classList.add("sidebar-hidden");
        } else {
          // user had it visible by default — keep visible (or you can re-hide; sticking with restore)
          document.body.classList.remove("sidebar-hidden");
        }

        // show the floating tab again
        tab.style.display = "";
        tempReveal = false;
      });

      // Extra: if user moves quickly from the tab into the sidebar, keep it shown.
      // Also handle pointerout from tab to sidebar — do nothing because mouseover already removed tab.
      // If user clicks the tab (existing click handler remains), we still toggle as before.
    })();

    // Restore linkmap state (default hidden)
    if (localStorage.getItem("mini_ide_linkmap_hidden") === null) {
      localStorage.setItem("mini_ide_linkmap_hidden", "1"); // hide by default
    }
    if (localStorage.getItem("mini_ide_linkmap_hidden") !== "1") {
      document.body.classList.add("linkmap-visible");
    }

    (function setupFloatingLinkmapReveal() {
      const tab = $("#btnToggleLinks");
      const panelEl = document.querySelector(".linkmap-panel");
      if (!tab || !panelEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");

      // pointer enters the floating tab -> reveal
      tab.addEventListener("mouseenter", () => {
        prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");
        document.body.classList.add("linkmap-visible");
        tempReveal = true;
        tab.style.display = "none"; // hide tab while interacting
      });

      // when pointer leaves the panel, restore preference
      panelEl.addEventListener("mouseleave", () => {
        document.body.classList.remove("linkmap-visible");
        if (!tempReveal) return;
        if (prevStoredHidden === "1") {
          document.body.classList.remove("linkmap-visible");
        } else {
          document.body.classList.add("linkmap-visible");
        }
        tab.style.display = "";
        tempReveal = false;
      });
    })();

    // Keyboard shortcuts (capture)
    window.addEventListener(
      "keydown",
      (e) => {
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
          e.preventDefault(); e.stopPropagation();
          saveCurrent();
        }
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
          e.preventDefault(); toggleSidebar();
        }
      },
      true
    );

    // Preview toggle
    $("#togglePreview")?.addEventListener("change", syncPreviewVisibility);

    // Sidebar toggle button
    $("#btnToggleSidebar")?.addEventListener("click", toggleSidebar);

    // Restore sidebar state
    if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
      document.body.classList.add("sidebar-hidden");
    }
  }

  function toggleSidebar() {
    const hidden = document.body.classList.toggle("sidebar-hidden");
    localStorage.setItem("mini_ide_sidebar_hidden", hidden ? "1" : "0");
  }
  
    function toggleLinkmap() {
    const hidden = document.body.classList.toggle("linkmap-visible");
    // store the inverse meaning: "hidden" = 1
    localStorage.setItem("mini_ide_linkmap_hidden", hidden ? "0" : "1");
  }


  function syncPreviewVisibility() {
    const on = $("#togglePreview")?.checked;
    if (on == null) return;
    $("#preview").style.display = on ? "block" : "none";
    document.querySelector(".editorWrap").style.gridTemplateColumns = on ? "1fr 40%" : "1fr";
  }

  // ---------- Save status / dirty UI ----------
  function setSaveBusy(on) {
    // big overlay
    const overlay = $("#pageLoader");
    if (overlay) overlay.style.display = on ? "flex" : "none";

    // header spinner
    const loader = $("#saveLoader");
    if (loader) loader.style.visibility = on ? "visible" : "hidden";

    const dot = $("#saveStatusDot");
    if (dot && on) dot.classList.remove("ok", "err", "dirty");
  }
  function setSaveDirty(on) {
    const btn = $("#btnSave");
    if (btn) btn.style.borderColor = on ? "#c9e8dc" : "var(--border)";
  }
  function markSaveOK() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("err", "dirty");
    dot.classList.add("ok");
    setTimeout(() => dot.classList.remove("ok"), 900);
  }
  function markSaveErr() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("ok", "dirty");
    dot.classList.add("err");
  }
  function updateDirtyUI() {
    // header asterisk
    const cp = $("#currentPath");
    if (cp) cp.textContent = (state.currentRel || "—") + (state.dirty ? " *" : "");

    // tree asterisk (on current file only)
    $$("#tree .file .name").forEach((n) => n.classList.remove("dirty"));
    if (state.currentRel) {
      const n = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"] .name`);
      if (n && state.dirty) n.classList.add("dirty");
    }

    // status dot amber while dirty
    const dot = $("#saveStatusDot");
    if (dot) {
      dot.classList.remove("ok", "err");
      dot.classList.toggle("dirty", !!state.dirty);
    }
  }

  // ---------- Scope helpers ----------
  function setScope(rel) {
    state.scopeRel = (rel || "").replace(/^\/+|\/+$/g, "");
    localStorage.setItem("mini_ide_scope", state.scopeRel);
    refreshTree();
  }

  function renderScopeCrumbs(){
    const wrap = $("#scopeCrumbs"); if (!wrap) return;
    wrap.innerHTML = "";
    const mk = (txt, rel, isCurrent=false) => {
      const s = document.createElement("span");
      s.className = "crumb" + (isCurrent ? " current" : "");
      s.textContent = txt;
      s.onclick = () => !isCurrent && setScope(rel);
      if (isCurrent) s.title = "Current folder";
      return s;
    };
    const parts = (state.scopeRel || "").split("/").filter(Boolean);
    wrap.appendChild(mk("root", "", parts.length === 0));
    let acc = "";
    parts.forEach((p, i) => {
      const sep = document.createElement("span");
      sep.className = "sep"; sep.textContent = "/";
      wrap.appendChild(sep);
      acc = acc ? acc + "/" + p : p;
      wrap.appendChild(mk(p, acc, i === parts.length - 1));
    });
  }

  // ---------- Tree (server-scoped, collapsible) ----------
  function toNested(items) {
    const root = { name: "", rel: "", dirs: {}, files: [] };
    const entries = items
      .map((it) => ({ type: it.type, rel: (it.rel || "").replace(/^\/+/, "") }))
      .filter((e) => e.rel);

    // ensure nodes
    entries.forEach((e) => {
      const parts = e.rel.split("/");
      if (e.type === "dir") {
        let cur = root;
        for (const part of parts) {
          cur.dirs[part] = cur.dirs[part] || {
            name: part,
            rel: (cur.rel ? cur.rel + "/" : "") + part,
            dirs: {},
            files: []
          };
          cur = cur.dirs[part];
        }
      }
    });

    // place files
    entries.filter((e) => e.type === "file").forEach((e) => {
      const parts = e.rel.split("/");
      const name = parts.pop();
      let cur = root;
      for (const part of parts) {
        cur.dirs[part] = cur.dirs[part] || {
          name: part,
          rel: (cur.rel ? cur.rel + "/" : "") + part,
          dirs: {},
          files: []
        };
        cur = cur.dirs[part];
      }
      cur.files.push({ name, rel: e.rel });
    });

    // sort
    (function sort(n) {
      Object.values(n.dirs).forEach(sort);
      n.files.sort((a, b) => a.name.localeCompare(b.name));
    })(root);

    return root;
  }

  function renderTree(rootNode, container) {
    const openState = JSON.parse(localStorage.getItem("mini_ide_open_dirs") || "{}");
    const setOpen = (rel, open) => {
      if (open) openState[rel || "/"] = 1;
      else delete openState[rel || "/"];
      localStorage.setItem("mini_ide_open_dirs", JSON.stringify(openState));
    };

    container.innerHTML = "";

    const mkDir = (node) => {
      const det = document.createElement("details");
      if (openState[node.rel || "/"]) det.open = true;

      const sum = document.createElement("summary");
      sum.innerHTML = `
        <span class="chev">›</span>
        <span class="label name">📁 ${node.name || "root"}</span>
        <button class="focus" type="button" title="Focus here" data-rel="${node.rel}">📌 Focus</button>
      `;
      det.appendChild(sum);

      // node.rel from server is already full root-relative path
      const dirRel = (node.rel || "");

      // focus controls
      sum.ondblclick = (e) => { e.preventDefault(); setScope(dirRel); };
      sum.querySelector(".focus").onclick = (e) => { e.stopPropagation(); setScope(dirRel); };

      det.addEventListener("toggle", () => setOpen(node.rel, det.open));

      // child dirs
      const dirNames = Object.keys(node.dirs).sort((a, b) => a.localeCompare(b));
      dirNames.forEach((name) => det.appendChild(mkDir(node.dirs[name])));

      // files: use rel from server as-is (already full)
      node.files.forEach((f) => {
        const div = document.createElement("div");
        div.className = "file";
        div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
        div.dataset.rel = f.rel;
        div.onclick = () => openFile(f.rel);
        det.appendChild(div);
      });

      return det;
    };

    // top-level dirs
    const dirNames = Object.keys(rootNode.dirs).sort((a, b) => a.localeCompare(b));
    dirNames.forEach((name) => container.appendChild(mkDir(rootNode.dirs[name])));

    // top-level files: use rel as-is
    rootNode.files.forEach((f) => {
      const div = document.createElement("div");
      div.className = "file";
      div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
      div.dataset.rel = f.rel;
      div.onclick = () => openFile(f.rel);
      container.appendChild(div);
    });
  }

  async function refreshTree() {
    try {
      const res = await api({ action: "tree", rel: state.scopeRel });
      if (!res || res.ok !== true || !Array.isArray(res.items)) {
        const msg = res && res.error ? res.error : "Tree request failed";
        alert("Mini IDE error: " + msg);
        console.error("Tree response:", res);
        return;
      }
      const nested = toNested(res.items);
      renderTree(nested, $("#tree"));
      renderScopeCrumbs();

      // re-highlight current file if visible
      if (state.currentRel) {
        $$("#tree .file").forEach((n) => n.classList.remove("active"));
        const hit = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"]`);
        if (hit) hit.classList.add("active");
        updateDirtyUI();
      }
    } catch (e) {
      alert("Mini IDE fetch error (tree): " + e);
      console.error(e);
    }
  }

  // ---------- File ops ----------
  async function openFile(rel) {
    const res = await api({ action: "read", rel });
    if (!res.ok) return alert(res.error || "Failed to read");
    state.currentRel = rel;

    // highlight selected
    $$("#tree .file").forEach((n) => n.classList.remove("active"));
    const hit = $(`#tree .file[data-rel="${CSS.escape(rel)}"]`);
    if (hit) hit.classList.add("active");

    if (res.file.editable) {
      setEditorContent(res.file.content || "");
    } else {
      setEditorContent("// Not editable (binary or large). Size: " + res.file.size + " bytes\n");
    }
    state.dirty = false;
    setSaveDirty(false);
    updateDirtyUI();
    refreshPreview();
  }

  function getEditorContent() {
    if (state.ace) return state.ace.getValue();
    return $("#ta").value;
  }

  function setEditorContent(txt) {
    if (state.ace) {
      state.ace.setValue(txt ?? "", -1);
      // Set mode from extension
      const ext = (state.currentRel.split(".").pop() || "").toLowerCase();
      const map = {
        js: "javascript",
        css: "css",
        php: "php",
        html: "html",
        htm: "html",
        json: "json",
        md: "markdown",
        py: "python",
        yml: "yaml",
        yaml: "yaml",
        c: "c_cpp",
        cpp: "c_cpp",
        h: "c_cpp",
        sql: "sql",
        csv: "text",
        txt: "text"
      };
      state.ace.session.setMode("ace/mode/" + (map[ext] || "text"));
    } else {
      $("#ta").classList.remove("hidden");
      $("#ta").value = txt ?? "";
    }
  }

  async function saveCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (state.saving) return; // prevent double-taps
    state.saving = true;
    setSaveBusy(true);

    try {
      const content = getEditorContent();
      const res = await api({ action: "save", rel: state.currentRel, content });
      if (!res.ok) {
        markSaveErr();
        throw new Error(res.error || "Save failed");
      }

      // Server verification
      let verified = !!res.verify_ok;

      // mtime wait (shared hosts can be slow)
      const targetMtime = res.mtime || 0;
      const okStat = await waitForStat(state.currentRel, targetMtime, 8, 150);
      if (!okStat) console.warn("Proceeding without stat confirmation");

      // Optional client-side readback if server said not verified
      if (!verified) {
        try {
          const rb = await api({ action: "read", rel: state.currentRel });
          if (rb.ok && rb.file && typeof rb.file.content === "string") {
            verified = (rb.file.content === content);
          }
        } catch {}
      }

      if (!verified) {
        markSaveErr();
        alert("Save did not verify. The file contents on disk did not match what was sent.");
        return; // do not mark OK or clear dirty
      }

      state.dirty = false;
      setSaveDirty(false);
      updateDirtyUI();   // clear asterisks + amber dot
      markSaveOK();      // green blip
      refreshPreview(true);

    } catch (e) {
      console.error(e);
      alert("Save error: " + e.message);
      markSaveErr();     // red
    } finally {
      state.saving = false;
      setSaveBusy(false);
    }
  }

  // poll stat until mtime >= target
  async function waitForStat(rel, target, tries = 8, delay = 150) {
    for (let i = 0; i < tries; i++) {
      try {
        const s = await api({ action: "stat", rel });
        if (s.ok && (s.mtime || 0) >= target) return true;
      } catch {}
      await new Promise((r) => setTimeout(r, delay));
    }
    return false;
  }

  async function renameCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    const to = prompt("Rename to (path):", state.currentRel);
    if (!to || to === state.currentRel) return;
    const res = await api({ action: "rename", from: state.currentRel, to });
    if (!res.ok) return alert(res.error || "Rename failed");
    state.currentRel = to;
    $("#currentPath").textContent = to;
    await refreshTree();
    refreshPreview();
  }

  async function deleteCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (!confirm("Delete " + state.currentRel + " ?")) return;
    const res = await api({ action: "delete", rel: state.currentRel });
    if (!res.ok) return alert(res.error || "Delete failed");
    state.currentRel = "";
    $("#currentPath").textContent = "—";
    if (state.ace) state.ace.setValue("");
    else $("#ta").value = "";
    await refreshTree();
    refreshPreview();
  }

  async function createFromInput(isDir) {
    const raw = $("#newPath").value.trim();
    if (!raw) return;

    const relPath = state.scopeRel
      ? (state.scopeRel + "/" + raw).replace(/\/+/g, "/").replace(/^\/|\/$/g, "")
      : raw;

    const res = await api({ action: "create", rel: relPath, type: isDir ? "dir" : "file" });
    if (!res.ok) return alert(res.error || "Create failed");
    $("#newPath").value = "";
    await refreshTree();
    if (!isDir) openFile(relPath);
  }

  async function bulkCreate() {
    const lines = $("#bulkLines").value;
    if (!lines.trim()) return;

    // Prefix each line's path with scope (keep labels 'dir:'/'file:' intact)
    const scoped = lines.split("\n").map((t) => {
      const ln = t.trim();
      if (!ln) return ln;
      const m = ln.match(/^(dir:|file:)?\s*(.*)$/i);
      if (!m) return ln;
      const label = (m[1] || "").toLowerCase();
      let p = m[2].trim();
      if (p && state.scopeRel && !p.startsWith("/")) {
        p = (state.scopeRel + "/" + p).replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
      }
      return (label ? label + " " : "") + p;
    }).join("\n");

    const res = await api({ action: "bulk", lines: scoped });
    if (!res.ok) return alert(res.error || "Bulk failed");
    await refreshTree();
    alert("Bulk operations complete.");
  }

  // ---------- Preview ----------
  function refreshPreview(/* force = false */) {
    const on = $("#togglePreview")?.checked;
    if (!on) return;

    // Prefer current file; otherwise fall back to index.html (safer default)
    let rel = state.currentRel || "index.html";
    if (!/\.(php|html?|md|txt|css|js)$/i.test(rel)) rel = "index.html";

    const base = (state.cfg && state.cfg.workspace_url) || "/";
    const url = (base.endsWith("/") ? base : base + "/") + rel;

    const ifr = $("#preview");
    ifr.src = url + (url.includes("?") ? "&" : "?") + "_t=" + Date.now(); // cache-bust
  }

  // ---------- Link scan ----------
  async function scanLinks() {
    const res = await api({ action: "links" });
    if (!res.ok) return alert(res.error || "Scan failed");
    const box = $("#links");
    box.innerHTML = "";
    if (!res.edges.length) {
      box.textContent = "No references found.";
      return;
    }
    res.edges.forEach((e) => {
      const div = document.createElement("div");
      div.className = "edge";
      div.textContent = `${e.from}  →  ${e.to}`;
      box.appendChild(div);
    });
  }

  // ---------- Go ----------
  init();
})();


===== FILE: webide/assets/app.js @ 2025-10-19 23:09:51 =====
(async function () {
  // ---------- DOM helpers ----------
  const $ = (q, el = document) => el.querySelector(q),
        $$ = (qAll, el = document) => Array.from(el.querySelectorAll(qAll));

  // ---------- API helper (POST FormData) ----------
  const api = async (params) => {
    const form = new FormData();
    Object.entries(params).forEach(([k, v]) => {
      if (Array.isArray(v)) v.forEach((x) => form.append(k + "[]", x));
      else form.append(k, v);
    });
    const r = await fetch("api.php", { method: "POST", body: form });
    return r.json();
  };

  // ---------- State ----------
  const state = {
    currentRel: "",
    ace: null,
    cfg: null,
    dirty: false,
    saving: false,
    scopeRel: "" // '' = workspace root; otherwise e.g. 'admin/pages'
  };

  // ---------- Init ----------
  async function init() {
    state.cfg = await api({ action: "config" });

    // restore scope
    state.scopeRel = localStorage.getItem("mini_ide_scope") || "";

    bindUI();
    await refreshTree();

    // Editor: ACE if available, fallback to <textarea>
    if (state.cfg.ace && window.ace) {
      state.ace = ace.edit("editor");
      state.ace.setTheme("ace/theme/textmate");
      state.ace.session.setUseSoftTabs(true);
      state.ace.session.setTabSize(2);
      state.ace.setOption("fontSize", "13px");
      $("#ta").classList.add("hidden");
      state.ace.session.on("change", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    } else {
      $("#editor").classList.add("hidden");
      $("#ta").addEventListener("input", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    }

    // Load a sensible default (index.*) if present
    const guess = $$("#tree .file").find((n) =>
      /^(index\.(php|html?|md|txt))$/i.test(n.dataset.rel || "")
    );
    if (guess) openFile(guess.dataset.rel);
  }

  // ---------- UI wiring ----------
  function bindUI() {
    $("#refreshTree")?.addEventListener("click", refreshTree);
    $("#btnCreateFile")?.addEventListener("click", () => createFromInput(false));
    $("#btnCreateDir")?.addEventListener("click", () => createFromInput(true));
    $("#btnBulk")?.addEventListener("click", bulkCreate);
    $("#btnSave")?.addEventListener("click", saveCurrent);
    $("#btnRename")?.addEventListener("click", renameCurrent);
    $("#btnDelete")?.addEventListener("click", deleteCurrent);
    $("#btnScanLinks")?.addEventListener("click", scanLinks);
    $("#scopeReset")?.addEventListener("click", () => setScope(""));
    
        // existing
    $("#btnScanLinks")?.addEventListener("click", scanLinks);

    // right-panel linkmap controls
    $("#btnToggleLinks")?.addEventListener("click", toggleLinkmap);
    $("#refreshLinks")?.addEventListener("click", scanLinks);
    $("#btnCloseLinks")?.addEventListener("click", () => {
      document.body.classList.remove("linkmap-visible");
      localStorage.setItem("mini_ide_linkmap_hidden", "1");
    });

    
        // ---------- Floating tab reveal behaviour ----------
    // Keeps a temporary reveal state so we don't overwrite user's stored choice.
    // set default to hidden on first visit
if (localStorage.getItem("mini_ide_sidebar_hidden") === null) {
  localStorage.setItem("mini_ide_sidebar_hidden", "1");
}

// Restore sidebar state (existing code)
if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
  document.body.classList.add("sidebar-hidden");
}


    (function setupFloatingTabReveal() {
      const tab = $("#btnToggleSidebar");
      const sidebarEl = document.querySelector(".sidebar");
      if (!tab || !sidebarEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden"); // "1" if user had hidden

      // When pointer enters the floating tab: reveal the sidebar and hide the tab itself.
      tab.addEventListener("mouseenter", (e) => {
        // remember stored preference so we can restore it later
        prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden");
        document.body.classList.add("sidebar-visible");
        tempReveal = true;

        // reveal sidebar visually (do NOT change stored preference)
        document.body.classList.remove("sidebar-hidden");
        // hide the floating tab while user is interacting with sidebar
        tab.style.display = "none";
      });

      // When pointer leaves the sidebar area, hide the sidebar again if it was shown by the tab
      sidebarEl.addEventListener("mouseleave", (e) => {
        // Only auto-hide when this reveal was triggered by the tab (avoid interfering with manual toggle)
       // toggleSidebar();
       document.body.classList.remove("sidebar-visible");
        if (!tempReveal) return;

        // restore to user's stored preference
        if (prevStoredHidden === "1") {
          // user had explicitly hidden -> re-hide sidebar
          document.body.classList.add("sidebar-hidden");
        } else {
          // user had it visible by default — keep visible (or you can re-hide; sticking with restore)
          document.body.classList.remove("sidebar-hidden");
        }

        // show the floating tab again
        tab.style.display = "";
        tempReveal = false;
      });

      // Extra: if user moves quickly from the tab into the sidebar, keep it shown.
      // Also handle pointerout from tab to sidebar — do nothing because mouseover already removed tab.
      // If user clicks the tab (existing click handler remains), we still toggle as before.
    })();

    // Restore linkmap state (default hidden)
    if (localStorage.getItem("mini_ide_linkmap_hidden") === null) {
      localStorage.setItem("mini_ide_linkmap_hidden", "1"); // hide by default
    }
    if (localStorage.getItem("mini_ide_linkmap_hidden") !== "1") {
      document.body.classList.add("linkmap-visible");
    }

    (function setupFloatingLinkmapReveal() {
      const tab = $("#btnToggleLinks");
      const panelEl = document.querySelector(".linkmap-panel");
      if (!tab || !panelEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");

      // pointer enters the floating tab -> reveal
      tab.addEventListener("mouseenter", () => {
        prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");
        document.body.classList.add("linkmap-visible");
        tempReveal = true;
        tab.style.display = "none"; // hide tab while interacting
      });

      // when pointer leaves the panel, restore preference
      panelEl.addEventListener("mouseleave", () => {
        document.body.classList.remove("linkmap-visible");
        if (!tempReveal) return;
        if (prevStoredHidden === "1") {
          document.body.classList.remove("linkmap-visible");
        } else {
          document.body.classList.add("linkmap-visible");
        }
        tab.style.display = "";
        tempReveal = false;
      });
    })();

    // Keyboard shortcuts (capture)
    window.addEventListener(
      "keydown",
      (e) => {
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
          e.preventDefault(); e.stopPropagation();
          saveCurrent();
        }
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
          e.preventDefault(); toggleSidebar();
        }
      },
      true
    );

    // Preview toggle
    $("#togglePreview")?.addEventListener("change", syncPreviewVisibility);

    // Sidebar toggle button
    $("#btnToggleSidebar")?.addEventListener("click", toggleSidebar);

    // Restore sidebar state
    if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
      document.body.classList.add("sidebar-hidden");
    }
  }

  function toggleSidebar() {
    const hidden = document.body.classList.toggle("sidebar-hidden");
    localStorage.setItem("mini_ide_sidebar_hidden", hidden ? "1" : "0");
  }
  
    function toggleLinkmap() {
    const hidden = document.body.classList.toggle("linkmap-visible");
    // store the inverse meaning: "hidden" = 1
    localStorage.setItem("mini_ide_linkmap_hidden", hidden ? "0" : "1");
  }


  function syncPreviewVisibility() {
    const on = $("#togglePreview")?.checked;
    if (on == null) return;
    $("#preview").style.display = on ? "block" : "none";
    document.querySelector(".editorWrap").style.gridTemplateColumns = on ? "1fr 40%" : "1fr";
  }

  // ---------- Save status / dirty UI ----------
  function setSaveBusy(on) {
    // big overlay
    const overlay = $("#pageLoader");
    if (overlay) overlay.style.display = on ? "flex" : "none";

    // header spinner
    const loader = $("#saveLoader");
    if (loader) loader.style.visibility = on ? "visible" : "hidden";

    const dot = $("#saveStatusDot");
    if (dot && on) dot.classList.remove("ok", "err", "dirty");
  }
  function setSaveDirty(on) {
    const btn = $("#btnSave");
    if (btn) btn.style.borderColor = on ? "#c9e8dc" : "var(--border)";
  }
  function markSaveOK() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("err", "dirty");
    dot.classList.add("ok");
    setTimeout(() => dot.classList.remove("ok"), 900);
  }
  function markSaveErr() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("ok", "dirty");
    dot.classList.add("err");
  }
  function updateDirtyUI() {
    // header asterisk
    const cp = $("#currentPath");
    if (cp) cp.textContent = (state.currentRel || "—") + (state.dirty ? " *" : "");

    // tree asterisk (on current file only)
    $$("#tree .file .name").forEach((n) => n.classList.remove("dirty"));
    if (state.currentRel) {
      const n = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"] .name`);
      if (n && state.dirty) n.classList.add("dirty");
    }

    // status dot amber while dirty
    const dot = $("#saveStatusDot");
    if (dot) {
      dot.classList.remove("ok", "err");
      dot.classList.toggle("dirty", !!state.dirty);
    }
  }

  // ---------- Scope helpers ----------
  function setScope(rel) {
    state.scopeRel = (rel || "").replace(/^\/+|\/+$/g, "");
    localStorage.setItem("mini_ide_scope", state.scopeRel);
    refreshTree();
  }

  function renderScopeCrumbs(){
    const wrap = $("#scopeCrumbs"); if (!wrap) return;
    wrap.innerHTML = "";
    const mk = (txt, rel, isCurrent=false) => {
      const s = document.createElement("span");
      s.className = "crumb" + (isCurrent ? " current" : "");
      s.textContent = txt;
      s.onclick = () => !isCurrent && setScope(rel);
      if (isCurrent) s.title = "Current folder";
      return s;
    };
    const parts = (state.scopeRel || "").split("/").filter(Boolean);
    wrap.appendChild(mk("root", "", parts.length === 0));
    let acc = "";
    parts.forEach((p, i) => {
      const sep = document.createElement("span");
      sep.className = "sep"; sep.textContent = "/";
      wrap.appendChild(sep);
      acc = acc ? acc + "/" + p : p;
      wrap.appendChild(mk(p, acc, i === parts.length - 1));
    });
  }

  // ---------- Tree (server-scoped, collapsible) ----------
  function toNested(items) {
    const root = { name: "", rel: "", dirs: {}, files: [] };
    const entries = items
      .map((it) => ({ type: it.type, rel: (it.rel || "").replace(/^\/+/, "") }))
      .filter((e) => e.rel);

    // ensure nodes
    entries.forEach((e) => {
      const parts = e.rel.split("/");
      if (e.type === "dir") {
        let cur = root;
        for (const part of parts) {
          cur.dirs[part] = cur.dirs[part] || {
            name: part,
            rel: (cur.rel ? cur.rel + "/" : "") + part,
            dirs: {},
            files: []
          };
          cur = cur.dirs[part];
        }
      }
    });

    // place files
    entries.filter((e) => e.type === "file").forEach((e) => {
      const parts = e.rel.split("/");
      const name = parts.pop();
      let cur = root;
      for (const part of parts) {
        cur.dirs[part] = cur.dirs[part] || {
          name: part,
          rel: (cur.rel ? cur.rel + "/" : "") + part,
          dirs: {},
          files: []
        };
        cur = cur.dirs[part];
      }
      cur.files.push({ name, rel: e.rel });
    });

    // sort
    (function sort(n) {
      Object.values(n.dirs).forEach(sort);
      n.files.sort((a, b) => a.name.localeCompare(b.name));
    })(root);

    return root;
  }

  function renderTree(rootNode, container) {
    const openState = JSON.parse(localStorage.getItem("mini_ide_open_dirs") || "{}");
    const setOpen = (rel, open) => {
      if (open) openState[rel || "/"] = 1;
      else delete openState[rel || "/"];
      localStorage.setItem("mini_ide_open_dirs", JSON.stringify(openState));
    };

    container.innerHTML = "";

    const mkDir = (node) => {
      const det = document.createElement("details");
      if (openState[node.rel || "/"]) det.open = true;

      const sum = document.createElement("summary");
      sum.innerHTML = `
        <span class="chev">›</span>
        <span class="label name">📁 ${node.name || "root"}</span>
        <button class="focus" type="button" title="Focus here" data-rel="${node.rel}">📌 Focus</button>
      `;
      det.appendChild(sum);

      // node.rel from server is already full root-relative path
      const dirRel = (node.rel || "");

      // focus controls
      sum.ondblclick = (e) => { e.preventDefault(); setScope(dirRel); };
      sum.querySelector(".focus").onclick = (e) => { e.stopPropagation(); setScope(dirRel); };

      det.addEventListener("toggle", () => setOpen(node.rel, det.open));

      // child dirs
      const dirNames = Object.keys(node.dirs).sort((a, b) => a.localeCompare(b));
      dirNames.forEach((name) => det.appendChild(mkDir(node.dirs[name])));

      // files: use rel from server as-is (already full)
      node.files.forEach((f) => {
        const div = document.createElement("div");
        div.className = "file";
        div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
        div.dataset.rel = f.rel;
        div.onclick = () => openFile(f.rel);
        det.appendChild(div);
      });

      return det;
    };

    // top-level dirs
    const dirNames = Object.keys(rootNode.dirs).sort((a, b) => a.localeCompare(b));
    dirNames.forEach((name) => container.appendChild(mkDir(rootNode.dirs[name])));

    // top-level files: use rel as-is
    rootNode.files.forEach((f) => {
      const div = document.createElement("div");
      div.className = "file";
      div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
      div.dataset.rel = f.rel;
      div.onclick = () => openFile(f.rel);
      container.appendChild(div);
    });
  }

  async function refreshTree() {
    try {
      const res = await api({ action: "tree", rel: state.scopeRel });
      if (!res || res.ok !== true || !Array.isArray(res.items)) {
        const msg = res && res.error ? res.error : "Tree request failed";
        alert("Mini IDE error: " + msg);
        console.error("Tree response:", res);
        return;
      }
      const nested = toNested(res.items);
      renderTree(nested, $("#tree"));
      renderScopeCrumbs();

      // re-highlight current file if visible
      if (state.currentRel) {
        $$("#tree .file").forEach((n) => n.classList.remove("active"));
        const hit = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"]`);
        if (hit) hit.classList.add("active");
        updateDirtyUI();
      }
    } catch (e) {
      alert("Mini IDE fetch error (tree): " + e);
      console.error(e);
    }
  }

  // ---------- File ops ----------
  async function openFile(rel) {
    const res = await api({ action: "read", rel });
    if (!res.ok) return alert(res.error || "Failed to read");
    state.currentRel = rel;

    // highlight selected
    $$("#tree .file").forEach((n) => n.classList.remove("active"));
    const hit = $(`#tree .file[data-rel="${CSS.escape(rel)}"]`);
    if (hit) hit.classList.add("active");

    if (res.file.editable) {
      setEditorContent(res.file.content || "");
    } else {
      setEditorContent("// Not editable (binary or large). Size: " + res.file.size + " bytes\n");
    }
    state.dirty = false;
    setSaveDirty(false);
    updateDirtyUI();
    refreshPreview();
  }

  function getEditorContent() {
    if (state.ace) return state.ace.getValue();
    return $("#ta").value;
  }

  function setEditorContent(txt) {
    if (state.ace) {
      state.ace.setValue(txt ?? "", -1);
      // Set mode from extension
      const ext = (state.currentRel.split(".").pop() || "").toLowerCase();
      const map = {
        js: "javascript",
        css: "css",
        php: "php",
        html: "html",
        htm: "html",
        json: "json",
        md: "markdown",
        py: "python",
        yml: "yaml",
        yaml: "yaml",
        c: "c_cpp",
        cpp: "c_cpp",
        h: "c_cpp",
        sql: "sql",
        csv: "text",
        txt: "text"
      };
      state.ace.session.setMode("ace/mode/" + (map[ext] || "text"));
    } else {
      $("#ta").classList.remove("hidden");
      $("#ta").value = txt ?? "";
    }
  }

  async function saveCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (state.saving) return; // prevent double-taps
    state.saving = true;
    setSaveBusy(true);

    try {
      const content = getEditorContent();
      const res = await api({ action: "save", rel: state.currentRel, content });
      if (!res.ok) {
        markSaveErr();
        throw new Error(res.error || "Save failed");
      }

      // Server verification
      let verified = !!res.verify_ok;

      // mtime wait (shared hosts can be slow)
      const targetMtime = res.mtime || 0;
      const okStat = await waitForStat(state.currentRel, targetMtime, 8, 150);
      if (!okStat) console.warn("Proceeding without stat confirmation");

      // Optional client-side readback if server said not verified
      if (!verified) {
        try {
          const rb = await api({ action: "read", rel: state.currentRel });
          if (rb.ok && rb.file && typeof rb.file.content === "string") {
            verified = (rb.file.content === content);
          }
        } catch {}
      }

      if (!verified) {
        markSaveErr();
        alert("Save did not verify. The file contents on disk did not match what was sent.");
        return; // do not mark OK or clear dirty
      }

      state.dirty = false;
      setSaveDirty(false);
      updateDirtyUI();   // clear asterisks + amber dot
      markSaveOK();      // green blip
      refreshPreview(true);

    } catch (e) {
      console.error(e);
      alert("Save error: " + e.message);
      markSaveErr();     // red
    } finally {
      state.saving = false;
      setSaveBusy(false);
    }
  }

  // poll stat until mtime >= target
  async function waitForStat(rel, target, tries = 8, delay = 150) {
    for (let i = 0; i < tries; i++) {
      try {
        const s = await api({ action: "stat", rel });
        if (s.ok && (s.mtime || 0) >= target) return true;
      } catch {}
      await new Promise((r) => setTimeout(r, delay));
    }
    return false;
  }

  async function renameCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    const to = prompt("Rename to (path):", state.currentRel);
    if (!to || to === state.currentRel) return;
    const res = await api({ action: "rename", from: state.currentRel, to });
    if (!res.ok) return alert(res.error || "Rename failed");
    state.currentRel = to;
    $("#currentPath").textContent = to;
    await refreshTree();
    refreshPreview();
  }

  async function deleteCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (!confirm("Delete " + state.currentRel + " ?")) return;
    const res = await api({ action: "delete", rel: state.currentRel });
    if (!res.ok) return alert(res.error || "Delete failed");
    state.currentRel = "";
    $("#currentPath").textContent = "—";
    if (state.ace) state.ace.setValue("");
    else $("#ta").value = "";
    await refreshTree();
    refreshPreview();
  }

  async function createFromInput(isDir) {
    const raw = $("#newPath").value.trim();
    if (!raw) return;

    const relPath = state.scopeRel
      ? (state.scopeRel + "/" + raw).replace(/\/+/g, "/").replace(/^\/|\/$/g, "")
      : raw;

    const res = await api({ action: "create", rel: relPath, type: isDir ? "dir" : "file" });
    if (!res.ok) return alert(res.error || "Create failed");
    $("#newPath").value = "";
    await refreshTree();
    if (!isDir) openFile(relPath);
  }

  async function bulkCreate() {
    const lines = $("#bulkLines").value;
    if (!lines.trim()) return;

    // Prefix each line's path with scope (keep labels 'dir:'/'file:' intact)
    const scoped = lines.split("\n").map((t) => {
      const ln = t.trim();
      if (!ln) return ln;
      const m = ln.match(/^(dir:|file:)?\s*(.*)$/i);
      if (!m) return ln;
      const label = (m[1] || "").toLowerCase();
      let p = m[2].trim();
      if (p && state.scopeRel && !p.startsWith("/")) {
        p = (state.scopeRel + "/" + p).replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
      }
      return (label ? label + " " : "") + p;
    }).join("\n");

    const res = await api({ action: "bulk", lines: scoped });
    if (!res.ok) return alert(res.error || "Bulk failed");
    await refreshTree();
    alert("Bulk operations complete.");
  }

  // ---------- Preview ----------
  function refreshPreview(/* force = false */) {
    const on = $("#togglePreview")?.checked;
    if (!on) return;

    // Prefer current file; otherwise fall back to index.html (safer default)
    let rel = state.currentRel || "index.html";
    if (!/\.(php|html?|md|txt|css|js)$/i.test(rel)) rel = "index.html";

    const base = (state.cfg && state.cfg.workspace_url) || "/";
    const url = (base.endsWith("/") ? base : base + "/") + rel;

    const ifr = $("#preview");
    ifr.src = url + (url.includes("?") ? "&" : "?") + "_t=" + Date.now(); // cache-bust
  }

  // ---------- Link scan ----------
    async function scanLinks() {
    const res = await api({ action: "links" });
    if (!res.ok) return alert(res.error || "Scan failed");
    const box = document.getElementById("linksPanel");
    if (!box) return alert("Link panel not found.");
    box.innerHTML = "";
    if (!res.edges || !res.edges.length) {
      box.textContent = "No references found.";
      return;
    }
    res.edges.forEach((e) => {
      const div = document.createElement("div");
      div.className = "edge";
      // make clickable filenames optionally
      div.innerHTML = `<div style="display:flex;justify-content:space-between;gap:8px">
                         <div style="flex:1;min-width:0; overflow:hidden; text-overflow:ellipsis;">
                           <strong>${escapeHtml(e.from)}</strong> &nbsp;→&nbsp; ${escapeHtml(e.to)}
                         </div>
                         <div style="flex:0 0 auto">
                           <button class="openLinkFile" data-rel="${e.from}">Open</button>
                         </div>
                       </div>`;
      box.appendChild(div);
    });

    // wire open buttons
    $$(".openLinkFile", box).forEach((b) => {
      b.addEventListener("click", () => {
        const rel = b.dataset.rel;
        if (rel) {
          // ensure the panel closes so tree is visible (optional)
          document.body.classList.remove("linkmap-visible");
          openFile(rel);
        }
      });
    });
  }


  // ---------- Go ----------
  init();
})();


===== FILE: webide/assets/app.js @ 2025-10-19 23:10:17 =====
(async function () {
  // ---------- DOM helpers ----------
  const $ = (q, el = document) => el.querySelector(q),
        $$ = (qAll, el = document) => Array.from(el.querySelectorAll(qAll));

  // ---------- API helper (POST FormData) ----------
  const api = async (params) => {
    const form = new FormData();
    Object.entries(params).forEach(([k, v]) => {
      if (Array.isArray(v)) v.forEach((x) => form.append(k + "[]", x));
      else form.append(k, v);
    });
    const r = await fetch("api.php", { method: "POST", body: form });
    return r.json();
  };
  
    function escapeHtml(s) {
    return String(s || "").replace(/[&<>"'`]/g, (c) => ({
      "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","`":"&#96;"
    }[c]));
  }


  // ---------- State ----------
  const state = {
    currentRel: "",
    ace: null,
    cfg: null,
    dirty: false,
    saving: false,
    scopeRel: "" // '' = workspace root; otherwise e.g. 'admin/pages'
  };

  // ---------- Init ----------
  async function init() {
    state.cfg = await api({ action: "config" });

    // restore scope
    state.scopeRel = localStorage.getItem("mini_ide_scope") || "";

    bindUI();
    await refreshTree();

    // Editor: ACE if available, fallback to <textarea>
    if (state.cfg.ace && window.ace) {
      state.ace = ace.edit("editor");
      state.ace.setTheme("ace/theme/textmate");
      state.ace.session.setUseSoftTabs(true);
      state.ace.session.setTabSize(2);
      state.ace.setOption("fontSize", "13px");
      $("#ta").classList.add("hidden");
      state.ace.session.on("change", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    } else {
      $("#editor").classList.add("hidden");
      $("#ta").addEventListener("input", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    }

    // Load a sensible default (index.*) if present
    const guess = $$("#tree .file").find((n) =>
      /^(index\.(php|html?|md|txt))$/i.test(n.dataset.rel || "")
    );
    if (guess) openFile(guess.dataset.rel);
  }

  // ---------- UI wiring ----------
  function bindUI() {
    $("#refreshTree")?.addEventListener("click", refreshTree);
    $("#btnCreateFile")?.addEventListener("click", () => createFromInput(false));
    $("#btnCreateDir")?.addEventListener("click", () => createFromInput(true));
    $("#btnBulk")?.addEventListener("click", bulkCreate);
    $("#btnSave")?.addEventListener("click", saveCurrent);
    $("#btnRename")?.addEventListener("click", renameCurrent);
    $("#btnDelete")?.addEventListener("click", deleteCurrent);
    $("#btnScanLinks")?.addEventListener("click", scanLinks);
    $("#scopeReset")?.addEventListener("click", () => setScope(""));
    
        // existing
    $("#btnScanLinks")?.addEventListener("click", scanLinks);

    // right-panel linkmap controls
    $("#btnToggleLinks")?.addEventListener("click", toggleLinkmap);
    $("#refreshLinks")?.addEventListener("click", scanLinks);
    $("#btnCloseLinks")?.addEventListener("click", () => {
      document.body.classList.remove("linkmap-visible");
      localStorage.setItem("mini_ide_linkmap_hidden", "1");
    });

    
        // ---------- Floating tab reveal behaviour ----------
    // Keeps a temporary reveal state so we don't overwrite user's stored choice.
    // set default to hidden on first visit
if (localStorage.getItem("mini_ide_sidebar_hidden") === null) {
  localStorage.setItem("mini_ide_sidebar_hidden", "1");
}

// Restore sidebar state (existing code)
if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
  document.body.classList.add("sidebar-hidden");
}


    (function setupFloatingTabReveal() {
      const tab = $("#btnToggleSidebar");
      const sidebarEl = document.querySelector(".sidebar");
      if (!tab || !sidebarEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden"); // "1" if user had hidden

      // When pointer enters the floating tab: reveal the sidebar and hide the tab itself.
      tab.addEventListener("mouseenter", (e) => {
        // remember stored preference so we can restore it later
        prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden");
        document.body.classList.add("sidebar-visible");
        tempReveal = true;

        // reveal sidebar visually (do NOT change stored preference)
        document.body.classList.remove("sidebar-hidden");
        // hide the floating tab while user is interacting with sidebar
        tab.style.display = "none";
      });

      // When pointer leaves the sidebar area, hide the sidebar again if it was shown by the tab
      sidebarEl.addEventListener("mouseleave", (e) => {
        // Only auto-hide when this reveal was triggered by the tab (avoid interfering with manual toggle)
       // toggleSidebar();
       document.body.classList.remove("sidebar-visible");
        if (!tempReveal) return;

        // restore to user's stored preference
        if (prevStoredHidden === "1") {
          // user had explicitly hidden -> re-hide sidebar
          document.body.classList.add("sidebar-hidden");
        } else {
          // user had it visible by default — keep visible (or you can re-hide; sticking with restore)
          document.body.classList.remove("sidebar-hidden");
        }

        // show the floating tab again
        tab.style.display = "";
        tempReveal = false;
      });

      // Extra: if user moves quickly from the tab into the sidebar, keep it shown.
      // Also handle pointerout from tab to sidebar — do nothing because mouseover already removed tab.
      // If user clicks the tab (existing click handler remains), we still toggle as before.
    })();

    // Restore linkmap state (default hidden)
    if (localStorage.getItem("mini_ide_linkmap_hidden") === null) {
      localStorage.setItem("mini_ide_linkmap_hidden", "1"); // hide by default
    }
    if (localStorage.getItem("mini_ide_linkmap_hidden") !== "1") {
      document.body.classList.add("linkmap-visible");
    }

    (function setupFloatingLinkmapReveal() {
      const tab = $("#btnToggleLinks");
      const panelEl = document.querySelector(".linkmap-panel");
      if (!tab || !panelEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");

      // pointer enters the floating tab -> reveal
      tab.addEventListener("mouseenter", () => {
        prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");
        document.body.classList.add("linkmap-visible");
        tempReveal = true;
        tab.style.display = "none"; // hide tab while interacting
      });

      // when pointer leaves the panel, restore preference
      panelEl.addEventListener("mouseleave", () => {
        document.body.classList.remove("linkmap-visible");
        if (!tempReveal) return;
        if (prevStoredHidden === "1") {
          document.body.classList.remove("linkmap-visible");
        } else {
          document.body.classList.add("linkmap-visible");
        }
        tab.style.display = "";
        tempReveal = false;
      });
    })();

    // Keyboard shortcuts (capture)
    window.addEventListener(
      "keydown",
      (e) => {
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
          e.preventDefault(); e.stopPropagation();
          saveCurrent();
        }
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
          e.preventDefault(); toggleSidebar();
        }
      },
      true
    );

    // Preview toggle
    $("#togglePreview")?.addEventListener("change", syncPreviewVisibility);

    // Sidebar toggle button
    $("#btnToggleSidebar")?.addEventListener("click", toggleSidebar);

    // Restore sidebar state
    if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
      document.body.classList.add("sidebar-hidden");
    }
  }

  function toggleSidebar() {
    const hidden = document.body.classList.toggle("sidebar-hidden");
    localStorage.setItem("mini_ide_sidebar_hidden", hidden ? "1" : "0");
  }
  
    function toggleLinkmap() {
    const hidden = document.body.classList.toggle("linkmap-visible");
    // store the inverse meaning: "hidden" = 1
    localStorage.setItem("mini_ide_linkmap_hidden", hidden ? "0" : "1");
  }


  function syncPreviewVisibility() {
    const on = $("#togglePreview")?.checked;
    if (on == null) return;
    $("#preview").style.display = on ? "block" : "none";
    document.querySelector(".editorWrap").style.gridTemplateColumns = on ? "1fr 40%" : "1fr";
  }

  // ---------- Save status / dirty UI ----------
  function setSaveBusy(on) {
    // big overlay
    const overlay = $("#pageLoader");
    if (overlay) overlay.style.display = on ? "flex" : "none";

    // header spinner
    const loader = $("#saveLoader");
    if (loader) loader.style.visibility = on ? "visible" : "hidden";

    const dot = $("#saveStatusDot");
    if (dot && on) dot.classList.remove("ok", "err", "dirty");
  }
  function setSaveDirty(on) {
    const btn = $("#btnSave");
    if (btn) btn.style.borderColor = on ? "#c9e8dc" : "var(--border)";
  }
  function markSaveOK() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("err", "dirty");
    dot.classList.add("ok");
    setTimeout(() => dot.classList.remove("ok"), 900);
  }
  function markSaveErr() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("ok", "dirty");
    dot.classList.add("err");
  }
  function updateDirtyUI() {
    // header asterisk
    const cp = $("#currentPath");
    if (cp) cp.textContent = (state.currentRel || "—") + (state.dirty ? " *" : "");

    // tree asterisk (on current file only)
    $$("#tree .file .name").forEach((n) => n.classList.remove("dirty"));
    if (state.currentRel) {
      const n = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"] .name`);
      if (n && state.dirty) n.classList.add("dirty");
    }

    // status dot amber while dirty
    const dot = $("#saveStatusDot");
    if (dot) {
      dot.classList.remove("ok", "err");
      dot.classList.toggle("dirty", !!state.dirty);
    }
  }

  // ---------- Scope helpers ----------
  function setScope(rel) {
    state.scopeRel = (rel || "").replace(/^\/+|\/+$/g, "");
    localStorage.setItem("mini_ide_scope", state.scopeRel);
    refreshTree();
  }

  function renderScopeCrumbs(){
    const wrap = $("#scopeCrumbs"); if (!wrap) return;
    wrap.innerHTML = "";
    const mk = (txt, rel, isCurrent=false) => {
      const s = document.createElement("span");
      s.className = "crumb" + (isCurrent ? " current" : "");
      s.textContent = txt;
      s.onclick = () => !isCurrent && setScope(rel);
      if (isCurrent) s.title = "Current folder";
      return s;
    };
    const parts = (state.scopeRel || "").split("/").filter(Boolean);
    wrap.appendChild(mk("root", "", parts.length === 0));
    let acc = "";
    parts.forEach((p, i) => {
      const sep = document.createElement("span");
      sep.className = "sep"; sep.textContent = "/";
      wrap.appendChild(sep);
      acc = acc ? acc + "/" + p : p;
      wrap.appendChild(mk(p, acc, i === parts.length - 1));
    });
  }

  // ---------- Tree (server-scoped, collapsible) ----------
  function toNested(items) {
    const root = { name: "", rel: "", dirs: {}, files: [] };
    const entries = items
      .map((it) => ({ type: it.type, rel: (it.rel || "").replace(/^\/+/, "") }))
      .filter((e) => e.rel);

    // ensure nodes
    entries.forEach((e) => {
      const parts = e.rel.split("/");
      if (e.type === "dir") {
        let cur = root;
        for (const part of parts) {
          cur.dirs[part] = cur.dirs[part] || {
            name: part,
            rel: (cur.rel ? cur.rel + "/" : "") + part,
            dirs: {},
            files: []
          };
          cur = cur.dirs[part];
        }
      }
    });

    // place files
    entries.filter((e) => e.type === "file").forEach((e) => {
      const parts = e.rel.split("/");
      const name = parts.pop();
      let cur = root;
      for (const part of parts) {
        cur.dirs[part] = cur.dirs[part] || {
          name: part,
          rel: (cur.rel ? cur.rel + "/" : "") + part,
          dirs: {},
          files: []
        };
        cur = cur.dirs[part];
      }
      cur.files.push({ name, rel: e.rel });
    });

    // sort
    (function sort(n) {
      Object.values(n.dirs).forEach(sort);
      n.files.sort((a, b) => a.name.localeCompare(b.name));
    })(root);

    return root;
  }

  function renderTree(rootNode, container) {
    const openState = JSON.parse(localStorage.getItem("mini_ide_open_dirs") || "{}");
    const setOpen = (rel, open) => {
      if (open) openState[rel || "/"] = 1;
      else delete openState[rel || "/"];
      localStorage.setItem("mini_ide_open_dirs", JSON.stringify(openState));
    };

    container.innerHTML = "";

    const mkDir = (node) => {
      const det = document.createElement("details");
      if (openState[node.rel || "/"]) det.open = true;

      const sum = document.createElement("summary");
      sum.innerHTML = `
        <span class="chev">›</span>
        <span class="label name">📁 ${node.name || "root"}</span>
        <button class="focus" type="button" title="Focus here" data-rel="${node.rel}">📌 Focus</button>
      `;
      det.appendChild(sum);

      // node.rel from server is already full root-relative path
      const dirRel = (node.rel || "");

      // focus controls
      sum.ondblclick = (e) => { e.preventDefault(); setScope(dirRel); };
      sum.querySelector(".focus").onclick = (e) => { e.stopPropagation(); setScope(dirRel); };

      det.addEventListener("toggle", () => setOpen(node.rel, det.open));

      // child dirs
      const dirNames = Object.keys(node.dirs).sort((a, b) => a.localeCompare(b));
      dirNames.forEach((name) => det.appendChild(mkDir(node.dirs[name])));

      // files: use rel from server as-is (already full)
      node.files.forEach((f) => {
        const div = document.createElement("div");
        div.className = "file";
        div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
        div.dataset.rel = f.rel;
        div.onclick = () => openFile(f.rel);
        det.appendChild(div);
      });

      return det;
    };

    // top-level dirs
    const dirNames = Object.keys(rootNode.dirs).sort((a, b) => a.localeCompare(b));
    dirNames.forEach((name) => container.appendChild(mkDir(rootNode.dirs[name])));

    // top-level files: use rel as-is
    rootNode.files.forEach((f) => {
      const div = document.createElement("div");
      div.className = "file";
      div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
      div.dataset.rel = f.rel;
      div.onclick = () => openFile(f.rel);
      container.appendChild(div);
    });
  }

  async function refreshTree() {
    try {
      const res = await api({ action: "tree", rel: state.scopeRel });
      if (!res || res.ok !== true || !Array.isArray(res.items)) {
        const msg = res && res.error ? res.error : "Tree request failed";
        alert("Mini IDE error: " + msg);
        console.error("Tree response:", res);
        return;
      }
      const nested = toNested(res.items);
      renderTree(nested, $("#tree"));
      renderScopeCrumbs();

      // re-highlight current file if visible
      if (state.currentRel) {
        $$("#tree .file").forEach((n) => n.classList.remove("active"));
        const hit = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"]`);
        if (hit) hit.classList.add("active");
        updateDirtyUI();
      }
    } catch (e) {
      alert("Mini IDE fetch error (tree): " + e);
      console.error(e);
    }
  }

  // ---------- File ops ----------
  async function openFile(rel) {
    const res = await api({ action: "read", rel });
    if (!res.ok) return alert(res.error || "Failed to read");
    state.currentRel = rel;

    // highlight selected
    $$("#tree .file").forEach((n) => n.classList.remove("active"));
    const hit = $(`#tree .file[data-rel="${CSS.escape(rel)}"]`);
    if (hit) hit.classList.add("active");

    if (res.file.editable) {
      setEditorContent(res.file.content || "");
    } else {
      setEditorContent("// Not editable (binary or large). Size: " + res.file.size + " bytes\n");
    }
    state.dirty = false;
    setSaveDirty(false);
    updateDirtyUI();
    refreshPreview();
  }

  function getEditorContent() {
    if (state.ace) return state.ace.getValue();
    return $("#ta").value;
  }

  function setEditorContent(txt) {
    if (state.ace) {
      state.ace.setValue(txt ?? "", -1);
      // Set mode from extension
      const ext = (state.currentRel.split(".").pop() || "").toLowerCase();
      const map = {
        js: "javascript",
        css: "css",
        php: "php",
        html: "html",
        htm: "html",
        json: "json",
        md: "markdown",
        py: "python",
        yml: "yaml",
        yaml: "yaml",
        c: "c_cpp",
        cpp: "c_cpp",
        h: "c_cpp",
        sql: "sql",
        csv: "text",
        txt: "text"
      };
      state.ace.session.setMode("ace/mode/" + (map[ext] || "text"));
    } else {
      $("#ta").classList.remove("hidden");
      $("#ta").value = txt ?? "";
    }
  }

  async function saveCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (state.saving) return; // prevent double-taps
    state.saving = true;
    setSaveBusy(true);

    try {
      const content = getEditorContent();
      const res = await api({ action: "save", rel: state.currentRel, content });
      if (!res.ok) {
        markSaveErr();
        throw new Error(res.error || "Save failed");
      }

      // Server verification
      let verified = !!res.verify_ok;

      // mtime wait (shared hosts can be slow)
      const targetMtime = res.mtime || 0;
      const okStat = await waitForStat(state.currentRel, targetMtime, 8, 150);
      if (!okStat) console.warn("Proceeding without stat confirmation");

      // Optional client-side readback if server said not verified
      if (!verified) {
        try {
          const rb = await api({ action: "read", rel: state.currentRel });
          if (rb.ok && rb.file && typeof rb.file.content === "string") {
            verified = (rb.file.content === content);
          }
        } catch {}
      }

      if (!verified) {
        markSaveErr();
        alert("Save did not verify. The file contents on disk did not match what was sent.");
        return; // do not mark OK or clear dirty
      }

      state.dirty = false;
      setSaveDirty(false);
      updateDirtyUI();   // clear asterisks + amber dot
      markSaveOK();      // green blip
      refreshPreview(true);

    } catch (e) {
      console.error(e);
      alert("Save error: " + e.message);
      markSaveErr();     // red
    } finally {
      state.saving = false;
      setSaveBusy(false);
    }
  }

  // poll stat until mtime >= target
  async function waitForStat(rel, target, tries = 8, delay = 150) {
    for (let i = 0; i < tries; i++) {
      try {
        const s = await api({ action: "stat", rel });
        if (s.ok && (s.mtime || 0) >= target) return true;
      } catch {}
      await new Promise((r) => setTimeout(r, delay));
    }
    return false;
  }

  async function renameCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    const to = prompt("Rename to (path):", state.currentRel);
    if (!to || to === state.currentRel) return;
    const res = await api({ action: "rename", from: state.currentRel, to });
    if (!res.ok) return alert(res.error || "Rename failed");
    state.currentRel = to;
    $("#currentPath").textContent = to;
    await refreshTree();
    refreshPreview();
  }

  async function deleteCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (!confirm("Delete " + state.currentRel + " ?")) return;
    const res = await api({ action: "delete", rel: state.currentRel });
    if (!res.ok) return alert(res.error || "Delete failed");
    state.currentRel = "";
    $("#currentPath").textContent = "—";
    if (state.ace) state.ace.setValue("");
    else $("#ta").value = "";
    await refreshTree();
    refreshPreview();
  }

  async function createFromInput(isDir) {
    const raw = $("#newPath").value.trim();
    if (!raw) return;

    const relPath = state.scopeRel
      ? (state.scopeRel + "/" + raw).replace(/\/+/g, "/").replace(/^\/|\/$/g, "")
      : raw;

    const res = await api({ action: "create", rel: relPath, type: isDir ? "dir" : "file" });
    if (!res.ok) return alert(res.error || "Create failed");
    $("#newPath").value = "";
    await refreshTree();
    if (!isDir) openFile(relPath);
  }

  async function bulkCreate() {
    const lines = $("#bulkLines").value;
    if (!lines.trim()) return;

    // Prefix each line's path with scope (keep labels 'dir:'/'file:' intact)
    const scoped = lines.split("\n").map((t) => {
      const ln = t.trim();
      if (!ln) return ln;
      const m = ln.match(/^(dir:|file:)?\s*(.*)$/i);
      if (!m) return ln;
      const label = (m[1] || "").toLowerCase();
      let p = m[2].trim();
      if (p && state.scopeRel && !p.startsWith("/")) {
        p = (state.scopeRel + "/" + p).replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
      }
      return (label ? label + " " : "") + p;
    }).join("\n");

    const res = await api({ action: "bulk", lines: scoped });
    if (!res.ok) return alert(res.error || "Bulk failed");
    await refreshTree();
    alert("Bulk operations complete.");
  }

  // ---------- Preview ----------
  function refreshPreview(/* force = false */) {
    const on = $("#togglePreview")?.checked;
    if (!on) return;

    // Prefer current file; otherwise fall back to index.html (safer default)
    let rel = state.currentRel || "index.html";
    if (!/\.(php|html?|md|txt|css|js)$/i.test(rel)) rel = "index.html";

    const base = (state.cfg && state.cfg.workspace_url) || "/";
    const url = (base.endsWith("/") ? base : base + "/") + rel;

    const ifr = $("#preview");
    ifr.src = url + (url.includes("?") ? "&" : "?") + "_t=" + Date.now(); // cache-bust
  }

  // ---------- Link scan ----------
    async function scanLinks() {
    const res = await api({ action: "links" });
    if (!res.ok) return alert(res.error || "Scan failed");
    const box = document.getElementById("linksPanel");
    if (!box) return alert("Link panel not found.");
    box.innerHTML = "";
    if (!res.edges || !res.edges.length) {
      box.textContent = "No references found.";
      return;
    }
    res.edges.forEach((e) => {
      const div = document.createElement("div");
      div.className = "edge";
      // make clickable filenames optionally
      div.innerHTML = `<div style="display:flex;justify-content:space-between;gap:8px">
                         <div style="flex:1;min-width:0; overflow:hidden; text-overflow:ellipsis;">
                           <strong>${escapeHtml(e.from)}</strong> &nbsp;→&nbsp; ${escapeHtml(e.to)}
                         </div>
                         <div style="flex:0 0 auto">
                           <button class="openLinkFile" data-rel="${e.from}">Open</button>
                         </div>
                       </div>`;
      box.appendChild(div);
    });

    // wire open buttons
    $$(".openLinkFile", box).forEach((b) => {
      b.addEventListener("click", () => {
        const rel = b.dataset.rel;
        if (rel) {
          // ensure the panel closes so tree is visible (optional)
          document.body.classList.remove("linkmap-visible");
          openFile(rel);
        }
      });
    });
  }


  // ---------- Go ----------
  init();
})();


===== FILE: webide/index.php @ 2025-10-19 23:12:09 =====
<?php
// index.php
declare(strict_types=1);
require_once __DIR__ . '/_inc/helpers.php';
ensure_auth();
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initital-scale=1" />
<title>Mini Web IDE</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
<link rel="stylesheet" href="assets/style.css" />
<link rel="icon" type="png" href="cool.png"/>
<?php
require __DIR__.'/_inc/config.php';
if ($USE_ACE_CDN): ?>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.9/ace.js" crossorigin="anonymous"></script>

<?php endif; ?>
</head>
<body>
    <div id="saveStatusDot" title="save status"></div>

<div class="layout">
  <aside class="sidebar">
    <div class="topbar">
      <strong>Files</strong>
      <button id="refreshTree" title="Refresh">↻</button>
    </div>
    <div class="tree-controls">
        <label>Folder: 
    <input id="folderFilter" placeholder="(leave blank for all)" />
  </label><br><br>
  <input id="treeFilter" placeholder="Filter files…" />
  <button id="btnExpandAll" title="Expand all">＋</button>
  <button id="btnCollapseAll" title="Collapse all">－</button>
</div>
<div class="scopebar">
  <span>📂</span>
  <span id="scopeCrumbs"></span>
  <button id="scopeReset" title="Back to workspace root">Reset</button>
</div>

    <div id="tree" class="tree"></div>

    <div class="createBox">
      <h4>Create</h4>
      <input id="newPath" placeholder="path/to/file.ext or folder/" />
      <div class="row">
        <button data-kind="file" id="btnCreateFile">+ File</button>
        <button data-kind="dir"  id="btnCreateDir">+ Folder</button>
      </div>
      <details>
        <summary>Bulk create</summary>
        <p class="hint">One per line. Prefix “dir:” for folders, otherwise a file is created.</p>
        <textarea id="bulkLines" rows="6" placeholder="dir: assets/
file: assets/app.js
file: index.php"></textarea>
        <button id="btnBulk">Run Bulk</button>
      </details>
    </div>

    
  </aside>

  <main class="main">
    <header class="mainbar">
  <div class="path">
    <span id="currentPath">—</span>
  </div>
  <div class="actions">
    <button id="btnToggleSidebar" title="Hide/Show file panel">☰ Files</button>
    <button id="btnSave">Save (Ctrl/Cmd+S)</button>
    <button id="btnRename">Rename</button>
    <button id="btnDelete" class="danger">Delete</button>
    <label class="previewToggle"><input type="checkbox" id="togglePreview" checked> Preview</label>
  </div>
</header>


    <section class="editorWrap">
      <div id="editor"></div>
      <textarea id="ta" class="hidden"></textarea>
      <iframe id="preview" title="Preview"></iframe>
    </section>
  </main>
    <!-- right-hand slideout for Link map -->
  <aside class="linkmap-panel" aria-hidden="true">
    <div class="topbar">
      <strong>Link map</strong>
      <button id="refreshLinks" title="Refresh">↻</button>
    </div>

    <div class="linkmap-controls" style="padding:10px 12px; border-bottom:1px solid var(--border);">
      <button id="btnScanLinks">Scan</button>
      <div class="linkmap-controls" style="padding:10px 12px; border-bottom:1px solid var(--border);">
  <button id="btnScanLinks">Scan</button>
  <button id="btnCloseLinks" style="margin-left:8px">Close</button>
</div>

      <button id="btnCloseLinks" style="margin-left:8px">Close</button>
    </div>

    <div id="linksPanel" class="linksContent" style="padding:10px 12px; overflow:auto; flex:1;"></div>
  </aside>

  <!-- floating right tab to reveal linkmap -->
  <button id="btnToggleLinks" title="Show link map">☰ Links</button>

</div>

<script>window.MINI_IDE = { base: '' };</script>
<script src="../webide/assets/app.js"></script>
<!-- Big centered loader overlay -->
<div id="pageLoader" aria-hidden="true">
  <div class="spinner" role="status" title="Saving…"></div>
</div>

</body>
</html>


===== FILE: webide/index.php @ 2025-10-19 23:12:40 =====
<?php
// index.php
declare(strict_types=1);
require_once __DIR__ . '/_inc/helpers.php';
ensure_auth();
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initital-scale=1" />
<title>Mini Web IDE</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
<link rel="stylesheet" href="assets/style.css" />
<link rel="icon" type="png" href="cool.png"/>
<?php
require __DIR__.'/_inc/config.php';
if ($USE_ACE_CDN): ?>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.9/ace.js" crossorigin="anonymous"></script>

<?php endif; ?>
</head>
<body>
    <div id="saveStatusDot" title="save status"></div>

<div class="layout">
  <aside class="sidebar">
    <div class="topbar">
      <strong>Files</strong>
      <button id="refreshTree" title="Refresh">↻</button>
    </div>
    <div class="tree-controls">
        <label>Folder: 
    <input id="folderFilter" placeholder="(leave blank for all)" />
  </label><br><br>
  <input id="treeFilter" placeholder="Filter files…" />
  <button id="btnExpandAll" title="Expand all">＋</button>
  <button id="btnCollapseAll" title="Collapse all">－</button>
</div>
<div class="scopebar">
  <span>📂</span>
  <span id="scopeCrumbs"></span>
  <button id="scopeReset" title="Back to workspace root">Reset</button>
</div>

    <div id="tree" class="tree"></div>

    <div class="createBox">
      <h4>Create</h4>
      <input id="newPath" placeholder="path/to/file.ext or folder/" />
      <div class="row">
        <button data-kind="file" id="btnCreateFile">+ File</button>
        <button data-kind="dir"  id="btnCreateDir">+ Folder</button>
      </div>
      <details>
        <summary>Bulk create</summary>
        <p class="hint">One per line. Prefix “dir:” for folders, otherwise a file is created.</p>
        <textarea id="bulkLines" rows="6" placeholder="dir: assets/
file: assets/app.js
file: index.php"></textarea>
        <button id="btnBulk">Run Bulk</button>
      </details>
    </div>

    
  </aside>

  <main class="main">
    <header class="mainbar">
  <div class="path">
    <span id="currentPath">—</span>
  </div>
  <div class="actions">
    <button id="btnToggleSidebar" title="Hide/Show file panel">☰ Files</button>
    <button id="btnSave">Save (Ctrl/Cmd+S)</button>
    <button id="btnRename">Rename</button>
    <button id="btnDelete" class="danger">Delete</button>
    <label class="previewToggle"><input type="checkbox" id="togglePreview" checked> Preview</label>
  </div>
</header>


    <section class="editorWrap">
      <div id="editor"></div>
      <textarea id="ta" class="hidden"></textarea>
      <iframe id="preview" title="Preview"></iframe>
    </section>
  </main>
    <!-- right-hand slideout for Link map -->
  <aside class="linkmap-panel" aria-hidden="true">
    <div class="topbar">
      <strong>Link map</strong>
      <button id="refreshLinks" title="Refresh">↻</button>
    </div>

    <div class="linkmap-controls" style="padding:10px 12px; border-bottom:1px solid var(--border); display:flex; gap:8px; align-items:center;">
  <button id="btnScanLinks">Scan</button>
  <button id="btnDownloadLinks" title="Download links as .txt">Download</button>
  <button id="btnCloseLinks" style="margin-left:auto">Close</button>
</div>


    <div id="linksPanel" class="linksContent" style="padding:10px 12px; overflow:auto; flex:1;"></div>
  </aside>

  <!-- floating right tab to reveal linkmap -->
  <button id="btnToggleLinks" title="Show link map">☰ Links</button>

</div>

<script>window.MINI_IDE = { base: '' };</script>
<script src="../webide/assets/app.js"></script>
<!-- Big centered loader overlay -->
<div id="pageLoader" aria-hidden="true">
  <div class="spinner" role="status" title="Saving…"></div>
</div>

</body>
</html>


===== FILE: webide/assets/app.js @ 2025-10-19 23:13:54 =====
(async function () {
  // ---------- DOM helpers ----------
  const $ = (q, el = document) => el.querySelector(q),
        $$ = (qAll, el = document) => Array.from(el.querySelectorAll(qAll));

  // ---------- API helper (POST FormData) ----------
  const api = async (params) => {
    const form = new FormData();
    Object.entries(params).forEach(([k, v]) => {
      if (Array.isArray(v)) v.forEach((x) => form.append(k + "[]", x));
      else form.append(k, v);
    });
    const r = await fetch("api.php", { method: "POST", body: form });
    return r.json();
  };
  
    function escapeHtml(s) {
    return String(s || "").replace(/[&<>"'`]/g, (c) => ({
      "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","`":"&#96;"
    }[c]));
  }


  // ---------- State ----------
  const state = {
    currentRel: "",
    ace: null,
    cfg: null,
    dirty: false,
    saving: false,
    scopeRel: "" // '' = workspace root; otherwise e.g. 'admin/pages'
    lastLinks: [] // cached edges from last scan
  };

  // ---------- Init ----------
  async function init() {
    state.cfg = await api({ action: "config" });

    // restore scope
    state.scopeRel = localStorage.getItem("mini_ide_scope") || "";

    bindUI();
    await refreshTree();

    // Editor: ACE if available, fallback to <textarea>
    if (state.cfg.ace && window.ace) {
      state.ace = ace.edit("editor");
      state.ace.setTheme("ace/theme/textmate");
      state.ace.session.setUseSoftTabs(true);
      state.ace.session.setTabSize(2);
      state.ace.setOption("fontSize", "13px");
      $("#ta").classList.add("hidden");
      state.ace.session.on("change", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    } else {
      $("#editor").classList.add("hidden");
      $("#ta").addEventListener("input", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    }

    // Load a sensible default (index.*) if present
    const guess = $$("#tree .file").find((n) =>
      /^(index\.(php|html?|md|txt))$/i.test(n.dataset.rel || "")
    );
    if (guess) openFile(guess.dataset.rel);
  }

  // ---------- UI wiring ----------
  function bindUI() {
    $("#refreshTree")?.addEventListener("click", refreshTree);
    $("#btnCreateFile")?.addEventListener("click", () => createFromInput(false));
    $("#btnCreateDir")?.addEventListener("click", () => createFromInput(true));
    $("#btnBulk")?.addEventListener("click", bulkCreate);
    $("#btnSave")?.addEventListener("click", saveCurrent);
    $("#btnRename")?.addEventListener("click", renameCurrent);
    $("#btnDelete")?.addEventListener("click", deleteCurrent);
    $("#btnScanLinks")?.addEventListener("click", scanLinks);
    $("#scopeReset")?.addEventListener("click", () => setScope(""));
    
        // existing
    $("#btnScanLinks")?.addEventListener("click", scanLinks);

    // right-panel linkmap controls
    $("#btnToggleLinks")?.addEventListener("click", toggleLinkmap);
    $("#refreshLinks")?.addEventListener("click", scanLinks);
    $("#btnCloseLinks")?.addEventListener("click", () => {
      document.body.classList.remove("linkmap-visible");
      localStorage.setItem("mini_ide_linkmap_hidden", "1");
    });

    
        // ---------- Floating tab reveal behaviour ----------
    // Keeps a temporary reveal state so we don't overwrite user's stored choice.
    // set default to hidden on first visit
if (localStorage.getItem("mini_ide_sidebar_hidden") === null) {
  localStorage.setItem("mini_ide_sidebar_hidden", "1");
}

// Restore sidebar state (existing code)
if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
  document.body.classList.add("sidebar-hidden");
}


    (function setupFloatingTabReveal() {
      const tab = $("#btnToggleSidebar");
      const sidebarEl = document.querySelector(".sidebar");
      if (!tab || !sidebarEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden"); // "1" if user had hidden

      // When pointer enters the floating tab: reveal the sidebar and hide the tab itself.
      tab.addEventListener("mouseenter", (e) => {
        // remember stored preference so we can restore it later
        prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden");
        document.body.classList.add("sidebar-visible");
        tempReveal = true;

        // reveal sidebar visually (do NOT change stored preference)
        document.body.classList.remove("sidebar-hidden");
        // hide the floating tab while user is interacting with sidebar
        tab.style.display = "none";
      });

      // When pointer leaves the sidebar area, hide the sidebar again if it was shown by the tab
      sidebarEl.addEventListener("mouseleave", (e) => {
        // Only auto-hide when this reveal was triggered by the tab (avoid interfering with manual toggle)
       // toggleSidebar();
       document.body.classList.remove("sidebar-visible");
        if (!tempReveal) return;

        // restore to user's stored preference
        if (prevStoredHidden === "1") {
          // user had explicitly hidden -> re-hide sidebar
          document.body.classList.add("sidebar-hidden");
        } else {
          // user had it visible by default — keep visible (or you can re-hide; sticking with restore)
          document.body.classList.remove("sidebar-hidden");
        }

        // show the floating tab again
        tab.style.display = "";
        tempReveal = false;
      });

      // Extra: if user moves quickly from the tab into the sidebar, keep it shown.
      // Also handle pointerout from tab to sidebar — do nothing because mouseover already removed tab.
      // If user clicks the tab (existing click handler remains), we still toggle as before.
    })();

    // Restore linkmap state (default hidden)
    if (localStorage.getItem("mini_ide_linkmap_hidden") === null) {
      localStorage.setItem("mini_ide_linkmap_hidden", "1"); // hide by default
    }
    if (localStorage.getItem("mini_ide_linkmap_hidden") !== "1") {
      document.body.classList.add("linkmap-visible");
    }

    (function setupFloatingLinkmapReveal() {
      const tab = $("#btnToggleLinks");
      const panelEl = document.querySelector(".linkmap-panel");
      if (!tab || !panelEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");

      // pointer enters the floating tab -> reveal
      tab.addEventListener("mouseenter", () => {
        prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");
        document.body.classList.add("linkmap-visible");
        tempReveal = true;
        tab.style.display = "none"; // hide tab while interacting
      });

      // when pointer leaves the panel, restore preference
      panelEl.addEventListener("mouseleave", () => {
        document.body.classList.remove("linkmap-visible");
        if (!tempReveal) return;
        if (prevStoredHidden === "1") {
          document.body.classList.remove("linkmap-visible");
        } else {
          document.body.classList.add("linkmap-visible");
        }
        tab.style.display = "";
        tempReveal = false;
      });
    })();

    // Keyboard shortcuts (capture)
    window.addEventListener(
      "keydown",
      (e) => {
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
          e.preventDefault(); e.stopPropagation();
          saveCurrent();
        }
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
          e.preventDefault(); toggleSidebar();
        }
      },
      true
    );

    // Preview toggle
    $("#togglePreview")?.addEventListener("change", syncPreviewVisibility);

    // Sidebar toggle button
    $("#btnToggleSidebar")?.addEventListener("click", toggleSidebar);

    // Restore sidebar state
    if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
      document.body.classList.add("sidebar-hidden");
    }
  }

  function toggleSidebar() {
    const hidden = document.body.classList.toggle("sidebar-hidden");
    localStorage.setItem("mini_ide_sidebar_hidden", hidden ? "1" : "0");
  }
  
    function toggleLinkmap() {
    const hidden = document.body.classList.toggle("linkmap-visible");
    // store the inverse meaning: "hidden" = 1
    localStorage.setItem("mini_ide_linkmap_hidden", hidden ? "0" : "1");
  }


  function syncPreviewVisibility() {
    const on = $("#togglePreview")?.checked;
    if (on == null) return;
    $("#preview").style.display = on ? "block" : "none";
    document.querySelector(".editorWrap").style.gridTemplateColumns = on ? "1fr 40%" : "1fr";
  }

  // ---------- Save status / dirty UI ----------
  function setSaveBusy(on) {
    // big overlay
    const overlay = $("#pageLoader");
    if (overlay) overlay.style.display = on ? "flex" : "none";

    // header spinner
    const loader = $("#saveLoader");
    if (loader) loader.style.visibility = on ? "visible" : "hidden";

    const dot = $("#saveStatusDot");
    if (dot && on) dot.classList.remove("ok", "err", "dirty");
  }
  function setSaveDirty(on) {
    const btn = $("#btnSave");
    if (btn) btn.style.borderColor = on ? "#c9e8dc" : "var(--border)";
  }
  function markSaveOK() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("err", "dirty");
    dot.classList.add("ok");
    setTimeout(() => dot.classList.remove("ok"), 900);
  }
  function markSaveErr() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("ok", "dirty");
    dot.classList.add("err");
  }
  function updateDirtyUI() {
    // header asterisk
    const cp = $("#currentPath");
    if (cp) cp.textContent = (state.currentRel || "—") + (state.dirty ? " *" : "");

    // tree asterisk (on current file only)
    $$("#tree .file .name").forEach((n) => n.classList.remove("dirty"));
    if (state.currentRel) {
      const n = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"] .name`);
      if (n && state.dirty) n.classList.add("dirty");
    }

    // status dot amber while dirty
    const dot = $("#saveStatusDot");
    if (dot) {
      dot.classList.remove("ok", "err");
      dot.classList.toggle("dirty", !!state.dirty);
    }
  }

  // ---------- Scope helpers ----------
  function setScope(rel) {
    state.scopeRel = (rel || "").replace(/^\/+|\/+$/g, "");
    localStorage.setItem("mini_ide_scope", state.scopeRel);
    refreshTree();
  }

  function renderScopeCrumbs(){
    const wrap = $("#scopeCrumbs"); if (!wrap) return;
    wrap.innerHTML = "";
    const mk = (txt, rel, isCurrent=false) => {
      const s = document.createElement("span");
      s.className = "crumb" + (isCurrent ? " current" : "");
      s.textContent = txt;
      s.onclick = () => !isCurrent && setScope(rel);
      if (isCurrent) s.title = "Current folder";
      return s;
    };
    const parts = (state.scopeRel || "").split("/").filter(Boolean);
    wrap.appendChild(mk("root", "", parts.length === 0));
    let acc = "";
    parts.forEach((p, i) => {
      const sep = document.createElement("span");
      sep.className = "sep"; sep.textContent = "/";
      wrap.appendChild(sep);
      acc = acc ? acc + "/" + p : p;
      wrap.appendChild(mk(p, acc, i === parts.length - 1));
    });
  }

  // ---------- Tree (server-scoped, collapsible) ----------
  function toNested(items) {
    const root = { name: "", rel: "", dirs: {}, files: [] };
    const entries = items
      .map((it) => ({ type: it.type, rel: (it.rel || "").replace(/^\/+/, "") }))
      .filter((e) => e.rel);

    // ensure nodes
    entries.forEach((e) => {
      const parts = e.rel.split("/");
      if (e.type === "dir") {
        let cur = root;
        for (const part of parts) {
          cur.dirs[part] = cur.dirs[part] || {
            name: part,
            rel: (cur.rel ? cur.rel + "/" : "") + part,
            dirs: {},
            files: []
          };
          cur = cur.dirs[part];
        }
      }
    });

    // place files
    entries.filter((e) => e.type === "file").forEach((e) => {
      const parts = e.rel.split("/");
      const name = parts.pop();
      let cur = root;
      for (const part of parts) {
        cur.dirs[part] = cur.dirs[part] || {
          name: part,
          rel: (cur.rel ? cur.rel + "/" : "") + part,
          dirs: {},
          files: []
        };
        cur = cur.dirs[part];
      }
      cur.files.push({ name, rel: e.rel });
    });

    // sort
    (function sort(n) {
      Object.values(n.dirs).forEach(sort);
      n.files.sort((a, b) => a.name.localeCompare(b.name));
    })(root);

    return root;
  }

  function renderTree(rootNode, container) {
    const openState = JSON.parse(localStorage.getItem("mini_ide_open_dirs") || "{}");
    const setOpen = (rel, open) => {
      if (open) openState[rel || "/"] = 1;
      else delete openState[rel || "/"];
      localStorage.setItem("mini_ide_open_dirs", JSON.stringify(openState));
    };

    container.innerHTML = "";

    const mkDir = (node) => {
      const det = document.createElement("details");
      if (openState[node.rel || "/"]) det.open = true;

      const sum = document.createElement("summary");
      sum.innerHTML = `
        <span class="chev">›</span>
        <span class="label name">📁 ${node.name || "root"}</span>
        <button class="focus" type="button" title="Focus here" data-rel="${node.rel}">📌 Focus</button>
      `;
      det.appendChild(sum);

      // node.rel from server is already full root-relative path
      const dirRel = (node.rel || "");

      // focus controls
      sum.ondblclick = (e) => { e.preventDefault(); setScope(dirRel); };
      sum.querySelector(".focus").onclick = (e) => { e.stopPropagation(); setScope(dirRel); };

      det.addEventListener("toggle", () => setOpen(node.rel, det.open));

      // child dirs
      const dirNames = Object.keys(node.dirs).sort((a, b) => a.localeCompare(b));
      dirNames.forEach((name) => det.appendChild(mkDir(node.dirs[name])));

      // files: use rel from server as-is (already full)
      node.files.forEach((f) => {
        const div = document.createElement("div");
        div.className = "file";
        div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
        div.dataset.rel = f.rel;
        div.onclick = () => openFile(f.rel);
        det.appendChild(div);
      });

      return det;
    };

    // top-level dirs
    const dirNames = Object.keys(rootNode.dirs).sort((a, b) => a.localeCompare(b));
    dirNames.forEach((name) => container.appendChild(mkDir(rootNode.dirs[name])));

    // top-level files: use rel as-is
    rootNode.files.forEach((f) => {
      const div = document.createElement("div");
      div.className = "file";
      div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
      div.dataset.rel = f.rel;
      div.onclick = () => openFile(f.rel);
      container.appendChild(div);
    });
  }

  async function refreshTree() {
    try {
      const res = await api({ action: "tree", rel: state.scopeRel });
      if (!res || res.ok !== true || !Array.isArray(res.items)) {
        const msg = res && res.error ? res.error : "Tree request failed";
        alert("Mini IDE error: " + msg);
        console.error("Tree response:", res);
        return;
      }
      const nested = toNested(res.items);
      renderTree(nested, $("#tree"));
      renderScopeCrumbs();

      // re-highlight current file if visible
      if (state.currentRel) {
        $$("#tree .file").forEach((n) => n.classList.remove("active"));
        const hit = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"]`);
        if (hit) hit.classList.add("active");
        updateDirtyUI();
      }
    } catch (e) {
      alert("Mini IDE fetch error (tree): " + e);
      console.error(e);
    }
  }

  // ---------- File ops ----------
  async function openFile(rel) {
    const res = await api({ action: "read", rel });
    if (!res.ok) return alert(res.error || "Failed to read");
    state.currentRel = rel;

    // highlight selected
    $$("#tree .file").forEach((n) => n.classList.remove("active"));
    const hit = $(`#tree .file[data-rel="${CSS.escape(rel)}"]`);
    if (hit) hit.classList.add("active");

    if (res.file.editable) {
      setEditorContent(res.file.content || "");
    } else {
      setEditorContent("// Not editable (binary or large). Size: " + res.file.size + " bytes\n");
    }
    state.dirty = false;
    setSaveDirty(false);
    updateDirtyUI();
    refreshPreview();
  }

  function getEditorContent() {
    if (state.ace) return state.ace.getValue();
    return $("#ta").value;
  }

  function setEditorContent(txt) {
    if (state.ace) {
      state.ace.setValue(txt ?? "", -1);
      // Set mode from extension
      const ext = (state.currentRel.split(".").pop() || "").toLowerCase();
      const map = {
        js: "javascript",
        css: "css",
        php: "php",
        html: "html",
        htm: "html",
        json: "json",
        md: "markdown",
        py: "python",
        yml: "yaml",
        yaml: "yaml",
        c: "c_cpp",
        cpp: "c_cpp",
        h: "c_cpp",
        sql: "sql",
        csv: "text",
        txt: "text"
      };
      state.ace.session.setMode("ace/mode/" + (map[ext] || "text"));
    } else {
      $("#ta").classList.remove("hidden");
      $("#ta").value = txt ?? "";
    }
  }

  async function saveCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (state.saving) return; // prevent double-taps
    state.saving = true;
    setSaveBusy(true);

    try {
      const content = getEditorContent();
      const res = await api({ action: "save", rel: state.currentRel, content });
      if (!res.ok) {
        markSaveErr();
        throw new Error(res.error || "Save failed");
      }

      // Server verification
      let verified = !!res.verify_ok;

      // mtime wait (shared hosts can be slow)
      const targetMtime = res.mtime || 0;
      const okStat = await waitForStat(state.currentRel, targetMtime, 8, 150);
      if (!okStat) console.warn("Proceeding without stat confirmation");

      // Optional client-side readback if server said not verified
      if (!verified) {
        try {
          const rb = await api({ action: "read", rel: state.currentRel });
          if (rb.ok && rb.file && typeof rb.file.content === "string") {
            verified = (rb.file.content === content);
          }
        } catch {}
      }

      if (!verified) {
        markSaveErr();
        alert("Save did not verify. The file contents on disk did not match what was sent.");
        return; // do not mark OK or clear dirty
      }

      state.dirty = false;
      setSaveDirty(false);
      updateDirtyUI();   // clear asterisks + amber dot
      markSaveOK();      // green blip
      refreshPreview(true);

    } catch (e) {
      console.error(e);
      alert("Save error: " + e.message);
      markSaveErr();     // red
    } finally {
      state.saving = false;
      setSaveBusy(false);
    }
  }

  // poll stat until mtime >= target
  async function waitForStat(rel, target, tries = 8, delay = 150) {
    for (let i = 0; i < tries; i++) {
      try {
        const s = await api({ action: "stat", rel });
        if (s.ok && (s.mtime || 0) >= target) return true;
      } catch {}
      await new Promise((r) => setTimeout(r, delay));
    }
    return false;
  }

  async function renameCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    const to = prompt("Rename to (path):", state.currentRel);
    if (!to || to === state.currentRel) return;
    const res = await api({ action: "rename", from: state.currentRel, to });
    if (!res.ok) return alert(res.error || "Rename failed");
    state.currentRel = to;
    $("#currentPath").textContent = to;
    await refreshTree();
    refreshPreview();
  }

  async function deleteCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (!confirm("Delete " + state.currentRel + " ?")) return;
    const res = await api({ action: "delete", rel: state.currentRel });
    if (!res.ok) return alert(res.error || "Delete failed");
    state.currentRel = "";
    $("#currentPath").textContent = "—";
    if (state.ace) state.ace.setValue("");
    else $("#ta").value = "";
    await refreshTree();
    refreshPreview();
  }

  async function createFromInput(isDir) {
    const raw = $("#newPath").value.trim();
    if (!raw) return;

    const relPath = state.scopeRel
      ? (state.scopeRel + "/" + raw).replace(/\/+/g, "/").replace(/^\/|\/$/g, "")
      : raw;

    const res = await api({ action: "create", rel: relPath, type: isDir ? "dir" : "file" });
    if (!res.ok) return alert(res.error || "Create failed");
    $("#newPath").value = "";
    await refreshTree();
    if (!isDir) openFile(relPath);
  }

  async function bulkCreate() {
    const lines = $("#bulkLines").value;
    if (!lines.trim()) return;

    // Prefix each line's path with scope (keep labels 'dir:'/'file:' intact)
    const scoped = lines.split("\n").map((t) => {
      const ln = t.trim();
      if (!ln) return ln;
      const m = ln.match(/^(dir:|file:)?\s*(.*)$/i);
      if (!m) return ln;
      const label = (m[1] || "").toLowerCase();
      let p = m[2].trim();
      if (p && state.scopeRel && !p.startsWith("/")) {
        p = (state.scopeRel + "/" + p).replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
      }
      return (label ? label + " " : "") + p;
    }).join("\n");

    const res = await api({ action: "bulk", lines: scoped });
    if (!res.ok) return alert(res.error || "Bulk failed");
    await refreshTree();
    alert("Bulk operations complete.");
  }

  // ---------- Preview ----------
  function refreshPreview(/* force = false */) {
    const on = $("#togglePreview")?.checked;
    if (!on) return;

    // Prefer current file; otherwise fall back to index.html (safer default)
    let rel = state.currentRel || "index.html";
    if (!/\.(php|html?|md|txt|css|js)$/i.test(rel)) rel = "index.html";

    const base = (state.cfg && state.cfg.workspace_url) || "/";
    const url = (base.endsWith("/") ? base : base + "/") + rel;

    const ifr = $("#preview");
    ifr.src = url + (url.includes("?") ? "&" : "?") + "_t=" + Date.now(); // cache-bust
  }

  // ---------- Link scan ----------
    async function scanLinks() {
    const res = await api({ action: "links" });
    if (!res.ok) return alert(res.error || "Scan failed");
    const box = document.getElementById("linksPanel");
    if (!box) return alert("Link panel not found.");
    box.innerHTML = "";
    if (!res.edges || !res.edges.length) {
      box.textContent = "No references found.";
      return;
    }
    res.edges.forEach((e) => {
      const div = document.createElement("div");
      div.className = "edge";
      // make clickable filenames optionally
      div.innerHTML = `<div style="display:flex;justify-content:space-between;gap:8px">
                         <div style="flex:1;min-width:0; overflow:hidden; text-overflow:ellipsis;">
                           <strong>${escapeHtml(e.from)}</strong> &nbsp;→&nbsp; ${escapeHtml(e.to)}
                         </div>
                         <div style="flex:0 0 auto">
                           <button class="openLinkFile" data-rel="${e.from}">Open</button>
                         </div>
                       </div>`;
      box.appendChild(div);
    });

    // wire open buttons
    $$(".openLinkFile", box).forEach((b) => {
      b.addEventListener("click", () => {
        const rel = b.dataset.rel;
        if (rel) {
          // ensure the panel closes so tree is visible (optional)
          document.body.classList.remove("linkmap-visible");
          openFile(rel);
        }
      });
    });
  }


  // ---------- Go ----------
  init();
})();


===== FILE: webide/assets/app.js @ 2025-10-19 23:14:26 =====
(async function () {
  // ---------- DOM helpers ----------
  const $ = (q, el = document) => el.querySelector(q),
        $$ = (qAll, el = document) => Array.from(el.querySelectorAll(qAll));

  // ---------- API helper (POST FormData) ----------
  const api = async (params) => {
    const form = new FormData();
    Object.entries(params).forEach(([k, v]) => {
      if (Array.isArray(v)) v.forEach((x) => form.append(k + "[]", x));
      else form.append(k, v);
    });
    const r = await fetch("api.php", { method: "POST", body: form });
    return r.json();
  };
  
    function escapeHtml(s) {
    return String(s || "").replace(/[&<>"'`]/g, (c) => ({
      "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","`":"&#96;"
    }[c]));
  }


  // ---------- State ----------
  const state = {
    currentRel: "",
    ace: null,
    cfg: null,
    dirty: false,
    saving: false,
    scopeRel: "" // '' = workspace root; otherwise e.g. 'admin/pages'
    lastLinks: [] // cached edges from last scan
  };

  // ---------- Init ----------
  async function init() {
    state.cfg = await api({ action: "config" });

    // restore scope
    state.scopeRel = localStorage.getItem("mini_ide_scope") || "";

    bindUI();
    await refreshTree();

    // Editor: ACE if available, fallback to <textarea>
    if (state.cfg.ace && window.ace) {
      state.ace = ace.edit("editor");
      state.ace.setTheme("ace/theme/textmate");
      state.ace.session.setUseSoftTabs(true);
      state.ace.session.setTabSize(2);
      state.ace.setOption("fontSize", "13px");
      $("#ta").classList.add("hidden");
      state.ace.session.on("change", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    } else {
      $("#editor").classList.add("hidden");
      $("#ta").addEventListener("input", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    }

    // Load a sensible default (index.*) if present
    const guess = $$("#tree .file").find((n) =>
      /^(index\.(php|html?|md|txt))$/i.test(n.dataset.rel || "")
    );
    if (guess) openFile(guess.dataset.rel);
  }

  // ---------- UI wiring ----------
  function bindUI() {
    $("#refreshTree")?.addEventListener("click", refreshTree);
    $("#btnCreateFile")?.addEventListener("click", () => createFromInput(false));
    $("#btnCreateDir")?.addEventListener("click", () => createFromInput(true));
    $("#btnBulk")?.addEventListener("click", bulkCreate);
    $("#btnSave")?.addEventListener("click", saveCurrent);
    $("#btnRename")?.addEventListener("click", renameCurrent);
    $("#btnDelete")?.addEventListener("click", deleteCurrent);
    $("#btnScanLinks")?.addEventListener("click", scanLinks);
    $("#scopeReset")?.addEventListener("click", () => setScope(""));
    $("#btnDownloadLinks")?.addEventListener("click", downloadLinksTxt);

    
        // existing
    $("#btnScanLinks")?.addEventListener("click", scanLinks);

    // right-panel linkmap controls
    $("#btnToggleLinks")?.addEventListener("click", toggleLinkmap);
    $("#refreshLinks")?.addEventListener("click", scanLinks);
    $("#btnCloseLinks")?.addEventListener("click", () => {
      document.body.classList.remove("linkmap-visible");
      localStorage.setItem("mini_ide_linkmap_hidden", "1");
    });

    
        // ---------- Floating tab reveal behaviour ----------
    // Keeps a temporary reveal state so we don't overwrite user's stored choice.
    // set default to hidden on first visit
if (localStorage.getItem("mini_ide_sidebar_hidden") === null) {
  localStorage.setItem("mini_ide_sidebar_hidden", "1");
}

// Restore sidebar state (existing code)
if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
  document.body.classList.add("sidebar-hidden");
}


    (function setupFloatingTabReveal() {
      const tab = $("#btnToggleSidebar");
      const sidebarEl = document.querySelector(".sidebar");
      if (!tab || !sidebarEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden"); // "1" if user had hidden

      // When pointer enters the floating tab: reveal the sidebar and hide the tab itself.
      tab.addEventListener("mouseenter", (e) => {
        // remember stored preference so we can restore it later
        prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden");
        document.body.classList.add("sidebar-visible");
        tempReveal = true;

        // reveal sidebar visually (do NOT change stored preference)
        document.body.classList.remove("sidebar-hidden");
        // hide the floating tab while user is interacting with sidebar
        tab.style.display = "none";
      });

      // When pointer leaves the sidebar area, hide the sidebar again if it was shown by the tab
      sidebarEl.addEventListener("mouseleave", (e) => {
        // Only auto-hide when this reveal was triggered by the tab (avoid interfering with manual toggle)
       // toggleSidebar();
       document.body.classList.remove("sidebar-visible");
        if (!tempReveal) return;

        // restore to user's stored preference
        if (prevStoredHidden === "1") {
          // user had explicitly hidden -> re-hide sidebar
          document.body.classList.add("sidebar-hidden");
        } else {
          // user had it visible by default — keep visible (or you can re-hide; sticking with restore)
          document.body.classList.remove("sidebar-hidden");
        }

        // show the floating tab again
        tab.style.display = "";
        tempReveal = false;
      });

      // Extra: if user moves quickly from the tab into the sidebar, keep it shown.
      // Also handle pointerout from tab to sidebar — do nothing because mouseover already removed tab.
      // If user clicks the tab (existing click handler remains), we still toggle as before.
    })();

    // Restore linkmap state (default hidden)
    if (localStorage.getItem("mini_ide_linkmap_hidden") === null) {
      localStorage.setItem("mini_ide_linkmap_hidden", "1"); // hide by default
    }
    if (localStorage.getItem("mini_ide_linkmap_hidden") !== "1") {
      document.body.classList.add("linkmap-visible");
    }

    (function setupFloatingLinkmapReveal() {
      const tab = $("#btnToggleLinks");
      const panelEl = document.querySelector(".linkmap-panel");
      if (!tab || !panelEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");

      // pointer enters the floating tab -> reveal
      tab.addEventListener("mouseenter", () => {
        prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");
        document.body.classList.add("linkmap-visible");
        tempReveal = true;
        tab.style.display = "none"; // hide tab while interacting
      });

      // when pointer leaves the panel, restore preference
      panelEl.addEventListener("mouseleave", () => {
        document.body.classList.remove("linkmap-visible");
        if (!tempReveal) return;
        if (prevStoredHidden === "1") {
          document.body.classList.remove("linkmap-visible");
        } else {
          document.body.classList.add("linkmap-visible");
        }
        tab.style.display = "";
        tempReveal = false;
      });
    })();

    // Keyboard shortcuts (capture)
    window.addEventListener(
      "keydown",
      (e) => {
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
          e.preventDefault(); e.stopPropagation();
          saveCurrent();
        }
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
          e.preventDefault(); toggleSidebar();
        }
      },
      true
    );

    // Preview toggle
    $("#togglePreview")?.addEventListener("change", syncPreviewVisibility);

    // Sidebar toggle button
    $("#btnToggleSidebar")?.addEventListener("click", toggleSidebar);

    // Restore sidebar state
    if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
      document.body.classList.add("sidebar-hidden");
    }
  }

  function toggleSidebar() {
    const hidden = document.body.classList.toggle("sidebar-hidden");
    localStorage.setItem("mini_ide_sidebar_hidden", hidden ? "1" : "0");
  }
  
    function toggleLinkmap() {
    const hidden = document.body.classList.toggle("linkmap-visible");
    // store the inverse meaning: "hidden" = 1
    localStorage.setItem("mini_ide_linkmap_hidden", hidden ? "0" : "1");
  }


  function syncPreviewVisibility() {
    const on = $("#togglePreview")?.checked;
    if (on == null) return;
    $("#preview").style.display = on ? "block" : "none";
    document.querySelector(".editorWrap").style.gridTemplateColumns = on ? "1fr 40%" : "1fr";
  }

  // ---------- Save status / dirty UI ----------
  function setSaveBusy(on) {
    // big overlay
    const overlay = $("#pageLoader");
    if (overlay) overlay.style.display = on ? "flex" : "none";

    // header spinner
    const loader = $("#saveLoader");
    if (loader) loader.style.visibility = on ? "visible" : "hidden";

    const dot = $("#saveStatusDot");
    if (dot && on) dot.classList.remove("ok", "err", "dirty");
  }
  function setSaveDirty(on) {
    const btn = $("#btnSave");
    if (btn) btn.style.borderColor = on ? "#c9e8dc" : "var(--border)";
  }
  function markSaveOK() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("err", "dirty");
    dot.classList.add("ok");
    setTimeout(() => dot.classList.remove("ok"), 900);
  }
  function markSaveErr() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("ok", "dirty");
    dot.classList.add("err");
  }
  function updateDirtyUI() {
    // header asterisk
    const cp = $("#currentPath");
    if (cp) cp.textContent = (state.currentRel || "—") + (state.dirty ? " *" : "");

    // tree asterisk (on current file only)
    $$("#tree .file .name").forEach((n) => n.classList.remove("dirty"));
    if (state.currentRel) {
      const n = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"] .name`);
      if (n && state.dirty) n.classList.add("dirty");
    }

    // status dot amber while dirty
    const dot = $("#saveStatusDot");
    if (dot) {
      dot.classList.remove("ok", "err");
      dot.classList.toggle("dirty", !!state.dirty);
    }
  }

  // ---------- Scope helpers ----------
  function setScope(rel) {
    state.scopeRel = (rel || "").replace(/^\/+|\/+$/g, "");
    localStorage.setItem("mini_ide_scope", state.scopeRel);
    refreshTree();
  }

  function renderScopeCrumbs(){
    const wrap = $("#scopeCrumbs"); if (!wrap) return;
    wrap.innerHTML = "";
    const mk = (txt, rel, isCurrent=false) => {
      const s = document.createElement("span");
      s.className = "crumb" + (isCurrent ? " current" : "");
      s.textContent = txt;
      s.onclick = () => !isCurrent && setScope(rel);
      if (isCurrent) s.title = "Current folder";
      return s;
    };
    const parts = (state.scopeRel || "").split("/").filter(Boolean);
    wrap.appendChild(mk("root", "", parts.length === 0));
    let acc = "";
    parts.forEach((p, i) => {
      const sep = document.createElement("span");
      sep.className = "sep"; sep.textContent = "/";
      wrap.appendChild(sep);
      acc = acc ? acc + "/" + p : p;
      wrap.appendChild(mk(p, acc, i === parts.length - 1));
    });
  }

  // ---------- Tree (server-scoped, collapsible) ----------
  function toNested(items) {
    const root = { name: "", rel: "", dirs: {}, files: [] };
    const entries = items
      .map((it) => ({ type: it.type, rel: (it.rel || "").replace(/^\/+/, "") }))
      .filter((e) => e.rel);

    // ensure nodes
    entries.forEach((e) => {
      const parts = e.rel.split("/");
      if (e.type === "dir") {
        let cur = root;
        for (const part of parts) {
          cur.dirs[part] = cur.dirs[part] || {
            name: part,
            rel: (cur.rel ? cur.rel + "/" : "") + part,
            dirs: {},
            files: []
          };
          cur = cur.dirs[part];
        }
      }
    });

    // place files
    entries.filter((e) => e.type === "file").forEach((e) => {
      const parts = e.rel.split("/");
      const name = parts.pop();
      let cur = root;
      for (const part of parts) {
        cur.dirs[part] = cur.dirs[part] || {
          name: part,
          rel: (cur.rel ? cur.rel + "/" : "") + part,
          dirs: {},
          files: []
        };
        cur = cur.dirs[part];
      }
      cur.files.push({ name, rel: e.rel });
    });

    // sort
    (function sort(n) {
      Object.values(n.dirs).forEach(sort);
      n.files.sort((a, b) => a.name.localeCompare(b.name));
    })(root);

    return root;
  }

  function renderTree(rootNode, container) {
    const openState = JSON.parse(localStorage.getItem("mini_ide_open_dirs") || "{}");
    const setOpen = (rel, open) => {
      if (open) openState[rel || "/"] = 1;
      else delete openState[rel || "/"];
      localStorage.setItem("mini_ide_open_dirs", JSON.stringify(openState));
    };

    container.innerHTML = "";

    const mkDir = (node) => {
      const det = document.createElement("details");
      if (openState[node.rel || "/"]) det.open = true;

      const sum = document.createElement("summary");
      sum.innerHTML = `
        <span class="chev">›</span>
        <span class="label name">📁 ${node.name || "root"}</span>
        <button class="focus" type="button" title="Focus here" data-rel="${node.rel}">📌 Focus</button>
      `;
      det.appendChild(sum);

      // node.rel from server is already full root-relative path
      const dirRel = (node.rel || "");

      // focus controls
      sum.ondblclick = (e) => { e.preventDefault(); setScope(dirRel); };
      sum.querySelector(".focus").onclick = (e) => { e.stopPropagation(); setScope(dirRel); };

      det.addEventListener("toggle", () => setOpen(node.rel, det.open));

      // child dirs
      const dirNames = Object.keys(node.dirs).sort((a, b) => a.localeCompare(b));
      dirNames.forEach((name) => det.appendChild(mkDir(node.dirs[name])));

      // files: use rel from server as-is (already full)
      node.files.forEach((f) => {
        const div = document.createElement("div");
        div.className = "file";
        div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
        div.dataset.rel = f.rel;
        div.onclick = () => openFile(f.rel);
        det.appendChild(div);
      });

      return det;
    };

    // top-level dirs
    const dirNames = Object.keys(rootNode.dirs).sort((a, b) => a.localeCompare(b));
    dirNames.forEach((name) => container.appendChild(mkDir(rootNode.dirs[name])));

    // top-level files: use rel as-is
    rootNode.files.forEach((f) => {
      const div = document.createElement("div");
      div.className = "file";
      div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
      div.dataset.rel = f.rel;
      div.onclick = () => openFile(f.rel);
      container.appendChild(div);
    });
  }

  async function refreshTree() {
    try {
      const res = await api({ action: "tree", rel: state.scopeRel });
      if (!res || res.ok !== true || !Array.isArray(res.items)) {
        const msg = res && res.error ? res.error : "Tree request failed";
        alert("Mini IDE error: " + msg);
        console.error("Tree response:", res);
        return;
      }
      const nested = toNested(res.items);
      renderTree(nested, $("#tree"));
      renderScopeCrumbs();

      // re-highlight current file if visible
      if (state.currentRel) {
        $$("#tree .file").forEach((n) => n.classList.remove("active"));
        const hit = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"]`);
        if (hit) hit.classList.add("active");
        updateDirtyUI();
      }
    } catch (e) {
      alert("Mini IDE fetch error (tree): " + e);
      console.error(e);
    }
  }

  // ---------- File ops ----------
  async function openFile(rel) {
    const res = await api({ action: "read", rel });
    if (!res.ok) return alert(res.error || "Failed to read");
    state.currentRel = rel;

    // highlight selected
    $$("#tree .file").forEach((n) => n.classList.remove("active"));
    const hit = $(`#tree .file[data-rel="${CSS.escape(rel)}"]`);
    if (hit) hit.classList.add("active");

    if (res.file.editable) {
      setEditorContent(res.file.content || "");
    } else {
      setEditorContent("// Not editable (binary or large). Size: " + res.file.size + " bytes\n");
    }
    state.dirty = false;
    setSaveDirty(false);
    updateDirtyUI();
    refreshPreview();
  }

  function getEditorContent() {
    if (state.ace) return state.ace.getValue();
    return $("#ta").value;
  }

  function setEditorContent(txt) {
    if (state.ace) {
      state.ace.setValue(txt ?? "", -1);
      // Set mode from extension
      const ext = (state.currentRel.split(".").pop() || "").toLowerCase();
      const map = {
        js: "javascript",
        css: "css",
        php: "php",
        html: "html",
        htm: "html",
        json: "json",
        md: "markdown",
        py: "python",
        yml: "yaml",
        yaml: "yaml",
        c: "c_cpp",
        cpp: "c_cpp",
        h: "c_cpp",
        sql: "sql",
        csv: "text",
        txt: "text"
      };
      state.ace.session.setMode("ace/mode/" + (map[ext] || "text"));
    } else {
      $("#ta").classList.remove("hidden");
      $("#ta").value = txt ?? "";
    }
  }

  async function saveCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (state.saving) return; // prevent double-taps
    state.saving = true;
    setSaveBusy(true);

    try {
      const content = getEditorContent();
      const res = await api({ action: "save", rel: state.currentRel, content });
      if (!res.ok) {
        markSaveErr();
        throw new Error(res.error || "Save failed");
      }

      // Server verification
      let verified = !!res.verify_ok;

      // mtime wait (shared hosts can be slow)
      const targetMtime = res.mtime || 0;
      const okStat = await waitForStat(state.currentRel, targetMtime, 8, 150);
      if (!okStat) console.warn("Proceeding without stat confirmation");

      // Optional client-side readback if server said not verified
      if (!verified) {
        try {
          const rb = await api({ action: "read", rel: state.currentRel });
          if (rb.ok && rb.file && typeof rb.file.content === "string") {
            verified = (rb.file.content === content);
          }
        } catch {}
      }

      if (!verified) {
        markSaveErr();
        alert("Save did not verify. The file contents on disk did not match what was sent.");
        return; // do not mark OK or clear dirty
      }

      state.dirty = false;
      setSaveDirty(false);
      updateDirtyUI();   // clear asterisks + amber dot
      markSaveOK();      // green blip
      refreshPreview(true);

    } catch (e) {
      console.error(e);
      alert("Save error: " + e.message);
      markSaveErr();     // red
    } finally {
      state.saving = false;
      setSaveBusy(false);
    }
  }

  // poll stat until mtime >= target
  async function waitForStat(rel, target, tries = 8, delay = 150) {
    for (let i = 0; i < tries; i++) {
      try {
        const s = await api({ action: "stat", rel });
        if (s.ok && (s.mtime || 0) >= target) return true;
      } catch {}
      await new Promise((r) => setTimeout(r, delay));
    }
    return false;
  }

  async function renameCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    const to = prompt("Rename to (path):", state.currentRel);
    if (!to || to === state.currentRel) return;
    const res = await api({ action: "rename", from: state.currentRel, to });
    if (!res.ok) return alert(res.error || "Rename failed");
    state.currentRel = to;
    $("#currentPath").textContent = to;
    await refreshTree();
    refreshPreview();
  }

  async function deleteCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (!confirm("Delete " + state.currentRel + " ?")) return;
    const res = await api({ action: "delete", rel: state.currentRel });
    if (!res.ok) return alert(res.error || "Delete failed");
    state.currentRel = "";
    $("#currentPath").textContent = "—";
    if (state.ace) state.ace.setValue("");
    else $("#ta").value = "";
    await refreshTree();
    refreshPreview();
  }

  async function createFromInput(isDir) {
    const raw = $("#newPath").value.trim();
    if (!raw) return;

    const relPath = state.scopeRel
      ? (state.scopeRel + "/" + raw).replace(/\/+/g, "/").replace(/^\/|\/$/g, "")
      : raw;

    const res = await api({ action: "create", rel: relPath, type: isDir ? "dir" : "file" });
    if (!res.ok) return alert(res.error || "Create failed");
    $("#newPath").value = "";
    await refreshTree();
    if (!isDir) openFile(relPath);
  }

  async function bulkCreate() {
    const lines = $("#bulkLines").value;
    if (!lines.trim()) return;

    // Prefix each line's path with scope (keep labels 'dir:'/'file:' intact)
    const scoped = lines.split("\n").map((t) => {
      const ln = t.trim();
      if (!ln) return ln;
      const m = ln.match(/^(dir:|file:)?\s*(.*)$/i);
      if (!m) return ln;
      const label = (m[1] || "").toLowerCase();
      let p = m[2].trim();
      if (p && state.scopeRel && !p.startsWith("/")) {
        p = (state.scopeRel + "/" + p).replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
      }
      return (label ? label + " " : "") + p;
    }).join("\n");

    const res = await api({ action: "bulk", lines: scoped });
    if (!res.ok) return alert(res.error || "Bulk failed");
    await refreshTree();
    alert("Bulk operations complete.");
  }

  // ---------- Preview ----------
  function refreshPreview(/* force = false */) {
    const on = $("#togglePreview")?.checked;
    if (!on) return;

    // Prefer current file; otherwise fall back to index.html (safer default)
    let rel = state.currentRel || "index.html";
    if (!/\.(php|html?|md|txt|css|js)$/i.test(rel)) rel = "index.html";

    const base = (state.cfg && state.cfg.workspace_url) || "/";
    const url = (base.endsWith("/") ? base : base + "/") + rel;

    const ifr = $("#preview");
    ifr.src = url + (url.includes("?") ? "&" : "?") + "_t=" + Date.now(); // cache-bust
  }

  // ---------- Link scan ----------
    async function scanLinks() {
    const res = await api({ action: "links" });
    if (!res.ok) return alert(res.error || "Scan failed");
    const box = document.getElementById("linksPanel");
    if (!box) return alert("Link panel not found.");
    box.innerHTML = "";
    if (!res.edges || !res.edges.length) {
      box.textContent = "No references found.";
      return;
    }
    res.edges.forEach((e) => {
      const div = document.createElement("div");
      div.className = "edge";
      // make clickable filenames optionally
      div.innerHTML = `<div style="display:flex;justify-content:space-between;gap:8px">
                         <div style="flex:1;min-width:0; overflow:hidden; text-overflow:ellipsis;">
                           <strong>${escapeHtml(e.from)}</strong> &nbsp;→&nbsp; ${escapeHtml(e.to)}
                         </div>
                         <div style="flex:0 0 auto">
                           <button class="openLinkFile" data-rel="${e.from}">Open</button>
                         </div>
                       </div>`;
      box.appendChild(div);
    });

    // wire open buttons
    $$(".openLinkFile", box).forEach((b) => {
      b.addEventListener("click", () => {
        const rel = b.dataset.rel;
        if (rel) {
          // ensure the panel closes so tree is visible (optional)
          document.body.classList.remove("linkmap-visible");
          openFile(rel);
        }
      });
    });
  }


  // ---------- Go ----------
  init();
})();


===== FILE: webide/assets/app.js @ 2025-10-19 23:14:45 =====
(async function () {
  // ---------- DOM helpers ----------
  const $ = (q, el = document) => el.querySelector(q),
        $$ = (qAll, el = document) => Array.from(el.querySelectorAll(qAll));

  // ---------- API helper (POST FormData) ----------
  const api = async (params) => {
    const form = new FormData();
    Object.entries(params).forEach(([k, v]) => {
      if (Array.isArray(v)) v.forEach((x) => form.append(k + "[]", x));
      else form.append(k, v);
    });
    const r = await fetch("api.php", { method: "POST", body: form });
    return r.json();
  };
  
    function escapeHtml(s) {
    return String(s || "").replace(/[&<>"'`]/g, (c) => ({
      "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","`":"&#96;"
    }[c]));
  }


  // ---------- State ----------
  const state = {
    currentRel: "",
    ace: null,
    cfg: null,
    dirty: false,
    saving: false,
    scopeRel: "" // '' = workspace root; otherwise e.g. 'admin/pages'
    lastLinks: [] // cached edges from last scan
  };

  // ---------- Init ----------
  async function init() {
    state.cfg = await api({ action: "config" });

    // restore scope
    state.scopeRel = localStorage.getItem("mini_ide_scope") || "";

    bindUI();
    await refreshTree();

    // Editor: ACE if available, fallback to <textarea>
    if (state.cfg.ace && window.ace) {
      state.ace = ace.edit("editor");
      state.ace.setTheme("ace/theme/textmate");
      state.ace.session.setUseSoftTabs(true);
      state.ace.session.setTabSize(2);
      state.ace.setOption("fontSize", "13px");
      $("#ta").classList.add("hidden");
      state.ace.session.on("change", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    } else {
      $("#editor").classList.add("hidden");
      $("#ta").addEventListener("input", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    }

    // Load a sensible default (index.*) if present
    const guess = $$("#tree .file").find((n) =>
      /^(index\.(php|html?|md|txt))$/i.test(n.dataset.rel || "")
    );
    if (guess) openFile(guess.dataset.rel);
  }

  // ---------- UI wiring ----------
  function bindUI() {
    $("#refreshTree")?.addEventListener("click", refreshTree);
    $("#btnCreateFile")?.addEventListener("click", () => createFromInput(false));
    $("#btnCreateDir")?.addEventListener("click", () => createFromInput(true));
    $("#btnBulk")?.addEventListener("click", bulkCreate);
    $("#btnSave")?.addEventListener("click", saveCurrent);
    $("#btnRename")?.addEventListener("click", renameCurrent);
    $("#btnDelete")?.addEventListener("click", deleteCurrent);
    $("#btnScanLinks")?.addEventListener("click", scanLinks);
    $("#scopeReset")?.addEventListener("click", () => setScope(""));
    $("#btnDownloadLinks")?.addEventListener("click", downloadLinksTxt);

    
        // existing
    $("#btnScanLinks")?.addEventListener("click", scanLinks);

    // right-panel linkmap controls
    $("#btnToggleLinks")?.addEventListener("click", toggleLinkmap);
    $("#refreshLinks")?.addEventListener("click", scanLinks);
    $("#btnCloseLinks")?.addEventListener("click", () => {
      document.body.classList.remove("linkmap-visible");
      localStorage.setItem("mini_ide_linkmap_hidden", "1");
    });

    
        // ---------- Floating tab reveal behaviour ----------
    // Keeps a temporary reveal state so we don't overwrite user's stored choice.
    // set default to hidden on first visit
if (localStorage.getItem("mini_ide_sidebar_hidden") === null) {
  localStorage.setItem("mini_ide_sidebar_hidden", "1");
}

// Restore sidebar state (existing code)
if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
  document.body.classList.add("sidebar-hidden");
}


    (function setupFloatingTabReveal() {
      const tab = $("#btnToggleSidebar");
      const sidebarEl = document.querySelector(".sidebar");
      if (!tab || !sidebarEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden"); // "1" if user had hidden

      // When pointer enters the floating tab: reveal the sidebar and hide the tab itself.
      tab.addEventListener("mouseenter", (e) => {
        // remember stored preference so we can restore it later
        prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden");
        document.body.classList.add("sidebar-visible");
        tempReveal = true;

        // reveal sidebar visually (do NOT change stored preference)
        document.body.classList.remove("sidebar-hidden");
        // hide the floating tab while user is interacting with sidebar
        tab.style.display = "none";
      });

      // When pointer leaves the sidebar area, hide the sidebar again if it was shown by the tab
      sidebarEl.addEventListener("mouseleave", (e) => {
        // Only auto-hide when this reveal was triggered by the tab (avoid interfering with manual toggle)
       // toggleSidebar();
       document.body.classList.remove("sidebar-visible");
        if (!tempReveal) return;

        // restore to user's stored preference
        if (prevStoredHidden === "1") {
          // user had explicitly hidden -> re-hide sidebar
          document.body.classList.add("sidebar-hidden");
        } else {
          // user had it visible by default — keep visible (or you can re-hide; sticking with restore)
          document.body.classList.remove("sidebar-hidden");
        }

        // show the floating tab again
        tab.style.display = "";
        tempReveal = false;
      });

      // Extra: if user moves quickly from the tab into the sidebar, keep it shown.
      // Also handle pointerout from tab to sidebar — do nothing because mouseover already removed tab.
      // If user clicks the tab (existing click handler remains), we still toggle as before.
    })();

    // Restore linkmap state (default hidden)
    if (localStorage.getItem("mini_ide_linkmap_hidden") === null) {
      localStorage.setItem("mini_ide_linkmap_hidden", "1"); // hide by default
    }
    if (localStorage.getItem("mini_ide_linkmap_hidden") !== "1") {
      document.body.classList.add("linkmap-visible");
    }

    (function setupFloatingLinkmapReveal() {
      const tab = $("#btnToggleLinks");
      const panelEl = document.querySelector(".linkmap-panel");
      if (!tab || !panelEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");

      // pointer enters the floating tab -> reveal
      tab.addEventListener("mouseenter", () => {
        prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");
        document.body.classList.add("linkmap-visible");
        tempReveal = true;
        tab.style.display = "none"; // hide tab while interacting
      });

      // when pointer leaves the panel, restore preference
      panelEl.addEventListener("mouseleave", () => {
        document.body.classList.remove("linkmap-visible");
        if (!tempReveal) return;
        if (prevStoredHidden === "1") {
          document.body.classList.remove("linkmap-visible");
        } else {
          document.body.classList.add("linkmap-visible");
        }
        tab.style.display = "";
        tempReveal = false;
      });
    })();

    // Keyboard shortcuts (capture)
    window.addEventListener(
      "keydown",
      (e) => {
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
          e.preventDefault(); e.stopPropagation();
          saveCurrent();
        }
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
          e.preventDefault(); toggleSidebar();
        }
      },
      true
    );

    // Preview toggle
    $("#togglePreview")?.addEventListener("change", syncPreviewVisibility);

    // Sidebar toggle button
    $("#btnToggleSidebar")?.addEventListener("click", toggleSidebar);

    // Restore sidebar state
    if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
      document.body.classList.add("sidebar-hidden");
    }
  }

  function toggleSidebar() {
    const hidden = document.body.classList.toggle("sidebar-hidden");
    localStorage.setItem("mini_ide_sidebar_hidden", hidden ? "1" : "0");
  }
  
    function toggleLinkmap() {
    const hidden = document.body.classList.toggle("linkmap-visible");
    // store the inverse meaning: "hidden" = 1
    localStorage.setItem("mini_ide_linkmap_hidden", hidden ? "0" : "1");
  }


  function syncPreviewVisibility() {
    const on = $("#togglePreview")?.checked;
    if (on == null) return;
    $("#preview").style.display = on ? "block" : "none";
    document.querySelector(".editorWrap").style.gridTemplateColumns = on ? "1fr 40%" : "1fr";
  }

  // ---------- Save status / dirty UI ----------
  function setSaveBusy(on) {
    // big overlay
    const overlay = $("#pageLoader");
    if (overlay) overlay.style.display = on ? "flex" : "none";

    // header spinner
    const loader = $("#saveLoader");
    if (loader) loader.style.visibility = on ? "visible" : "hidden";

    const dot = $("#saveStatusDot");
    if (dot && on) dot.classList.remove("ok", "err", "dirty");
  }
  function setSaveDirty(on) {
    const btn = $("#btnSave");
    if (btn) btn.style.borderColor = on ? "#c9e8dc" : "var(--border)";
  }
  function markSaveOK() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("err", "dirty");
    dot.classList.add("ok");
    setTimeout(() => dot.classList.remove("ok"), 900);
  }
  function markSaveErr() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("ok", "dirty");
    dot.classList.add("err");
  }
  function updateDirtyUI() {
    // header asterisk
    const cp = $("#currentPath");
    if (cp) cp.textContent = (state.currentRel || "—") + (state.dirty ? " *" : "");

    // tree asterisk (on current file only)
    $$("#tree .file .name").forEach((n) => n.classList.remove("dirty"));
    if (state.currentRel) {
      const n = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"] .name`);
      if (n && state.dirty) n.classList.add("dirty");
    }

    // status dot amber while dirty
    const dot = $("#saveStatusDot");
    if (dot) {
      dot.classList.remove("ok", "err");
      dot.classList.toggle("dirty", !!state.dirty);
    }
  }

  // ---------- Scope helpers ----------
  function setScope(rel) {
    state.scopeRel = (rel || "").replace(/^\/+|\/+$/g, "");
    localStorage.setItem("mini_ide_scope", state.scopeRel);
    refreshTree();
  }

  function renderScopeCrumbs(){
    const wrap = $("#scopeCrumbs"); if (!wrap) return;
    wrap.innerHTML = "";
    const mk = (txt, rel, isCurrent=false) => {
      const s = document.createElement("span");
      s.className = "crumb" + (isCurrent ? " current" : "");
      s.textContent = txt;
      s.onclick = () => !isCurrent && setScope(rel);
      if (isCurrent) s.title = "Current folder";
      return s;
    };
    const parts = (state.scopeRel || "").split("/").filter(Boolean);
    wrap.appendChild(mk("root", "", parts.length === 0));
    let acc = "";
    parts.forEach((p, i) => {
      const sep = document.createElement("span");
      sep.className = "sep"; sep.textContent = "/";
      wrap.appendChild(sep);
      acc = acc ? acc + "/" + p : p;
      wrap.appendChild(mk(p, acc, i === parts.length - 1));
    });
  }

  // ---------- Tree (server-scoped, collapsible) ----------
  function toNested(items) {
    const root = { name: "", rel: "", dirs: {}, files: [] };
    const entries = items
      .map((it) => ({ type: it.type, rel: (it.rel || "").replace(/^\/+/, "") }))
      .filter((e) => e.rel);

    // ensure nodes
    entries.forEach((e) => {
      const parts = e.rel.split("/");
      if (e.type === "dir") {
        let cur = root;
        for (const part of parts) {
          cur.dirs[part] = cur.dirs[part] || {
            name: part,
            rel: (cur.rel ? cur.rel + "/" : "") + part,
            dirs: {},
            files: []
          };
          cur = cur.dirs[part];
        }
      }
    });

    // place files
    entries.filter((e) => e.type === "file").forEach((e) => {
      const parts = e.rel.split("/");
      const name = parts.pop();
      let cur = root;
      for (const part of parts) {
        cur.dirs[part] = cur.dirs[part] || {
          name: part,
          rel: (cur.rel ? cur.rel + "/" : "") + part,
          dirs: {},
          files: []
        };
        cur = cur.dirs[part];
      }
      cur.files.push({ name, rel: e.rel });
    });

    // sort
    (function sort(n) {
      Object.values(n.dirs).forEach(sort);
      n.files.sort((a, b) => a.name.localeCompare(b.name));
    })(root);

    return root;
  }

  function renderTree(rootNode, container) {
    const openState = JSON.parse(localStorage.getItem("mini_ide_open_dirs") || "{}");
    const setOpen = (rel, open) => {
      if (open) openState[rel || "/"] = 1;
      else delete openState[rel || "/"];
      localStorage.setItem("mini_ide_open_dirs", JSON.stringify(openState));
    };

    container.innerHTML = "";

    const mkDir = (node) => {
      const det = document.createElement("details");
      if (openState[node.rel || "/"]) det.open = true;

      const sum = document.createElement("summary");
      sum.innerHTML = `
        <span class="chev">›</span>
        <span class="label name">📁 ${node.name || "root"}</span>
        <button class="focus" type="button" title="Focus here" data-rel="${node.rel}">📌 Focus</button>
      `;
      det.appendChild(sum);

      // node.rel from server is already full root-relative path
      const dirRel = (node.rel || "");

      // focus controls
      sum.ondblclick = (e) => { e.preventDefault(); setScope(dirRel); };
      sum.querySelector(".focus").onclick = (e) => { e.stopPropagation(); setScope(dirRel); };

      det.addEventListener("toggle", () => setOpen(node.rel, det.open));

      // child dirs
      const dirNames = Object.keys(node.dirs).sort((a, b) => a.localeCompare(b));
      dirNames.forEach((name) => det.appendChild(mkDir(node.dirs[name])));

      // files: use rel from server as-is (already full)
      node.files.forEach((f) => {
        const div = document.createElement("div");
        div.className = "file";
        div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
        div.dataset.rel = f.rel;
        div.onclick = () => openFile(f.rel);
        det.appendChild(div);
      });

      return det;
    };

    // top-level dirs
    const dirNames = Object.keys(rootNode.dirs).sort((a, b) => a.localeCompare(b));
    dirNames.forEach((name) => container.appendChild(mkDir(rootNode.dirs[name])));

    // top-level files: use rel as-is
    rootNode.files.forEach((f) => {
      const div = document.createElement("div");
      div.className = "file";
      div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
      div.dataset.rel = f.rel;
      div.onclick = () => openFile(f.rel);
      container.appendChild(div);
    });
  }

  async function refreshTree() {
    try {
      const res = await api({ action: "tree", rel: state.scopeRel });
      if (!res || res.ok !== true || !Array.isArray(res.items)) {
        const msg = res && res.error ? res.error : "Tree request failed";
        alert("Mini IDE error: " + msg);
        console.error("Tree response:", res);
        return;
      }
      const nested = toNested(res.items);
      renderTree(nested, $("#tree"));
      renderScopeCrumbs();

      // re-highlight current file if visible
      if (state.currentRel) {
        $$("#tree .file").forEach((n) => n.classList.remove("active"));
        const hit = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"]`);
        if (hit) hit.classList.add("active");
        updateDirtyUI();
      }
    } catch (e) {
      alert("Mini IDE fetch error (tree): " + e);
      console.error(e);
    }
  }

  // ---------- File ops ----------
  async function openFile(rel) {
    const res = await api({ action: "read", rel });
    if (!res.ok) return alert(res.error || "Failed to read");
    state.currentRel = rel;

    // highlight selected
    $$("#tree .file").forEach((n) => n.classList.remove("active"));
    const hit = $(`#tree .file[data-rel="${CSS.escape(rel)}"]`);
    if (hit) hit.classList.add("active");

    if (res.file.editable) {
      setEditorContent(res.file.content || "");
    } else {
      setEditorContent("// Not editable (binary or large). Size: " + res.file.size + " bytes\n");
    }
    state.dirty = false;
    setSaveDirty(false);
    updateDirtyUI();
    refreshPreview();
  }

  function getEditorContent() {
    if (state.ace) return state.ace.getValue();
    return $("#ta").value;
  }

  function setEditorContent(txt) {
    if (state.ace) {
      state.ace.setValue(txt ?? "", -1);
      // Set mode from extension
      const ext = (state.currentRel.split(".").pop() || "").toLowerCase();
      const map = {
        js: "javascript",
        css: "css",
        php: "php",
        html: "html",
        htm: "html",
        json: "json",
        md: "markdown",
        py: "python",
        yml: "yaml",
        yaml: "yaml",
        c: "c_cpp",
        cpp: "c_cpp",
        h: "c_cpp",
        sql: "sql",
        csv: "text",
        txt: "text"
      };
      state.ace.session.setMode("ace/mode/" + (map[ext] || "text"));
    } else {
      $("#ta").classList.remove("hidden");
      $("#ta").value = txt ?? "";
    }
  }

  async function saveCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (state.saving) return; // prevent double-taps
    state.saving = true;
    setSaveBusy(true);

    try {
      const content = getEditorContent();
      const res = await api({ action: "save", rel: state.currentRel, content });
      if (!res.ok) {
        markSaveErr();
        throw new Error(res.error || "Save failed");
      }

      // Server verification
      let verified = !!res.verify_ok;

      // mtime wait (shared hosts can be slow)
      const targetMtime = res.mtime || 0;
      const okStat = await waitForStat(state.currentRel, targetMtime, 8, 150);
      if (!okStat) console.warn("Proceeding without stat confirmation");

      // Optional client-side readback if server said not verified
      if (!verified) {
        try {
          const rb = await api({ action: "read", rel: state.currentRel });
          if (rb.ok && rb.file && typeof rb.file.content === "string") {
            verified = (rb.file.content === content);
          }
        } catch {}
      }

      if (!verified) {
        markSaveErr();
        alert("Save did not verify. The file contents on disk did not match what was sent.");
        return; // do not mark OK or clear dirty
      }

      state.dirty = false;
      setSaveDirty(false);
      updateDirtyUI();   // clear asterisks + amber dot
      markSaveOK();      // green blip
      refreshPreview(true);

    } catch (e) {
      console.error(e);
      alert("Save error: " + e.message);
      markSaveErr();     // red
    } finally {
      state.saving = false;
      setSaveBusy(false);
    }
  }

  // poll stat until mtime >= target
  async function waitForStat(rel, target, tries = 8, delay = 150) {
    for (let i = 0; i < tries; i++) {
      try {
        const s = await api({ action: "stat", rel });
        if (s.ok && (s.mtime || 0) >= target) return true;
      } catch {}
      await new Promise((r) => setTimeout(r, delay));
    }
    return false;
  }

  async function renameCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    const to = prompt("Rename to (path):", state.currentRel);
    if (!to || to === state.currentRel) return;
    const res = await api({ action: "rename", from: state.currentRel, to });
    if (!res.ok) return alert(res.error || "Rename failed");
    state.currentRel = to;
    $("#currentPath").textContent = to;
    await refreshTree();
    refreshPreview();
  }

  async function deleteCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (!confirm("Delete " + state.currentRel + " ?")) return;
    const res = await api({ action: "delete", rel: state.currentRel });
    if (!res.ok) return alert(res.error || "Delete failed");
    state.currentRel = "";
    $("#currentPath").textContent = "—";
    if (state.ace) state.ace.setValue("");
    else $("#ta").value = "";
    await refreshTree();
    refreshPreview();
  }

  async function createFromInput(isDir) {
    const raw = $("#newPath").value.trim();
    if (!raw) return;

    const relPath = state.scopeRel
      ? (state.scopeRel + "/" + raw).replace(/\/+/g, "/").replace(/^\/|\/$/g, "")
      : raw;

    const res = await api({ action: "create", rel: relPath, type: isDir ? "dir" : "file" });
    if (!res.ok) return alert(res.error || "Create failed");
    $("#newPath").value = "";
    await refreshTree();
    if (!isDir) openFile(relPath);
  }

  async function bulkCreate() {
    const lines = $("#bulkLines").value;
    if (!lines.trim()) return;

    // Prefix each line's path with scope (keep labels 'dir:'/'file:' intact)
    const scoped = lines.split("\n").map((t) => {
      const ln = t.trim();
      if (!ln) return ln;
      const m = ln.match(/^(dir:|file:)?\s*(.*)$/i);
      if (!m) return ln;
      const label = (m[1] || "").toLowerCase();
      let p = m[2].trim();
      if (p && state.scopeRel && !p.startsWith("/")) {
        p = (state.scopeRel + "/" + p).replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
      }
      return (label ? label + " " : "") + p;
    }).join("\n");

    const res = await api({ action: "bulk", lines: scoped });
    if (!res.ok) return alert(res.error || "Bulk failed");
    await refreshTree();
    alert("Bulk operations complete.");
  }

  // ---------- Preview ----------
  function refreshPreview(/* force = false */) {
    const on = $("#togglePreview")?.checked;
    if (!on) return;

    // Prefer current file; otherwise fall back to index.html (safer default)
    let rel = state.currentRel || "index.html";
    if (!/\.(php|html?|md|txt|css|js)$/i.test(rel)) rel = "index.html";

    const base = (state.cfg && state.cfg.workspace_url) || "/";
    const url = (base.endsWith("/") ? base : base + "/") + rel;

    const ifr = $("#preview");
    ifr.src = url + (url.includes("?") ? "&" : "?") + "_t=" + Date.now(); // cache-bust
  }

  // ---------- Link scan ----------
      async function scanLinks() {
    const res = await api({ action: "links" });
    if (!res.ok) return alert(res.error || "Scan failed");
    const box = document.getElementById("linksPanel");
    if (!box) return alert("Link panel not found.");
    box.innerHTML = "";
    state.lastLinks = Array.isArray(res.edges) ? res.edges : [];

    if (!state.lastLinks.length) {
      box.textContent = "No references found.";
      return;
    }

    state.lastLinks.forEach((e) => {
      const div = document.createElement("div");
      div.className = "edge";
      div.innerHTML = `<div style="display:flex;justify-content:space-between;gap:8px">
                         <div style="flex:1;min-width:0; overflow:hidden; text-overflow:ellipsis;">
                           <strong>${escapeHtml(e.from)}</strong> &nbsp;→&nbsp; ${escapeHtml(e.to)}
                         </div>
                         <div style="flex:0 0 auto">
                           <button class="openLinkFile" data-rel="${escapeHtml(e.from)}">Open</button>
                         </div>
                       </div>`;
      box.appendChild(div);
    });

    // wire open buttons
    $$(".openLinkFile", box).forEach((b) => {
      b.addEventListener("click", () => {
        const rel = b.dataset.rel;
        if (rel) {
          document.body.classList.remove("linkmap-visible");
          openFile(rel);
        }
      });
    });
  }



  // ---------- Go ----------
  init();
})();


===== FILE: webide/assets/app.js @ 2025-10-19 23:15:11 =====
(async function () {
  // ---------- DOM helpers ----------
  const $ = (q, el = document) => el.querySelector(q),
        $$ = (qAll, el = document) => Array.from(el.querySelectorAll(qAll));

  // ---------- API helper (POST FormData) ----------
  const api = async (params) => {
    const form = new FormData();
    Object.entries(params).forEach(([k, v]) => {
      if (Array.isArray(v)) v.forEach((x) => form.append(k + "[]", x));
      else form.append(k, v);
    });
    const r = await fetch("api.php", { method: "POST", body: form });
    return r.json();
  };
  
    function escapeHtml(s) {
    return String(s || "").replace(/[&<>"'`]/g, (c) => ({
      "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","`":"&#96;"
    }[c]));
  }


  // ---------- State ----------
  const state = {
    currentRel: "",
    ace: null,
    cfg: null,
    dirty: false,
    saving: false,
    scopeRel: "" // '' = workspace root; otherwise e.g. 'admin/pages'
    lastLinks: [] // cached edges from last scan
  };

  // ---------- Init ----------
  async function init() {
    state.cfg = await api({ action: "config" });

    // restore scope
    state.scopeRel = localStorage.getItem("mini_ide_scope") || "";

    bindUI();
    await refreshTree();

    // Editor: ACE if available, fallback to <textarea>
    if (state.cfg.ace && window.ace) {
      state.ace = ace.edit("editor");
      state.ace.setTheme("ace/theme/textmate");
      state.ace.session.setUseSoftTabs(true);
      state.ace.session.setTabSize(2);
      state.ace.setOption("fontSize", "13px");
      $("#ta").classList.add("hidden");
      state.ace.session.on("change", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    } else {
      $("#editor").classList.add("hidden");
      $("#ta").addEventListener("input", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    }

    // Load a sensible default (index.*) if present
    const guess = $$("#tree .file").find((n) =>
      /^(index\.(php|html?|md|txt))$/i.test(n.dataset.rel || "")
    );
    if (guess) openFile(guess.dataset.rel);
  }

  // ---------- UI wiring ----------
  function bindUI() {
    $("#refreshTree")?.addEventListener("click", refreshTree);
    $("#btnCreateFile")?.addEventListener("click", () => createFromInput(false));
    $("#btnCreateDir")?.addEventListener("click", () => createFromInput(true));
    $("#btnBulk")?.addEventListener("click", bulkCreate);
    $("#btnSave")?.addEventListener("click", saveCurrent);
    $("#btnRename")?.addEventListener("click", renameCurrent);
    $("#btnDelete")?.addEventListener("click", deleteCurrent);
    $("#btnScanLinks")?.addEventListener("click", scanLinks);
    $("#scopeReset")?.addEventListener("click", () => setScope(""));
    $("#btnDownloadLinks")?.addEventListener("click", downloadLinksTxt);

    
        // existing
    $("#btnScanLinks")?.addEventListener("click", scanLinks);

    // right-panel linkmap controls
    $("#btnToggleLinks")?.addEventListener("click", toggleLinkmap);
    $("#refreshLinks")?.addEventListener("click", scanLinks);
    $("#btnCloseLinks")?.addEventListener("click", () => {
      document.body.classList.remove("linkmap-visible");
      localStorage.setItem("mini_ide_linkmap_hidden", "1");
    });

    
        // ---------- Floating tab reveal behaviour ----------
    // Keeps a temporary reveal state so we don't overwrite user's stored choice.
    // set default to hidden on first visit
if (localStorage.getItem("mini_ide_sidebar_hidden") === null) {
  localStorage.setItem("mini_ide_sidebar_hidden", "1");
}

// Restore sidebar state (existing code)
if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
  document.body.classList.add("sidebar-hidden");
}


    (function setupFloatingTabReveal() {
      const tab = $("#btnToggleSidebar");
      const sidebarEl = document.querySelector(".sidebar");
      if (!tab || !sidebarEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden"); // "1" if user had hidden

      // When pointer enters the floating tab: reveal the sidebar and hide the tab itself.
      tab.addEventListener("mouseenter", (e) => {
        // remember stored preference so we can restore it later
        prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden");
        document.body.classList.add("sidebar-visible");
        tempReveal = true;

        // reveal sidebar visually (do NOT change stored preference)
        document.body.classList.remove("sidebar-hidden");
        // hide the floating tab while user is interacting with sidebar
        tab.style.display = "none";
      });

      // When pointer leaves the sidebar area, hide the sidebar again if it was shown by the tab
      sidebarEl.addEventListener("mouseleave", (e) => {
        // Only auto-hide when this reveal was triggered by the tab (avoid interfering with manual toggle)
       // toggleSidebar();
       document.body.classList.remove("sidebar-visible");
        if (!tempReveal) return;

        // restore to user's stored preference
        if (prevStoredHidden === "1") {
          // user had explicitly hidden -> re-hide sidebar
          document.body.classList.add("sidebar-hidden");
        } else {
          // user had it visible by default — keep visible (or you can re-hide; sticking with restore)
          document.body.classList.remove("sidebar-hidden");
        }

        // show the floating tab again
        tab.style.display = "";
        tempReveal = false;
      });

      // Extra: if user moves quickly from the tab into the sidebar, keep it shown.
      // Also handle pointerout from tab to sidebar — do nothing because mouseover already removed tab.
      // If user clicks the tab (existing click handler remains), we still toggle as before.
    })();

    // Restore linkmap state (default hidden)
    if (localStorage.getItem("mini_ide_linkmap_hidden") === null) {
      localStorage.setItem("mini_ide_linkmap_hidden", "1"); // hide by default
    }
    if (localStorage.getItem("mini_ide_linkmap_hidden") !== "1") {
      document.body.classList.add("linkmap-visible");
    }

    (function setupFloatingLinkmapReveal() {
      const tab = $("#btnToggleLinks");
      const panelEl = document.querySelector(".linkmap-panel");
      if (!tab || !panelEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");

      // pointer enters the floating tab -> reveal
      tab.addEventListener("mouseenter", () => {
        prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");
        document.body.classList.add("linkmap-visible");
        tempReveal = true;
        tab.style.display = "none"; // hide tab while interacting
      });

      // when pointer leaves the panel, restore preference
      panelEl.addEventListener("mouseleave", () => {
        document.body.classList.remove("linkmap-visible");
        if (!tempReveal) return;
        if (prevStoredHidden === "1") {
          document.body.classList.remove("linkmap-visible");
        } else {
          document.body.classList.add("linkmap-visible");
        }
        tab.style.display = "";
        tempReveal = false;
      });
    })();

    // Keyboard shortcuts (capture)
    window.addEventListener(
      "keydown",
      (e) => {
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
          e.preventDefault(); e.stopPropagation();
          saveCurrent();
        }
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
          e.preventDefault(); toggleSidebar();
        }
      },
      true
    );

    // Preview toggle
    $("#togglePreview")?.addEventListener("change", syncPreviewVisibility);

    // Sidebar toggle button
    $("#btnToggleSidebar")?.addEventListener("click", toggleSidebar);

    // Restore sidebar state
    if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
      document.body.classList.add("sidebar-hidden");
    }
  }

  function toggleSidebar() {
    const hidden = document.body.classList.toggle("sidebar-hidden");
    localStorage.setItem("mini_ide_sidebar_hidden", hidden ? "1" : "0");
  }
  
    function toggleLinkmap() {
    const hidden = document.body.classList.toggle("linkmap-visible");
    // store the inverse meaning: "hidden" = 1
    localStorage.setItem("mini_ide_linkmap_hidden", hidden ? "0" : "1");
  }


  function syncPreviewVisibility() {
    const on = $("#togglePreview")?.checked;
    if (on == null) return;
    $("#preview").style.display = on ? "block" : "none";
    document.querySelector(".editorWrap").style.gridTemplateColumns = on ? "1fr 40%" : "1fr";
  }

  // ---------- Save status / dirty UI ----------
  function setSaveBusy(on) {
    // big overlay
    const overlay = $("#pageLoader");
    if (overlay) overlay.style.display = on ? "flex" : "none";

    // header spinner
    const loader = $("#saveLoader");
    if (loader) loader.style.visibility = on ? "visible" : "hidden";

    const dot = $("#saveStatusDot");
    if (dot && on) dot.classList.remove("ok", "err", "dirty");
  }
  function setSaveDirty(on) {
    const btn = $("#btnSave");
    if (btn) btn.style.borderColor = on ? "#c9e8dc" : "var(--border)";
  }
  function markSaveOK() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("err", "dirty");
    dot.classList.add("ok");
    setTimeout(() => dot.classList.remove("ok"), 900);
  }
  function markSaveErr() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("ok", "dirty");
    dot.classList.add("err");
  }
  function updateDirtyUI() {
    // header asterisk
    const cp = $("#currentPath");
    if (cp) cp.textContent = (state.currentRel || "—") + (state.dirty ? " *" : "");

    // tree asterisk (on current file only)
    $$("#tree .file .name").forEach((n) => n.classList.remove("dirty"));
    if (state.currentRel) {
      const n = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"] .name`);
      if (n && state.dirty) n.classList.add("dirty");
    }

    // status dot amber while dirty
    const dot = $("#saveStatusDot");
    if (dot) {
      dot.classList.remove("ok", "err");
      dot.classList.toggle("dirty", !!state.dirty);
    }
  }

  function downloadLinksTxt() {
    const edges = state.lastLinks || [];
    if (!edges.length) return alert("No links to download — run Scan first.");

    // build lines "from  →  to"
    const lines = edges.map((e) => {
      // avoid null/undefined
      const a = (e.from || "").replace(/\r?\n/g, " ");
      const b = (e.to   || "").replace(/\r?\n/g, " ");
      return `${a}  →  ${b}`;
    }).join("\n");

    const now = new Date();
    const pad = (n) => String(n).padStart(2, "0");
    const fname = `links-${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.txt`;

    const blob = new Blob([lines], { type: "text/plain;charset=utf-8" });
    const url = URL.createObjectURL(blob);

    const a = document.createElement("a");
    a.href = url;
    a.download = fname;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
  }


  // ---------- Scope helpers ----------
  function setScope(rel) {
    state.scopeRel = (rel || "").replace(/^\/+|\/+$/g, "");
    localStorage.setItem("mini_ide_scope", state.scopeRel);
    refreshTree();
  }

  function renderScopeCrumbs(){
    const wrap = $("#scopeCrumbs"); if (!wrap) return;
    wrap.innerHTML = "";
    const mk = (txt, rel, isCurrent=false) => {
      const s = document.createElement("span");
      s.className = "crumb" + (isCurrent ? " current" : "");
      s.textContent = txt;
      s.onclick = () => !isCurrent && setScope(rel);
      if (isCurrent) s.title = "Current folder";
      return s;
    };
    const parts = (state.scopeRel || "").split("/").filter(Boolean);
    wrap.appendChild(mk("root", "", parts.length === 0));
    let acc = "";
    parts.forEach((p, i) => {
      const sep = document.createElement("span");
      sep.className = "sep"; sep.textContent = "/";
      wrap.appendChild(sep);
      acc = acc ? acc + "/" + p : p;
      wrap.appendChild(mk(p, acc, i === parts.length - 1));
    });
  }

  // ---------- Tree (server-scoped, collapsible) ----------
  function toNested(items) {
    const root = { name: "", rel: "", dirs: {}, files: [] };
    const entries = items
      .map((it) => ({ type: it.type, rel: (it.rel || "").replace(/^\/+/, "") }))
      .filter((e) => e.rel);

    // ensure nodes
    entries.forEach((e) => {
      const parts = e.rel.split("/");
      if (e.type === "dir") {
        let cur = root;
        for (const part of parts) {
          cur.dirs[part] = cur.dirs[part] || {
            name: part,
            rel: (cur.rel ? cur.rel + "/" : "") + part,
            dirs: {},
            files: []
          };
          cur = cur.dirs[part];
        }
      }
    });

    // place files
    entries.filter((e) => e.type === "file").forEach((e) => {
      const parts = e.rel.split("/");
      const name = parts.pop();
      let cur = root;
      for (const part of parts) {
        cur.dirs[part] = cur.dirs[part] || {
          name: part,
          rel: (cur.rel ? cur.rel + "/" : "") + part,
          dirs: {},
          files: []
        };
        cur = cur.dirs[part];
      }
      cur.files.push({ name, rel: e.rel });
    });

    // sort
    (function sort(n) {
      Object.values(n.dirs).forEach(sort);
      n.files.sort((a, b) => a.name.localeCompare(b.name));
    })(root);

    return root;
  }

  function renderTree(rootNode, container) {
    const openState = JSON.parse(localStorage.getItem("mini_ide_open_dirs") || "{}");
    const setOpen = (rel, open) => {
      if (open) openState[rel || "/"] = 1;
      else delete openState[rel || "/"];
      localStorage.setItem("mini_ide_open_dirs", JSON.stringify(openState));
    };

    container.innerHTML = "";

    const mkDir = (node) => {
      const det = document.createElement("details");
      if (openState[node.rel || "/"]) det.open = true;

      const sum = document.createElement("summary");
      sum.innerHTML = `
        <span class="chev">›</span>
        <span class="label name">📁 ${node.name || "root"}</span>
        <button class="focus" type="button" title="Focus here" data-rel="${node.rel}">📌 Focus</button>
      `;
      det.appendChild(sum);

      // node.rel from server is already full root-relative path
      const dirRel = (node.rel || "");

      // focus controls
      sum.ondblclick = (e) => { e.preventDefault(); setScope(dirRel); };
      sum.querySelector(".focus").onclick = (e) => { e.stopPropagation(); setScope(dirRel); };

      det.addEventListener("toggle", () => setOpen(node.rel, det.open));

      // child dirs
      const dirNames = Object.keys(node.dirs).sort((a, b) => a.localeCompare(b));
      dirNames.forEach((name) => det.appendChild(mkDir(node.dirs[name])));

      // files: use rel from server as-is (already full)
      node.files.forEach((f) => {
        const div = document.createElement("div");
        div.className = "file";
        div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
        div.dataset.rel = f.rel;
        div.onclick = () => openFile(f.rel);
        det.appendChild(div);
      });

      return det;
    };

    // top-level dirs
    const dirNames = Object.keys(rootNode.dirs).sort((a, b) => a.localeCompare(b));
    dirNames.forEach((name) => container.appendChild(mkDir(rootNode.dirs[name])));

    // top-level files: use rel as-is
    rootNode.files.forEach((f) => {
      const div = document.createElement("div");
      div.className = "file";
      div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
      div.dataset.rel = f.rel;
      div.onclick = () => openFile(f.rel);
      container.appendChild(div);
    });
  }

  async function refreshTree() {
    try {
      const res = await api({ action: "tree", rel: state.scopeRel });
      if (!res || res.ok !== true || !Array.isArray(res.items)) {
        const msg = res && res.error ? res.error : "Tree request failed";
        alert("Mini IDE error: " + msg);
        console.error("Tree response:", res);
        return;
      }
      const nested = toNested(res.items);
      renderTree(nested, $("#tree"));
      renderScopeCrumbs();

      // re-highlight current file if visible
      if (state.currentRel) {
        $$("#tree .file").forEach((n) => n.classList.remove("active"));
        const hit = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"]`);
        if (hit) hit.classList.add("active");
        updateDirtyUI();
      }
    } catch (e) {
      alert("Mini IDE fetch error (tree): " + e);
      console.error(e);
    }
  }

  // ---------- File ops ----------
  async function openFile(rel) {
    const res = await api({ action: "read", rel });
    if (!res.ok) return alert(res.error || "Failed to read");
    state.currentRel = rel;

    // highlight selected
    $$("#tree .file").forEach((n) => n.classList.remove("active"));
    const hit = $(`#tree .file[data-rel="${CSS.escape(rel)}"]`);
    if (hit) hit.classList.add("active");

    if (res.file.editable) {
      setEditorContent(res.file.content || "");
    } else {
      setEditorContent("// Not editable (binary or large). Size: " + res.file.size + " bytes\n");
    }
    state.dirty = false;
    setSaveDirty(false);
    updateDirtyUI();
    refreshPreview();
  }

  function getEditorContent() {
    if (state.ace) return state.ace.getValue();
    return $("#ta").value;
  }

  function setEditorContent(txt) {
    if (state.ace) {
      state.ace.setValue(txt ?? "", -1);
      // Set mode from extension
      const ext = (state.currentRel.split(".").pop() || "").toLowerCase();
      const map = {
        js: "javascript",
        css: "css",
        php: "php",
        html: "html",
        htm: "html",
        json: "json",
        md: "markdown",
        py: "python",
        yml: "yaml",
        yaml: "yaml",
        c: "c_cpp",
        cpp: "c_cpp",
        h: "c_cpp",
        sql: "sql",
        csv: "text",
        txt: "text"
      };
      state.ace.session.setMode("ace/mode/" + (map[ext] || "text"));
    } else {
      $("#ta").classList.remove("hidden");
      $("#ta").value = txt ?? "";
    }
  }

  async function saveCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (state.saving) return; // prevent double-taps
    state.saving = true;
    setSaveBusy(true);

    try {
      const content = getEditorContent();
      const res = await api({ action: "save", rel: state.currentRel, content });
      if (!res.ok) {
        markSaveErr();
        throw new Error(res.error || "Save failed");
      }

      // Server verification
      let verified = !!res.verify_ok;

      // mtime wait (shared hosts can be slow)
      const targetMtime = res.mtime || 0;
      const okStat = await waitForStat(state.currentRel, targetMtime, 8, 150);
      if (!okStat) console.warn("Proceeding without stat confirmation");

      // Optional client-side readback if server said not verified
      if (!verified) {
        try {
          const rb = await api({ action: "read", rel: state.currentRel });
          if (rb.ok && rb.file && typeof rb.file.content === "string") {
            verified = (rb.file.content === content);
          }
        } catch {}
      }

      if (!verified) {
        markSaveErr();
        alert("Save did not verify. The file contents on disk did not match what was sent.");
        return; // do not mark OK or clear dirty
      }

      state.dirty = false;
      setSaveDirty(false);
      updateDirtyUI();   // clear asterisks + amber dot
      markSaveOK();      // green blip
      refreshPreview(true);

    } catch (e) {
      console.error(e);
      alert("Save error: " + e.message);
      markSaveErr();     // red
    } finally {
      state.saving = false;
      setSaveBusy(false);
    }
  }

  // poll stat until mtime >= target
  async function waitForStat(rel, target, tries = 8, delay = 150) {
    for (let i = 0; i < tries; i++) {
      try {
        const s = await api({ action: "stat", rel });
        if (s.ok && (s.mtime || 0) >= target) return true;
      } catch {}
      await new Promise((r) => setTimeout(r, delay));
    }
    return false;
  }

  async function renameCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    const to = prompt("Rename to (path):", state.currentRel);
    if (!to || to === state.currentRel) return;
    const res = await api({ action: "rename", from: state.currentRel, to });
    if (!res.ok) return alert(res.error || "Rename failed");
    state.currentRel = to;
    $("#currentPath").textContent = to;
    await refreshTree();
    refreshPreview();
  }

  async function deleteCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (!confirm("Delete " + state.currentRel + " ?")) return;
    const res = await api({ action: "delete", rel: state.currentRel });
    if (!res.ok) return alert(res.error || "Delete failed");
    state.currentRel = "";
    $("#currentPath").textContent = "—";
    if (state.ace) state.ace.setValue("");
    else $("#ta").value = "";
    await refreshTree();
    refreshPreview();
  }

  async function createFromInput(isDir) {
    const raw = $("#newPath").value.trim();
    if (!raw) return;

    const relPath = state.scopeRel
      ? (state.scopeRel + "/" + raw).replace(/\/+/g, "/").replace(/^\/|\/$/g, "")
      : raw;

    const res = await api({ action: "create", rel: relPath, type: isDir ? "dir" : "file" });
    if (!res.ok) return alert(res.error || "Create failed");
    $("#newPath").value = "";
    await refreshTree();
    if (!isDir) openFile(relPath);
  }

  async function bulkCreate() {
    const lines = $("#bulkLines").value;
    if (!lines.trim()) return;

    // Prefix each line's path with scope (keep labels 'dir:'/'file:' intact)
    const scoped = lines.split("\n").map((t) => {
      const ln = t.trim();
      if (!ln) return ln;
      const m = ln.match(/^(dir:|file:)?\s*(.*)$/i);
      if (!m) return ln;
      const label = (m[1] || "").toLowerCase();
      let p = m[2].trim();
      if (p && state.scopeRel && !p.startsWith("/")) {
        p = (state.scopeRel + "/" + p).replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
      }
      return (label ? label + " " : "") + p;
    }).join("\n");

    const res = await api({ action: "bulk", lines: scoped });
    if (!res.ok) return alert(res.error || "Bulk failed");
    await refreshTree();
    alert("Bulk operations complete.");
  }

  // ---------- Preview ----------
  function refreshPreview(/* force = false */) {
    const on = $("#togglePreview")?.checked;
    if (!on) return;

    // Prefer current file; otherwise fall back to index.html (safer default)
    let rel = state.currentRel || "index.html";
    if (!/\.(php|html?|md|txt|css|js)$/i.test(rel)) rel = "index.html";

    const base = (state.cfg && state.cfg.workspace_url) || "/";
    const url = (base.endsWith("/") ? base : base + "/") + rel;

    const ifr = $("#preview");
    ifr.src = url + (url.includes("?") ? "&" : "?") + "_t=" + Date.now(); // cache-bust
  }

  // ---------- Link scan ----------
      async function scanLinks() {
    const res = await api({ action: "links" });
    if (!res.ok) return alert(res.error || "Scan failed");
    const box = document.getElementById("linksPanel");
    if (!box) return alert("Link panel not found.");
    box.innerHTML = "";
    state.lastLinks = Array.isArray(res.edges) ? res.edges : [];

    if (!state.lastLinks.length) {
      box.textContent = "No references found.";
      return;
    }

    state.lastLinks.forEach((e) => {
      const div = document.createElement("div");
      div.className = "edge";
      div.innerHTML = `<div style="display:flex;justify-content:space-between;gap:8px">
                         <div style="flex:1;min-width:0; overflow:hidden; text-overflow:ellipsis;">
                           <strong>${escapeHtml(e.from)}</strong> &nbsp;→&nbsp; ${escapeHtml(e.to)}
                         </div>
                         <div style="flex:0 0 auto">
                           <button class="openLinkFile" data-rel="${escapeHtml(e.from)}">Open</button>
                         </div>
                       </div>`;
      box.appendChild(div);
    });

    // wire open buttons
    $$(".openLinkFile", box).forEach((b) => {
      b.addEventListener("click", () => {
        const rel = b.dataset.rel;
        if (rel) {
          document.body.classList.remove("linkmap-visible");
          openFile(rel);
        }
      });
    });
  }



  // ---------- Go ----------
  init();
})();


===== FILE: webide/assets/app.js @ 2025-10-19 23:19:32 =====
(async function () {
  // ---------- DOM helpers ----------
  const $ = (q, el = document) => el.querySelector(q),
        $$ = (qAll, el = document) => Array.from(el.querySelectorAll(qAll));

  // ---------- API helper (POST FormData) ----------
  const api = async (params) => {
    const form = new FormData();
    Object.entries(params).forEach(([k, v]) => {
      if (Array.isArray(v)) v.forEach((x) => form.append(k + "[]", x));
      else form.append(k, v);
    });
    const r = await fetch("api.php", { method: "POST", body: form });
    return r.json();
  };
  
    function escapeHtml(s) {
    return String(s || "").replace(/[&<>"'`]/g, (c) => ({
      "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","`":"&#96;"
    }[c]));
  }


  // ---------- State ----------
  const state = {
    currentRel: "",
    ace: null,
    cfg: null,
    dirty: false,
    saving: false,
    scopeRel: "", // '' = workspace root; otherwise e.g. 'admin/pages'
    lastLinks: [] // cached edges from last scan
  };

  // ---------- Init ----------
  async function init() {
    state.cfg = await api({ action: "config" });

    // restore scope
    state.scopeRel = localStorage.getItem("mini_ide_scope") || "";

    bindUI();
    await refreshTree();

    // Editor: ACE if available, fallback to <textarea>
    if (state.cfg.ace && window.ace) {
      state.ace = ace.edit("editor");
      state.ace.setTheme("ace/theme/textmate");
      state.ace.session.setUseSoftTabs(true);
      state.ace.session.setTabSize(2);
      state.ace.setOption("fontSize", "13px");
      $("#ta").classList.add("hidden");
      state.ace.session.on("change", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    } else {
      $("#editor").classList.add("hidden");
      $("#ta").addEventListener("input", () => {
        state.dirty = true;
        setSaveDirty(true);
        updateDirtyUI();
      });
    }

    // Load a sensible default (index.*) if present
    const guess = $$("#tree .file").find((n) =>
      /^(index\.(php|html?|md|txt))$/i.test(n.dataset.rel || "")
    );
    if (guess) openFile(guess.dataset.rel);
  }

  // ---------- UI wiring ----------
  function bindUI() {
    $("#refreshTree")?.addEventListener("click", refreshTree);
    $("#btnCreateFile")?.addEventListener("click", () => createFromInput(false));
    $("#btnCreateDir")?.addEventListener("click", () => createFromInput(true));
    $("#btnBulk")?.addEventListener("click", bulkCreate);
    $("#btnSave")?.addEventListener("click", saveCurrent);
    $("#btnRename")?.addEventListener("click", renameCurrent);
    $("#btnDelete")?.addEventListener("click", deleteCurrent);
    $("#btnScanLinks")?.addEventListener("click", scanLinks);
    $("#scopeReset")?.addEventListener("click", () => setScope(""));
    $("#btnDownloadLinks")?.addEventListener("click", downloadLinksTxt);

    
        // existing
    $("#btnScanLinks")?.addEventListener("click", scanLinks);

    // right-panel linkmap controls
    $("#btnToggleLinks")?.addEventListener("click", toggleLinkmap);
    $("#refreshLinks")?.addEventListener("click", scanLinks);
    $("#btnCloseLinks")?.addEventListener("click", () => {
      document.body.classList.remove("linkmap-visible");
      localStorage.setItem("mini_ide_linkmap_hidden", "1");
    });

    
        // ---------- Floating tab reveal behaviour ----------
    // Keeps a temporary reveal state so we don't overwrite user's stored choice.
    // set default to hidden on first visit
if (localStorage.getItem("mini_ide_sidebar_hidden") === null) {
  localStorage.setItem("mini_ide_sidebar_hidden", "1");
}

// Restore sidebar state (existing code)
if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
  document.body.classList.add("sidebar-hidden");
}


    (function setupFloatingTabReveal() {
      const tab = $("#btnToggleSidebar");
      const sidebarEl = document.querySelector(".sidebar");
      if (!tab || !sidebarEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden"); // "1" if user had hidden

      // When pointer enters the floating tab: reveal the sidebar and hide the tab itself.
      tab.addEventListener("mouseenter", (e) => {
        // remember stored preference so we can restore it later
        prevStoredHidden = localStorage.getItem("mini_ide_sidebar_hidden");
        document.body.classList.add("sidebar-visible");
        tempReveal = true;

        // reveal sidebar visually (do NOT change stored preference)
        document.body.classList.remove("sidebar-hidden");
        // hide the floating tab while user is interacting with sidebar
        tab.style.display = "none";
      });

      // When pointer leaves the sidebar area, hide the sidebar again if it was shown by the tab
      sidebarEl.addEventListener("mouseleave", (e) => {
        // Only auto-hide when this reveal was triggered by the tab (avoid interfering with manual toggle)
       // toggleSidebar();
       document.body.classList.remove("sidebar-visible");
        if (!tempReveal) return;

        // restore to user's stored preference
        if (prevStoredHidden === "1") {
          // user had explicitly hidden -> re-hide sidebar
          document.body.classList.add("sidebar-hidden");
        } else {
          // user had it visible by default — keep visible (or you can re-hide; sticking with restore)
          document.body.classList.remove("sidebar-hidden");
        }

        // show the floating tab again
        tab.style.display = "";
        tempReveal = false;
      });

      // Extra: if user moves quickly from the tab into the sidebar, keep it shown.
      // Also handle pointerout from tab to sidebar — do nothing because mouseover already removed tab.
      // If user clicks the tab (existing click handler remains), we still toggle as before.
    })();

    // Restore linkmap state (default hidden)
    if (localStorage.getItem("mini_ide_linkmap_hidden") === null) {
      localStorage.setItem("mini_ide_linkmap_hidden", "1"); // hide by default
    }
    if (localStorage.getItem("mini_ide_linkmap_hidden") !== "1") {
      document.body.classList.add("linkmap-visible");
    }

    (function setupFloatingLinkmapReveal() {
      const tab = $("#btnToggleLinks");
      const panelEl = document.querySelector(".linkmap-panel");
      if (!tab || !panelEl) return;

      let tempReveal = false;
      let prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");

      // pointer enters the floating tab -> reveal
      tab.addEventListener("mouseenter", () => {
        prevStoredHidden = localStorage.getItem("mini_ide_linkmap_hidden");
        document.body.classList.add("linkmap-visible");
        tempReveal = true;
        tab.style.display = "none"; // hide tab while interacting
      });

      // when pointer leaves the panel, restore preference
      panelEl.addEventListener("mouseleave", () => {
        document.body.classList.remove("linkmap-visible");
        if (!tempReveal) return;
        if (prevStoredHidden === "1") {
          document.body.classList.remove("linkmap-visible");
        } else {
          document.body.classList.add("linkmap-visible");
        }
        tab.style.display = "";
        tempReveal = false;
      });
    })();

    // Keyboard shortcuts (capture)
    window.addEventListener(
      "keydown",
      (e) => {
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
          e.preventDefault(); e.stopPropagation();
          saveCurrent();
        }
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
          e.preventDefault(); toggleSidebar();
        }
      },
      true
    );

    // Preview toggle
    $("#togglePreview")?.addEventListener("change", syncPreviewVisibility);

    // Sidebar toggle button
    $("#btnToggleSidebar")?.addEventListener("click", toggleSidebar);

    // Restore sidebar state
    if (localStorage.getItem("mini_ide_sidebar_hidden") === "1") {
      document.body.classList.add("sidebar-hidden");
    }
  }

  function toggleSidebar() {
    const hidden = document.body.classList.toggle("sidebar-hidden");
    localStorage.setItem("mini_ide_sidebar_hidden", hidden ? "1" : "0");
  }
  
    function toggleLinkmap() {
    const hidden = document.body.classList.toggle("linkmap-visible");
    // store the inverse meaning: "hidden" = 1
    localStorage.setItem("mini_ide_linkmap_hidden", hidden ? "0" : "1");
  }


  function syncPreviewVisibility() {
    const on = $("#togglePreview")?.checked;
    if (on == null) return;
    $("#preview").style.display = on ? "block" : "none";
    document.querySelector(".editorWrap").style.gridTemplateColumns = on ? "1fr 40%" : "1fr";
  }

  // ---------- Save status / dirty UI ----------
  function setSaveBusy(on) {
    // big overlay
    const overlay = $("#pageLoader");
    if (overlay) overlay.style.display = on ? "flex" : "none";

    // header spinner
    const loader = $("#saveLoader");
    if (loader) loader.style.visibility = on ? "visible" : "hidden";

    const dot = $("#saveStatusDot");
    if (dot && on) dot.classList.remove("ok", "err", "dirty");
  }
  function setSaveDirty(on) {
    const btn = $("#btnSave");
    if (btn) btn.style.borderColor = on ? "#c9e8dc" : "var(--border)";
  }
  function markSaveOK() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("err", "dirty");
    dot.classList.add("ok");
    setTimeout(() => dot.classList.remove("ok"), 900);
  }
  function markSaveErr() {
    const dot = $("#saveStatusDot");
    if (!dot) return;
    dot.classList.remove("ok", "dirty");
    dot.classList.add("err");
  }
  function updateDirtyUI() {
    // header asterisk
    const cp = $("#currentPath");
    if (cp) cp.textContent = (state.currentRel || "—") + (state.dirty ? " *" : "");

    // tree asterisk (on current file only)
    $$("#tree .file .name").forEach((n) => n.classList.remove("dirty"));
    if (state.currentRel) {
      const n = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"] .name`);
      if (n && state.dirty) n.classList.add("dirty");
    }

    // status dot amber while dirty
    const dot = $("#saveStatusDot");
    if (dot) {
      dot.classList.remove("ok", "err");
      dot.classList.toggle("dirty", !!state.dirty);
    }
  }

  function downloadLinksTxt() {
    const edges = state.lastLinks || [];
    if (!edges.length) return alert("No links to download — run Scan first.");

    // build lines "from  →  to"
    const lines = edges.map((e) => {
      // avoid null/undefined
      const a = (e.from || "").replace(/\r?\n/g, " ");
      const b = (e.to   || "").replace(/\r?\n/g, " ");
      return `${a}  →  ${b}`;
    }).join("\n");

    const now = new Date();
    const pad = (n) => String(n).padStart(2, "0");
    const fname = `links-${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.txt`;

    const blob = new Blob([lines], { type: "text/plain;charset=utf-8" });
    const url = URL.createObjectURL(blob);

    const a = document.createElement("a");
    a.href = url;
    a.download = fname;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
  }


  // ---------- Scope helpers ----------
  function setScope(rel) {
    state.scopeRel = (rel || "").replace(/^\/+|\/+$/g, "");
    localStorage.setItem("mini_ide_scope", state.scopeRel);
    refreshTree();
  }

  function renderScopeCrumbs(){
    const wrap = $("#scopeCrumbs"); if (!wrap) return;
    wrap.innerHTML = "";
    const mk = (txt, rel, isCurrent=false) => {
      const s = document.createElement("span");
      s.className = "crumb" + (isCurrent ? " current" : "");
      s.textContent = txt;
      s.onclick = () => !isCurrent && setScope(rel);
      if (isCurrent) s.title = "Current folder";
      return s;
    };
    const parts = (state.scopeRel || "").split("/").filter(Boolean);
    wrap.appendChild(mk("root", "", parts.length === 0));
    let acc = "";
    parts.forEach((p, i) => {
      const sep = document.createElement("span");
      sep.className = "sep"; sep.textContent = "/";
      wrap.appendChild(sep);
      acc = acc ? acc + "/" + p : p;
      wrap.appendChild(mk(p, acc, i === parts.length - 1));
    });
  }

  // ---------- Tree (server-scoped, collapsible) ----------
  function toNested(items) {
    const root = { name: "", rel: "", dirs: {}, files: [] };
    const entries = items
      .map((it) => ({ type: it.type, rel: (it.rel || "").replace(/^\/+/, "") }))
      .filter((e) => e.rel);

    // ensure nodes
    entries.forEach((e) => {
      const parts = e.rel.split("/");
      if (e.type === "dir") {
        let cur = root;
        for (const part of parts) {
          cur.dirs[part] = cur.dirs[part] || {
            name: part,
            rel: (cur.rel ? cur.rel + "/" : "") + part,
            dirs: {},
            files: []
          };
          cur = cur.dirs[part];
        }
      }
    });

    // place files
    entries.filter((e) => e.type === "file").forEach((e) => {
      const parts = e.rel.split("/");
      const name = parts.pop();
      let cur = root;
      for (const part of parts) {
        cur.dirs[part] = cur.dirs[part] || {
          name: part,
          rel: (cur.rel ? cur.rel + "/" : "") + part,
          dirs: {},
          files: []
        };
        cur = cur.dirs[part];
      }
      cur.files.push({ name, rel: e.rel });
    });

    // sort
    (function sort(n) {
      Object.values(n.dirs).forEach(sort);
      n.files.sort((a, b) => a.name.localeCompare(b.name));
    })(root);

    return root;
  }

  function renderTree(rootNode, container) {
    const openState = JSON.parse(localStorage.getItem("mini_ide_open_dirs") || "{}");
    const setOpen = (rel, open) => {
      if (open) openState[rel || "/"] = 1;
      else delete openState[rel || "/"];
      localStorage.setItem("mini_ide_open_dirs", JSON.stringify(openState));
    };

    container.innerHTML = "";

    const mkDir = (node) => {
      const det = document.createElement("details");
      if (openState[node.rel || "/"]) det.open = true;

      const sum = document.createElement("summary");
      sum.innerHTML = `
        <span class="chev">›</span>
        <span class="label name">📁 ${node.name || "root"}</span>
        <button class="focus" type="button" title="Focus here" data-rel="${node.rel}">📌 Focus</button>
      `;
      det.appendChild(sum);

      // node.rel from server is already full root-relative path
      const dirRel = (node.rel || "");

      // focus controls
      sum.ondblclick = (e) => { e.preventDefault(); setScope(dirRel); };
      sum.querySelector(".focus").onclick = (e) => { e.stopPropagation(); setScope(dirRel); };

      det.addEventListener("toggle", () => setOpen(node.rel, det.open));

      // child dirs
      const dirNames = Object.keys(node.dirs).sort((a, b) => a.localeCompare(b));
      dirNames.forEach((name) => det.appendChild(mkDir(node.dirs[name])));

      // files: use rel from server as-is (already full)
      node.files.forEach((f) => {
        const div = document.createElement("div");
        div.className = "file";
        div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
        div.dataset.rel = f.rel;
        div.onclick = () => openFile(f.rel);
        det.appendChild(div);
      });

      return det;
    };

    // top-level dirs
    const dirNames = Object.keys(rootNode.dirs).sort((a, b) => a.localeCompare(b));
    dirNames.forEach((name) => container.appendChild(mkDir(rootNode.dirs[name])));

    // top-level files: use rel as-is
    rootNode.files.forEach((f) => {
      const div = document.createElement("div");
      div.className = "file";
      div.innerHTML = `<span>📄</span><span class="name">${f.name}</span>`;
      div.dataset.rel = f.rel;
      div.onclick = () => openFile(f.rel);
      container.appendChild(div);
    });
  }

  async function refreshTree() {
    try {
      const res = await api({ action: "tree", rel: state.scopeRel });
      if (!res || res.ok !== true || !Array.isArray(res.items)) {
        const msg = res && res.error ? res.error : "Tree request failed";
        alert("Mini IDE error: " + msg);
        console.error("Tree response:", res);
        return;
      }
      const nested = toNested(res.items);
      renderTree(nested, $("#tree"));
      renderScopeCrumbs();

      // re-highlight current file if visible
      if (state.currentRel) {
        $$("#tree .file").forEach((n) => n.classList.remove("active"));
        const hit = $(`#tree .file[data-rel="${CSS.escape(state.currentRel)}"]`);
        if (hit) hit.classList.add("active");
        updateDirtyUI();
      }
    } catch (e) {
      alert("Mini IDE fetch error (tree): " + e);
      console.error(e);
    }
  }

  // ---------- File ops ----------
  async function openFile(rel) {
    const res = await api({ action: "read", rel });
    if (!res.ok) return alert(res.error || "Failed to read");
    state.currentRel = rel;

    // highlight selected
    $$("#tree .file").forEach((n) => n.classList.remove("active"));
    const hit = $(`#tree .file[data-rel="${CSS.escape(rel)}"]`);
    if (hit) hit.classList.add("active");

    if (res.file.editable) {
      setEditorContent(res.file.content || "");
    } else {
      setEditorContent("// Not editable (binary or large). Size: " + res.file.size + " bytes\n");
    }
    state.dirty = false;
    setSaveDirty(false);
    updateDirtyUI();
    refreshPreview();
  }

  function getEditorContent() {
    if (state.ace) return state.ace.getValue();
    return $("#ta").value;
  }

  function setEditorContent(txt) {
    if (state.ace) {
      state.ace.setValue(txt ?? "", -1);
      // Set mode from extension
      const ext = (state.currentRel.split(".").pop() || "").toLowerCase();
      const map = {
        js: "javascript",
        css: "css",
        php: "php",
        html: "html",
        htm: "html",
        json: "json",
        md: "markdown",
        py: "python",
        yml: "yaml",
        yaml: "yaml",
        c: "c_cpp",
        cpp: "c_cpp",
        h: "c_cpp",
        sql: "sql",
        csv: "text",
        txt: "text"
      };
      state.ace.session.setMode("ace/mode/" + (map[ext] || "text"));
    } else {
      $("#ta").classList.remove("hidden");
      $("#ta").value = txt ?? "";
    }
  }

  async function saveCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (state.saving) return; // prevent double-taps
    state.saving = true;
    setSaveBusy(true);

    try {
      const content = getEditorContent();
      const res = await api({ action: "save", rel: state.currentRel, content });
      if (!res.ok) {
        markSaveErr();
        throw new Error(res.error || "Save failed");
      }

      // Server verification
      let verified = !!res.verify_ok;

      // mtime wait (shared hosts can be slow)
      const targetMtime = res.mtime || 0;
      const okStat = await waitForStat(state.currentRel, targetMtime, 8, 150);
      if (!okStat) console.warn("Proceeding without stat confirmation");

      // Optional client-side readback if server said not verified
      if (!verified) {
        try {
          const rb = await api({ action: "read", rel: state.currentRel });
          if (rb.ok && rb.file && typeof rb.file.content === "string") {
            verified = (rb.file.content === content);
          }
        } catch {}
      }

      if (!verified) {
        markSaveErr();
        alert("Save did not verify. The file contents on disk did not match what was sent.");
        return; // do not mark OK or clear dirty
      }

      state.dirty = false;
      setSaveDirty(false);
      updateDirtyUI();   // clear asterisks + amber dot
      markSaveOK();      // green blip
      refreshPreview(true);

    } catch (e) {
      console.error(e);
      alert("Save error: " + e.message);
      markSaveErr();     // red
    } finally {
      state.saving = false;
      setSaveBusy(false);
    }
  }

  // poll stat until mtime >= target
  async function waitForStat(rel, target, tries = 8, delay = 150) {
    for (let i = 0; i < tries; i++) {
      try {
        const s = await api({ action: "stat", rel });
        if (s.ok && (s.mtime || 0) >= target) return true;
      } catch {}
      await new Promise((r) => setTimeout(r, delay));
    }
    return false;
  }

  async function renameCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    const to = prompt("Rename to (path):", state.currentRel);
    if (!to || to === state.currentRel) return;
    const res = await api({ action: "rename", from: state.currentRel, to });
    if (!res.ok) return alert(res.error || "Rename failed");
    state.currentRel = to;
    $("#currentPath").textContent = to;
    await refreshTree();
    refreshPreview();
  }

  async function deleteCurrent() {
    if (!state.currentRel) return alert("No file selected.");
    if (!confirm("Delete " + state.currentRel + " ?")) return;
    const res = await api({ action: "delete", rel: state.currentRel });
    if (!res.ok) return alert(res.error || "Delete failed");
    state.currentRel = "";
    $("#currentPath").textContent = "—";
    if (state.ace) state.ace.setValue("");
    else $("#ta").value = "";
    await refreshTree();
    refreshPreview();
  }

  async function createFromInput(isDir) {
    const raw = $("#newPath").value.trim();
    if (!raw) return;

    const relPath = state.scopeRel
      ? (state.scopeRel + "/" + raw).replace(/\/+/g, "/").replace(/^\/|\/$/g, "")
      : raw;

    const res = await api({ action: "create", rel: relPath, type: isDir ? "dir" : "file" });
    if (!res.ok) return alert(res.error || "Create failed");
    $("#newPath").value = "";
    await refreshTree();
    if (!isDir) openFile(relPath);
  }

  async function bulkCreate() {
    const lines = $("#bulkLines").value;
    if (!lines.trim()) return;

    // Prefix each line's path with scope (keep labels 'dir:'/'file:' intact)
    const scoped = lines.split("\n").map((t) => {
      const ln = t.trim();
      if (!ln) return ln;
      const m = ln.match(/^(dir:|file:)?\s*(.*)$/i);
      if (!m) return ln;
      const label = (m[1] || "").toLowerCase();
      let p = m[2].trim();
      if (p && state.scopeRel && !p.startsWith("/")) {
        p = (state.scopeRel + "/" + p).replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
      }
      return (label ? label + " " : "") + p;
    }).join("\n");

    const res = await api({ action: "bulk", lines: scoped });
    if (!res.ok) return alert(res.error || "Bulk failed");
    await refreshTree();
    alert("Bulk operations complete.");
  }

  // ---------- Preview ----------
  function refreshPreview(/* force = false */) {
    const on = $("#togglePreview")?.checked;
    if (!on) return;

    // Prefer current file; otherwise fall back to index.html (safer default)
    let rel = state.currentRel || "index.html";
    if (!/\.(php|html?|md|txt|css|js)$/i.test(rel)) rel = "index.html";

    const base = (state.cfg && state.cfg.workspace_url) || "/";
    const url = (base.endsWith("/") ? base : base + "/") + rel;

    const ifr = $("#preview");
    ifr.src = url + (url.includes("?") ? "&" : "?") + "_t=" + Date.now(); // cache-bust
  }

  // ---------- Link scan ----------
      // ---------- Client-side focused Link scan (current file only) ----------
  async function scanLinks() {
    // Ensure we have a current file
    const current = state.currentRel || "";
    if (!current) return alert("Open a file first (click a file in the tree).");

    // Get editor contents
    const content = getEditorContent() || "";

    // Extract candidate file references and variables
    const fileCandidates = extractFileRefs(content);      // Set of path-like strings
    const varCandidates  = extractVariables(content);    // [{name, value}] from current file

    // Build preliminary edges
    const edges = [];
    const seen = new Set();

    // 1) file references (direct)
    for (const raw of fileCandidates) {
      // normalize simple relative paths (remove surrounding quotes/slashes whitespace)
      const rel = raw.trim().replace(/^['"`]+|['"`]+$/g, "");
      if (!rel) continue;
      const key = `${current}→${rel}`;
      if (seen.has(key)) continue;
      seen.add(key);
      edges.push({ from: current, to: rel, viaVar: null, exists: null });
    }

    // 2) references coming from variables assigned to path-like strings
    // e.g. const img = 'assets/img.png';  -> record edge current -> assets/img.png and tag viaVar
    for (const v of varCandidates) {
      const val = (v.value || "").trim();
      if (!val) continue;
      // heuristic: treat as path if contains a slash or a dot and length < 300
      if (/[\/.]/.test(val) && val.length < 300) {
        const rel = val.replace(/^['"`]+|['"`]+$/g, "");
        const key = `${current}→${rel}`;
        if (!seen.has(key)) {
          seen.add(key);
          edges.push({ from: current, to: rel, viaVar: v.name, exists: null });
        } else {
          // if edge exists, annotate viaVar if missing
          const ex = edges.find((e) => e.from === current && e.to === rel);
          if (ex && !ex.viaVar) ex.viaVar = v.name;
        }
      }
    }

    // 3) Optional: look for import/require/include statements that reference modules
    // already covered by extractFileRefs; this is just defensive.

    // Show a quick waiting indicator (reuses pageLoader)
    setSaveBusy(true);

    // Verify existence of each referenced file via api read (best-effort)
    await Promise.all(
      edges.map(async (e) => {
        try {
          // normalize path: if it looks relative and current has folders, keep as-is.
          // We'll just ask server if it can read that path.
          const r = await api({ action: "stat", rel: e.to });
          e.exists = !!(r && r.ok);
        } catch (err) {
          e.exists = false;
        }
      })
    );

    setSaveBusy(false);

    // Save into state and render into the link panel
    state.lastLinks = edges;

    const box = document.getElementById("linksPanel");
    if (!box) return alert("Link panel not found.");
    box.innerHTML = "";

    if (!edges.length) {
      box.textContent = "No file references or variables detected in the open file.";
      // disable download button if you implemented that
      const dl = document.getElementById("btnDownloadLinks"); if (dl) dl.disabled = true;
      return;
    }

    // enable download button
    const dl = document.getElementById("btnDownloadLinks"); if (dl) dl.disabled = false;

    // Render each edge with existence badge and optional variable
    edges.forEach((e) => {
      const row = document.createElement("div");
      row.className = "edge";
      const existsLabel = e.exists === null ? "?" : (e.exists ? "exists" : "missing");
      row.innerHTML = `<div style="display:flex;align-items:center;gap:8px;justify-content:space-between">
          <div style="flex:1;min-width:0; overflow:hidden; text-overflow:ellipsis;">
            <strong>${escapeHtml(e.from)}</strong> &nbsp;→&nbsp;
            <span title="${escapeHtml(e.to)}">${escapeHtml(e.to)}</span>
            ${e.viaVar ? `<small style="margin-left:6px;color:var(--sub)">(${escapeHtml(e.viaVar)})</small>` : ""}
          </div>
          <div style="flex:0 0 auto; display:flex; gap:6px; align-items:center;">
            <span style="font-size:12px; padding:4px 8px; border-radius:8px; border:1px solid var(--border); background:#fff;">
              ${escapeHtml(existsLabel)}
            </span>
            <button class="openLinkFile" data-rel="${escapeHtml(e.to)}">Open</button>
          </div>
        </div>`;
      box.appendChild(row);
    });

    // wire open buttons
    $$(".openLinkFile", box).forEach((b) => {
      b.addEventListener("click", () => {
        const rel = b.dataset.rel;
        if (rel) {
          document.body.classList.remove("linkmap-visible");
          openFile(rel);
        }
      });
    });
  }

  // Helper: extract path-like tokens and import/include targets from source text
  function extractFileRef




  // ---------- Go ----------
  init();
})();
