diff --git a/public/app.js b/public/app.js index fd258dc..6d5cfe7 100644 --- a/public/app.js +++ b/public/app.js @@ -1,255 +1,129 @@ -const profiles = { - mvp: { - name: 'MVP (2x2)', - gridLabel: 'Sorting grid (MVP - 2x2)', - zones: [ - { id:'must', label:'Required / Must Have', copy:'Critical for the solution. Necessary for MVP.', icon:'☆', color:'#16A34A', bg:'rgba(22,163,74,.06)', milestoneId:'now', status:'must', left:13 }, - { id:'should', label:'Should Have', copy:'Important but not critical. Adds significant value.', icon:'○', color:'#2563EB', bg:'rgba(37,99,235,.055)', milestoneId:'next', status:'should', left:47 }, - { id:'nice', label:'Nice to Have', copy:'Good to include if possible. Not essential.', icon:'♡', color:'#F59E0B', bg:'rgba(245,158,11,.07)', milestoneId:'later', status:'nice', left:68 }, - { id:'stretch', label:'Stretch Goal', copy:'Low priority ideas. Consider for the future.', icon:'ϟ', color:'#7C3AED', bg:'rgba(124,58,237,.065)', milestoneId:'later', status:'stretch', left:88 }, - ], - }, - rice: { - name: 'RICE Score', - gridLabel: 'Sorting grid (RICE)', - zones: [ - { id:'must', label:'High RICE / Build Now', copy:'Reach, impact, confidence all justify immediate work.', icon:'R', color:'#16A34A', bg:'rgba(22,163,74,.06)', milestoneId:'now', status:'must', left:13 }, - { id:'should', label:'Promising / Validate', copy:'Strong impact but needs confidence or reach proof.', icon:'I', color:'#2563EB', bg:'rgba(37,99,235,.055)', milestoneId:'next', status:'should', left:47 }, - { id:'nice', label:'Low Reach / Batch', copy:'Useful, but only after higher-reach work is handled.', icon:'C', color:'#F59E0B', bg:'rgba(245,158,11,.07)', milestoneId:'later', status:'nice', left:68 }, - { id:'stretch', label:'Expensive Bet', copy:'Interesting, but effort or uncertainty is too high right now.', icon:'E', color:'#7C3AED', bg:'rgba(124,58,237,.065)', milestoneId:'later', status:'stretch', left:88 }, - ], - }, - value: { - name: 'Value vs Effort', - gridLabel: 'Sorting grid (Value vs Effort)', - zones: [ - { id:'must', label:'High Value / Low Effort', copy:'Quick win. Pull into the near-term roadmap.', icon:'↗', color:'#16A34A', bg:'rgba(22,163,74,.06)', milestoneId:'now', status:'must', left:13 }, - { id:'should', label:'High Value / High Effort', copy:'Strategic work. Shape before committing.', icon:'◆', color:'#2563EB', bg:'rgba(37,99,235,.055)', milestoneId:'next', status:'should', left:47 }, - { id:'nice', label:'Low Value / Low Effort', copy:'Polish work. Batch when context is cheap.', icon:'◇', color:'#F59E0B', bg:'rgba(245,158,11,.07)', milestoneId:'later', status:'nice', left:68 }, - { id:'stretch', label:'Low Value / High Effort', copy:'Avoid unless strategy changes.', icon:'×', color:'#7C3AED', bg:'rgba(124,58,237,.065)', milestoneId:'later', status:'stretch', left:88 }, - ], - }, - kano: { - name: 'Kano Model', - gridLabel: 'Sorting grid (Kano)', - zones: [ - { id:'must', label:'Basic Expectation', copy:'Users will notice if this is missing.', icon:'B', color:'#16A34A', bg:'rgba(22,163,74,.06)', milestoneId:'now', status:'must', left:13 }, - { id:'should', label:'Performance Driver', copy:'More of this directly improves satisfaction.', icon:'P', color:'#2563EB', bg:'rgba(37,99,235,.055)', milestoneId:'next', status:'should', left:47 }, - { id:'nice', label:'Delighter', copy:'Memorable, but not necessary for usefulness.', icon:'D', color:'#F59E0B', bg:'rgba(245,158,11,.07)', milestoneId:'later', status:'nice', left:68 }, - { id:'stretch', label:'Indifferent / Future', copy:'Weak user pull. Keep it outside the core release.', icon:'?', color:'#7C3AED', bg:'rgba(124,58,237,.065)', milestoneId:'later', status:'stretch', left:88 }, - ], - }, - custom: { - name: 'Custom', - gridLabel: 'Sorting grid (Custom)', - zones: [ - { id:'must', label:'Commit', copy:'This belongs in the next real delivery slice.', icon:'✓', color:'#16A34A', bg:'rgba(22,163,74,.06)', milestoneId:'now', status:'must', left:13 }, - { id:'should', label:'Shape', copy:'Worth pursuing after requirements are sharpened.', icon:'✎', color:'#2563EB', bg:'rgba(37,99,235,.055)', milestoneId:'next', status:'should', left:47 }, - { id:'nice', label:'Queue', copy:'Keep visible, but do not distract the team now.', icon:'≡', color:'#F59E0B', bg:'rgba(245,158,11,.07)', milestoneId:'later', status:'nice', left:68 }, - { id:'stretch', label:'Someday', copy:'A possible future bet, not part of this plan.', icon:'∞', color:'#7C3AED', bg:'rgba(124,58,237,.065)', milestoneId:'later', status:'stretch', left:88 }, - ], - }, +const sample = { + idea: 'I’m building a lightweight product feedback tool for people with too many ideas and too many possible features. It should feel useful before becoming a workspace.', + optionsText: `- Ranked feedback map for pasted feature lists +- Expert reflections on the top options +- Accounts and saved workspaces +- Team voting on feature priority +- Exportable decision brief for Slack or Notion +- Custom criteria builder +- Paid deeper product strategy pass`, + context: 'MVP, solo builder, needs to feel valuable in under two minutes, avoid dashboard swamp.', + mode: 'progress', }; -const state = { ideas: [], milestones: [], activity: [], activeId: null, selected: null, profileId: localStorage.getItem('rank-profile') || 'mvp', undo: null, recentPlacement: null }; -const sampleBacklog = { - format: 'prioritix-feature-set-v1', - name: 'Messy SaaS launch backlog', - features: [ - { title: 'Mobile sorting flow feels clumsy', description: 'Users can capture ideas, but the phone flow makes prioritizing feel like work instead of relief.', labels: ['Mobile', 'Activation'], impact: 9, effort: 4, confidence: 8, urgency: 8 }, - { title: 'Team voting on every feature', description: 'Stakeholders want input, but voting may create politics before the product has a clear decision model.', labels: ['Collaboration'], impact: 7, effort: 7, confidence: 4, urgency: 5 }, - { title: 'Export roadmap for sales calls', description: 'Turn sorted decisions into something founders can share with clients or internal buyers.', labels: ['Sales', 'Export'], impact: 8, effort: 5, confidence: 7, urgency: 7 }, - { title: 'Dark mode polish', description: 'Nice for taste, but unlikely to decide whether anyone trusts the core prioritization.', labels: ['Polish'], impact: 4, effort: 3, confidence: 8, urgency: 2 }, - { title: 'AI explains why an idea moved', description: 'Every ranking should show a reason, risk, and what evidence would change the decision.', labels: ['Trust', 'AI'], impact: 9, effort: 6, confidence: 6, urgency: 8 } - ] -}; -const aiReadyPrompt = `You are preparing features for Rank / Prioritix, a prioritization workbench that imports Prioritix Feature Set v1 JSON. Convert my messy backlog, notes, stakeholder requests, or product ideas into valid JSON only — no markdown, no commentary. -Return this exact shape: -{ - "format": "prioritix-feature-set-v1", - "name": "Short project or backlog name", - "features": [ - { - "title": "Clear feature title", - "description": "Plain-language reason this feature matters and what outcome it supports.", - "labels": ["Category", "Optional second category"], - "impact": 1-10, - "effort": 1-10, - "confidence": 1-10, - "urgency": 1-10, - "notes": "Risks, assumptions, dependencies, or evidence that would change the ranking." - } - ] +const form = document.querySelector('#rankForm'); +const results = document.querySelector('#results'); +const toastEl = document.querySelector('#toast'); + +function escapeHtml(value) { + return String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); } -Scoring guidance: impact means user/business value, effort means delivery difficulty, confidence means how sure the evidence is, urgency means time sensitivity. Use the full 1-10 range honestly. Split vague bundles into separate features. Merge duplicates. Label polish as polish, bets as validation/research, and must-have product work as core. If evidence is weak, lower confidence rather than pretending. Keep titles short enough to scan on a card.`; -function loadSampleBacklog(){ - const input = $('#featureSetInput'); - if(!input) return; - input.value = JSON.stringify(sampleBacklog, null, 2); - document.querySelector('#feature-sets')?.scrollIntoView({ behavior:'smooth', block:'start' }); - toast('Sample backlog loaded. Import it or replace it with your own chaos.'); +function metricPct(value) { + return Math.max(0, Math.min(100, Math.round(Number(value || 0) * 10))); } -async function copyAiPrompt(){ - const status = $('#aiPromptStatus'); + +function toast(message) { + toastEl.textContent = message; + toastEl.hidden = false; + clearTimeout(toastEl._timer); + toastEl._timer = setTimeout(() => { toastEl.hidden = true; }, 2600); +} + +function fillSample() { + form.idea.value = sample.idea; + form.optionsText.value = sample.optionsText; + form.context.value = sample.context; + form.mode.value = sample.mode; + document.querySelector('#try')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + toast('Sample loaded. Run it or replace it with your own mess.'); +} + +function laneClass(lane) { + return `lane-${lane?.id || 'defer'}`; +} + +function renderMetrics(metrics) { + const items = [ + ['Value', metrics.value], + ['Feasible', metrics.feasibility], + ['Confidence', metrics.confidence], + ['Revenue', metrics.revenue], + ['Risk', metrics.risk], + ]; + return `
${items.map(([label, value]) => ` +
${label}
+ `).join('')}
`; +} + +function renderResults(data) { + const ranked = data.ranked || []; + const brief = data.brief || {}; + results.hidden = false; + results.innerHTML = ` +
+

${escapeHtml(data.mode?.label || 'Ranked feedback')}

+

${escapeHtml(brief.headline || 'Ranked feedback map')}

+

${escapeHtml(brief.summary || '')}

+
+ +
+ ${ranked.map((item) => ` +
+
+ #${item.rank} + ${escapeHtml(item.lane?.label || 'Ranked')} + ${item.metrics.score} +
+

${escapeHtml(item.title)}

+ ${item.description && item.description !== item.title ? `

${escapeHtml(item.description)}

` : ''} +

Why: ${escapeHtml(item.reason)}.

+

Concern: ${escapeHtml(item.concern)}

+ ${renderMetrics(item.metrics)} +
+ `).join('')} +
+ +
+
+ Next 48 hours +
    ${(brief.next48Hours || []).map((step) => `
  1. ${escapeHtml(step)}
  2. `).join('')}
+
+ ${(brief.expertReflections || []).map((reflection) => ` +
+ ${escapeHtml(reflection.lens)} +

${escapeHtml(reflection.text)}

+
+ `).join('')} +
+

${escapeHtml(brief.caution || 'First-pass judgement, not an oracle.')}

+ `; + results.scrollIntoView({ behavior: 'smooth', block: 'start' }); +} + +async function createFeedbackMap(event) { + event.preventDefault(); + const submit = form.querySelector('button[type="submit"]'); + const payload = Object.fromEntries(new FormData(form).entries()); + submit.disabled = true; + submit.textContent = 'Ranking…'; try { - await navigator.clipboard.writeText(aiReadyPrompt); - if(status) status.textContent = 'Copied. Paste it into any AI, then paste the returned JSON back here.'; - toast('AI-ready prompt copied'); - } catch { - const input = $('#featureSetInput'); - if(input){ - input.value = aiReadyPrompt; - input.focus(); - input.select(); - } - if(status) status.textContent = 'Clipboard blocked — the prompt is selected in the text area for manual copy.'; - toast('Clipboard blocked. Prompt selected for manual copy.'); + const response = await fetch('/api/rank-feedback', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + const data = await response.json(); + if (!response.ok) throw new Error(data.error || 'Could not rank this list.'); + renderResults(data); + } catch (error) { + toast(error.message); + } finally { + submit.disabled = false; + submit.textContent = 'Create ranked feedback map'; } } -const $ = (sel, root=document) => root.querySelector(sel); -const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel)); -const featureDeck = $('#featureDeck'); const sortingGrid = $('#sortingGrid'); const timeline = $('#timeline'); -const detail = $('#detail'); const detailForm = $('#detailForm'); const toastEl = $('#toast'); -function profile(){ return profiles[state.profileId] || profiles.mvp; } -function zones(){ return profile().zones; } -function escapeHtml(value){ return String(value ?? '').replace(/[&<>'"]/g, ch => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[ch])); } -function short(text, len=130){ const v=String(text||'').trim(); return v.length>len ? `${v.slice(0,len-1)}…` : v; } -function categoryOf(idea){ return (idea.labels && idea.labels[0]) || idea.sourceName || 'UI / UX'; } -function activeIdeas(){ return state.ideas.filter(i => !i.archived && (!i.status || i.status === 'inbox' || i.milestoneId === 'inbox')); } -function sortedIdeas(){ return state.ideas.filter(i => !i.archived && !activeIdeas().some(a => a.id === i.id)); } -function utilityIdeas(){ return state.ideas.filter(i => ['park','investigate'].includes(i.status)); } -function zoneFor(idea){ return zones().find(z=>z.status===idea.status) || profiles.mvp.zones.find(z=>z.status===idea.status) || zones()[0]; } -function isMobile(){ return window.matchMedia('(max-width: 680px)').matches; } -async function api(path, options={}){ const res = await fetch(path,{ headers:{'Content-Type':'application/json',...(options.headers||{})}, ...options, body: options.body && typeof options.body !== 'string' ? JSON.stringify(options.body) : options.body }); const text=await res.text(); const data=text?JSON.parse(text):null; if(!res.ok) throw new Error(data?.error||res.statusText); return data; } -function toast(message, action){ toastEl.innerHTML = `${escapeHtml(message)}${action ? '' : ''}`; toastEl.hidden = false; const btn = $('button', toastEl); if(btn) btn.addEventListener('click', action, { once:true }); clearTimeout(toastEl._timer); toastEl._timer = setTimeout(()=>{toastEl.hidden=true}, action ? 5200 : 2400); } -function currentActive(){ const queue = activeIdeas().sort((a,b)=>(a.rank-b.rank)||(new Date(a.createdAt)-new Date(b.createdAt))); if(!state.activeId || !queue.some(i=>i.id===state.activeId)) state.activeId = queue[0]?.id || null; return queue.find(i=>i.id===state.activeId) || null; } -function renderProgress(){ const total = state.ideas.filter(i=>!i.archived).length; const sorted = sortedIdeas().length; const active = activeIdeas().length; const pct = total ? Math.round((sorted/total)*100) : 0; $('#progressCard').innerHTML = `${sorted} / ${total}features sorted
`; const home=$('#mobileHomeProgress'); if(home) home.innerHTML = `${sorted} / ${total} features sorted
${active} active · ${utilityIdeas().length} parked/investigate`; } -function renderProfile(){ const p=profile(); $('.profile-card strong').textContent=p.name; $('#gridLabel').textContent=p.gridLabel; $$('#profileMenu [data-profile]').forEach(btn=>{ const active=btn.dataset.profile===state.profileId; btn.classList.toggle('selected', active); btn.textContent = profiles[btn.dataset.profile].name + (active ? ' ✓' : ''); }); } -function renderDeck(){ const idea = currentActive(); if(!idea){ featureDeck.innerHTML = `
No active feature
Add a feature above and it will become the next card in the decision deck.
`; return; } featureDeck.innerHTML = `
${escapeHtml(categoryOf(idea))}

${escapeHtml(idea.title)}

${escapeHtml(short(idea.description || 'No brief yet. Open details to add one.'))}

`; $('.feature-card').addEventListener('dragstart', e => e.dataTransfer.setData('text/plain', idea.id)); $('.open-details').addEventListener('click', () => openDetail(idea.id)); $('.feature-card').addEventListener('dblclick', () => openDetail(idea.id)); } -function renderGrid(){ sortingGrid.innerHTML = zones().map((z,idx) => { const placed = state.recentPlacement?.zoneId === z.id ? `
${escapeHtml(state.recentPlacement.label)}${escapeHtml(short(state.recentPlacement.title,44))}
` : ''; return ``; }).join(''); $$('.zone').forEach(el => { el.addEventListener('click', () => placeActive(el.dataset.zone)); el.addEventListener('dragover', e => { e.preventDefault(); el.classList.add('drag-over'); }); el.addEventListener('dragleave', () => el.classList.remove('drag-over')); el.addEventListener('drop', e => { e.preventDefault(); el.classList.remove('drag-over'); const id=e.dataTransfer.getData('text/plain') || state.activeId; placeFeature(id, el.dataset.zone); }); }); } -function renderTimeline(){ const items = sortedIdeas().filter(i => !['park','investigate','remove'].includes(i.status)).sort((a,b)=>(a.rank-b.rank)||(new Date(a.updatedAt)-new Date(b.updatedAt))); const headCopy=$('.timeline-head p'); if(headCopy) headCopy.textContent = isMobile() ? 'Sorted features become vertical roadmap cards. Tap a card to inspect details.' : 'Sorted features become nodes. Drag nodes horizontally to reorder the roadmap; click a node to inspect details.'; const milestones = [{label:'MVP',left:8,color:'#16A34A'},{label:'Beta',left:42,color:'#2563EB'},{label:'1.0',left:66,color:'#DC2626'},{label:'Stretch Goal',left:88,color:'#7C3AED'}]; if(isMobile()){ const grouped = zones().map(z => ({z, items: items.filter(i=>i.status===z.status)})); timeline.innerHTML = `
${grouped.map(group=>`

${escapeHtml(group.z.label)}

${group.items.length ? group.items.map((idea,idx)=>``).join('') : '
+ Add feature
'}
`).join('')}
Parked / Investigate / Removed · View all
`; $$('.roadmap-item', timeline).forEach(n => n.addEventListener('click', () => openDetail(n.dataset.id))); return; } const positions = items.map((idea, idx) => { const z = zoneFor(idea); const base = typeof idea.rank === 'number' && idea.rank > 0 ? Math.max(7, Math.min(94, idea.rank / 1000)) : z.left; return {idea,z,left: Math.min(96, base + (idx%3)*1.5)}; }); timeline.innerHTML = `
${milestones.map(m=>`${m.label}`).join('')}${positions.map((p,idx)=>``).join('')}${positions.slice(0,1).map(p=>`
${escapeHtml(short(p.idea.title,32))}${escapeHtml(categoryOf(p.idea))} · ${escapeHtml(p.z.label)}
`).join('')}
`; const line=$('#timelineLine'); line.addEventListener('dragover', e => { e.preventDefault(); line.classList.add('drag-over'); }); line.addEventListener('dragleave', () => line.classList.remove('drag-over')); line.addEventListener('drop', e => { e.preventDefault(); line.classList.remove('drag-over'); const id=e.dataTransfer.getData('text/plain'); if(id) reorderOnTimeline(id,e); }); $$('.node', timeline).forEach(n => { n.addEventListener('click', () => openDetail(n.dataset.id)); n.addEventListener('dragstart', e => e.dataTransfer.setData('text/plain', n.dataset.id)); }); } -function renderBacklog(){ const items = utilityIdeas(); $('#backlogList').innerHTML = items.length ? items.map(i=>``).join('') : '
Clean utility lanesParked and investigate items will appear here.
'; $$('.mini-card[data-id]').forEach(c => c.addEventListener('click',()=>openDetail(c.dataset.id))); } -function renderReview(){ const el=$('#reviewPanel'); if(!el) return; const total=state.ideas.filter(i=>!i.archived).length; const sorted=sortedIdeas().length; const remaining=activeIdeas().length; const complete=total>0 && remaining===0; const groups=zones().map(z=>`
${z.icon}${escapeHtml(z.label)}${state.ideas.filter(i=>!i.archived && i.status===z.status).length}
`).join(''); el.innerHTML = `
${complete?'✓':'↻'}

${complete?'Great job!':'Review roadmap'}

${complete?'All active features are sorted.':'Keep sorting or export the current roadmap setup.'}

${sorted}Sorted
${remaining}Active
${profile().name}Profile
${groups}
Continue sorting
`; $('#reviewExport')?.addEventListener('click',()=>$('#exportFeatureSet')?.click()); } -function render(){ renderProfile(); renderProgress(); renderDeck(); renderGrid(); renderTimeline(); renderBacklog(); renderReview(); } -function sortedSetupFeatures(){ - const ordered = state.ideas - .filter(i => !i.archived) - .slice() - .sort((a,b) => { - const aActive = activeIdeas().some(i => i.id === a.id) ? 0 : 1; - const bActive = activeIdeas().some(i => i.id === b.id) ? 0 : 1; - return (aActive - bActive) || ((a.rank || 0) - (b.rank || 0)) || ((b.score || 0) - (a.score || 0)) || String(a.title).localeCompare(String(b.title)); - }); - return ordered.map((idea, index) => { - const z = zoneFor(idea); - return { - title: idea.title, - description: idea.description || '', - labels: idea.labels || [], - impact: idea.impact ?? 5, - effort: idea.effort ?? 5, - confidence: idea.confidence ?? 5, - urgency: idea.urgency ?? 5, - score: idea.score ?? 0, - status: idea.status || 'inbox', - milestoneId: idea.milestoneId || 'inbox', - rank: idea.rank ?? 0, - notes: idea.notes || '', - sortedSetup: { - order: index + 1, - profile: state.profileId, - lane: activeIdeas().some(i => i.id === idea.id) ? 'Inbox / unsorted' : z.label, - milestoneId: idea.milestoneId || 'inbox', - status: idea.status || 'inbox' - } - }; - }); -} -function featureSetPayload(){ - return { - format: 'prioritix-feature-set-v1', - exportedAt: new Date().toISOString(), - sortingProfile: state.profileId, - scoring: '((impact×2.4)+(confidence×1.2)+(urgency×1.4))/effort', - features: sortedSetupFeatures() - }; -} -function parseFeatureSetText(text){ - let parsed; - try { parsed = JSON.parse(text); } catch { throw new Error('Paste valid Prioritix Feature Set v1 JSON. Required shape: { "format":"prioritix-feature-set-v1", "features":[{"title":"..."}] }.'); } - const features = Array.isArray(parsed) ? parsed : parsed.features; - if(!Array.isArray(features) || !features.length) throw new Error('Feature set must contain a non-empty features array.'); - const cleaned = features.map((feature, index) => { - const title = String(feature?.title || feature?.name || '').trim(); - if(!title) throw new Error(`Feature ${index + 1} is missing title.`); - return { - title, - description: feature.description || feature.brief || '', - labels: Array.isArray(feature.labels) ? feature.labels : String(feature.labels || feature.category || '').split(',').map(s=>s.trim()).filter(Boolean), - impact: feature.impact ?? 5, - effort: feature.effort ?? 5, - confidence: feature.confidence ?? 6, - urgency: feature.urgency ?? 5, - status: feature.status || 'inbox', - milestoneId: feature.milestoneId || feature.milestone || 'inbox', - rank: feature.rank ?? 0, - notes: feature.notes || '' - }; - }); - return { name: parsed.name || 'Pasted feature set', format: parsed.format || 'prioritix-feature-set-v1', features: cleaned }; -} -function downloadJson(filename, payload){ - const blob = new Blob([JSON.stringify(payload, null, 2) + '\n'], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); - URL.revokeObjectURL(url); -} -async function importFeatureSet(){ - const input = $('#featureSetInput'); - const payload = parseFeatureSetText(input.value.trim()); - const data = await api('/api/feature-set/import', { method:'POST', body: payload }); - (data.created || []).forEach(replaceIdea); - state.activeId = data.created?.[0]?.id || state.activeId; - render(); - toast(data.errors?.length ? `Imported ${data.imported}; ${data.errors.length} skipped` : `Imported ${data.imported} feature${data.imported === 1 ? '' : 's'}`); -} -function replaceIdea(idea){ const idx=state.ideas.findIndex(i=>i.id===idea.id); if(idx>=0) state.ideas[idx]=idea; else state.ideas.unshift(idea); } -async function placeActive(zoneId){ const idea = currentActive(); if(!idea) return toast('Capture a feature first'); return placeFeature(idea.id, zoneId); } -async function placeFeature(id, zoneId){ const z = zones().find(z=>z.id===zoneId); const idea = state.ideas.find(i=>i.id===id); if(!z || !idea) return; const previous = {...idea}; state.recentPlacement = { id, zoneId, title: idea.title, label: categoryOf(idea) }; Object.assign(idea,{status:z.status,milestoneId:z.milestoneId,rank:Math.round(z.left*1000)}); render(); clearTimeout(state.recentPlacementTimer); state.recentPlacementTimer=setTimeout(()=>{ if(state.recentPlacement?.id===id){ state.recentPlacement=null; renderGrid(); } }, 1800); try{ const updated = await api(`/api/ideas/${id}`, {method:'PATCH', body:{status:z.status,milestoneId:z.milestoneId,rank:idea.rank}}); replaceIdea(updated); state.activeId=null; render(); toast(`Placed in ${z.label}`, () => undoIdea(previous)); } catch(error){ state.recentPlacement=null; Object.assign(idea, previous); render(); toast(error.message); } } -async function undoIdea(previous){ if(!previous?.id) return; try{ const updated=await api(`/api/ideas/${previous.id}`,{method:'PATCH',body:{status:previous.status,milestoneId:previous.milestoneId,rank:previous.rank,archived:previous.archived}}); replaceIdea(updated); state.activeId=updated.status === 'inbox' ? updated.id : state.activeId; render(); toast('Undone'); }catch(error){ toast(error.message); } } -async function setUtility(action){ const idea=currentActive(); if(!idea) return toast('No active feature'); if(action==='remove') return archiveIdea(idea.id); const previous={...idea}; Object.assign(idea,{status:action,milestoneId:'later',rank:Date.now()%100000}); render(); try{ const updated=await api(`/api/ideas/${idea.id}`,{method:'PATCH',body:{status:action,milestoneId:'later',rank:idea.rank}}); replaceIdea(updated); state.activeId=null; render(); toast(action === 'park' ? 'Parked' : 'Sent to investigate', () => undoIdea(previous)); } catch(error){ Object.assign(idea, previous); render(); toast(error.message); } } -async function reorderOnTimeline(id,e){ const line=e.currentTarget; const idea=state.ideas.find(i=>i.id===id); if(!idea) return; const rect=line.getBoundingClientRect(); const pct=Math.max(6,Math.min(94,((e.clientX-rect.left)/rect.width)*100)); const z=zones().reduce((best,item)=>Math.abs(item.left-pct) undoIdea(previous)); }catch(error){ Object.assign(idea, previous); render(); toast(error.message); } } -function openDetail(id){ const idea=state.ideas.find(i=>i.id===id); if(!idea) return; state.selected=id; detailForm.title.value=idea.title||''; detailForm.description.value=idea.description||''; detailForm.labels.value=(idea.labels||[]).join(', '); detailForm.impact.value=idea.impact??5; detailForm.effort.value=idea.effort??5; detailForm.confidence.value=idea.confidence??5; detailForm.urgency.value=idea.urgency??5; detailForm.notes.value=idea.notes||''; $('#detailCategory').textContent=categoryOf(idea); $('.detail-head .chip').textContent=zoneFor(idea).label; detail.classList.add('open'); detail.setAttribute('aria-hidden','false'); } -function closeDetail(){ detail.classList.remove('open'); detail.setAttribute('aria-hidden','true'); state.selected=null; } -async function archiveIdea(id=state.selected){ if(!id) return; const previous=state.ideas.find(i=>i.id===id); try{ await api(`/api/ideas/${id}`,{method:'PATCH',body:{archived:true,status:'remove'}}); state.ideas = state.ideas.filter(i=>i.id!==id); closeDetail(); render(); toast('Removed', previous ? () => undoIdea({...previous, archived:false}) : null); } catch(error){ toast(error.message); } } -$('#sampleBacklog')?.addEventListener('click', loadSampleBacklog); -$('#copyAiPrompt')?.addEventListener('click', copyAiPrompt); -$('#featureSetFile')?.addEventListener('change', async e => { - const file = e.currentTarget.files?.[0]; - if(!file) return; - try { $('#featureSetInput').value = await file.text(); toast(`Loaded ${file.name}`); } - catch(error){ toast(error.message || 'Could not read file'); } -}); -$('#importFeatureSet')?.addEventListener('click', async () => { - try { await importFeatureSet(); } catch(error){ toast(error.message); } -}); -$('#exportFeatureSet')?.addEventListener('click', () => { - const payload = featureSetPayload(); - $('#featureSetInput').value = JSON.stringify(payload, null, 2); - downloadJson(`prioritix-feature-set-${new Date().toISOString().slice(0,10)}.json`, payload); - toast(`Exported ${payload.features.length} feature${payload.features.length === 1 ? '' : 's'}`); -}); -$('#ideaForm').addEventListener('submit', async e => { e.preventDefault(); const form=e.currentTarget; const fd=new FormData(form); const payload=Object.fromEntries(fd.entries()); payload.labels=String(payload.labels||'UI / UX').split(',').map(s=>s.trim()).filter(Boolean); payload.source='human'; payload.status='inbox'; try{ const idea=await api('/api/ideas',{method:'POST',body:payload}); replaceIdea(idea); state.activeId=idea.id; form.reset(); $('#title').value=''; $('#description').value=''; $('#labels').value=''; render(); toast('Captured'); }catch(error){ toast(error.message); } }); -$('#skipFeature').addEventListener('click',()=>{ const q=activeIdeas(); if(q.length<2) return toast('No next feature'); const idx=q.findIndex(i=>i.id===state.activeId); state.activeId=q[(idx+1)%q.length].id; renderDeck(); }); -$('#postponeFeature').addEventListener('click',async()=>{ const idea=currentActive(); if(!idea) return; idea.rank=Date.now()%100000; state.activeId=null; render(); try{ const updated=await api(`/api/ideas/${idea.id}`,{method:'PATCH',body:{rank:idea.rank}}); replaceIdea(updated); render(); toast('Postponed'); }catch(error){ toast(error.message); await load(); } }); -$$('[data-utility]').forEach(b=>b.addEventListener('click',()=>setUtility(b.dataset.utility))); -$('#closeDetail').addEventListener('click',closeDetail); $('#archiveIdea').addEventListener('click',()=>archiveIdea()); $('#parkDetail').addEventListener('click',async()=>{ if(!state.selected) return; const id=state.selected; const previous=state.ideas.find(i=>i.id===id); try{ const updated=await api(`/api/ideas/${id}`,{method:'PATCH',body:{status:'park',milestoneId:'later'}}); replaceIdea(updated); closeDetail(); render(); toast('Parked', previous ? () => undoIdea(previous) : null); }catch(error){toast(error.message)} }); -detailForm.addEventListener('submit', async e => { e.preventDefault(); if(!state.selected) return; const payload=Object.fromEntries(new FormData(detailForm).entries()); payload.labels=String(payload.labels||'').split(',').map(s=>s.trim()).filter(Boolean); try{ const idea=await api(`/api/ideas/${state.selected}`,{method:'PATCH',body:payload}); replaceIdea(idea); closeDetail(); render(); toast('Saved'); }catch(error){ toast(error.message); } }); -$('#profileToggle').addEventListener('click',()=>{ const m=$('#profileMenu'); m.hidden=!m.hidden; }); -$$('#profileMenu [data-profile]').forEach(btn=>btn.addEventListener('click',()=>{ state.profileId=btn.dataset.profile; localStorage.setItem('rank-profile',state.profileId); $('#profileMenu').hidden=true; render(); toast(`Profile: ${profile().name}`); })); -document.addEventListener('keydown', e => { const typing=['INPUT','TEXTAREA'].includes(document.activeElement.tagName); if(e.key==='/' && !typing){ e.preventDefault(); $('#title').focus(); return; } if(e.key==='Escape') closeDetail(); if(typing) return; if(['1','2','3','4'].includes(e.key)){ e.preventDefault(); placeActive(zones()[Number(e.key)-1]?.id); } if(e.key.toLowerCase()==='p') setUtility('park'); if(e.key.toLowerCase()==='i') setUtility('investigate'); if(e.key.toLowerCase()==='u' && toastEl.querySelector('button')) toastEl.querySelector('button').click(); }); -async function load(){ try{ const data=await api('/api/bootstrap'); state.ideas=data.ideas||[]; state.milestones=data.milestones||[]; state.activity=data.activity||[]; render(); } catch(error){ featureDeck.innerHTML=`
Backend is grumpy
${escapeHtml(error.message)}
`; renderGrid(); } } -load(); +form.addEventListener('submit', createFeedbackMap); +document.querySelector('#loadSample')?.addEventListener('click', fillSample); +document.querySelector('#loadSampleTop')?.addEventListener('click', fillSample); diff --git a/public/index.html b/public/index.html index 9fba48c..9ae20c8 100644 --- a/public/index.html +++ b/public/index.html @@ -3,224 +3,126 @@ - - Rank — Work your ideas before they work you - - + + + Ranker — ranked feedback maps for messy decisions + -
- -
-
+
- For scattered founders, product teams, and backlog-heavy brains -

Your backlog is not a roadmap.

-

Rank is a nice-looking tool that understands your chaos. Paste messy ideas and work them into a clear build order before they start working you.

+

Decision feedback for scatterminds, builders, and structured people

+

Dump your options. See what deserves attention first.

+

Ranker turns a messy list of ideas, features, offers, roadmap items, or next moves into a ranked feedback map: what to test, build, defer, or drop — and why.

- Paste my messy backlog - + Rank a messy list +
-
- Build now +
+ Do first Validate next Park safely - Explain why + Expert reflections
-
-
- Before -

Messy idea pile

-
    -
  • AI onboarding coach
  • -
  • Team voting
  • -
  • Dark mode
  • -
  • Stripe export
  • -
  • Better mobile sorting
  • -
+ +
+
+ Ranked Feedback Map + Fastest useful progress
- -
- After -

Defendable build order

-
    -
  1. Build nowMobile sorting removes the biggest usage friction.
  2. -
  3. Validate nextTeam voting needs proof before it becomes process theatre.
  4. -
  5. ParkDark mode is nice, not the reason anyone buys.
  6. -
+
    +
  1. Manual offer critiqueHigh signal, easy to test, clear buyer pain.
  2. +
  3. Pricing calculatorUseful, but generic unless paired with judgement.
  4. +
  5. Client dashboardPark it. Too much machinery before proof.
  6. +
+
+ Pattern noticed +

Your strongest wedge is judgement, not automation. Test whether people want their offer criticized before building a platform around it.

-
-
- -
-
No dashboard first.Proof before controls. You see the transformation before learning the system.
-
No fake AI mysticism.Every decision needs a plain reason, risk, and next evidence.
-
No surprise gate.Work the ideas first; export, save, and deeper workflows come after value is obvious.
-
- -
- -
-

Rank idea workbench

-

Project goal: turn rough feature ideas into a clear, defendable build order.

-
-
- Sorting profile - MVP (2x2) - - -
-
-
- -
-
9:41Rank
-
- Project overview -

Work your ideas

-

Paste the chaos. Sort what to build now, validate next, and park without guilt.

-
-
- -
- -
-
- - - - - - - - - -
-
- -
-
- -

Drop in the rough list. Rank turns it into a build order.

-

Start with a messy backlog, stakeholder requests, or five half-formed ideas. Rank helps you decide what to build now, what needs evidence, what can wait, and what is just noise wearing a nice hat. JSON import still works if you need it.

-
-
- -
- - - - -
-

Copies a prompt that makes another AI return import-ready Rank JSON.

-
-
- -
-
- -
-
- - -
-
- -
- -
-

Shortcuts: 1-4 place active card · P park · I investigate · U undo · / capture

-
-
- -
- - - -
- -
-
-
-

Timeline view

-

Sorted features become nodes. Drag nodes horizontally to reorder the roadmap; click a node to inspect details.

-
-
-
-
-
- -
- -
-
- -
-
-
-
-
- - +
+
Ranked order

Clear first-pass order with do / test / defer / park labels.

+
Reason per item

Every option gets a plain-language reason and concern.

+
Expert reflections

Product expert, scattermind simplifier, and structured operator lenses.

+
Next 48 hours

The output ends with a concrete move instead of more thinking fog.

+
+ + +
+

Positioning

+

Scattermind clarifies one idea. Ranker judges the possible moves.

+

That makes Ranker broader: scattered people get relief, structured people get a second opinion, and builders get a defensible build order before they waste time on the wrong piece.

+
+ - + diff --git a/public/styles.css b/public/styles.css index 60d12fd..39df459 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1,58 +1,22 @@ :root{ - color-scheme: light; - --navy-950:#061B33; --navy-900:#071E3A; --navy-800:#09264b; - --blue-600:#0B63F6; --blue-500:#2563EB; --green-600:#16A34A; - --amber-500:#F59E0B; --purple-600:#7C3AED; --red-600:#DC2626; - --slate-900:#101828; --slate-700:#344054; --slate-600:#667085; --slate-400:#98A2B3; - --page:#F6F8FB; --surface:#FFFFFF; --border:#D9E2EC; --muted:#E6ECF2; - --shadow:0 18px 50px rgba(16,24,40,.10); --shadow-soft:0 6px 18px rgba(16,24,40,.06); - --radius:6px; --font-ui:Inter,"SF Pro Display","SF Pro Text","Segoe UI",system-ui,sans-serif; - font-family:var(--font-ui); -} -*{box-sizing:border-box} html,body{margin:0;min-height:100%;background:var(--page);color:var(--slate-900)} -body{background:radial-gradient(circle at 20% 0%,#fff 0,#f4f7fc 36rem,transparent 37rem),linear-gradient(135deg,#F6F8FB 0%,#EEF3FA 100%)} -button,input,textarea{font:inherit} button{cursor:pointer} a{color:inherit;text-decoration:none} -.app-shell{display:grid;grid-template-columns:104px 1fr;min-height:100vh}.sidebar{position:sticky;top:0;height:100vh;background:linear-gradient(180deg,var(--navy-950),#031223);color:white;display:flex;flex-direction:column;align-items:center;padding:20px 8px;border-right:1px solid rgba(255,255,255,.08);box-shadow:16px 0 40px rgba(6,27,51,.12);z-index:10}.brand-mark{width:52px;height:52px;display:grid;place-items:center;margin-bottom:34px;border-radius:10px;background:linear-gradient(135deg,#45B8FF,#6D3BFF);font-weight:900;font-size:28px;letter-spacing:-.12em;box-shadow:0 14px 34px rgba(11,99,246,.32)} -.sidebar nav{display:grid;gap:10px;width:100%}.sidebar a{display:grid;place-items:center;gap:6px;padding:13px 6px;border-radius:6px;color:#D9E8FF;font-size:12px}.sidebar a span{font-size:25px;line-height:1}.sidebar a.active{background:linear-gradient(135deg,#0B63F6,#4E2AD8);color:#fff;box-shadow:0 10px 28px rgba(11,99,246,.30)}.collapse{margin-top:auto;background:transparent;border:0;color:white;font-size:28px;opacity:.9} -.workspace{padding:22px 32px 42px;max-width:1440px;width:100%;margin:0 auto}.topbar{display:grid;grid-template-columns:auto 1fr minmax(220px,270px) 190px;align-items:center;gap:22px;background:rgba(255,255,255,.72);backdrop-filter:blur(18px);border:1px solid var(--border);box-shadow:var(--shadow-soft);border-radius:var(--radius);padding:18px 22px;margin-bottom:18px}.menu-button{width:48px;height:48px;border:1px solid var(--border);border-radius:6px;background:var(--navy-950);color:white;font-size:24px}.project-title h1{font-size:26px;letter-spacing:-.04em;margin:0 0 4px}.project-title p{margin:0;color:var(--slate-700);font-size:14px}.profile-card,.progress-card{position:relative;background:white;border:1px solid var(--border);border-radius:var(--radius);padding:12px 16px;box-shadow:var(--shadow-soft)}.profile-card span,.progress-card span{display:block;color:var(--slate-600);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}.profile-card strong,.progress-card strong{font-size:16px}.profile-card>button{position:absolute;right:10px;top:22px;border:0;background:transparent;font-size:20px;color:var(--slate-900)}.profile-menu{position:absolute;top:64px;left:0;right:0;background:#fff;border:1px solid var(--border);border-radius:6px;box-shadow:var(--shadow);padding:8px;z-index:20}.profile-menu button{display:block;width:100%;text-align:left;border:0;background:white;padding:9px 10px;border-radius:4px}.profile-menu .selected{background:#F0E9FF;color:var(--purple-600);font-weight:800}.progressbar{height:5px;background:var(--muted);border-radius:999px;margin-top:10px;overflow:hidden}.progressbar i{display:block;height:100%;background:linear-gradient(90deg,var(--purple-600),var(--blue-600));border-radius:999px}.capture-strip{background:white;border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-soft);padding:12px;margin-bottom:18px}.capture-strip form{display:grid;grid-template-columns:1.4fr 1.4fr .8fr 132px;gap:10px;align-items:end}.capture-strip label,.detail label{display:grid;gap:6px;color:var(--slate-600);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}.capture-strip input,.detail input,.detail textarea{width:100%;border:1px solid var(--border);border-radius:4px;background:#FBFCFF;color:var(--slate-900);padding:12px 13px;outline:none}.capture-strip input:focus,.detail input:focus,.detail textarea:focus{border-color:var(--blue-600);box-shadow:0 0 0 3px rgba(11,99,246,.10)}.capture-strip button,.deck-actions button,.utility-row button,.detail-actions button,.zoom-controls button{border:1px solid var(--border);background:#fff;color:var(--slate-900);border-radius:4px;box-shadow:var(--shadow-soft);padding:12px 16px;font-weight:800}.capture-strip button{background:var(--navy-950);color:white;height:43px}.sorting-layout{display:grid;grid-template-columns:minmax(330px,470px) 1fr;gap:28px;align-items:start;margin-top:18px}.section-label{text-transform:uppercase;letter-spacing:.06em;color:var(--navy-900);font-size:13px;font-weight:900;margin-bottom:12px}.active-column{padding:10px 0}.feature-deck{position:relative;min-height:250px;display:grid;place-items:center}.feature-card{position:relative;width:min(390px,100%);min-height:185px;background:white;border:1px solid var(--border);border-radius:6px;box-shadow:var(--shadow);padding:22px 22px 18px;display:grid;gap:12px;z-index:3;transition:.18s ease}.feature-card::before,.feature-card::after{content:"";position:absolute;inset:14px -18px -14px 18px;background:white;border:1px solid var(--border);border-radius:6px;box-shadow:var(--shadow-soft);z-index:-1}.feature-card::before{border-right:6px solid var(--amber-500);transform:rotate(-1.4deg)}.feature-card::after{inset:28px -34px -28px 34px;border-right:6px solid var(--green-600);transform:rotate(1.5deg);z-index:-2}.feature-meta,.detail-head>div{display:flex;align-items:center;gap:9px;color:var(--slate-700);font-size:13px;font-weight:700}.category-dot{width:12px;height:12px;border-radius:50%;background:var(--purple-600);box-shadow:0 0 0 4px rgba(124,58,237,.10)}.feature-card h2{margin:0;font-size:22px;letter-spacing:-.03em}.feature-card p{margin:0;color:var(--slate-700);line-height:1.45}.open-details{justify-self:end;width:40px;height:40px;border:1px solid var(--border);border-radius:5px;background:#F8FAFF;color:var(--navy-900);font-size:20px}.empty-deck{background:white;border:1px dashed var(--border);border-radius:6px;box-shadow:var(--shadow-soft);padding:28px;text-align:center;color:var(--slate-600);max-width:390px}.deck-actions{display:grid;grid-template-columns:1fr 1fr;gap:12px;width:min(390px,100%);margin:4px auto 0}.sorting-grid{display:grid;grid-template-columns:repeat(2,minmax(220px,1fr));gap:14px}.zone{min-height:188px;border:1.5px dashed var(--zone-color);background:linear-gradient(135deg,rgba(255,255,255,.94),var(--zone-bg));border-radius:6px;display:grid;place-items:center;text-align:center;padding:22px;transition:.16s ease}.zone:hover,.zone.drag-over{transform:translateY(-2px);box-shadow:var(--shadow);border-style:solid}.zone-icon{font-size:42px;color:var(--zone-color);line-height:1}.zone strong{display:block;color:var(--zone-color);font-size:18px;margin:8px 0}.zone span{display:block;color:var(--slate-700);font-size:14px;line-height:1.35}.drop-hint{margin-top:18px;display:inline-grid;place-items:center;width:42px;height:42px;border:1px dashed var(--border);border-radius:6px;color:var(--slate-600);background:white}.utility-row{display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--border);border-radius:6px;background:white;box-shadow:var(--shadow-soft);overflow:hidden;margin:18px 0}.utility-row button{display:grid;grid-template-columns:42px 1fr;grid-template-rows:auto auto;text-align:left;border:0;border-right:1px solid var(--border);border-radius:0;box-shadow:none;background:white;padding:18px}.utility-row button:last-child{border-right:0}.utility-row span{grid-row:1/3;font-size:28px;color:var(--navy-900)}.utility-row strong{font-size:15px}.utility-row small{color:var(--slate-600);font-size:12px;line-height:1.35}.timeline-panel,.backlog-panel{background:white;border:1px solid var(--border);border-radius:6px;box-shadow:var(--shadow-soft);padding:18px;margin-top:18px}.timeline-head{display:flex;justify-content:space-between;gap:16px;align-items:start}.timeline-head h2{font-size:18px;margin:0 0 4px}.timeline-head p{margin:0;color:var(--slate-600);font-size:13px}.zoom-controls{display:flex;gap:6px}.zoom-controls button{padding:9px 13px}.timeline{position:relative;min-height:145px;padding:54px 26px 26px}.timeline-line{position:relative;height:2px;background:#AAB4C0}.milestone-label{position:absolute;top:-30px;transform:translateX(-50%);font-weight:800;font-size:13px;color:var(--milestone-color)}.milestone-tick{position:absolute;top:-17px;height:34px;border-left:3px solid var(--milestone-color)}.node{position:absolute;top:-13px;transform:translateX(-50%);width:28px;height:28px;border-radius:50%;border:3px solid var(--node-color);background:white;display:grid;place-items:center;font-size:11px;font-weight:900;color:var(--node-color);box-shadow:0 3px 10px rgba(16,24,40,.12)}.node-card{position:absolute;top:42px;transform:translateX(-50%);min-width:230px;max-width:300px;background:white;border:1px solid var(--border);border-radius:6px;box-shadow:var(--shadow-soft);padding:11px}.node-card strong{display:block;font-size:13px}.node-card span{color:var(--slate-600);font-size:12px}.backlog-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:10px}.mini-card{border:1px solid var(--border);border-radius:6px;background:#FBFCFF;padding:12px;display:grid;gap:7px}.mini-card strong{font-size:14px}.mini-card span{font-size:12px;color:var(--slate-600)}.chip{background:#EEE6FF;color:var(--purple-600);padding:6px 10px;border-radius:8px;font-size:12px;font-weight:900}.detail{position:fixed;right:20px;top:20px;bottom:20px;width:min(520px,calc(100vw - 40px));z-index:50;background:white;border:1px solid var(--border);border-radius:12px;box-shadow:0 28px 90px rgba(16,24,40,.20);padding:22px;transform:translateX(calc(100% + 40px));transition:.18s ease;overflow:auto}.detail.open{transform:translateX(0)}.detail form{display:grid;gap:14px}.detail-head{display:grid;grid-template-columns:1fr auto auto;gap:12px;align-items:center}.detail-head button{border:0;background:#F5F7FB;border-radius:6px;width:36px;height:36px;font-size:24px}.detail-title{font-size:22px!important;font-weight:850;letter-spacing:-.03em}.detail-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.detail-actions{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}.detail-actions .danger{border-color:#F8C9C9;color:var(--red-600);background:#FFF6F6}.toast{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);z-index:90;background:var(--navy-950);color:white;border-radius:6px;padding:12px 16px;box-shadow:var(--shadow);font-weight:800}.loading{color:var(--slate-600);padding:20px;text-align:center} -@media(max-width:1050px){.app-shell{grid-template-columns:1fr}.sidebar{display:none}.workspace{padding:14px}.topbar{grid-template-columns:auto 1fr}.profile-card,.progress-card{grid-column:1/-1}.sorting-layout{grid-template-columns:1fr}.capture-strip form{grid-template-columns:1fr 1fr}.capture-strip button{grid-column:1/-1}.utility-row{grid-template-columns:1fr}.utility-row button{border-right:0;border-bottom:1px solid var(--border)}.utility-row button:last-child{border-bottom:0}} -.shortcut-hint{margin:10px 2px 0;color:var(--slate-600);font-size:12px}.shortcut-hint kbd{display:inline-grid;place-items:center;min-width:22px;height:20px;padding:0 5px;border:1px solid var(--border);border-bottom-width:2px;border-radius:4px;background:#fff;color:var(--navy-950);font-weight:800}.timeline-line.drag-over{outline:2px dashed var(--blue-600);outline-offset:8px;background:linear-gradient(90deg,rgba(22,163,74,.06),rgba(37,99,235,.06),rgba(245,158,11,.06),rgba(124,58,237,.06))}.node[draggable="true"]{cursor:grab}.node[draggable="true"]:active{cursor:grabbing}.toast{display:flex;align-items:center;gap:12px}.toast button{border:1px solid rgba(255,255,255,.35);background:rgba(255,255,255,.12);color:#fff;border-radius:4px;padding:6px 9px;font-weight:800}.detail label input[name="labels"]{width:100%;border:1px solid var(--border);border-radius:6px;padding:11px 12px;background:#F8FAFC;color:var(--slate-900);margin-top:6px}.profile-menu button.selected{background:#F0E9FF;color:var(--purple-600);font-weight:900} -@media(max-width:680px){.workspace{padding:10px}.topbar{padding:12px;gap:12px}.project-title h1{font-size:21px}.sorting-grid{grid-template-columns:1fr 1fr;gap:9px}.zone{min-height:142px;padding:12px}.zone-icon{font-size:30px}.zone strong{font-size:14px}.zone span{font-size:12px}.capture-strip form,.deck-actions,.detail-grid,.detail-actions{grid-template-columns:1fr}.timeline{overflow-x:auto}.timeline-line{min-width:760px}.feature-card{min-height:160px}.feature-card::before,.feature-card::after{display:none}} - -.feature-set-panel{display:grid;grid-template-columns:minmax(280px,.9fr) minmax(420px,1.4fr);gap:16px;background:linear-gradient(135deg,rgba(6,27,51,.96),rgba(9,38,75,.92));color:white;border:1px solid rgba(255,255,255,.12);border-radius:var(--radius);box-shadow:var(--shadow);padding:16px;margin-bottom:18px;overflow:hidden;position:relative}.feature-set-panel::before{content:"";position:absolute;inset:auto -80px -120px auto;width:260px;height:260px;background:radial-gradient(circle,rgba(69,184,255,.25),transparent 65%);pointer-events:none}.feature-set-copy{position:relative;z-index:1}.feature-set-copy .section-label{color:#A7D8FF}.feature-set-copy h2{margin:7px 0 8px;font-size:22px;letter-spacing:-.03em}.feature-set-copy p{margin:0;color:#D9E8FF;line-height:1.5;font-size:13px}.feature-set-copy code{background:rgba(255,255,255,.10);border:1px solid rgba(255,255,255,.10);border-radius:4px;padding:1px 5px;color:#fff}.feature-set-actions{position:relative;z-index:1;display:grid;gap:10px}.feature-set-actions textarea{width:100%;min-height:178px;resize:vertical;border:1px solid rgba(255,255,255,.16);border-radius:6px;background:rgba(2,8,23,.64);color:#EAF6FF;padding:12px;font-family:"JetBrains Mono","SFMono-Regular",ui-monospace,monospace;font-size:12px;line-height:1.45;outline:none}.feature-set-actions textarea:focus{border-color:#45B8FF;box-shadow:0 0 0 3px rgba(69,184,255,.16)}.feature-set-toolbar{display:flex;gap:10px;flex-wrap:wrap;align-items:center}.feature-set-toolbar button,.file-pick{border:1px solid rgba(255,255,255,.20);border-radius:6px;background:rgba(255,255,255,.10);color:white;padding:10px 12px;font-weight:850;box-shadow:0 10px 24px rgba(0,0,0,.12)}.feature-set-toolbar button:hover,.file-pick:hover{background:rgba(255,255,255,.16)}.feature-set-toolbar .ai-prompt-button{background:linear-gradient(135deg,#0B63F6,#7C3AED);border-color:rgba(255,255,255,.24)}.ai-prompt-status{margin:-2px 0 0;color:#BFD7F2;font-size:12px;line-height:1.4}.file-pick input{position:absolute;inline-size:1px;block-size:1px;opacity:0;pointer-events:none}@media(max-width:1050px){.feature-set-panel{grid-template-columns:1fr}.feature-set-actions textarea{min-height:220px}} - - -.mobile-home{display:none}.review-panel{background:white;border:1px solid var(--border);border-radius:6px;box-shadow:var(--shadow-soft);padding:18px;margin-top:18px}.placed-card{margin:10px auto 0;padding:9px 10px;border:1px solid var(--border);border-radius:12px;background:white;box-shadow:var(--shadow-soft);text-align:left;max-width:170px}.placed-card small{display:block!important;color:var(--slate-600)!important;font-size:10px!important;text-transform:uppercase;letter-spacing:.04em}.placed-card strong{display:block!important;margin:2px 0 0!important;color:var(--slate-900)!important;font-size:12px!important}.review-hero{text-align:center;padding:10px 0 16px}.success-mark{width:76px;height:76px;margin:0 auto 12px;border-radius:50%;display:grid;place-items:center;background:linear-gradient(135deg,#16A34A,#22C55E);color:white;font-size:44px;font-weight:900;box-shadow:0 18px 40px rgba(22,163,74,.22)}.review-hero h2{margin:0 0 4px;font-size:24px}.review-hero p{margin:0;color:var(--slate-600)}.review-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.review-stats div,.review-row{border:1px solid var(--border);border-radius:8px;background:#FBFCFF;padding:12px}.review-stats strong{display:block;color:var(--navy-950)}.review-stats span{color:var(--slate-600);font-size:12px}.review-groups{display:grid;gap:8px;margin:12px 0}.review-row{display:grid;grid-template-columns:34px 1fr auto;align-items:center}.review-row span{color:var(--node-color);font-size:22px}.review-actions{display:grid;grid-template-columns:1fr 1fr;gap:10px}.review-actions a,.review-actions button{display:grid;place-items:center;border:1px solid var(--border);border-radius:6px;background:white;color:var(--navy-950);padding:12px;font-weight:900}.review-actions button{background:linear-gradient(135deg,var(--blue-600),var(--purple-600));color:white;border:0}.roadmap-list{display:grid;gap:13px}.roadmap-phase{position:relative;padding-left:18px;border-left:3px solid var(--node-color)}.roadmap-phase h3{margin:0 0 8px;color:var(--node-color);font-size:13px}.roadmap-item{width:100%;display:grid;grid-template-columns:34px 1fr auto;gap:10px;align-items:center;text-align:left;border:1px solid var(--border);border-radius:16px;background:white;padding:12px;box-shadow:var(--shadow-soft)}.roadmap-dot{width:28px;height:28px;border-radius:50%;display:grid;place-items:center;border:3px solid var(--node-color);color:var(--node-color);font-weight:900}.roadmap-item strong{display:block;color:var(--slate-900)}.roadmap-item small{display:block;color:var(--slate-600);margin-top:2px}.roadmap-empty{border:1px dashed var(--border);border-radius:14px;color:var(--slate-400);padding:14px;text-align:center;background:#FBFCFF}.roadmap-utilities{border:1px solid var(--border);border-radius:14px;padding:12px;color:var(--slate-600);font-size:12px}.roadmap-utilities a{color:var(--blue-600);font-weight:900} -.toast[hidden]{display:none!important} - -@media(max-width:680px){ - :root{--radius:20px;color-scheme:light}.app-shell{display:block}.workspace{padding:12px 12px calc(104px + env(safe-area-inset-bottom));max-width:430px}.topbar{display:none}.mobile-home{display:block;padding:0 0 12px}.mobile-statusbar{height:34px;display:flex;justify-content:space-between;align-items:center;color:var(--navy-950);font-weight:900;padding:0 6px}.mobile-project-card{background:linear-gradient(180deg,var(--navy-950),#082856);color:white;border-radius:0 0 28px 28px;padding:22px 18px 24px;margin:0 -12px 12px;box-shadow:0 18px 45px rgba(6,27,51,.18)}.eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#A7D8FF;font-weight:900}.mobile-project-card h2{font-size:26px;letter-spacing:-.04em;margin:8px 0}.mobile-project-card p{margin:0;color:#D9E8FF;line-height:1.45}.mobile-home-progress{margin-top:18px}.mobile-home-progress strong{display:block}.mobile-home-progress span{display:block;color:#C6D8EF;font-size:12px;margin-top:7px}.mobile-flow-cards{display:grid;gap:10px}.mobile-flow-cards a{display:grid;grid-template-columns:42px 1fr;gap:10px;align-items:center;background:white;border:1px solid var(--border);border-radius:18px;padding:14px;box-shadow:var(--shadow-soft)}.mobile-flow-cards span{width:34px;height:34px;display:grid;place-items:center;border-radius:11px;background:#EEF4FF;color:var(--blue-600);font-size:18px;grid-row:1/3}.mobile-flow-cards strong{display:block;color:var(--navy-950);grid-column:2}.mobile-flow-cards small{display:block;color:var(--slate-600);margin-top:3px;grid-column:2}.sidebar{display:block!important;position:fixed;left:0;right:0;top:auto;bottom:0;height:auto;min-height:74px;padding:7px 10px calc(8px + env(safe-area-inset-bottom));background:rgba(255,255,255,.96);backdrop-filter:blur(18px);color:var(--slate-700);border-top:1px solid var(--border);border-right:0;box-shadow:0 -16px 40px rgba(16,24,40,.12);z-index:80}.brand-mark,.collapse{display:none}.sidebar nav{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}.sidebar a{color:var(--slate-600);padding:7px 4px;border-radius:14px;font-size:10px}.sidebar a span{font-size:19px}.sidebar a.active,.sidebar a:target{background:#EEF4FF;color:var(--blue-600);box-shadow:none}.capture-strip{border-radius:22px;padding:14px;margin:12px 0}.capture-strip form{grid-template-columns:1fr!important}.capture-strip button{border-radius:14px;background:linear-gradient(135deg,var(--blue-600),var(--purple-600));height:48px}.feature-set-panel{border-radius:22px;padding:14px;max-height:94px;overflow:hidden;transition:max-height .2s ease}.feature-set-panel:focus-within,.feature-set-panel:hover{max-height:760px}.feature-set-copy h2{font-size:17px}.feature-set-copy p{display:none}.sorting-layout{display:grid;grid-template-columns:1fr!important;gap:14px;margin-top:14px}.active-column,.grid-column{background:white;border:1px solid var(--border);border-radius:22px;padding:15px;box-shadow:var(--shadow-soft)}.section-label{font-size:11px;color:var(--slate-600)}.feature-deck{min-height:174px}.feature-card{width:100%;min-height:162px;border-radius:22px;box-shadow:0 16px 36px rgba(16,24,40,.12)}.feature-card h2{font-size:20px}.deck-actions{width:100%;grid-template-columns:1fr 1fr}.deck-actions button{border-radius:14px}.sorting-grid{grid-template-columns:1fr 1fr!important;gap:10px}.zone{min-height:132px;border-radius:20px;padding:12px}.zone-icon{font-size:28px}.zone strong{font-size:13px}.zone span{display:none}.drop-hint{width:30px;height:30px;margin-top:9px;border-radius:10px}.shortcut-hint{display:none}.utility-row{position:static;bottom:auto;z-index:auto;margin:12px 0;border-radius:22px;grid-template-columns:repeat(3,1fr)!important;box-shadow:0 18px 48px rgba(16,24,40,.18)}.utility-row button{display:grid;grid-template-columns:1fr;text-align:center;padding:12px 8px;border-bottom:0!important;border-right:1px solid var(--border);gap:4px}.utility-row button:last-child{border-right:0}.utility-row span{grid-row:auto;font-size:24px}.utility-row small{display:none}.timeline-panel,.backlog-panel,.review-panel{border-radius:22px;padding:16px}.timeline-head{display:block}.timeline-head p{font-size:12px}.zoom-controls{display:none}.timeline{padding:14px 2px 4px;min-height:auto;overflow:visible}.detail{inset:0;width:100vw;height:100dvh;right:auto;top:auto;bottom:auto;border-radius:0;border:0;padding:18px 16px calc(104px + env(safe-area-inset-bottom));transform:translateX(100%)}.detail.open{transform:translateX(0)}.detail-head{position:sticky;top:-18px;background:white;z-index:2;padding:12px 0 10px}.detail-title{font-size:24px!important}.detail-actions{grid-template-columns:1fr 1fr 1fr}.review-stats,.review-actions{grid-template-columns:1fr}.toast{left:12px;right:12px;bottom:92px;transform:none;justify-content:space-between;border-radius:16px}.placed-card{max-width:132px;padding:7px}.placed-card strong{font-size:11px!important} -} - - -/* Sales-first Rank layer: proof before controls, pretty before cockpit. */ -.sales-hero{ - position:relative;display:grid;grid-template-columns:minmax(320px,0.95fr) minmax(420px,1.05fr);gap:22px;align-items:stretch; - margin:0 0 18px;padding:24px;border:1px solid rgba(11,99,246,.16);border-radius:28px; - background: - radial-gradient(circle at 8% 8%,rgba(69,184,255,.26),transparent 30rem), - radial-gradient(circle at 92% 12%,rgba(124,58,237,.18),transparent 28rem), - linear-gradient(135deg,rgba(255,255,255,.94),rgba(246,248,251,.82)); - box-shadow:0 24px 70px rgba(6,27,51,.12);overflow:hidden; -} -.sales-hero::after{content:"";position:absolute;inset:auto -80px -120px auto;width:320px;height:320px;background:radial-gradient(circle,rgba(11,99,246,.18),transparent 68%);pointer-events:none}.hero-copy,.proof-card{position:relative;z-index:1}.hero-copy{display:flex;flex-direction:column;justify-content:center;padding:8px}.sales-hero .eyebrow{display:inline-flex;width:max-content;max-width:100%;padding:7px 10px;border:1px solid rgba(11,99,246,.18);border-radius:999px;background:rgba(255,255,255,.70);color:#0B63F6;font-size:11px;font-weight:950;text-transform:uppercase;letter-spacing:.075em}.sales-hero h1{margin:16px 0 10px;font-size:clamp(38px,5.4vw,72px);line-height:.92;letter-spacing:-.07em;color:var(--navy-950)}.hero-lede{margin:0;max-width:720px;color:#344054;font-size:clamp(18px,2vw,23px);line-height:1.32}.hero-actions{display:flex;gap:12px;flex-wrap:wrap;margin:24px 0 18px}.primary-cta,.secondary-cta{display:inline-flex;align-items:center;justify-content:center;min-height:48px;padding:0 18px;border-radius:999px;font-weight:950;box-shadow:0 12px 28px rgba(11,99,246,.18)}.primary-cta{background:linear-gradient(135deg,#0B63F6,#7C3AED);color:white}.secondary-cta{border:1px solid rgba(6,27,51,.14);background:white;color:var(--navy-950)}.trust-row{display:flex;gap:8px;flex-wrap:wrap}.trust-row span{padding:8px 10px;border:1px solid rgba(6,27,51,.10);border-radius:999px;background:rgba(255,255,255,.64);color:#344054;font-size:12px;font-weight:850}.proof-card{display:grid;grid-template-columns:1fr auto 1.08fr;gap:12px;align-items:center}.proof-column{min-height:100%;border:1px solid rgba(6,27,51,.10);border-radius:22px;background:rgba(255,255,255,.82);box-shadow:0 16px 42px rgba(16,24,40,.08);padding:18px}.proof-column span{display:inline-flex;margin-bottom:8px;color:#667085;font-size:11px;text-transform:uppercase;letter-spacing:.08em;font-weight:950}.proof-column h2{margin:0 0 12px;font-size:22px;letter-spacing:-.04em;color:var(--navy-950)}.proof-column ul,.proof-column ol{margin:0;padding:0;list-style:none;display:grid;gap:9px}.proof-column li{border:1px solid rgba(6,27,51,.08);border-radius:14px;background:#fff;padding:10px 11px;color:#344054}.proof-column.clear li{display:grid;gap:3px}.proof-column.clear b{color:#061B33}.proof-column.clear small{color:#667085;line-height:1.35}.proof-arrow{width:42px;height:42px;display:grid;place-items:center;border-radius:999px;background:var(--navy-950);color:white;font-weight:950;font-size:22px;box-shadow:0 14px 28px rgba(6,27,51,.24)}.approach-strip{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:0 0 18px}.approach-strip article{border:1px solid rgba(6,27,51,.10);border-radius:20px;background:rgba(255,255,255,.76);box-shadow:var(--shadow-soft);padding:16px}.approach-strip strong{display:block;color:var(--navy-950);font-size:15px}.approach-strip span{display:block;margin-top:5px;color:#667085;line-height:1.45;font-size:13px}.feature-set-panel{background:linear-gradient(135deg,#061B33,#0b2a53 54%,#123f75)!important;border-radius:24px!important}.feature-set-copy h2{font-size:clamp(24px,2.6vw,36px)!important;line-height:1.02}.feature-set-copy p{font-size:15px!important}.capture-title{color:var(--navy-950)} -@media(max-width:1050px){.sales-hero{grid-template-columns:1fr}.proof-card{grid-template-columns:1fr}.proof-arrow{margin:auto;transform:rotate(90deg)}.approach-strip{grid-template-columns:1fr}} -@media(max-width:680px){.sales-hero{padding:18px 14px;border-radius:26px;margin-bottom:12px}.sales-hero .eyebrow{width:auto;font-size:10px}.sales-hero h1{font-size:42px}.hero-lede{font-size:17px}.hero-actions{display:grid}.primary-cta,.secondary-cta{width:100%}.proof-column{padding:14px}.proof-column.messy li:nth-child(n+4){display:none}.approach-strip{display:none}.feature-set-panel{border-radius:22px!important}.feature-set-copy h2{font-size:25px!important}} - - -/* Mobile overflow fix for the sales hero. */ -@media(max-width:680px){ - html,body{overflow-x:hidden;width:100%} - .workspace{width:100%;max-width:none;overflow-x:hidden} - .sales-hero,.hero-copy,.proof-card,.proof-column{min-width:0;max-width:100%} - .sales-hero{width:100%;margin-left:0;margin-right:0;box-sizing:border-box;overflow:hidden} - .sales-hero .eyebrow{display:block;width:100%;line-height:1.25;white-space:normal} - .sales-hero h1{font-size:38px;line-height:.98;overflow-wrap:anywhere} - .hero-lede{overflow-wrap:anywhere} - .primary-cta,.secondary-cta{white-space:normal;text-align:center;box-sizing:border-box} + color-scheme:dark; + --bg:#0b1020; + --bg2:#11182b; + --card:#f7f3ea; + --paper:#fffaf0; + --ink:#141824; + --muted:#6d7280; + --line:rgba(20,24,36,.14); + --blue:#5c8dff; + --violet:#9a6cff; + --green:#46c981; + --amber:#e8a13a; + --red:#e86d63; + --shadow:0 28px 80px rgba(0,0,0,.28); + --radius:28px; + font-family:Inter,"SF Pro Display","Segoe UI",system-ui,sans-serif; } +*{box-sizing:border-box} html{scroll-behavior:smooth} body{margin:0;min-height:100vh;background: + radial-gradient(circle at 12% -10%,rgba(92,141,255,.42),transparent 34rem), + radial-gradient(circle at 88% 0%,rgba(154,108,255,.30),transparent 32rem), + linear-gradient(135deg,var(--bg),#070a12 70%);color:#f9fafb} button,input,textarea{font:inherit} button{cursor:pointer} a{color:inherit;text-decoration:none}.page-shell{width:min(1180px,calc(100% - 32px));margin:0 auto;padding:18px 0 72px}.topline{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:36px}.brand{display:inline-flex;align-items:center;gap:10px;font-weight:950;letter-spacing:-.04em}.brand span{width:38px;height:38px;display:grid;place-items:center;border-radius:13px;background:linear-gradient(135deg,var(--blue),var(--violet));box-shadow:0 14px 36px rgba(92,141,255,.28)}.nav-links{display:flex;gap:8px;flex-wrap:wrap}.nav-links a{padding:9px 12px;border:1px solid rgba(255,255,255,.12);border-radius:999px;color:#dce5ff;background:rgba(255,255,255,.04)}.hero{padding:12px 0 28px}.hero-grid{display:grid;grid-template-columns:minmax(0,1.04fr) minmax(360px,.96fr);gap:22px;align-items:stretch}.hero-copy,.artifact-card,.decision-tool,.results,.proof-section,.why-section{border:1px solid rgba(255,255,255,.13);box-shadow:var(--shadow)}.hero-copy{padding:clamp(28px,5vw,58px);border-radius:36px;background:linear-gradient(145deg,rgba(255,255,255,.12),rgba(255,255,255,.04));backdrop-filter:blur(18px)}.eyebrow{margin:0 0 12px;color:#a9bbff;text-transform:uppercase;letter-spacing:.11em;font-size:12px;font-weight:950}.hero h1{max-width:780px;margin:0;color:#fff;font-size:clamp(44px,7vw,86px);line-height:.9;letter-spacing:-.08em}.lede{max-width:740px;margin:20px 0 0;color:#dce3f4;font-size:clamp(18px,2vw,23px);line-height:1.38}.hero-actions,.form-actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:26px}.button{display:inline-flex;align-items:center;justify-content:center;min-height:50px;padding:0 18px;border-radius:999px;border:1px solid transparent;font-weight:950}.button.primary{background:linear-gradient(135deg,var(--blue),var(--violet));color:#fff;box-shadow:0 16px 40px rgba(92,141,255,.25)}.button.ghost{background:rgba(255,255,255,.08);border-color:rgba(255,255,255,.16);color:#fff}.button:disabled{opacity:.65;cursor:wait}.promise-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:22px}.promise-row span{padding:8px 10px;border-radius:999px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.12);color:#e7edff;font-size:13px;font-weight:800}.artifact-card{display:flex;flex-direction:column;gap:16px;border-radius:36px;padding:22px;background:linear-gradient(180deg,var(--paper),#eee7d8);color:var(--ink)}.artifact-head{display:flex;justify-content:space-between;gap:14px;align-items:center}.artifact-head span,.brief-card span{color:var(--muted);text-transform:uppercase;letter-spacing:.09em;font-size:11px;font-weight:950}.artifact-head b{padding:7px 10px;border-radius:999px;background:#e8eefc;color:#244280;font-size:12px}.mini-ranking{display:grid;gap:10px;margin:0;padding:0;list-style:none;counter-reset:item}.mini-ranking li{counter-increment:item;display:grid;grid-template-columns:34px 1fr;gap:10px;padding:14px;border:1px solid var(--line);border-radius:18px;background:rgba(255,255,255,.62)}.mini-ranking li::before{content:counter(item);display:grid;place-items:center;width:30px;height:30px;border-radius:10px;background:#141824;color:#fff;font-weight:950}.mini-ranking strong{display:block}.mini-ranking small{display:block;margin-top:4px;color:var(--muted);line-height:1.35}.mini-brief{margin-top:auto;padding:16px;border-radius:20px;background:#141824;color:#fff}.mini-brief p{margin:6px 0 0;color:#d8dfeb;line-height:1.45}.decision-tool{display:grid;grid-template-columns:minmax(260px,.72fr) minmax(0,1.28fr);gap:18px;margin-top:22px;padding:22px;border-radius:36px;background:rgba(255,255,255,.08);backdrop-filter:blur(18px)}.tool-intro{padding:10px}.tool-intro h2,.proof-section h2,.why-section h2,.results-head h2{margin:0;color:#fff;font-size:clamp(32px,4vw,54px);line-height:.98;letter-spacing:-.06em}.tool-intro p:not(.eyebrow),.proof-section p,.why-section p,.results-head p{color:#dce3f4;line-height:1.55}.rank-form{display:grid;gap:14px;padding:16px;border-radius:26px;background:var(--paper);color:var(--ink)}label span,.mode-picker legend{display:flex;justify-content:space-between;margin:0 0 7px;color:#303644;font-weight:950;font-size:13px}label b,label em{font-size:11px;color:var(--muted);font-style:normal;text-transform:uppercase;letter-spacing:.07em}textarea,input{width:100%;border:1px solid var(--line);border-radius:18px;background:#fff;color:var(--ink);padding:13px 14px;outline:none;resize:vertical}textarea:focus,input:focus{border-color:rgba(92,141,255,.72);box-shadow:0 0 0 4px rgba(92,141,255,.14)}.mode-picker{border:0;padding:0;margin:0;display:grid;grid-template-columns:repeat(5,1fr);gap:8px}.mode-picker label{position:relative;margin:0}.mode-picker input{position:absolute;inset:0;opacity:0}.mode-picker span{display:grid;place-items:center;min-height:58px;margin:0;padding:10px;border:1px solid var(--line);border-radius:16px;background:#fff;text-align:center;font-size:12px;line-height:1.2}.mode-picker input:checked+span{background:#141824;color:#fff;border-color:#141824}.rank-form .button.ghost{color:var(--ink);border-color:var(--line);background:#fff}.results{margin-top:22px;padding:22px;border-radius:36px;background:rgba(255,255,255,.08);backdrop-filter:blur(18px)}.results-head{margin-bottom:16px}.ranked-list{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}.rank-card{padding:16px;border-radius:24px;background:var(--paper);color:var(--ink);border:1px solid var(--line)}.rank-topline{display:flex;align-items:center;gap:8px}.rank-topline strong{margin-left:auto;color:#fff;background:#141824;border-radius:999px;padding:6px 9px}.rank-number{font-weight:950}.lane-pill{padding:6px 9px;border-radius:999px;font-size:12px;font-weight:950}.lane-do .lane-pill{background:rgba(70,201,129,.18);color:#116b3b}.lane-test .lane-pill{background:rgba(92,141,255,.18);color:#244280}.lane-defer .lane-pill{background:rgba(232,161,58,.22);color:#7a4b08}.lane-park .lane-pill{background:rgba(232,109,99,.18);color:#8a2e27}.rank-card h3{margin:14px 0 8px;font-size:22px;line-height:1.05;letter-spacing:-.04em}.rank-card p{color:#3f4654;line-height:1.45}.item-description{color:#626b7a!important}.metrics{display:grid;gap:8px;margin-top:14px}.metric{display:grid;grid-template-columns:78px 1fr;gap:8px;align-items:center}.metric span{font-size:11px;color:var(--muted);font-weight:900}.metric i{height:7px;border-radius:999px;background:#e6e1d7;overflow:hidden}.metric b{display:block;height:100%;border-radius:999px;background:linear-gradient(90deg,var(--blue),var(--violet))}.brief-grid{display:grid;grid-template-columns:1.2fr repeat(3,1fr);gap:12px;margin-top:14px}.brief-card{padding:16px;border-radius:22px;background:#141824;border:1px solid rgba(255,255,255,.12)}.brief-card p,.brief-card li{color:#dce3f4;line-height:1.5}.brief-card ol{padding-left:20px;margin:10px 0 0}.next-card{background:linear-gradient(135deg,#162548,#271c4d)}.caution{margin:14px 0 0;color:#aab5cb}.proof-section,.why-section{margin-top:22px;border-radius:36px;padding:28px;background:linear-gradient(145deg,rgba(255,255,255,.10),rgba(255,255,255,.04))}.proof-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-top:18px}.proof-grid article{padding:17px;border-radius:22px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.11)}.proof-grid strong{display:block;color:#fff}.why-section{background:linear-gradient(135deg,var(--paper),#e9e2d5);color:var(--ink)}.why-section h2{color:var(--ink)}.why-section p{max-width:860px;color:#3f4654;font-size:18px}.toast{position:fixed;left:50%;bottom:20px;transform:translateX(-50%);z-index:50;padding:12px 14px;border-radius:999px;background:#fff;color:#141824;box-shadow:var(--shadow);font-weight:850}.toast[hidden]{display:none!important}@media(max-width:980px){.hero-grid,.decision-tool{grid-template-columns:1fr}.ranked-list{grid-template-columns:1fr 1fr}.brief-grid,.proof-grid{grid-template-columns:1fr 1fr}.mode-picker{grid-template-columns:1fr 1fr}}@media(max-width:640px){.page-shell{width:min(100% - 20px,460px);padding-top:10px}.topline{align-items:flex-start}.nav-links{justify-content:flex-end}.nav-links a{font-size:13px}.hero-copy,.artifact-card,.decision-tool,.results,.proof-section,.why-section{border-radius:26px}.hero h1{font-size:44px}.lede{font-size:17px}.hero-actions,.form-actions{display:grid}.button{width:100%}.decision-tool,.results{padding:14px}.rank-form{padding:12px}.mode-picker,.ranked-list,.brief-grid,.proof-grid{grid-template-columns:1fr}.artifact-head{align-items:flex-start;flex-direction:column}.tool-intro h2,.proof-section h2,.why-section h2,.results-head h2{font-size:34px}.toast{width:calc(100% - 32px);text-align:center;border-radius:18px}} \ No newline at end of file diff --git a/server.js b/server.js index 6cbb2c9..f65d4f5 100644 --- a/server.js +++ b/server.js @@ -302,6 +302,167 @@ app.post('/api/reorder', requireAgent, async (req, res) => { res.json({ changed }); }); +const judgementModes = { + progress: { + label: 'Fastest useful progress', + weights: { value: 1.2, feasibility: 1.55, confidence: 1.25, urgency: 1.15, revenue: 0.55, novelty: 0.35, risk: -1.05 }, + next: 'Test the highest-ranked low-effort option manually before adding product machinery.', + }, + mvp: { + label: 'Best MVP order', + weights: { value: 1.55, feasibility: 1.25, confidence: 1.15, urgency: 0.85, revenue: 0.75, novelty: 0.45, risk: -1.2 }, + next: 'Build the smallest slice that proves the core user promise, then defer everything that needs the proof first.', + }, + revenue: { + label: 'Revenue potential', + weights: { value: 1.15, feasibility: 0.75, confidence: 0.95, urgency: 1.05, revenue: 1.85, novelty: 0.35, risk: -1.0 }, + next: 'Validate willingness to pay before polishing delivery. A paid manual version beats a beautiful unpaid feature.', + }, + risk: { + label: 'Risk reduction', + weights: { value: 0.95, feasibility: 1.0, confidence: 1.45, urgency: 0.8, revenue: 0.45, novelty: 0.25, risk: -1.85 }, + next: 'Start where uncertainty is highest and evidence is cheapest. Do not build around an untested assumption.', + }, + originality: { + label: 'Most original / differentiated', + weights: { value: 1.0, feasibility: 0.65, confidence: 0.75, urgency: 0.55, revenue: 0.65, novelty: 1.85, risk: -0.9 }, + next: 'Keep the unusual angle, but demand one proof step so originality does not become expensive weirdness.', + }, +}; + +const wordSets = { + revenue: ['pay', 'paid', 'price', 'pricing', 'stripe', 'checkout', 'invoice', 'sales', 'sell', 'buyer', 'subscription', 'revenue', 'client', 'customer', 'conversion', 'upsell'], + value: ['pain', 'problem', 'save', 'faster', 'clear', 'clarity', 'decision', 'feedback', 'user', 'customer', 'relief', 'manual', 'core', 'must', 'need', 'trust'], + effort: ['dashboard', 'workspace', 'workspaces', 'collaboration', 'realtime', 'integration', 'integrations', 'automation', 'accounts', 'auth', 'billing', 'ai', 'model', 'mobile', 'sync', 'admin', 'export', 'exportable', 'slack', 'notion', 'team', 'voting'], + risk: ['unclear', 'maybe', 'complex', 'hard', 'risky', 'unknown', 'depends', 'enterprise', 'platform', 'everything', 'all users', 'team voting', 'marketplace', 'saved workspaces', 'team voting'], + novelty: ['new', 'novel', 'different', 'unique', 'original', 'weird', 'unexpected', 'expert', 'judgement', 'reflection', 'rank', 'compare'], + urgency: ['now', 'launch', 'blocker', 'first', 'mvp', 'today', 'week', 'urgent', 'stuck', 'before', 'next'], +}; + +function hits(text, words) { + const lower = ` ${String(text || '').toLowerCase()} `; + return words.reduce((count, word) => count + (lower.includes(word) ? 1 : 0), 0); +} + +function parseOptionsFromText(value) { + const text = cleanMultiline(value, 12000); + const lines = text.split('\n').map(line => line.replace(/^\s*[-*•\d.)]+\s*/, '').trim()).filter(Boolean); + const optionLines = lines.length >= 2 ? lines : text.split(/[;|]/).map(part => part.trim()).filter(Boolean); + return optionLines.slice(0, 24).map((line, index) => { + const [rawTitle, ...rest] = line.split(/\s[-–—:]\s/); + return { + id: `option-${index + 1}`, + title: cleanText(rawTitle || line, 140), + description: cleanText(rest.join(' — ') || line, 420), + }; + }).filter(item => item.title); +} + +function scoreOption(option, mode, context = '') { + const text = `${option.title} ${option.description} ${context}`; + const effortHits = hits(text, wordSets.effort); + const riskHits = hits(text, wordSets.risk); + const coreLoopHits = hits(text, ['ranked feedback map', 'feedback map', 'pasted feature', 'feature lists', 'first-pass', 'decision brief', 'expert reflections']); + const metrics = { + value: Math.min(10, 4.8 + hits(text, wordSets.value) * 0.9 + coreLoopHits * 0.8 + hits(context, wordSets.value) * 0.15), + feasibility: Math.max(1, Math.min(10, 8.2 - effortHits * 0.8 - riskHits * 0.35 + Math.min(1.1, coreLoopHits * 0.28))), + confidence: Math.max(1, Math.min(10, 5.8 + coreLoopHits * 0.35 + hits(text, ['manual', 'existing', 'already', 'simple', 'clear', 'known']) * 0.7 - hits(text, ['maybe', 'unknown', 'new market', 'all users']) * 0.9)), + urgency: Math.min(10, 4.9 + hits(text, wordSets.urgency) * 0.9), + revenue: Math.min(10, 3.8 + hits(text, wordSets.revenue) * 1.05), + novelty: Math.min(10, 4.1 + hits(text, wordSets.novelty) * 0.95), + risk: Math.min(10, 2.5 + riskHits * 1.1 + Math.max(0, effortHits - 2) * 0.45), + }; + const weights = mode.weights; + const weighted = Object.entries(weights).reduce((sum, [key, weight]) => sum + metrics[key] * weight, 0); + const possible = Object.entries(weights).reduce((sum, [_key, weight]) => sum + Math.abs(weight) * 10, 0); + const score = Math.max(0, Math.min(100, Math.round((weighted + Math.abs(Math.min(0, weights.risk || 0)) * 10) / possible * 100))); + return { ...metrics, score }; +} + +function laneFor(option, rankIndex, total) { + if (rankIndex === 0) return { id: 'do', label: 'Do first', action: 'Build/test now' }; + if (rankIndex <= Math.max(1, Math.ceil(total * 0.32))) return { id: 'test', label: 'Validate next', action: 'Find evidence' }; + if (rankIndex >= Math.max(2, Math.floor(total * 0.72))) return { id: 'park', label: 'Park / cut', action: 'Keep out of the active plan' }; + return { id: 'defer', label: 'Defer', action: 'Sequence after proof' }; +} + +function reasonFor(option) { + const m = option.metrics; + if (m.feasibility >= 7.2 && m.value >= 6.2) return 'high enough value with low enough delivery drag to create fast signal'; + if (m.revenue >= 6.4) return 'clearer buyer or money signal than the rest of the list'; + if (m.risk >= 6.5) return 'interesting, but it carries assumption risk that should be tested before build'; + if (m.novelty >= 6.7) return 'more differentiated than the safe options, but still needs proof'; + return 'balanced tradeoff across value, effort, confidence, and timing'; +} + +function concernFor(option) { + const m = option.metrics; + if (m.risk >= 6.5) return 'The hidden risk is pretending this is ready to build before the core assumption is proven.'; + if (m.feasibility <= 4.5) return 'The likely trap is scope creep: this may need too much machinery for an MVP.'; + if (m.confidence <= 4.5) return 'Evidence looks thin. Treat this as a question, not a roadmap item.'; + if (m.revenue <= 4 && m.value >= 6) return 'Useful does not automatically mean sellable. Check willingness to pay or repeat use.'; + return 'The main risk is sequencing: do it only if it supports the first useful proof.'; +} + +function createDecisionBrief({ idea, context, mode, ranked }) { + const top = ranked[0]; + const second = ranked[1]; + const risky = ranked.slice().sort((a, b) => b.metrics.risk - a.metrics.risk)[0]; + const deferred = ranked.filter(item => ['defer', 'park'].includes(item.lane.id)).slice(0, 3); + const theme = top ? `The strongest signal is “${top.title}” because it has ${reasonFor(top)}.` : 'The list needs at least two options before ranking becomes useful.'; + return { + headline: top ? `Start with ${top.title}` : 'Add options to get a ranked feedback map', + summary: `${theme}${second ? ` “${second.title}” is the nearest follow-up, not a parallel first step.` : ''}`, + expertReflections: [ + { + lens: 'Product expert', + text: top ? `The ranking says the first job is not to build more surface area; it is to prove the highest-signal option. ${mode.next}` : 'A product expert would ask for concrete alternatives before giving serious advice.', + }, + { + lens: 'Scattermind simplifier', + text: deferred.length ? `Park ${deferred.map(item => `“${item.title}”`).join(', ')} for now. Not bad ideas — just mental tabs you do not need open while testing the first move.` : 'The list is already narrow. Keep it that way; do not add a fake backlog around a simple decision.', + }, + { + lens: 'Structured operator', + text: risky ? `The decision quality depends on one assumption: ${risky.title}. What evidence would make this move up or down the list? Write that before committing resources.` : 'Expose the criteria, the confidence, and the thing that would change the decision. That is what makes this defensible.', + }, + ], + next48Hours: top ? [ + `Write a one-paragraph test for “${top.title}”.`, + 'Put it in front of 3–5 real people or run it manually once.', + `Do not touch ${deferred[0] ? `“${deferred[0].title}”` : 'the parked ideas'} until the first signal is real.`, + ] : ['Paste 3–10 options.', 'Choose what the ranking should care about.', 'Run the first-pass judgement.'], + caution: 'This is first-pass judgement, not an oracle. Change the criteria if the context changes.', + }; +} + +app.post('/api/rank-feedback', (req, res) => { + const idea = cleanMultiline(req.body?.idea || '', 3000); + const context = cleanMultiline(req.body?.context || '', 3000); + const modeId = cleanText(req.body?.mode || 'progress', 40); + const mode = judgementModes[modeId] || judgementModes.progress; + let options = Array.isArray(req.body?.options) + ? req.body.options.slice(0, 24).map((item, index) => ({ id: `option-${index + 1}`, title: cleanText(item?.title || item?.name || '', 140), description: cleanText(item?.description || item?.brief || '', 420) })).filter(item => item.title) + : parseOptionsFromText(req.body?.optionsText || ''); + if (options.length < 2) return res.status(400).json({ error: 'Paste at least two options, features, ideas, or next moves to rank.' }); + options = options.map(option => ({ ...option, metrics: scoreOption(option, mode, `${idea}\n${context}`) })) + .sort((a, b) => b.metrics.score - a.metrics.score || b.metrics.value - a.metrics.value || a.metrics.risk - b.metrics.risk) + .map((option, index, arr) => ({ + ...option, + rank: index + 1, + lane: laneFor(option, index, arr.length), + reason: reasonFor(option), + concern: concernFor(option), + })); + res.json({ + ok: true, + mode: { id: modeId in judgementModes ? modeId : 'progress', label: mode.label }, + input: { idea, context, optionCount: options.length }, + ranked: options, + brief: createDecisionBrief({ idea, context, mode, ranked: options }), + }); +}); + app.get(/.*/, (_req, res) => res.sendFile(path.join(__dirname, 'public', 'index.html'))); app.use((error, _req, res, _next) => {