diff --git a/public/app.js b/public/app.js index e28771b..81f2ce4 100644 --- a/public/app.js +++ b/public/app.js @@ -50,7 +50,7 @@ const profiles = { ], }, }; -const state = { ideas: [], milestones: [], activity: [], activeId: null, selected: null, profileId: localStorage.getItem('rank-profile') || 'mvp', undo: null }; +const state = { ideas: [], milestones: [], activity: [], activeId: null, selected: null, profileId: localStorage.getItem('rank-profile') || 'mvp', undo: null, recentPlacement: null }; 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'); @@ -64,16 +64,18 @@ function activeIdeas(){ return state.ideas.filter(i => !i.archived && (!i.status 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 pct = total ? Math.round((sorted/total)*100) : 0; $('#progressCard').innerHTML = `${sorted} / ${total}features sorted
`; } +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) => ``).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 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'}]; 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 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 render(){ renderProfile(); renderProgress(); renderDeck(); renderGrid(); renderTimeline(); renderBacklog(); } +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) @@ -159,7 +161,7 @@ async function importFeatureSet(){ } 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}; Object.assign(idea,{status:z.status,milestoneId:z.milestoneId,rank:Math.round(z.left*1000)}); render(); 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){ Object.assign(idea, previous); render(); toast(error.message); } } +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); } } diff --git a/public/index.html b/public/index.html index c20de4a..56fc72d 100644 --- a/public/index.html +++ b/public/index.html @@ -6,23 +6,23 @@ Prioritix — Feature Prioritization - +
-
+

Rank Prioritization Studio

@@ -43,6 +43,22 @@
+
+
9:41Rank
+
+ Project overview +

Rank Roadmap

+

Create a clear feature timeline from rough ideas, imports, and agent suggestions.

+
+
+ +
+
+ +
+
+
@@ -152,6 +172,6 @@ - + diff --git a/public/styles.css b/public/styles.css index f6d8b97..8887455 100644 --- a/public/styles.css +++ b/public/styles.css @@ -20,3 +20,11 @@ button,input,textarea{font:inherit} button{cursor:pointer} a{color:inherit;text- @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)}.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} +}