Skip to content
SEO clean, structured learning
AD-FREE focused lessons
PROJECTS finish what you start

Knitting courses that help you learn with confidence.

Minimalist, structured, and practical knitting education—built for beginners who want a clear path, and for experienced knitters who want to master techniques like cables, lace, and brioche.

Explore catalog
Clarity-first curriculum
Each course is a clean sequence: skill → pattern → project → feedback loop.
Technique-to-project mapping
Learn purl/ribbing, then immediately use them in wearable, gift-ready pieces.
Time-smart lessons
Short modules, clear checklists, and timers for practice sessions.
Next cohort starts in
Daily practice timer 00:00
Downloadable checklists

Printable lesson checklists and finishing guides (seams, blocking, gauge) so your projects look professional.

Practice timers

Use built-in timers for stitch drills, swatching, and breaks. Learn faster without burning out.

No clutter, no distractions

A minimalist learning environment: clear navigation, clean typography, and consistent course structure.

A learning path that matches your pace

Choose what you want to knit, how much time you have, and your current level. The Path Builder will recommend a realistic sequence of skills and projects—and pass the filters to the catalog so you can act immediately.

Skills Fundamentals, tension, ribbing, shaping, charts, finishing.
Projects Accessories → socks → sweaters → advanced textures & lace.
Pacing Daily minutes become a plan with weekly milestones.
Technique focus Pick what matters: fit, texture, speed, or complexity.

Today’s micro-challenge

A tiny drill you can complete in under 10 minutes to improve consistency.

Challenge

Need help choosing yarn?

Get a quick, practical answer. We’ll guide you by project and technique.

Contact
+1 (415) 805-2749

Start with a path, not a guess

Use the Path Builder to get a clear next step. Then jump straight to matching courses in the catalog—filters included.

Go to catalog

Path Builder

Tell us your level, goal, and time. We’ll generate a realistic 4-week sequence and pass matching filters to the catalog.

Apply to catalog

Your 4-week path

Generate to see weekly milestones and recommended course filters.

level: beginner 15 min/day
Week 1
Week 2
Week 3
Week 4
Catalog filters

Send it to yourself

Optional: email a summary of your generated path. Stored locally only; no external requests.

Ask a question

Tell us what you’re making and where you’re stuck. We’ll reply with a practical next step. Phone: +1 (415) 805-2749

What we can help with

A quick checklist so your message gets a precise answer.

Project detailsPattern link/name, yarn weight, needle size.
Where it goes wrongRow count, technique, photo description.
Your goalSpeed, fit, drape, or finishing quality.
Your levelBeginner / intermediate / advanced.

Info

Micro-challenge

Use the timer if you want. This is designed to be small and repeatable.

Timer 00:00
`; const footerFallback = ` `; async function tryFetch(url){ try{ const res = await fetch(url, {cache:'no-store'}); if(!res.ok) return null; const txt = await res.text(); if(!txt || !txt.trim()) return null; return txt; }catch(e){ return null; } } const [headerHTML, footerHTML] = await Promise.all([ tryFetch('./header.html'), tryFetch('./footer.html') ]); if(headerMount){ headerMount.innerHTML = headerHTML || headerFallback; const injectedHeader = headerMount.querySelector('header'); if(!injectedHeader){ headerMount.innerHTML = headerFallback; } } if(footerMount){ footerMount.innerHTML = footerHTML || footerFallback; const injectedFooter = footerMount.querySelector('footer'); if(!injectedFooter){ footerMount.innerHTML = footerFallback; } } } function cookie(){ const key = 'kc_cookie_consent_v1'; function get(){ try{ return localStorage.getItem(key); }catch(e){ return null; } } function set(v){ try{ localStorage.setItem(key, v); }catch(e){} } function init(){ const banner = $('#cookieBanner'); if(!banner) return; const existing = get(); if(existing !== 'accept' && existing !== 'reject'){ setAriaHidden(banner, false); } $('#cookieAccept')?.addEventListener('click', ()=>{ set('accept'); setAriaHidden(banner, true); }); $('#cookieReject')?.addEventListener('click', ()=>{ set('reject'); setAriaHidden(banner, true); }); $('#cookieClose')?.addEventListener('click', ()=>{ set('reject'); setAriaHidden(banner, true); }); } return {init, get, set}; } function dialogKit(dialog){ if(!dialog) return {open:()=>{}, close:()=>{}}; function open(){ if(typeof dialog.showModal === 'function'){ if(!dialog.open) dialog.showModal(); }else{ dialog.setAttribute('open',''); } trapFocus(dialog); } function close(){ if(typeof dialog.close === 'function' && dialog.open) dialog.close(); dialog.removeAttribute('open'); releaseTrap(dialog); } dialog.addEventListener('click', (e)=>{ const rect = dialog.getBoundingClientRect(); const isInDialog = (e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom); if(!isInDialog){ close(); } }); dialog.addEventListener('cancel', (e)=>{ e.preventDefault(); close(); }); return {open, close}; } const focusTraps = new Map(); function trapFocus(dialog){ const focusableSelector = 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'; const focusables = () => $$(focusableSelector, dialog).filter(el=>!el.hasAttribute('disabled') && el.getAttribute('aria-hidden')!=='true'); const first = () => focusables()[0]; const last = () => { const f = focusables(); return f[f.length-1]; }; const onKeyDown = (e)=>{ if(e.key === 'Escape') return; if(e.key !== 'Tab') return; const f = focusables(); if(!f.length) return; const active = document.activeElement; if(e.shiftKey){ if(active === f[0] || active === dialog){ e.preventDefault(); f[f.length-1].focus(); } }else{ if(active === f[f.length-1]){ e.preventDefault(); f[0].focus(); } } }; if(!focusTraps.has(dialog)){ dialog.addEventListener('keydown', onKeyDown); focusTraps.set(dialog, onKeyDown); } setTimeout(()=>{ (first() || dialog).focus?.(); }, 0); } function releaseTrap(dialog){ const fn = focusTraps.get(dialog); if(fn){ dialog.removeEventListener('keydown', fn); focusTraps.delete(dialog); } } function cohortCountdown(){ const el = $('#cohortCountdown'); if(!el) return; const now = new Date(); const start = new Date(now); start.setDate(start.getDate() + 9); start.setHours(10, 0, 0, 0); function tick(){ const n = new Date(); let diff = start.getTime() - n.getTime(); if(diff < 0) diff = 0; const d = Math.floor(diff / (1000*60*60*24)); const h = Math.floor((diff / (1000*60*60)) % 24); const m = Math.floor((diff / (1000*60)) % 60); el.textContent = d + 'd ' + pad2(h) + 'h ' + pad2(m) + 'm'; } tick(); setInterval(tick, 30_000); } function stopwatch(bind){ let running = false; let t0 = 0; let acc = 0; let raf = 0; function render(){ const ms = (running ? (performance.now()-t0) : 0) + acc; const totalSec = Math.floor(ms/1000); const mm = Math.floor(totalSec/60); const ss = totalSec%60; bind.onRender?.(pad2(mm)+':'+pad2(ss)); if(running) raf = requestAnimationFrame(render); } function start(){ if(running) return; running = true; t0 = performance.now(); bind.onRunningChange?.(true); raf = requestAnimationFrame(render); } function stop(){ if(!running) return; running = false; acc += performance.now()-t0; bind.onRunningChange?.(false); cancelAnimationFrame(raf); render(); } function reset(){ running = false; acc = 0; t0 = 0; bind.onRunningChange?.(false); cancelAnimationFrame(raf); bind.onRender?.('00:00'); } function toggle(){ running ? stop() : start(); } reset(); return {start, stop, reset, toggle, get running(){ return running; }}; } function validateMinutes(v){ const n = Number(String(v).trim()); if(!Number.isFinite(n)) return null; const i = Math.round(n); if(i < 5 || i > 180) return null; return i; } function setLocalJSON(key, obj){ try{ localStorage.setItem(key, JSON.stringify(obj)); }catch(e){} } function getLocalJSON(key, fallback){ try{ return safeJSONParse(localStorage.getItem(key), fallback); }catch(e){ return fallback; } } function buildCatalogURL(filters){ const params = new URLSearchParams(); Object.entries(filters||{}).forEach(([k,v])=>{ if(v == null) return; if(Array.isArray(v)){ v.filter(Boolean).forEach(x=>params.append(k, x)); }else{ if(String(v).trim() !== '') params.set(k, String(v)); } }); const qs = params.toString(); return './catalog.html' + (qs ? ('?' + qs) : ''); } function pathBuilderEngine(input){ const level = input.level; const goal = input.goal; const project = input.project; const minutes = input.minutes; const focus = input.focus || []; const intensity = minutes <= 10 ? 'micro' : minutes <= 20 ? 'steady' : minutes <= 35 ? 'strong' : 'deep'; const baseTech = []; if(level === 'beginner'){ baseTech.push('cast-on & bind-off', 'knit/purl control', 'tension & edges', 'simple decreases/increases'); }else if(level === 'intermediate'){ baseTech.push('consistent gauge', 'reading patterns', 'shaping confidence', 'clean finishing'); }else{ baseTech.push('chart fluency', 'precision shaping', 'refined finishing', 'problem-solving'); } const focusMap = { fundamentals: ['tension drills', 'edges', 'counting rows'], ribbing: ['ribbing setup', 'elastic bind-off', 'neat transitions'], shaping: ['increases/decreases', 'short rows', 'try-on methods'], cables: ['cable without needle', 'cable charts', 'fixing crossings'], lace: ['lifelines', 'chart reading', 'blocking lace'], brioche: ['brk/brp', 'fixing dropped brioche', 'two-color basics'], finishing: ['blocking', 'seaming', 'weaving ends'], speed: ['efficient movement', 'batching steps', 'error recovery'] }; const focusTopics = focus.flatMap(f=>focusMap[f]||[]); const techniqueFocus = Array.from(new Set([...baseTech, ...focusTopics])).slice(0, 8); const projectTrack = project === 'accessories' ? ['scarf sample', 'hat basics', 'finishing polish'] : project === 'socks' ? ['heel concepts', 'gusset shaping', 'toe & grafting'] : project === 'sweater' ? ['yoke/raglan shaping', 'fit checkpoints', 'neckline finishing'] : ['lace swatch', 'chart-based panel', 'blocking mastery']; const goalHint = goal === 'first_project' ? 'fast-to-finish projects with minimal new techniques' : goal === 'skills_building' ? 'foundations + repeatable drills' : goal === 'texture_and_fit' ? 'fit checkpoints + texture control' : 'advanced techniques + chart fluency'; const pace = intensity === 'micro' ? '2–3 short sessions/week' : intensity === 'steady' ? '4–5 sessions/week' : intensity === 'strong' ? '5–6 sessions/week' : 'daily sessions'; const weeklyMinutes = minutes * (intensity === 'micro' ? 3 : intensity === 'steady' ? 5 : intensity === 'strong' ? 6 : 7); function weekPlan(week){ const w = week; const t = techniqueFocus; const pt = projectTrack; const weekBlocks = []; if(w === 1){ weekBlocks.push(`Stitch control: ${t[0] || 'tension'} + ${t[1] || 'edges'}.`); weekBlocks.push(`Mini-project: start a ${pt[0]} to validate gauge and comfort.`); weekBlocks.push(`Checkpoint: count rows, measure, adjust needle size if needed.`); }else if(w === 2){ weekBlocks.push(`Build: ${t[2] || 'pattern reading'} + ${t[3] || 'shaping basics'}.`); weekBlocks.push(`Project step: progress your ${pt[1] || 'main piece'} with consistent rhythm.`); weekBlocks.push(`Fixing: practice ladder-down repair on a swatch for 5 minutes.`); }else if(w === 3){ const adv = (focus.includes('cables') || focus.includes('lace') || focus.includes('brioche') || goal === 'complex_techniques'); weekBlocks.push(adv ? `Technique focus: ${t[4] || 'charts'} + ${t[5] || 'precision steps'} (short drills).` : `Consistency: repeat Week 2 skills with cleaner edges and fewer mistakes.`); weekBlocks.push(`Project step: add a structured element for the ${pt[2] || 'final'} (e.g., shaping/texture).`); weekBlocks.push(`Checkpoint: block a small swatch; record your best gauge.`); }else{ weekBlocks.push(`Finishing: ${t[6] || 'weaving ends'} + ${t[7] || 'blocking'} with a clear checklist.`); weekBlocks.push(`Project finish: complete + review fit/drape; note what you’d do differently.`); weekBlocks.push(`Next step: choose 1 technique to deepen next month.`); } return weekBlocks.join(' '); } const weeks = [1,2,3,4].map(weekPlan); const filterLevel = level; const filterProject = project; let filterDifficulty = level === 'beginner' ? 'intro' : level === 'intermediate' ? 'core' : 'pro'; if(goal === 'first_project' && level !== 'beginner') filterDifficulty = 'core'; if(goal === 'complex_techniques' && level === 'beginner') filterDifficulty = 'core'; const focusNormalized = focus.length ? focus : (level === 'beginner' ? ['fundamentals','ribbing'] : level === 'intermediate' ? ['shaping','finishing'] : ['cables','lace']); const payload = { level: filterLevel, project: filterProject, difficulty: filterDifficulty, focus: focusNormalized, pace: intensity, minutes: minutes }; const subtitle = `${pace} · ~${weeklyMinutes} min/week · ${goalHint}`; const title = level === 'beginner' ? 'Your 4-week foundation path' : level === 'intermediate' ? 'Your 4-week skill-builder path' : 'Your 4-week advanced refinement path'; return {title, subtitle, weeks, payload}; } function initQuickStart(){ const qsLevel = $('#qsLevel'); const qsGoal = $('#qsGoal'); const qsTime = $('#qsTime'); const qsProject = $('#qsProject'); const err = $('#qsError'); const ok = $('#qsOk'); function showErr(msg){ if(err) err.textContent = msg || 'Please enter a valid daily time (5–180 minutes).'; setAriaHidden(err, false); setAriaHidden(ok, true); } function showOk(){ setAriaHidden(err, true); setAriaHidden(ok, false); setTimeout(()=>setAriaHidden(ok, true), 2200); } function getQS(){ const minutes = validateMinutes(qsTime?.value ?? ''); return { level: qsLevel?.value || 'beginner', goal: qsGoal?.value || 'first_project', project: qsProject?.value || 'accessories', minutes }; } function saveQS(){ const d = getQS(); if(d.minutes == null){ showErr('Please enter a valid daily time (5–180 minutes).'); return null; } const pref = getLocalJSON('kc_path_pref_v1', {}); pref.level = d.level; pref.goal = d.goal; pref.project = d.project; pref.minutes = d.minutes; if(!Array.isArray(pref.focus) || pref.focus.length===0){ pref.focus = d.level==='beginner' ? ['fundamentals','ribbing'] : d.level==='intermediate' ? ['shaping','finishing'] : ['cables','lace']; } setLocalJSON('kc_path_pref_v1', pref); showOk(); return pref; } $('#applyQuickFilters')?.addEventListener('click', (e)=>{ const pref = saveQS(); if(!pref){ e.preventDefault(); return; } const filters = { level: pref.level, project: pref.project, minutes: pref.minutes, focus: pref.focus }; e.currentTarget.setAttribute('href', buildCatalogURL(filters)); }); [qsLevel, qsGoal, qsProject].forEach(el=>{ el?.addEventListener('change', ()=>{ saveQS(); }); }); qsTime?.addEventListener('input', ()=>{ setAriaHidden(err, true); setAriaHidden(ok, true); }); qsTime?.addEventListener('blur', ()=>{ if(validateMinutes(qsTime.value)==null) showErr(); else saveQS(); }); const existing = getLocalJSON('kc_path_pref_v1', null); if(existing){ if(qsLevel) qsLevel.value = existing.level || qsLevel.value; if(qsGoal) qsGoal.value = existing.goal || qsGoal.value; if(qsProject) qsProject.value = existing.project || qsProject.value; if(qsTime && existing.minutes) qsTime.value = String(existing.minutes); } } function initPathBuilder(){ const dlg = $('#pathBuilderDialog'); const kit = dialogKit(dlg); const pbLevel = $('#pbLevel'); const pbGoal = $('#pbGoal'); const pbProject = $('#pbProject'); const pbMinutes = $('#pbMinutes'); const pbError = $('#pbError'); const pbOk = $('#pbOk'); const pbTitle = $('#pbTitle'); const pbSubtitle = $('#pbSubtitle'); const pbWeeks = $('#pbWeeks'); const pbPayload = $('#pbPayload'); const pbTagLevel = $('#pbTagLevel'); const pbTagMinutes = $('#pbTagMinutes'); const hintLevel = $('#pbLevelHint'); const hintGoal = $('#pbGoalHint'); const hintProject = $('#pbProjectHint'); const hintMinutes = $('#pbMinutesHint'); const focusButtons = $$('.u0p9b', dlg); function getFocus(){ return focusButtons.filter(b=>b.getAttribute('aria-pressed')==='true').map(b=>b.dataset.focus); } function setFocus(list){ const set = new Set(list||[]); focusButtons.forEach(b=>{ const on = set.has(b.dataset.focus); b.setAttribute('aria-pressed', on ? 'true' : 'false'); }); } function loadPrefs(){ const pref = getLocalJSON('kc_path_pref_v1', {}); if(pbLevel && pref.level) pbLevel.value = pref.level; if(pbGoal && pref.goal) pbGoal.value = pref.goal; if(pbProject && pref.project) pbProject.value = pref.project; if(pbMinutes && pref.minutes) pbMinutes.value = String(pref.minutes); if(pref.focus && Array.isArray(pref.focus) && pref.focus.length) setFocus(pref.focus); updateHints(); return pref; } function savePrefs(){ const minutes = validateMinutes(pbMinutes?.value ?? ''); const pref = getLocalJSON('kc_path_pref_v1', {}); pref.level = pbLevel?.value || 'beginner'; pref.goal = pbGoal?.value || 'first_project'; pref.project = pbProject?.value || 'accessories'; if(minutes != null) pref.minutes = minutes; pref.focus = getFocus(); setLocalJSON('kc_path_pref_v1', pref); return {pref, minutes}; } function showPBErr(msg){ if(pbError) pbError.textContent = msg || 'Enter valid minutes/day (5–180). Then generate.'; setAriaHidden(pbError, false); setAriaHidden(pbOk, true); } function showPBOk(){ setAriaHidden(pbError, true); setAriaHidden(pbOk, false); setTimeout(()=>setAriaHidden(pbOk, true), 2200); } function updateHints(){ if(hintLevel) hintLevel.textContent = (pbLevel?.value || 'beginner'); if(hintGoal) hintGoal.textContent = (pbGoal?.value || 'first_project').replaceAll('_',' '); if(hintProject) hintProject.textContent = (pbProject?.value || 'accessories'); const m = validateMinutes(pbMinutes?.value ?? ''); if(hintMinutes) hintMinutes.textContent = m == null ? '5–180' : String(m); if(pbTagLevel) pbTagLevel.textContent = 'level: ' + (pbLevel?.value || 'beginner'); if(pbTagMinutes) pbTagMinutes.textContent = (m == null ? '—' : (String(m) + ' min/day')); } function generate(){ const minutes = validateMinutes(pbMinutes?.value ?? ''); if(minutes == null){ showPBErr('Enter valid minutes/day (5–180).'); updateHints(); return null; } const focus = getFocus(); const input = {level: pbLevel?.value || 'beginner', goal: pbGoal?.value || 'first_project', project: pbProject?.value || 'accessories', minutes, focus}; const out = pathBuilderEngine(input); if(pbTitle) pbTitle.textContent = out.title; if(pbSubtitle) pbSubtitle.textContent = out.subtitle; if(pbWeeks){ const items = $$('.u5y2v', pbWeeks); const weeks = out.weeks; items.forEach((card, idx)=>{ const span = card.querySelector('span'); if(span) span.textContent = weeks[idx] || '—'; }); } const payloadText = JSON.stringify(out.payload); if(pbPayload) pbPayload.textContent = payloadText; const pref = getLocalJSON('kc_path_pref_v1', {}); pref.level = input.level; pref.goal = input.goal; pref.project = input.project; pref.minutes = minutes; pref.focus = focus; pref.payload = out.payload; pref.generatedAt = Date.now(); setLocalJSON('kc_path_pref_v1', pref); const url = buildCatalogURL(out.payload); $('#pbApplyToCatalog')?.setAttribute('href', url); showPBOk(); updateHints(); return out; } function copyPayload(){ const text = pbPayload?.textContent || ''; if(!text || text === '—') return; navigator.clipboard?.writeText(text).then(()=>{ if(pbOk){ pbOk.textContent = 'Copied.'; setAriaHidden(pbOk, false); setTimeout(()=>{ pbOk.textContent = 'Path updated. You can apply it to the catalog.'; setAriaHidden(pbOk, true); }, 1200); } }).catch(()=>{}); } function openFromQuickStart(){ const pref = getLocalJSON('kc_path_pref_v1', null); if(pref){ if(pbLevel && pref.level) pbLevel.value = pref.level; if(pbGoal && pref.goal) pbGoal.value = pref.goal; if(pbProject && pref.project) pbProject.value = pref.project; if(pbMinutes && pref.minutes) pbMinutes.value = String(pref.minutes); if(pref.focus) setFocus(pref.focus); updateHints(); if(pref.payload){ $('#pbApplyToCatalog')?.setAttribute('href', buildCatalogURL(pref.payload)); if(pbPayload) pbPayload.textContent = JSON.stringify(pref.payload); } } kit.open(); } $('#openPathBuilderTop')?.addEventListener('click', openFromQuickStart); $('#openPathBuilderSide')?.addEventListener('click', openFromQuickStart); $('#openPathBuilderBottom')?.addEventListener('click', openFromQuickStart); $('#closePathBuilder')?.addEventListener('click', kit.close); $('#pbGenerate')?.addEventListener('click', ()=>{ savePrefs(); generate(); }); $('#pbCopyPayload')?.addEventListener('click', copyPayload); [pbLevel, pbGoal, pbProject].forEach(el=>el?.addEventListener('change', ()=>{ savePrefs(); updateHints(); })); pbMinutes?.addEventListener('input', ()=>{ setAriaHidden(pbError, true); setAriaHidden(pbOk, true); updateHints(); }); pbMinutes?.addEventListener('blur', ()=>{ savePrefs(); if(validateMinutes(pbMinutes.value)==null) showPBErr('Enter valid minutes/day (5–180).'); }); focusButtons.forEach(b=>{ b.addEventListener('click', ()=>{ const on = b.getAttribute('aria-pressed') === 'true'; b.setAttribute('aria-pressed', on ? 'false' : 'true'); const {pref} = savePrefs(); updateHints(); if(pref.payload){ $('#pbApplyToCatalog')?.setAttribute('href', buildCatalogURL(pref.payload)); } }); }); loadPrefs(); const pref = getLocalJSON('kc_path_pref_v1', null); if(pref && pref.payload){ if(pbPayload) pbPayload.textContent = JSON.stringify(pref.payload); $('#pbApplyToCatalog')?.setAttribute('href', buildCatalogURL(pref.payload)); }else{ $('#pbApplyToCatalog')?.setAttribute('href', './catalog.html'); } const emailForm = $('#pbEmailForm'); const pbEmail = $('#pbEmail'); const pbName = $('#pbName'); const pbEmailErr = $('#pbEmailErr'); const pbEmailOk = $('#pbEmailOk'); const emailKey = 'kc_path_email_saved_v1'; function isValidEmail(v){ const s = String(v||'').trim(); if(s.length < 6) return false; return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(s); } emailForm?.addEventListener('submit', (e)=>{ e.preventDefault(); setAriaHidden(pbEmailErr, true); setAriaHidden(pbEmailOk, true); const email = String(pbEmail?.value||'').trim(); if(!isValidEmail(email)){ setAriaHidden(pbEmailErr, false); return; } const pref = getLocalJSON('kc_path_pref_v1', {}); if(!pref.payload){ const maybe = generate(); if(!maybe){ setAriaHidden(pbEmailErr, false); pbEmailErr.textContent = 'Generate a path first (valid minutes/day).'; return; } } const saved = { email, name: String(pbName?.value||'').trim(), savedAt: Date.now(), title: pbTitle?.textContent || 'Your 4-week path', subtitle: pbSubtitle?.textContent || '', payload: getLocalJSON('kc_path_pref_v1', {}).payload || null, weeks: $$('.u5y2v', pbWeeks).map(c=>({ week: c.querySelector('b')?.textContent || '', text: c.querySelector('span')?.textContent || '' })) }; setLocalJSON(emailKey, saved); setAriaHidden(pbEmailOk, false); setTimeout(()=>setAriaHidden(pbEmailOk, true), 2200); }); $('#pbEmailView')?.addEventListener('click', ()=>{ const saved = getLocalJSON(emailKey, null); const info = dialogKit($('#infoDialog')); if(!saved){ $('#infoTitle').textContent = 'Saved path'; $('#infoSubtitle').textContent = 'Nothing saved yet.'; $('#infoBody').innerHTML = '
TipGenerate a path, enter your email, then click “Save summary locally”.
'; info.open(); return; } const dt = new Date(saved.savedAt); $('#infoTitle').textContent = 'Saved path summary'; $('#infoSubtitle').textContent = saved.email + ' · ' + dt.toLocaleString(); const lines = (saved.weeks||[]).map(w=>`
${(w.week||'').replace('Week','Week')}${(w.text||'').replaceAll('<','<').replaceAll('>','>')}
`).join(''); const payload = saved.payload ? JSON.stringify(saved.payload) : '—'; $('#infoBody').innerHTML = `
${(saved.title||'Your path')}${(saved.subtitle||'').replaceAll('<','<').replaceAll('>','>')}
` + `
Catalog filters${payload.replaceAll('<','<').replaceAll('>','>')}
` + `
${lines}
`; info.open(); }); const infoDlg = $('#infoDialog'); const infoKit = dialogKit(infoDlg); $('#closeInfo')?.addEventListener('click', infoKit.close); } function initInfoModals(){ const infoDlg = $('#infoDialog'); const infoKit = dialogKit(infoDlg); $('#closeInfo')?.addEventListener('click', infoKit.close); function openInfo(title, subtitle, bodyHTML){ $('#infoTitle').textContent = title; $('#infoSubtitle').textContent = subtitle; $('#infoBody').innerHTML = bodyHTML; infoKit.open(); } $('#openChecklistInfo')?.addEventListener('click', ()=>{ openInfo( 'Downloadable checklists', 'A minimalist set of printable tools for cleaner results.', '
' + '
Gauge checklistSwatch size, washing notes, measuring method, and adjustment steps.
' + '
Finishing checklistWeave ends, block, seam, edge treatment, and final inspection.
' + '
Mistake recoveryLadder-down repairs, lifelines, and reading your stitches.
' + '
Project reviewFit/drape notes and what to change next time.
' + '
' ); }); $('#openTimersInfo')?.addEventListener('click', ()=>{ openInfo( 'Practice timers', 'Short sessions beat long, inconsistent ones.', '
' + '
Stitch drill5–8 minutes: knit/purl transitions, tension, and edge control.
' + '
Swatch block10–12 minutes: knit, measure, and record; repeat weekly.
' + '
Break cadenceUse short breaks to avoid hand fatigue and keep consistency.
' + '
' ); }); } function initConsult(){ const dlg = $('#consultDialog'); const kit = dialogKit(dlg); $('#openConsultTop')?.addEventListener('click', kit.open); $('#openConsultSide')?.addEventListener('click', kit.open); $('#closeConsult')?.addEventListener('click', kit.close); const form = $('#consultForm'); const name = $('#cfName'); const email = $('#cfEmail'); const topic = $('#cfTopic'); const consent = $('#cfConsent'); const msg = $('#cfMessage'); const err = $('#cfErr'); const ok = $('#cfOk'); const key = 'kc_consult_draft_v1'; function isValidEmail(v){ const s = String(v||'').trim(); if(s.length < 6) return false; return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(s); } function showErr(){ setAriaHidden(err, false); setAriaHidden(ok, true); } function showOk(){ setAriaHidden(err, true); setAriaHidden(ok, false); setTimeout(()=>setAriaHidden(ok, true), 2400); } function load(){ const d = getLocalJSON(key, null); if(!d) return; if(name) name.value = d.name || ''; if(email) email.value = d.email || ''; if(topic && d.topic) topic.value = d.topic; if(consent && d.reply) consent.value = d.reply; if(msg) msg.value = d.message || ''; } form?.addEventListener('submit', (e)=>{ e.preventDefault(); setAriaHidden(err, true); setAriaHidden(ok, true); const n = String(name?.value||'').trim(); const em = String(email?.value||'').trim(); const ms = String(msg?.value||'').trim(); if(n.length < 2 || !isValidEmail(em) || ms.length < 20){ showErr(); return; } const data = {name:n, email:em, topic: topic?.value || 'next', reply: consent?.value || '1bd', message: ms, savedAt: Date.now()}; setLocalJSON(key, data); showOk(); }); $('#cfView')?.addEventListener('click', ()=>{ const saved = getLocalJSON(key, null); const infoKit = dialogKit($('#infoDialog')); $('#closeInfo')?.addEventListener('click', infoKit.close); if(!saved){ $('#infoTitle').textContent = 'Saved message'; $('#infoSubtitle').textContent = 'No saved message found.'; $('#infoBody').innerHTML = '
TipFill the form and click “Save message locally”.
'; infoKit.open(); return; } const dt = new Date(saved.savedAt); $('#infoTitle').textContent = 'Saved message draft'; $('#infoSubtitle').textContent = saved.email + ' · ' + dt.toLocaleString(); $('#infoBody').innerHTML = '
' + `
Name${saved.name.replaceAll('<','<').replaceAll('>','>')}
` + `
Topic${String(saved.topic).replaceAll('_',' ').replaceAll('<','<').replaceAll('>','>')}
` + `
Reply window${saved.reply === '2bd' ? 'Within 2 business days' : 'Within 1 business day'}
` + `
Message${saved.message.replaceAll('<','<').replaceAll('>','>')}
` + '
'; infoKit.open(); }); load(); } function initPracticeTimer(){ const status = $('#practiceTimerStatus'); const btn = $('#togglePracticeTimer'); if(!status || !btn) return; const sw = stopwatch({ onRender: (txt)=>{ status.textContent = txt; }, onRunningChange: (is)=>{ btn.textContent = is ? 'Pause' : 'Start'; } }); btn.addEventListener('click', ()=> sw.toggle()); } function initChallenge(){ const challengePool = [ {title:'Neat ribbing edges', steps:[ 'Cast on 24 stitches and work 8 rows of 1x1 rib.', 'On each right-side row, focus on the first and last 2 stitches: keep tension consistent.', 'Switch to stockinette for 6 rows and observe the transition line.', 'Note: if it flares, try a smaller needle for ribbing next time.' ], minutes: 8}, {title:'Purl consistency drill', steps:[ 'Work 6 rows of garter, then switch to stockinette.', 'On purl rows, pause every 8 stitches and relax your grip.', 'Aim for even loop size; avoid pulling the working yarn too tight.', 'Finish with 6 rows of garter and compare both edges.' ], minutes: 9}, {title:'Read your stitches', steps:[ 'Knit 10 rows in stockinette.', 'On the next row, stop every 6 stitches and identify: knit vs purl bumps below.', 'Intentionally create one mistake, then ladder down to fix it.', 'Write one sentence: what did you notice about your tension?' ], minutes: 10}, {title:'Simple decrease accuracy', steps:[ 'Knit 2 rows stockinette on 30 stitches.', 'On the next right-side row: k2tog every 6 stitches across.', 'On the following right-side row: ssk every 6 stitches across.', 'Compare left-leaning vs right-leaning decreases.' ], minutes: 10}, {title:'Lifeline setup (lace-ready)', steps:[ 'Knit 12 stitches stockinette for 8 rows.', 'Thread a smooth scrap yarn through one full row of stitches (lifeline).', 'Knit 4 more rows, then intentionally drop a stitch above the lifeline.', 'Recover it and observe how the lifeline keeps you safe.' ], minutes: 9} ]; function pick(){ const seed = Math.floor(Date.now()/ (1000*60*10)); const idx = seed % challengePool.length; return challengePool[idx]; } const titleEl = $('#challengeTitle'); const dlg = $('#challengeDialog'); const kit = dialogKit(dlg); $('#closeChallenge')?.addEventListener('click', kit.close); let current = pick(); function render(){ current = pick(); if(titleEl) titleEl.textContent = current.title; $('#challengeModalTitle').textContent = current.title; const stepsHTML = '
' + current.steps.map((s,i)=>`
Step ${i+1}${String(s).replaceAll('<','<').replaceAll('>','>')}
`).join('') + '
'; $('#challengeSteps').innerHTML = stepsHTML; } $('#refreshChallenge')?.addEventListener('click', ()=>{ render(); }); $('#openChallengeModal')?.addEventListener('click', ()=>{ render(); kit.open(); }); const timerStatus = $('#challengeTimerStatus'); const tToggle = $('#challengeTimerToggle'); const tReset = $('#challengeTimerReset'); const sw = stopwatch({ onRender: (txt)=>{ if(timerStatus) timerStatus.textContent = txt; }, onRunningChange: (is)=>{ if(tToggle) tToggle.textContent = is ? 'Pause' : 'Start'; } }); tToggle?.addEventListener('click', ()=> sw.toggle()); tReset?.addEventListener('click', ()=> sw.reset()); render(); } function initThemeToggle(){ updateThemeIcon(); $('#themeToggle')?.addEventListener('click', ()=>{ const cur = getTheme(); applyTheme(cur === 'dark' ? 'light' : 'dark'); }); } function initCatalogCTAs(){ function attachDynamicHref(anchorId){ const a = $(anchorId); if(!a) return; a.addEventListener('click', (e)=>{ const pref = getLocalJSON('kc_path_pref_v1', null); if(pref && pref.payload){ a.setAttribute('href', buildCatalogURL(pref.payload)); }else{ a.setAttribute('href', './catalog.html'); } }); } attachDynamicHref('#ctaExploreCatalog'); attachDynamicHref('#ctaCatalogBottom'); } function closeInfoOnOutside(){ const infoDlg = $('#infoDialog'); const kit = dialogKit(infoDlg); $('#closeInfo')?.addEventListener('click', kit.close); } (async function init(){ await mountPartials(); cookie().init(); initThemeToggle(); initQuickStart(); initPathBuilder(); initInfoModals(); initConsult(); initPracticeTimer(); initChallenge(); initCatalogCTAs(); cohortCountdown(); closeInfoOnOutside(); })(); })();