const PROD_DB_BASE = 'https://tasks-c8880-default-rtdb.asia-southeast1.firebasedatabase.app'
const SANDBOX_DB_BASE = 'https://taskboard-sandbox-default-rtdb.asia-southeast1.firebasedatabase.app'

const host = typeof window !== 'undefined' ? window.location.hostname : ''
const ENV = host.includes('sandbox') || host.includes('localhost') || host.includes('127.0.0.1')
  ? 'sandbox'
  : 'production'
const DB_BASE = ENV === 'sandbox' ? SANDBOX_DB_BASE : PROD_DB_BASE
const DB_URL = `${DB_BASE}/taskboard`

async function readJson(path) {
  const res = await fetch(`${DB_URL}/${path}.json`)
  if (!res.ok) throw new Error(`Firebase read failed: ${res.status}`)
  return res.json()
}

async function writeJson(path, value) {
  const res = await fetch(`${DB_URL}/${path}.json`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(value),
  })
  if (!res.ok) throw new Error(`Firebase write failed: ${res.status}`)
  return res.json()
}

function normaliseTasks(value) {
  if (Array.isArray(value)) return value
  if (value && typeof value === 'object') return Object.values(value)
  return []
}

function subscribeTasks(callback) {
  let stopped = false
  const poll = async () => {
    try {
      const value = await readJson('tasks')
      if (!stopped) callback(normaliseTasks(value))
    } catch (e) {
      console.warn(e)
      // Keep current in-memory tasks on read failure.
      // Do not call callback([]), because that makes the board appear wiped.
    }
  }
  poll()
  const id = setInterval(poll, 5000)
  return () => { stopped = true; clearInterval(id) }
}

function saveTasks(tasks) {
  return writeJson('tasks', tasks || [])
}

function subscribeSettings(callback) {
  let stopped = false
  const poll = async () => {
    try {
      const value = await readJson('settings')
      if (!stopped) callback(value || {})
    } catch (e) {
      console.warn(e)
      // Keep current settings on read failure.
    }
  }
  poll()
  const id = setInterval(poll, 10000)
  return () => { stopped = true; clearInterval(id) }
}

function saveSettings(settings) {
  return writeJson('settings', settings || {})
}

const { useState, useEffect, useRef } = React

const ONESIGNAL_APP_ID  = 'ceb03ae9-364d-4161-9110-f3bfb19609c2'
const ONESIGNAL_API_KEY = 'os_v2_app_z2ydv2jwjvawdeiq6o73dfqjyihpd3s3pl7el2uu3uw4oarkx6atbnu6xteeqfap6zq24ytfzphxvirkh3p5w2lctzhnfbl7iz26e2a'
const APP_URL = 'https://taskboard-2zh.pages.dev'
const EMAILJS_SERVICE_ID  = 'service_vv3sdrd'
const EMAILJS_TEMPLATE_ID = 'template_5d4qmdn'
const EMAILJS_PUBLIC_KEY  = 'NpQiQ1lKOpSKDavDe'
const USERS = {
  troy:   { label: 'Troy',   email: 'troyenright@gmail.com',     color: '#60a5fa' },
  khatra: { label: 'Khatra', email: 'khatra.nekzad12@gmail.com',  color: '#f472b6' },
}
const COLS = [
  { id: 'inbox',   label: 'Inbox',   sub: 'To be allocated', accent: '#64748b', hideable: false },
  { id: 'troy',    label: 'Troy',    sub: 'Active tasks',    accent: '#60a5fa', hideable: false },
  { id: 'khatra',  label: 'Khatra',  sub: 'Active tasks',    accent: '#f472b6', hideable: false },
  { id: 'both',    label: 'Both',    sub: 'Shared tasks',    accent: '#a78bfa', hideable: false },
  { id: 'snoozed', label: 'Snoozed', sub: 'Wake up later',   accent: '#a78bfa', hideable: true  },
  { id: 'done',    label: 'Done',    sub: 'Completed',       accent: '#34d399', hideable: true  },
]
const OWNER_TO_COL = { troy:'troy', khatra:'khatra', both:'both', inbox:'inbox' }
const CATEGORIES = [
  { id: '',         label: 'No category' },
  { id: 'property', label: '🏠 Property' },
  { id: 'finance',  label: '💰 Finance'  },
  { id: 'work',     label: '💼 Work'     },
  { id: 'home',     label: '🛋 Home'     },
  { id: 'personal', label: '🙋 Personal' },
  { id: 'strata',   label: '🏢 Strata'   },
  { id: 'admin',    label: '📋 Admin'    },
  { id: 'other',    label: '· Other'     },
]
const SNOOZE_QUICK = [{ label:'30m', mins:30 },{ label:'1hr', mins:60 },{ label:'3hr', mins:180 }]
const SNOOZE_MORE = [
  { label:'Tonight 8pm', fn:()=>{ const d=new Date(); d.setHours(20,0,0,0); if(d<new Date()) d.setDate(d.getDate()+1); return d.toISOString() }},
  { label:'Tomorrow',    fn:()=>{ const d=new Date(); d.setDate(d.getDate()+1); d.setHours(9,0,0,0); return d.toISOString() }},
  { label:'3 days',      fn:()=>{ const d=new Date(); d.setDate(d.getDate()+3); d.setHours(9,0,0,0); return d.toISOString() }},
  { label:'1 week',      fn:()=>{ const d=new Date(); d.setDate(d.getDate()+7); d.setHours(9,0,0,0); return d.toISOString() }},
]
const RECUR_OPTIONS = ['none','daily','weekly','monthly','annually']
const OWNER_COLOR = { troy:'#60a5fa', khatra:'#f472b6', both:'#a78bfa', inbox:'#64748b' }
const PRI_COLOR   = { high:'#ef4444', medium:'#f59e0b', low:'#6ee7b7' }
const TAG_PALETTE = ['#60a5fa','#f472b6','#34d399','#f59e0b','#a78bfa','#fb923c','#38bdf8','#4ade80']
const APP_VERSION = 'v10-2026-04-24-brisbane'

const TZ       = 'Australia/Brisbane'
const uid      = () => Math.random().toString(36).slice(2,10)
const fmtDate  = iso => iso ? new Date(iso).toLocaleDateString('en-AU',{day:'numeric',month:'short',year:'numeric',timeZone:TZ}) : null
const fmtShort = iso => iso ? new Date(iso).toLocaleDateString('en-AU',{day:'numeric',month:'short',timeZone:TZ}) : null
const fmtDT    = iso => iso ? new Date(iso).toLocaleString('en-AU',{day:'numeric',month:'short',hour:'2-digit',minute:'2-digit',timeZone:TZ,hour12:true}) : null
const daysUntil= iso => {
  if(!iso) return null
  // Compare calendar dates in local time so timezone doesn't skew the count
  const parts = iso.split('T')[0].split('-').map(Number)
  const due = new Date(parts[0], parts[1]-1, parts[2])
  const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
  return Math.ceil((due - today) / 86400000)
}
const isOver   = (iso,col) => !!iso && col!=='done' && new Date(iso)<new Date()
const nowAEST  = () => { const d=new Date(); return new Date(d.toLocaleString('en-AU',{timeZone:TZ})) }
const isSnoozed= t => t.snoozedUntil && new Date(t.snoozedUntil)>new Date()

// Convert UTC ISO string → local datetime string for datetime-local inputs
// datetime-local expects "YYYY-MM-DDTHH:mm" in LOCAL time, not UTC
// When a due date is set, auto-calculate snooze to that date at 9am local
function dueDateToSnooze(dateStr) {
  if (!dateStr) return null
  const [y,m,d] = dateStr.split('-').map(Number)
  const dt = new Date(y, m-1, d, 9, 0, 0, 0) // 9am local time
  return dt > new Date() ? dt.toISOString() : null // only if future
}

function toLocalDT(iso) {
  if (!iso) return ''
  const d = new Date(iso)
  const offset = d.getTimezoneOffset() * 60000
  return new Date(d.getTime() - offset).toISOString().slice(0, 16)
}
const taskAgeDays= t => {
  if(!t.createdAt||t.colId==='done'||t.colId==='snoozed') return 0
  return Math.floor((Date.now()-new Date(t.createdAt))/86400000)
}
const tagColor = tag => { let h=0; for(let c of tag) h=(h*31+c.charCodeAt(0))&0xffffffff; return TAG_PALETTE[Math.abs(h)%TAG_PALETTE.length] }

function newTask(colId) {
  return { id:uid(), colId, assignee:colId==='inbox'?'inbox':colId,
    title:'', notes:'', priority:'medium', progress:'todo', category:'',
    dueDate:null, dueTime:'09:00', reminder:'onday', url:'', checklist:[], comments:[],
    tags:[], dependsOn:[], recur:'none', snoozedUntil:null, archivedAt:null,
    createdAt:new Date().toISOString(), updatedAt:new Date().toISOString(), updatedBy:null, completedAt:null }
}
function nextRecur(task) {
  if (!task.recur||task.recur==='none') return null
  // Calculate next due date — use today as base if no due date set
  const base = task.dueDate ? new Date(task.dueDate) : new Date()
  // If the base date is in the past, use today instead so we don't generate a past due date
  const from = base < new Date() ? new Date() : base
  const d = new Date(from)
  if(task.recur==='daily')    d.setDate(d.getDate()+1)
  if(task.recur==='weekly')   d.setDate(d.getDate()+7)
  if(task.recur==='monthly')  d.setMonth(d.getMonth()+1)
  if(task.recur==='annually') d.setFullYear(d.getFullYear()+1)
  return {
    ...newTask(task.colId),
    title:      task.title,
    notes:      task.notes,
    assignee:   task.assignee,
    priority:   task.priority,
    category:   task.category,
    dueDate:    task.dueDate ? d.toISOString().split('T')[0] : null,
    recur:      task.recur,
    tags:       task.tags||[],
    checklist:  (task.checklist||[]).map(i=>({...i, checked:false})),
    dependsOn:  [],
  }
}

function fireConfetti() {
  const canvas=document.createElement('canvas')
  canvas.style.cssText='position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9999'
  document.body.appendChild(canvas)
  const ctx=canvas.getContext('2d')
  canvas.width=window.innerWidth; canvas.height=window.innerHeight
  const bits=Array.from({length:120},()=>({
    x:Math.random()*canvas.width, y:-10,
    w:6+Math.random()*8, h:8+Math.random()*12,
    r:Math.random()*Math.PI*2, dr:(Math.random()-.5)*.2,
    vx:(Math.random()-.5)*6, vy:2+Math.random()*4,
    color:['#f59e0b','#34d399','#60a5fa','#f472b6','#a78bfa','#fb923c'][Math.floor(Math.random()*6)],
    alpha:1
  }))
  let frame=0
  const anim=()=>{
    ctx.clearRect(0,0,canvas.width,canvas.height)
    bits.forEach(b=>{
      b.x+=b.vx; b.y+=b.vy; b.r+=b.dr; b.vy+=.15; b.alpha-=.008
      ctx.save(); ctx.globalAlpha=Math.max(0,b.alpha)
      ctx.translate(b.x,b.y); ctx.rotate(b.r)
      ctx.fillStyle=b.color
      ctx.fillRect(-b.w/2,-b.h/2,b.w,b.h)
      ctx.restore()
    })
    frame++
    if(frame<180&&bits.some(b=>b.alpha>0)) requestAnimationFrame(anim)
    else document.body.removeChild(canvas)
  }
  requestAnimationFrame(anim)
}

async function sendPush(heading, message, userKey, urgent=false) {
  try {
    const body = {
      app_id: ONESIGNAL_APP_ID,
      headings: { en: heading },
      contents: { en: message },
      priority: urgent ? 10 : 5,
    }
    if (userKey) body.filters = [{ field:'tag', key:'user', relation:'=', value:userKey }]
    else body.included_segments = ['All']
    const res = await fetch('https://onesignal.com/api/v1/notifications',{
      method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)
    })
    return res.ok
  } catch(e){ console.warn('push failed',e); return false }
}
function formatComments(task) {
  const comments = task.comments || []
  if (!comments.length) return ''
  const lines = comments.map(cm => {
    const who = cm.who ? (cm.who.charAt(0).toUpperCase() + cm.who.slice(1)) : 'Unknown'
    const when = cm.at ? new Date(cm.at).toLocaleDateString('en-AU', { day:'numeric', month:'short', year:'numeric', timeZone:'Australia/Brisbane' }) : ''
    return who + ' (' + when + '): ' + cm.text
  }).join('\n')
  return '\n\nComments:\n' + lines
}

async function sendEmail(toEmail, toName, subject, message) {
  try {
    const fullMessage = message + '\n\n─────────────────\nOpen Task Board → ' + APP_URL
    const res = await fetch('https://api.emailjs.com/api/v1.0/email/send', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        service_id:  EMAILJS_SERVICE_ID,
        template_id: EMAILJS_TEMPLATE_ID,
        user_id:     EMAILJS_PUBLIC_KEY,
        template_params: {
          to_email: toEmail,
          name:     toName,
          subject:  subject,
          message:  fullMessage,
        },
      }),
    })
    const text = await res.text()
    if (res.ok && text === 'OK') {
      console.log('Email sent to', toEmail)
      return true
    }
    console.error('EmailJS error:', text)
    return false
  } catch(e) {
    console.error('Email fetch failed:', e)
    return false
  }
}

async function registerPushUser(userKey) {
  try { const OS=window.OneSignal; if(!OS) return; await OS.login(userKey); await OS.User.addTag('user',userKey) } catch(e){ console.warn(e) }
}

function loadPrefs() { try{ return JSON.parse(localStorage.getItem('tb_prefs')||'{}') }catch{ return {} } }
function savePrefs(p) { try{ localStorage.setItem('tb_prefs',JSON.stringify(p)) }catch{} }

// ─── STYLES ────────────────────────────────────────────────
function makeCSS(dark) {
  const dt = dark
  return `
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@600;700;800&family=DM+Sans:wght@400;500;600&family=DM+Mono:wght@400;500&display=swap');
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{height:100%;-webkit-tap-highlight-color:transparent}body{height:100%;overscroll-behavior:none}
:root{
${dt?`--bg:#0a0b10;--s1:#10121a;--s2:#171a25;--s3:#1d2030;--s4:#252840;
--b1:rgba(255,255,255,.06);--b2:rgba(255,255,255,.11);--b3:rgba(255,255,255,.18);
--t1:#eaecf4;--t2:#8890aa;--t3:#484e66;`:`--bg:#f0f2f7;--s1:#ffffff;--s2:#f5f6fa;--s3:#ebedf5;--s4:#dde0ec;
--b1:rgba(0,0,0,.07);--b2:rgba(0,0,0,.11);--b3:rgba(0,0,0,.18);
--t1:#1a1d2e;--t2:#555a72;--t3:#9298b0;`}
--amber:#f59e0b;--ag:rgba(245,158,11,.10);--green:#34d399;--gg:rgba(52,211,153,.10);
--red:#ef4444;--rg:rgba(239,68,68,.10);--purple:#a78bfa;--pg:rgba(167,139,250,.10);--r:10px;}
.app{min-height:100dvh;background:var(--bg);${dt?`background-image:radial-gradient(ellipse 60% 40% at 5% 0%,rgba(96,165,250,.05),transparent 70%),radial-gradient(ellipse 40% 40% at 95% 100%,rgba(244,114,182,.05),transparent 70%);`:''}display:flex;flex-direction:column;font-family:'DM Sans',sans-serif;color:var(--t1)}
.env-banner{background:linear-gradient(90deg,#f59e0b,#fb923c);color:#08090d;font-family:'DM Mono',monospace;font-size:11px;font-weight:700;letter-spacing:.8px;text-align:center;padding:7px 12px;border-bottom:1px solid rgba(0,0,0,.25);text-transform:uppercase}
.env-pill{font-family:'DM Mono',monospace;font-size:10px;font-weight:700;letter-spacing:1px;color:#08090d;background:#f59e0b;border:1px solid rgba(245,158,11,.55);padding:3px 7px;border-radius:999px;margin-left:8px;vertical-align:middle;text-transform:uppercase}
.hdr{padding:13px 20px 11px;border-bottom:1px solid var(--b1);display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;background:var(--s1)}
.hdr-l{display:flex;align-items:center;gap:10px}
.pill{font-family:'DM Mono',monospace;font-size:10px;letter-spacing:2px;text-transform:uppercase;color:var(--amber);background:var(--ag);border:1px solid rgba(245,158,11,.22);padding:4px 10px;border-radius:20px}
.hdr-title{font-family:'Syne',sans-serif;font-size:16px;font-weight:700;letter-spacing:-.3px}
.hdr-r{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.sdot{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 6px rgba(52,211,153,.5);flex-shrink:0}
.sdot.off{background:var(--t3);box-shadow:none}
.slbl{font-family:'DM Mono',monospace;font-size:10px;color:var(--t3);margin-right:4px}
.hbtn{background:var(--s2);border:1px solid var(--b2);border-radius:8px;color:var(--t2);font-size:12px;font-family:'DM Sans',sans-serif;font-weight:500;padding:6px 11px;cursor:pointer;display:flex;align-items:center;gap:5px;transition:all .14s;white-space:nowrap}
.hbtn:hover{background:var(--s3);color:var(--t1);border-color:var(--b3)}
.hbtn.on{color:var(--green);border-color:rgba(52,211,153,.3);background:var(--gg)}
.who-badge{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;cursor:pointer;border:2px solid transparent;transition:all .14s;flex-shrink:0}
.fbar{padding:7px 20px;border-bottom:1px solid var(--b1);display:flex;align-items:center;gap:5px;flex-wrap:wrap;background:var(--s1)}
.ftag{background:var(--s2);border:1px solid var(--b1);border-radius:20px;color:var(--t3);font-size:11px;padding:3px 10px;cursor:pointer;transition:all .13s}
.ftag:hover{color:var(--t2);border-color:var(--b2)}
.ftag.act{background:var(--ag);color:var(--amber);border-color:rgba(245,158,11,.3)}
.stats{display:flex;padding:0 20px;border-bottom:1px solid var(--b1);flex-wrap:wrap;background:var(--s1)}
.stat{padding:8px 14px 8px 0;margin-right:14px;border-right:1px solid var(--b1);display:flex;align-items:center;gap:7px}
.stat:last-child{border-right:none}
.stn{font-family:'Syne',sans-serif;font-size:16px;font-weight:700}
.stl{font-size:10px;color:var(--t3);text-transform:uppercase;letter-spacing:.4px}
.board{display:grid;grid-template-columns:1.1fr 1fr 1fr 1fr;gap:11px;padding:13px 20px 32px;align-items:start}
@media(max-width:900px){.board{grid-template-columns:1fr 1fr}}
@media(max-width:560px){.board{grid-template-columns:1fr}}
.col{background:var(--s1);border:1px solid var(--b1);border-radius:14px;display:flex;flex-direction:column}
.col-hd{padding:11px 11px 9px;border-bottom:1px solid var(--b1);display:flex;align-items:center;justify-content:space-between}
.col-tg{display:flex;align-items:center;gap:8px}
.col-stripe{width:3px;height:26px;border-radius:2px;flex-shrink:0}
.col-nm{font-family:'Syne',sans-serif;font-size:13px;font-weight:700}
.col-sb{font-size:10px;color:var(--t3);margin-top:1px}
.col-cnt{font-family:'DM Mono',monospace;font-size:11px;background:var(--s2);border:1px solid var(--b1);color:var(--t3);padding:2px 7px;border-radius:10px}
.addcol{width:26px;height:26px;background:var(--s2);border:1px solid var(--b1);border-radius:7px;color:var(--t3);font-size:18px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .14s;flex-shrink:0}
.addcol:hover{background:var(--ag);color:var(--amber);border-color:rgba(245,158,11,.3)}
.qa{margin:7px 7px 0;background:var(--s2);border:1px solid var(--b2);border-radius:var(--r);animation:popIn .15s ease}
@keyframes popIn{from{opacity:0;transform:translateY(-5px) scale(.98)}to{opacity:1;transform:none}}
.qa-in{width:100%;background:transparent;border:none;outline:none;padding:9px 10px 7px;color:var(--t1);font-size:13px;font-family:'DM Sans',sans-serif}
.qa-in::placeholder{color:var(--t3)}
.qa-row{display:flex;align-items:center;gap:4px;padding:3px 7px 8px;flex-wrap:wrap}
.qsel,.qdate{background:var(--s1);border:1px solid var(--b2);border-radius:6px;color:var(--t2);font-size:11px;font-family:'DM Sans',sans-serif;padding:4px 6px;cursor:pointer;outline:none;color-scheme:${dt?'dark':'light'}}
.qa-mic{background:var(--s1);border:1px solid var(--b2);border-radius:6px;color:var(--t2);font-size:12px;padding:3px 7px;cursor:pointer;transition:all .14s}
.qa-mic.lstn{color:var(--red);border-color:rgba(239,68,68,.3);background:var(--rg);animation:pulse .8s ease infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
.qa-acts{margin-left:auto;display:flex;gap:4px}
.qa-ok{background:var(--amber);color:#08090d;border:none;border-radius:6px;font-size:11px;font-weight:700;font-family:'DM Sans',sans-serif;padding:4px 11px;cursor:pointer}
.qa-ok:hover{opacity:.85}
.qa-x{background:transparent;border:1px solid var(--b2);border-radius:6px;color:var(--t3);font-size:11px;font-family:'DM Sans',sans-serif;padding:4px 7px;cursor:pointer}
.cards{display:flex;flex-direction:column;gap:6px;padding:7px 7px 11px;overflow-y:auto;max-height:72vh}
.cards::-webkit-scrollbar{width:3px}
.cards::-webkit-scrollbar-thumb{background:var(--s4);border-radius:2px}
.card{background:var(--s2);border:1px solid var(--b1);border-radius:var(--r);padding:10px 11px;cursor:pointer;transition:all .13s;animation:cardIn .15s ease;position:relative}
@keyframes cardIn{from{opacity:0;transform:scale(.97)}to{opacity:1;transform:scale(1)}}
.card:hover{border-color:var(--b2);background:var(--s3);transform:translateY(-1px);box-shadow:0 4px 16px rgba(0,0,0,.12)}
.card.dragging{opacity:.4;transform:scale(.97)}
.col.drag-over{border-color:var(--amber);background:var(--ag)}
.col.drag-over .col-hd{border-color:rgba(245,158,11,.3)}
.card.done-card{opacity:.5}
.card.snoozed-card{opacity:.6;border-style:dashed}
.card.woke-card{border-color:#a78bfa!important;box-shadow:0 0 12px rgba(167,139,250,.4)!important;animation:cardIn .15s ease,wokePulse 1.5s ease 3}
.card.pinned-card{border-color:rgba(245,158,11,.3)!important}
.stat-btn{background:transparent;border:none;cursor:pointer;display:flex;align-items:center;gap:7px;padding:8px 14px 8px 0;margin-right:14px;border-right:1px solid var(--b1);transition:opacity .14s}
.stat-btn:last-child{border-right:none}
.stat-btn:hover .stn{text-decoration:underline}
.stat-btn.active-stat .stn{color:var(--amber)!important}
.stat-btn.active-stat .stl{color:var(--amber)}
.search-bar{padding:7px 20px;border-bottom:1px solid var(--b1);display:flex;align-items:center;gap:8px;background:var(--s1)}
.search-in{flex:1;background:var(--s2);border:1px solid var(--b2);border-radius:8px;color:var(--t1);font-size:13px;font-family:'DM Sans',sans-serif;padding:8px 12px;outline:none}
.search-in:focus{border-color:rgba(245,158,11,.5)}
.search-in::placeholder{color:var(--t3)}
.search-x{background:transparent;border:none;color:var(--t3);font-size:16px;cursor:pointer;padding:4px}
.search-x:hover{color:var(--t1)}
.date-group{font-size:10px;color:var(--t3);text-transform:uppercase;letter-spacing:.5px;font-family:'DM Mono',monospace;padding:6px 2px 3px;margin-top:4px}
.mention-wrap{position:relative}
.mention-input-row{display:flex;gap:7px;align-items:flex-end}
.mention-dropdown{position:absolute;bottom:100%;left:0;margin-bottom:4px;background:var(--s1);border:1px solid var(--b2);border-radius:8px;overflow:hidden;box-shadow:0 8px 24px rgba(0,0,0,.3);z-index:50;min-width:160px}
.mention-opt{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;transition:background .12s;font-size:13px}
.mention-opt:hover,.mention-opt.active{background:var(--s3)}
.mention-av{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0}
.mention-badge{display:inline-flex;align-items:center;gap:3px;padding:1px 7px;border-radius:5px;font-size:11px;font-weight:600}

@keyframes wokePulse{0%,100%{box-shadow:0 0 12px rgba(167,139,250,.4)}50%{box-shadow:0 0 22px rgba(167,139,250,.8)}}
.card-top{display:flex;align-items:flex-start;gap:7px;margin-bottom:5px}
.card-title{font-size:13px;font-weight:500;line-height:1.4;flex:1}
.done-card .card-title{text-decoration:line-through;color:var(--t3)}
.pri-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;margin-top:5px}
.card-cat{font-size:10px;color:var(--t3);background:var(--s3);border:1px solid var(--b1);padding:1px 6px;border-radius:4px;font-family:'DM Mono',monospace;margin-bottom:5px;display:inline-block}
.card-notes{font-size:11px;color:var(--t3);line-height:1.5;margin-bottom:5px;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;overflow:visible}
.cl-prev{font-size:10px;color:var(--t3);margin-bottom:5px;display:flex;align-items:center;gap:5px;font-family:'DM Mono',monospace}
.cl-bar-w{flex:1;height:3px;background:var(--s4);border-radius:2px;overflow:hidden}
.cl-bar{height:100%;background:var(--green);border-radius:2px;transition:width .3s}
.card-tags{display:flex;gap:3px;flex-wrap:wrap;margin-bottom:5px}
.card-tag{font-size:10px;padding:1px 6px;border-radius:4px;font-family:'DM Mono',monospace;font-weight:500}
.card-ft{display:flex;align-items:center;justify-content:space-between;gap:4px;flex-wrap:wrap}
.chips{display:flex;align-items:center;gap:3px;flex-wrap:wrap}
.chip{font-size:10px;padding:2px 6px;border-radius:5px;font-family:'DM Mono',monospace;font-weight:500;white-space:nowrap}
.c-own{display:flex;align-items:center;gap:4px;font-size:11px;color:var(--t2)}
.c-odot{width:7px;height:7px;border-radius:50%}
.due-n{background:var(--s3);color:var(--t3)}
.due-s{background:rgba(245,158,11,.12);color:var(--amber)}
.due-o{background:var(--rg);color:#fca5a5}
.c-doing{background:rgba(245,158,11,.10);color:var(--amber)}
.c-snooze{background:var(--pg);color:var(--purple)}
.c-recur{background:var(--gg);color:var(--green)}
.c-lock{background:var(--rg);color:#fca5a5}
.dep-badge{position:absolute;top:7px;right:9px;font-size:13px;opacity:.7}
.strip{display:none;gap:3px;margin-top:7px;padding-top:7px;border-top:1px solid var(--b1);align-items:center;flex-wrap:wrap}
.card:hover .strip{display:flex}
.sbtn{background:var(--s1);border:1px solid var(--b2);border-radius:5px;color:var(--t2);font-size:10px;font-family:'DM Sans',sans-serif;font-weight:600;padding:3px 8px;cursor:pointer;transition:all .12s;white-space:nowrap}
.sbtn:hover{color:var(--t1);border-color:var(--b3);background:var(--s4)}
.st:hover{background:rgba(96,165,250,.12);color:#60a5fa;border-color:rgba(96,165,250,.3)}
.sk:hover{background:rgba(244,114,182,.12);color:#f472b6;border-color:rgba(244,114,182,.3)}
.si:hover{background:rgba(100,116,139,.12);color:var(--t2)}
.sd{margin-left:auto}
.sd:hover{background:var(--gg);color:var(--green);border-color:rgba(52,211,153,.3)}
.empty{display:flex;flex-direction:column;align-items:center;gap:5px;padding:22px 12px;color:var(--t3)}
.empty-ico{font-size:20px;opacity:.3}
.empty-txt{font-size:11px}
.overlay{position:fixed;inset:0;background:rgba(0,0,0,.72);backdrop-filter:blur(8px);z-index:200;display:flex;align-items:center;justify-content:center;padding:16px;animation:fIn .15s ease}
@keyframes fIn{from{opacity:0}to{opacity:1}}
.modal{background:var(--s1);border:1px solid var(--b2);border-radius:16px;width:100%;max-width:620px;max-height:calc(100dvh - 32px);display:flex;flex-direction:column;box-shadow:0 32px 80px rgba(0,0,0,.4);animation:mIn .18s ease}
@keyframes mIn{from{opacity:0;transform:scale(.94) translateY(12px)}to{opacity:1;transform:none}}
.m-hd{padding:14px 18px;border-bottom:1px solid var(--b1);display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
.m-title{font-family:'Syne',sans-serif;font-size:15px;font-weight:700}
.m-x{width:27px;height:27px;background:var(--s2);border:1px solid var(--b1);border-radius:7px;color:var(--t3);font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center}
.m-x:hover{color:var(--t1)}
.tabs{display:flex;background:var(--s2);border:1px solid var(--b1);border-radius:8px;padding:3px;margin:10px 18px 0;flex-shrink:0}
.tab{flex:1;background:transparent;border:none;border-radius:6px;color:var(--t3);font-size:11px;font-family:'DM Sans',sans-serif;font-weight:500;padding:6px 2px;cursor:pointer;transition:all .13s}
.tab:hover{color:var(--t2)}
.tab.active{background:var(--s3);color:var(--t1);border:1px solid var(--b2)}
.m-body{padding:16px 18px;display:flex;flex-direction:column;gap:13px;overflow-y:auto;flex:1}
.m-body::-webkit-scrollbar{width:3px}
.m-body::-webkit-scrollbar-thumb{background:var(--s4);border-radius:2px}
.fl{font-size:10px;color:var(--t3);text-transform:uppercase;letter-spacing:.6px;font-weight:600;margin-bottom:4px}
.fi{width:100%;background:var(--s2);border:1px solid var(--b2);border-radius:8px;color:var(--t1);font-size:13px;font-family:'DM Sans',sans-serif;padding:9px 11px;outline:none;transition:border-color .14s}
.fi:focus{border-color:rgba(245,158,11,.5)}
.fi.ta{resize:vertical;min-height:80px;line-height:1.6}
.fsel{width:100%;background:var(--s2);border:1px solid var(--b2);border-radius:8px;color:var(--t1);font-size:13px;font-family:'DM Sans',sans-serif;padding:9px 11px;outline:none;cursor:pointer;color-scheme:${dt?'dark':'light'}}
.fsel option{background:var(--s2)}
.frow{display:grid;grid-template-columns:1fr 1fr;gap:11px}
.cl-list{display:flex;flex-direction:column;gap:5px}
.cl-item{display:flex;align-items:center;gap:7px;background:var(--s2);border:1px solid var(--b1);border-radius:7px;padding:7px 9px}
.cl-cb{width:15px;height:15px;accent-color:var(--green);cursor:pointer;flex-shrink:0}
.cl-ti{flex:1;background:transparent;border:none;outline:none;color:var(--t1);font-size:13px;font-family:'DM Sans',sans-serif}
.cl-del{background:transparent;border:none;color:var(--t3);cursor:pointer;font-size:13px;padding:0 2px}
.cl-del:hover{color:#fca5a5}
.cl-add{display:flex;gap:6px}
.cl-new{flex:1;background:var(--s2);border:1px solid var(--b2);border-radius:7px;color:var(--t1);font-size:13px;font-family:'DM Sans',sans-serif;padding:7px 10px;outline:none}
.cl-new:focus{border-color:rgba(245,158,11,.5)}
.cl-new::placeholder{color:var(--t3)}
.cl-addbtn{background:var(--s3);border:1px solid var(--b2);border-radius:7px;color:var(--t2);font-size:12px;font-family:'DM Sans',sans-serif;padding:7px 13px;cursor:pointer}
.cl-addbtn:hover{background:var(--s4);color:var(--t1)}
.comment{background:var(--s2);border:1px solid var(--b1);border-radius:8px;padding:9px 11px}
.c-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}
.c-who{font-size:11px;font-weight:600}
.c-when{font-size:10px;color:var(--t3);font-family:'DM Mono',monospace}
.c-txt{font-size:12px;color:var(--t2);line-height:1.5}
.tags-wrap{display:flex;flex-wrap:wrap;gap:5px;background:var(--s2);border:1px solid var(--b2);border-radius:8px;padding:7px 10px;min-height:40px;align-items:center;cursor:text}
.tags-wrap:focus-within{border-color:rgba(245,158,11,.5)}
.tag-pill{display:flex;align-items:center;gap:4px;padding:2px 8px;border-radius:5px;font-size:11px;font-weight:600;font-family:'DM Mono',monospace}
.tag-rm{background:transparent;border:none;cursor:pointer;font-size:12px;padding:0;opacity:.6;color:inherit}
.tag-rm:hover{opacity:1}
.tags-in{background:transparent;border:none;outline:none;color:var(--t1);font-size:12px;font-family:'DM Sans',sans-serif;min-width:80px;flex:1}
.dep-item{display:flex;align-items:center;gap:8px;background:var(--s2);border:1px solid var(--b1);border-radius:7px;padding:7px 10px;font-size:12px;color:var(--t2);cursor:pointer}
.snooze-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:7px}
.sn-opt{background:var(--s2);border:1px solid var(--b2);border-radius:8px;color:var(--t2);font-size:12px;font-family:'DM Sans',sans-serif;font-weight:500;padding:10px;cursor:pointer;text-align:center;transition:all .13s}
.sn-opt:hover,.sn-opt.quick{background:var(--pg);color:var(--purple);border-color:rgba(167,139,250,.3)}
.m-ft{padding:11px 18px;border-top:1px solid var(--b1);display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;flex-shrink:0}
.ft-left,.ft-right{display:flex;gap:7px;align-items:center;flex-wrap:wrap}
.ft-left{margin-right:auto}
.ft-right{margin-left:auto;justify-content:flex-end}
@media(max-width:560px){.m-ft{align-items:stretch}.ft-left,.ft-right{width:100%;justify-content:flex-end}.ft-left{order:2}.ft-right{order:1}}
.btn-ok{background:var(--amber);color:#08090d;border:none;border-radius:8px;font-size:13px;font-weight:700;font-family:'DM Sans',sans-serif;padding:8px 20px;cursor:pointer}
.btn-ok:hover{opacity:.85}
.btn-ok:disabled{opacity:.4;cursor:not-allowed}
.btn-sec:disabled{opacity:.45;cursor:not-allowed;background:var(--s2)!important;color:var(--t3)!important;border-color:var(--b1)!important}
.btn-sec{background:transparent;border:1px solid var(--b2);border-radius:8px;color:var(--t2);font-size:13px;font-family:'DM Sans',sans-serif;padding:8px 13px;cursor:pointer}
.btn-sec:hover{background:var(--s2)}
.btn-del{background:transparent;border:1px solid rgba(239,68,68,.25);border-radius:8px;color:#fca5a5;font-size:13px;font-family:'DM Sans',sans-serif;padding:8px 13px;cursor:pointer}
.btn-del:hover{background:var(--rg)}
.btn-arch{background:transparent;border:1px solid var(--b2);border-radius:8px;color:var(--t3);font-size:13px;font-family:'DM Sans',sans-serif;padding:8px 13px;cursor:pointer}
.btn-arch:hover{background:var(--s2);color:var(--t2)}
.save-status{font-size:11px;color:var(--t3);font-family:'DM Mono',monospace;white-space:nowrap}
.save-status.saving{color:var(--amber)}
.save-status.saved{color:var(--green)}
.save-status.err{color:#fca5a5}
.srow{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--b1)}
.srow:last-child{border-bottom:none}
.slb{font-size:13px;font-weight:500}
.ssb{font-size:11px;color:var(--t3);margin-top:2px}
.toggle{width:36px;height:20px;border-radius:10px;cursor:pointer;border:none;padding:2px;display:flex;align-items:center;transition:background .2s;flex-shrink:0}
.toggle.on{background:var(--amber);justify-content:flex-end}
.toggle.off{background:var(--s4);justify-content:flex-start}
.tknob{width:16px;height:16px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,.3)}
.who-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:8px 0}
.who-card{border:2px solid var(--b2);border-radius:12px;padding:20px;text-align:center;cursor:pointer;transition:all .15s;background:var(--s2)}
.who-card:hover{border-color:var(--b3);background:var(--s3)}
.who-card.sel{border-color:var(--amber);background:var(--ag)}
.who-av{width:52px;height:52px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:700;margin:0 auto 10px}
.who-nm{font-family:'Syne',sans-serif;font-size:15px;font-weight:700}
.share-box{background:var(--s2);border:1px solid var(--b2);border-radius:8px;padding:11px;display:flex;gap:7px;align-items:center}
.share-url{flex:1;background:var(--s3);border:1px solid var(--b1);border-radius:6px;color:var(--t1);font-size:11px;font-family:'DM Mono',monospace;padding:7px 9px;outline:none;min-width:0}
.share-copy{background:var(--amber);color:#08090d;border:none;border-radius:6px;font-size:11px;font-weight:700;font-family:'DM Sans',sans-serif;padding:7px 13px;cursor:pointer;white-space:nowrap;flex-shrink:0}
.share-copy:hover{opacity:.85}
.share-copy.done{background:var(--green)}
.toast{position:fixed;bottom:max(20px,env(safe-area-inset-bottom));left:50%;transform:translateX(-50%);background:var(--s1);border:1px solid var(--b2);border-radius:10px;padding:9px 16px;font-size:13px;color:var(--t1);box-shadow:0 8px 32px rgba(0,0,0,.25);animation:tIn .2s ease;z-index:400;white-space:nowrap}
.update-banner{position:sticky;top:0;z-index:180;background:var(--amber);color:#08090d;border-bottom:1px solid rgba(0,0,0,.18);padding:8px 20px;display:flex;align-items:center;justify-content:center;gap:10px;font-size:12px;font-weight:700}
.update-banner button{background:#08090d;color:white;border:none;border-radius:7px;padding:5px 11px;font-family:'DM Sans',sans-serif;font-size:12px;font-weight:700;cursor:pointer}
@keyframes tIn{from{opacity:0;transform:translateX(-50%) translateY(8px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
.loading{display:flex;align-items:center;justify-content:center;height:100dvh;flex-direction:column;gap:12px;background:var(--bg)}
.loading-dot{width:10px;height:10px;border-radius:50%;background:var(--amber);animation:ldp 1s ease infinite}
@keyframes ldp{0%,100%{opacity:.2;transform:scale(.8)}50%{opacity:1;transform:scale(1)}}
`}

// ─── TOGGLE ─────────────────────────────────────────────────
function voiceInputAvailable() {
  if (typeof window === 'undefined') return false
  const SR = window.SpeechRecognition || window.webkitSpeechRecognition
  const mobileOrTouch = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) || navigator.maxTouchPoints > 0
  return !!SR && mobileOrTouch
}

function useVoice(onResult) {
  const [listening, setListening] = useState(false)
  const start = () => {
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition
    if (!SR) return
    const r = new SR(); r.lang = 'en-AU'; r.continuous = false; r.interimResults = false
    r.onstart  = () => setListening(true)
    r.onend    = () => setListening(false)
    r.onerror  = () => setListening(false)
    r.onresult = e => { onResult(e.results[0][0].transcript); setListening(false) }
    r.start()
  }
  return { listening, start }
}

function MicBtn({ onResult, style={} }) {
  const { listening, start } = useVoice(onResult)
  if (!voiceInputAvailable()) return null
  return (
    <button type="button" onClick={start} title="Voice input"
      style={{ background: listening ? 'var(--rg)' : 'var(--s3)', border: '1px solid ' + (listening ? 'rgba(239,68,68,.3)' : 'var(--b2)'),
        borderRadius: 7, color: listening ? '#fca5a5' : 'var(--t3)', fontSize: 13, padding: '6px 9px',
        cursor: 'pointer', transition: 'all .14s', animation: listening ? 'pulse .8s ease infinite' : 'none', ...style }}>
      🎤
    </button>
  )
}

function Toggle({on,onChange}){ return <button className={`toggle ${on?'on':'off'}`} onClick={()=>onChange(!on)}><div className="tknob"/></button> }

// ─── DUE CHIP ────────────────────────────────────────────────
function DueChip({date,col}){
  if(!date) return null
  const d=daysUntil(date),over=isOver(date,col)
  let cls='chip due-n',lbl=fmtShort(date)
  if(over){cls='chip due-o';lbl=`${Math.abs(d)}d overdue`}
  else if(d<=3){cls='chip due-s';lbl=d===0?'Today':d===1?'Tmrw':`${d}d`}
  return <span className={cls}>{lbl}</span>
}

// ─── CL BAR ──────────────────────────────────────────────────
function ClBar({items}){
  if(!items?.length) return null
  const done=items.filter(i=>i.checked).length
  return <div className="cl-prev">{done}/{items.length}<div className="cl-bar-w"><div className="cl-bar" style={{width:`${Math.round(done/items.length*100)}%`}}/></div></div>
}

// ─── QUICK ADD ───────────────────────────────────────────────
function QuickAdd({colId,onSave,onCancel}){
  const [title,setTitle]=useState('')
  const [assignee,setAssignee]=useState(colId==='inbox'?'inbox':colId==='both'?'both':colId)
  const [due,setDue]=useState('')
  const [priority,setPri]=useState('medium')
  const [lstn,setLstn]=useState(false)
  const ref=useRef()
  useEffect(()=>{ref.current?.focus()},[])

  const save=()=>{
    if(!title.trim()) return
    const destCol=assignee==='troy'?'troy':assignee==='khatra'?'khatra':assignee==='both'?'both':colId
    onSave({...newTask(destCol),title:title.trim(),assignee,dueDate:due||null,priority},destCol)
  }
  const voice=()=>{
    const SR=window.SpeechRecognition||window.webkitSpeechRecognition
    if(!SR){ alert('Voice input is not supported in this browser. Try Chrome or Safari.'); return }
    if(lstn){ setLstn(false); return }
    try {
      const r=new SR()
      r.lang='en-AU'; r.continuous=false; r.interimResults=false
      r.onstart=()=>setLstn(true)
      r.onerror=e=>{
        setLstn(false)
        if(e.error==='not-allowed') alert('Microphone access denied. Please allow microphone in your browser settings and try again.')
        else if(e.error==='network') alert('Voice recognition needs an internet connection.')
        else console.warn('Speech error:',e.error)
      }
      r.onend=()=>setLstn(false)
      r.onresult=e=>{ if(e.results.length>0) setTitle(e.results[0][0].transcript); setLstn(false) }
      r.start()
    } catch(e){ setLstn(false); console.warn('Voice start failed:',e) }
  }
  return (
    <div className="qa">
      <input ref={ref} className="qa-in" placeholder="Task name…" value={title}
        onChange={e=>setTitle(e.target.value)}
        onKeyDown={e=>{if(e.key==='Enter'||(e.key==='Enter'&&e.ctrlKey))save();if(e.key==='Escape')onCancel()}}/>
      <div className="qa-row">
        <select className="qsel" value={assignee} onChange={e=>setAssignee(e.target.value)}>
          <option value="inbox">Unassigned</option><option value="troy">Troy</option>
          <option value="khatra">Khatra</option><option value="both">Both</option>
        </select>
        <select className="qsel" value={priority} onChange={e=>setPri(e.target.value)}>
          <option value="high">⬆ High</option><option value="medium">● Med</option><option value="low">⬇ Low</option>
        </select>
        <input type="date" className="qdate" value={due} onChange={e=>setDue(e.target.value)}/>
        {voiceInputAvailable()&&<button className={`qa-mic${lstn?' lstn':''}`} onClick={voice} title={lstn?'Listening…':'Voice input'}>🎤</button>}
        <div className="qa-acts">
          <button className="qa-x" onClick={onCancel}>✕</button>
          <button className="qa-ok" onClick={save}>Add</button>
        </div>
      </div>
    </div>
  )
}

// ─── SNOOZE PANEL ────────────────────────────────────────────
function SnoozePanel({task,onSnooze,onClose}){
  const [custom,setCustom]=useState('')
  const snooze=iso=>{onSnooze(task.id,iso);onClose()}
  const snoozeMin=mins=>{const d=new Date();d.setMinutes(d.getMinutes()+mins);snooze(d.toISOString())}
  return (
    <div className="overlay" onClick={e=>e.target===e.currentTarget&&onClose()}>
      <div className="modal" style={{maxWidth:380}}>
        <div className="m-hd"><span className="m-title">💤 Snooze</span><button className="m-x" onClick={onClose}>×</button></div>
        <div className="m-body">
          <div><div className="fl" style={{marginBottom:8}}>Quick</div>
            <div className="snooze-grid">
              {SNOOZE_QUICK.map(s=><button key={s.label} className="sn-opt quick" onClick={()=>snoozeMin(s.mins)}>{s.label}</button>)}
            </div>
          </div>
          <div><div className="fl" style={{marginBottom:8}}>Later</div>
            <div className="snooze-grid">
              {SNOOZE_MORE.map(s=><button key={s.label} className="sn-opt" onClick={()=>snooze(s.fn())}>{s.label}</button>)}
            </div>
          </div>
          <div><div className="fl">Pick date &amp; time</div>
            <div style={{display:'flex',gap:7,marginTop:4}}>
              <input type="datetime-local" className="fi" style={{flex:1,colorScheme:'dark'}} value={custom} onChange={e=>setCustom(e.target.value)}
                min={toLocalDT(new Date().toISOString())}/>
              <button className="btn-ok" disabled={!custom} onClick={()=>{ if(custom) snooze(new Date(custom).toISOString()) }}>Set</button>
            </div>
          </div>
        </div>
        <div className="m-ft">
          <button className="btn-ok" onClick={onClose}>Done</button>
        </div>
      </div>
    </div>
  )
}

// ─── TASK CARD ───────────────────────────────────────────────
function TaskCard({task,colId,allTasks,onEdit,onAssign,onDone,onMove,onWake,onSnooze,onPin,onDragStart,onDragEnd}){
  const touchStart=useRef(null)
  const handleTouchStart=(e)=>{ touchStart.current={x:e.touches[0].clientX,y:e.touches[0].clientY} }
  const handleTouchEnd=(e)=>{
    if(!touchStart.current) return
    const dx=e.changedTouches[0].clientX-touchStart.current.x
    const dy=Math.abs(e.changedTouches[0].clientY-touchStart.current.y)
    if(Math.abs(dx)>80&&dy<40){
      e.preventDefault()
      if(dx>0&&colId!=='done') { if(blocked){ onDone(task.id) } else setConfirmQuickDone(true) }
      else if(dx<0) { const d=new Date(); d.setHours(d.getHours()+1); onSnooze(task.id,d.toISOString()) }
    }
    touchStart.current=null
  }
  const isDone=colId==='done',isInbox=colId==='inbox'
  const snzd=isSnoozed(task)
  const cat=CATEGORIES.find(c=>c.id===task.category)
  const blocked=(task.dependsOn||[]).some(id=>{ const d=allTasks.find(t=>t.id===id); return d&&d.colId!=='done' })
  const [showSnooze,setShowSnooze]=useState(false)
  const [confirmQuickDone,setConfirmQuickDone]=useState(false)
  const requestQuickDone=(e)=>{
    e?.stopPropagation?.()
    if(blocked){ onDone(task.id); return }
    setConfirmQuickDone(true)
  }
  const completeQuickDone=()=>{
    const ok=onDone(task.id)
    if(ok!==false) setConfirmQuickDone(false)
  }
  const notesText=task.notes||''
  const justWoke=task.wokeAt&&(Date.now()-task.wokeAt)<120000
  const age=taskAgeDays(task)
  const ageTint=age>=14?'rgba(239,68,68,.08)':age>=7?'rgba(245,158,11,.07)':undefined
  return (<>
      <div className={`card${isDone?' done-card':''}${snzd?' snoozed-card':''}${justWoke?' woke-card':''}${task.pinned?' pinned-card':''}`}
        draggable
        style={ageTint?{background:ageTint,borderColor:age>=14?'rgba(239,68,68,.15)':'rgba(245,158,11,.12)'}:{}}
        onClick={()=>onEdit(task)}
        onDragStart={e=>{e.stopPropagation();onDragStart(task.id)}}
        onDragEnd={()=>onDragEnd()}
        onTouchStart={handleTouchStart}
        onTouchEnd={handleTouchEnd}>
        {blocked&&<div className="dep-badge">🔒</div>}
        {task.pinned&&<div style={{position:'absolute',top:7,left:9,fontSize:11}}>📌</div>}
        {age>=7&&!isDone&&<div style={{position:'absolute',top:7,right:blocked?28:9,fontSize:9,fontFamily:"'DM Mono',monospace",color:age>=14?'#fca5a5':'var(--amber)',background:age>=14?'var(--rg)':'var(--ag)',padding:'1px 5px',borderRadius:4}}>{age}d old</div>}
        <div className="card-top">
          <div className="card-title">{task.title||<span style={{color:'var(--t3)'}}>Untitled</span>}</div>
          <div className="pri-dot" style={{background:PRI_COLOR[task.priority]||PRI_COLOR.medium}}/>
        </div>
        {cat?.id&&<div className="card-cat">{cat.label}</div>}
        {notesText&&<div className="card-notes">{notesText}</div>}
        {(task.tags||[]).length>0&&<div className="card-tags">{task.tags.map(t=><span key={t} className="card-tag" style={{background:tagColor(t)+'22',color:tagColor(t)}}>{t}</span>)}</div>}
        <ClBar items={task.checklist}/>
        <div className="card-ft">
          <div className="chips">
            {!isInbox&&task.assignee&&task.assignee!==colId&&task.assignee!=='inbox'&&(
              <span className="c-own"><span className="c-odot" style={{background:OWNER_COLOR[task.assignee]||'#64748b'}}/>{task.assignee==='both'?'Both':task.assignee}</span>
            )}
            <DueChip date={task.dueDate} col={colId}/>
            {task.recur&&task.recur!=='none'&&<span className="chip c-recur">↻</span>}
            {task.url&&<span className="chip" style={{background:'var(--s3)',color:'var(--t3)'}}>🔗</span>}
            {(task.comments||[]).length>0&&<span className="chip" style={{background:'var(--s3)',color:'var(--t3)'}}>💬{task.comments.length}</span>}
            {snzd&&<span className="chip c-snooze">💤</span>}
          </div>
          {!isDone&&task.progress==='doing'&&<span className="chip c-doing">In Progress</span>}
        </div>
        {isInbox&&(
          <div className="strip" onClick={e=>e.stopPropagation()}>
            <span style={{fontSize:10,color:'var(--t3)',marginRight:2}}>→</span>
            <button className="sbtn st" onClick={()=>onAssign(task.id,'troy')}>Troy</button>
            <button className="sbtn sk" onClick={()=>onAssign(task.id,'khatra')}>Khatra</button>
            <button className="sbtn" style={{color:'#a78bfa',borderColor:'rgba(167,139,250,.3)'}} onClick={()=>onAssign(task.id,'both')}>Both</button>
            <button className="sbtn" onClick={e=>{e.stopPropagation();setShowSnooze(true)}}>💤</button>
            <button className="sbtn sd" onClick={requestQuickDone}>✓</button>
          </div>
        )}
        {!isInbox&&!isDone&&(
          <div className="strip" onClick={e=>e.stopPropagation()}>
            <button className="sbtn si" onClick={()=>onMove(task.id,'inbox')}>← Inbox</button>
            {colId==='troy'&&<button className="sbtn sk" onClick={()=>onAssign(task.id,'khatra')}>→ Khatra</button>}
            {colId==='troy'&&<button className="sbtn" style={{color:'#a78bfa'}} onClick={()=>onAssign(task.id,'both')}>→ Both</button>}
            {colId==='khatra'&&<button className="sbtn st" onClick={()=>onAssign(task.id,'troy')}>→ Troy</button>}
            {colId==='khatra'&&<button className="sbtn" style={{color:'#a78bfa'}} onClick={()=>onAssign(task.id,'both')}>→ Both</button>}
            {colId==='both'&&<button className="sbtn st" onClick={()=>onAssign(task.id,'troy')}>→ Troy</button>}
            {colId==='both'&&<button className="sbtn sk" onClick={()=>onAssign(task.id,'khatra')}>→ Khatra</button>}
            <button className="sbtn" onClick={e=>{e.stopPropagation();setShowSnooze(true)}}>💤</button>
            {!blocked&&<button className="sbtn sd" onClick={requestQuickDone}>✓ Done</button>}
            {blocked&&<span style={{fontSize:10,color:'var(--t3)',marginLeft:'auto'}}>🔒 blocked</span>}
          </div>
        )}
        {isDone&&(
          <div className="strip" onClick={e=>e.stopPropagation()}>
            <button className="sbtn si" onClick={()=>onMove(task.id,'inbox')}>↩ Reopen</button>
          </div>
        )}
        {colId==='snoozed'&&(
          <div className="strip" onClick={e=>e.stopPropagation()}>
            <button className="sbtn si" onClick={()=>onWake(task.id,task.preSnoozeColId||'inbox')}>⏰ Wake up now</button>
          </div>
        )}
      </div>
      {confirmQuickDone&&(
        <div className="overlay" style={{zIndex:260}} onClick={e=>e.target===e.currentTarget&&setConfirmQuickDone(false)}>
          <div className="modal" style={{maxWidth:360}} onClick={e=>e.stopPropagation()}>
            <div className="m-hd"><span className="m-title">Mark task done?</span><button className="m-x" onClick={()=>setConfirmQuickDone(false)}>×</button></div>
            <div className="m-body">
              <p style={{fontSize:13,color:'var(--t2)',lineHeight:1.6}}>This quick action will move the task to Done.</p>
              <div style={{background:'var(--s2)',border:'1px solid var(--b1)',borderRadius:8,padding:10,fontSize:12,color:'var(--t1)'}}>{task.title||'Untitled task'}</div>
            </div>
            <div className="m-ft">
              <button className="btn-sec" onClick={()=>setConfirmQuickDone(false)}>Cancel</button>
              <button className="btn-ok" onClick={completeQuickDone}>✓ Mark Done</button>
            </div>
          </div>
        </div>
      )}
      {showSnooze&&<SnoozePanel task={task} onSnooze={onSnooze} onClose={()=>setShowSnooze(false)}/>}
      </>)
}


function MentionInput({ value, onChange, onSubmit, currentUser }) {
  const [showDropdown, setShowDropdown] = useState(false)
  const [activeIdx, setActiveIdx]       = useState(0)
  const ref  = useRef()
  const { listening, start } = useVoice(v => onChange(value + (value ? ' ' : '') + v))

  // Other person first in dropdown, self second
  const OTHER = currentUser === 'troy' ? 'khatra' : 'troy'
  const MENTION_OPTIONS = [
    { key: OTHER,        label: OTHER.charAt(0).toUpperCase()+OTHER.slice(1),   color: OWNER_COLOR[OTHER]        },
    { key: currentUser||'troy', label: (currentUser||'troy').charAt(0).toUpperCase()+(currentUser||'troy').slice(1)+' (you)', color: OWNER_COLOR[currentUser||'troy'] },
  ]

  // Render text with coloured @mention badges
  const renderPreview = (text) => {
    if (!text) return null
    const parts = text.split(/(@troy|@khatra)/gi)
    return parts.map((p, i) => {
      const m = MENTION_OPTIONS.find(o => p.toLowerCase() === '@' + o.key)
      if (m) return (
        <span key={i} className="mention-badge" style={{ background: m.color + '22', color: m.color }}>
          @{m.label}
        </span>
      )
      return <span key={i}>{p}</span>
    })
  }

  const handleKey = (e) => {
    if (showDropdown) {
      if (e.key === 'ArrowDown') { e.preventDefault(); setActiveIdx(i => (i+1)%MENTION_OPTIONS.length) }
      if (e.key === 'ArrowUp')   { e.preventDefault(); setActiveIdx(i => (i-1+MENTION_OPTIONS.length)%MENTION_OPTIONS.length) }
      if (e.key === 'Enter' || e.key === 'Tab') {
        e.preventDefault()
        insertMention(MENTION_OPTIONS[activeIdx])
        return
      }
      if (e.key === 'Escape') { setShowDropdown(false); return }
    }
    if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); onSubmit() }
  }

  const handleChange = (e) => {
    const v = e.target.value
    onChange(v)
    // Show dropdown when @ is the last non-space char typed
    const lastAt = v.lastIndexOf('@')
    if (lastAt >= 0 && lastAt === v.length - 1) {
      setShowDropdown(true); setActiveIdx(0)
    } else if (lastAt >= 0) {
      const fragment = v.slice(lastAt + 1).toLowerCase()
      const match = MENTION_OPTIONS.some(o => o.key.startsWith(fragment) || o.label.toLowerCase().startsWith(fragment))
      setShowDropdown(match)
    } else {
      setShowDropdown(false)
    }
  }

  const insertMention = (opt) => {
    const lastAt = value.lastIndexOf('@')
    const newVal = value.slice(0, lastAt) + '@' + opt.label + ' '
    onChange(newVal)
    setShowDropdown(false)
    ref.current?.focus()
  }

  const hasMentions = /@troy|@khatra/i.test(value)

  return (
    <div className="mention-wrap">
      {showDropdown && (
        <div className="mention-dropdown">
          {MENTION_OPTIONS.map((opt, i) => (
            <div key={opt.key} className={`mention-opt${i===activeIdx?' active':''}`}
              onMouseDown={e=>{ e.preventDefault(); insertMention(opt) }}>
              <div className="mention-av" style={{ background: opt.color+'22', color: opt.color }}>
                {opt.label[0]}
              </div>
              {opt.label}
            </div>
          ))}
        </div>
      )}
      {hasMentions && (
        <div style={{ background:'var(--s2)', border:'1px solid var(--b1)', borderRadius:7, padding:'6px 10px', marginBottom:6, fontSize:12, lineHeight:1.6 }}>
          {renderPreview(value)}
        </div>
      )}
      <div className="mention-input-row">
        <div style={{ position:'relative', flex:1 }}>
          <textarea ref={ref} className="fi ta" style={{ minHeight:60, paddingRight:42, resize:'vertical' }}
            placeholder={'Add a comment… (type @ to mention)'}
            value={value} onChange={handleChange} onKeyDown={handleKey}/>
          <MicBtn onResult={v=>onChange(value+(value?' ':'')+v)}
            style={{ position:'absolute', top:8, right:8, padding:'4px 6px', fontSize:12 }}/>
        </div>
        <button className="btn-ok" style={{ alignSelf:'flex-end', flexShrink:0 }} onClick={onSubmit}>Post</button>
      </div>
      <div style={{ fontSize:10, color:'var(--t3)', marginTop:4 }}>
        Type @ to mention · Ctrl+Enter to post · 🎤 for voice
      </div>
    </div>
  )
}

// ─── EDIT MODAL ─────────────────────────────────────────────
function EditModal({task,allTasks,currentUser,onSave,onDelete,onArchive,onDone,onPushNotify,onAddDep,onClose}){
  const draftKey='tb_draft_'+task.id

  // ── ALL HOOKS DECLARED FIRST (before any useEffect that references them) ──
  const fRef       = useRef(null)
  const clInputRef = useRef(null)
  const notesRef   = useRef(null)
  const autoSaveRef= useRef(null)
  const mountedRef = useRef(false)
  const lastSavedRef = useRef(null)
  const [f,setF]   = useState(()=>{
    const base={...task,checklist:[...(task.checklist||[])],tags:[...(task.tags||[])],comments:[...(task.comments||[])],dependsOn:[...(task.dependsOn||[])]}
    try{
      const draft=JSON.parse(localStorage.getItem('tb_draft_'+task.id)||'null')
      if(draft&&draft.id===task.id){
        const draftTime=new Date(draft.updatedAt||draft.createdAt||0).getTime()
        const taskTime =new Date(task.updatedAt ||task.createdAt ||0).getTime()
        if(draftTime>taskTime) return {...base,...draft,checklist:draft.checklist||[],tags:draft.tags||[],comments:draft.comments||[],dependsOn:draft.dependsOn||[]}
        localStorage.removeItem('tb_draft_'+task.id)
      }
    }catch{}
    return base
  })
  const [tab,setTab]           = useState('details')
  const [newItem,setNew]       = useState('')
  const [newTag,setNT]         = useState('')
  const [newCmt,setNC]         = useState('')
  const [newDepTitle,setNewDepTitle] = useState('')
  const [saveState,setSaveState] = useState('saved')
  const [lastSavedAt,setLastSavedAt] = useState(null)

  // ── EFFECTS (all refs and state are now declared above) ──
  useEffect(()=>{
    lastSavedRef.current=JSON.stringify(fRef.current||f)
    setLastSavedAt(new Date())
  },[])
  useEffect(()=>{ fRef.current=f },[f])
  useEffect(()=>{ if(tab==='checklist') setTimeout(()=>clInputRef.current?.focus(),50) },[tab])
  const resizeNotes=()=>{
    const el=notesRef.current
    if(!el) return
    el.style.height='auto'
    el.style.height=Math.max(150,el.scrollHeight)+'px'
  }
  useEffect(()=>{ if(tab==='details') setTimeout(resizeNotes,50) },[tab])
  useEffect(()=>{ if(tab==='details') resizeNotes() },[f.notes,tab])
  useEffect(()=>{
    const id=setInterval(()=>{ try{ if(fRef.current) localStorage.setItem(draftKey,JSON.stringify({...fRef.current,updatedAt:fRef.current.updatedAt||new Date().toISOString()})) }catch{} },20000)
    return ()=>{ clearInterval(id); try{ localStorage.removeItem(draftKey) }catch{} }
  },[])
  useEffect(()=>{
    if(!mountedRef.current){ mountedRef.current=true; return }
    const json=JSON.stringify(f)
    if(json===lastSavedRef.current) return
    setSaveState('saving')
    if(autoSaveRef.current) clearTimeout(autoSaveRef.current)
    autoSaveRef.current=setTimeout(()=>{
      try{
        onSave(f,false,{silent:true})
        lastSavedRef.current=JSON.stringify(fRef.current||f)
        setLastSavedAt(new Date())
        setSaveState('saved')
      }catch(e){
        console.warn('autosave failed',e)
        setSaveState('err')
      }
    },1500)
    return ()=>{ if(autoSaveRef.current) clearTimeout(autoSaveRef.current) }
  },[f])
  const hasUnsavedChanges=()=>JSON.stringify(fRef.current||f)!==lastSavedRef.current || saveState==='err' || saveState==='saving'
  const saveNow=(close=false)=>{
    if(autoSaveRef.current) clearTimeout(autoSaveRef.current)
    setSaveState('saving')
    try{
      const cur=fRef.current||f
      onSave(cur,close)
      lastSavedRef.current=JSON.stringify(cur)
      setLastSavedAt(new Date())
      setSaveState('saved')
    }catch(e){
      console.warn('save failed',e)
      setSaveState('err')
      if(close) return
    }
  }
  const handleClose=()=>{
    if(hasUnsavedChanges()){
      const msg=saveState==='saving'
        ? 'Changes are still saving. Close anyway?'
        : 'You have unsaved changes. Close without saving?'
      if(!window.confirm(msg)) return
    }
    onClose()
  }
  const touchDraft=obj=>({...obj,updatedAt:new Date().toISOString(),updatedBy:currentUser||obj.updatedBy||null})
  const set=(k,v)=>setF(p=>touchDraft({...p,[k]:v}))
  const addItem=()=>{ if(!newItem.trim()) return; set('checklist',[...f.checklist,{id:uid(),text:newItem.trim(),checked:false}]); setNew('') }
  const addTag=t=>{ const tag=(t||newTag).trim().toLowerCase().replace(/\s+/g,'-'); if(!tag||f.tags.includes(tag)) return; set('tags',[...f.tags,tag]); setNT('') }
  const addCmt=()=>{
    if(!newCmt.trim()) return
    const comment={id:uid(),who:currentUser||'troy',text:newCmt.trim(),at:new Date().toISOString()}
    set('comments',[...f.comments,comment])
    setNC('')
    // Email the other person when a comment is posted
    const other=Object.keys(USERS).find(k=>k!==(currentUser||'troy'))
    if(other){
      const sender=USERS[currentUser||'troy'].label
      const recipient=USERS[other]
      sendEmail(
        recipient.email,
        recipient.label,
        `💬 ${sender} commented on "${f.title||'Untitled'}"`,
        'Hi '+recipient.label+',\n\n'+sender+' left a comment on "'+( f.title||'Untitled')+'":'+
        '\n\n"'+comment.text+'"\n\nOpen the task board to reply.'
      )
    }
  }
  const toggleDep=id=>{ const deps=Array.isArray(f.dependsOn)?f.dependsOn:[]; set('dependsOn',deps.includes(id)?deps.filter(d=>d!==id):[...deps,id]) }
  const addDepTask=()=>{
    if(!newDepTitle.trim()) return
    const depTask={
      id:uid(),colId:'inbox',assignee:'inbox',title:newDepTitle.trim(),
      notes:'',priority:'medium',progress:'todo',category:'',
      dueDate:null,dueTime:'09:00',reminder:'onday',url:'',
      checklist:[],comments:[],tags:[],dependsOn:[],recur:'none',
      snoozedUntil:null,preSnoozeColId:null,archivedAt:null,
      createdAt:new Date().toISOString(),updatedAt:new Date().toISOString(),updatedBy:currentUser||null,completedAt:null,pinned:false
    }
    // Add to allTasks via onAddDep, and link as dependency
    onAddDep(depTask)
    set('dependsOn',[...(Array.isArray(f.dependsOn)?f.dependsOn:[]),depTask.id])
    setNewDepTitle('')
  }
  const clDone=(f.checklist||[]).filter(i=>i&&i.checked).length
  const otherTasks=(allTasks||[]).filter(t=>t&&t.id!==f.id&&t.colId!=='done')
  const safeDeps=Array.isArray(f.dependsOn)?f.dependsOn:[]
  const isBlocked=safeDeps.some(id=>{ const d=(allTasks||[]).find(t=>t.id===id); return d&&d.colId!=='done' })
  return (
    <div className="overlay" onClick={e=>e.target===e.currentTarget&&handleClose()}>
      <div className="modal" onKeyDown={e=>{ if(e.ctrlKey&&e.key==='Enter'&&tab==='details'){ e.preventDefault(); saveNow(true) } }}>
        <div className="m-hd">
          <span className="m-title">Edit Task</span>
          <div style={{display:'flex',alignItems:'center',gap:7}}>
            {f.createdAt&&<span style={{fontSize:10,color:'var(--t3)',fontFamily:"'DM Mono',monospace"}}>{fmtDate(f.createdAt)}</span>}
            <button className="m-x" onClick={handleClose}>×</button>
          </div>
        </div>
        <div className="tabs">
          {[['details','Details'],['comments',(f.comments||[]).length?`💬 ${f.comments.length}`:'Comments'],['deps','Dependencies'],['checklist',f.checklist.length?`✓ ${clDone}/${f.checklist.length}`:'Checklist']].map(([id,lbl])=>(
            <button key={id} className={`tab${tab===id?' active':''}`} onClick={()=>setTab(id)}>{lbl}</button>
          ))}
        </div>
        <div className="m-body">
          {tab==='details'&&<>
            <div><div className="fl">Task name</div>
              <div style={{display:'flex',gap:7}}>
                <input className="fi" value={f.title} onChange={e=>set('title',e.target.value)} placeholder="What needs to be done?" style={{flex:1}}/>
                <MicBtn onResult={v=>set('title',v)}/>
              </div>
            </div>
            <div><div className="fl">Notes</div>
              <div style={{position:'relative'}}>
                <textarea ref={notesRef} className="fi ta" value={f.notes||''}
                  onChange={e=>{ set('notes',e.target.value); requestAnimationFrame(resizeNotes) }}
                  placeholder="Context, background, links…"
                  style={{paddingRight:42,minHeight:150,resize:'vertical',overflow:'hidden',maxHeight:'45vh'}}/>
                <MicBtn onResult={v=>set('notes',(f.notes||'')+' '+v)} style={{position:'absolute',top:8,right:8,padding:'4px 6px',fontSize:12}}/>
              </div>
            </div>
            <div className="frow">
              <div><div className="fl">Owner</div>
                <select className="fsel" value={f.assignee} onChange={e=>set('assignee',e.target.value)}>
                  <option value="inbox">Unassigned</option><option value="troy">Troy</option>
                  <option value="khatra">Khatra</option><option value="both">Both</option>
                </select>
              </div>
              <div><div className="fl">Priority</div>
                <select className="fsel" value={f.priority} onChange={e=>set('priority',e.target.value)}>
                  <option value="high">⬆ High</option><option value="medium">● Medium</option><option value="low">⬇ Low</option>
                </select>
              </div>
            </div>
            <div className="frow">
              <div><div className="fl">Progress</div>
                <select className="fsel" value={f.progress||'todo'} onChange={e=>set('progress',e.target.value)}>
                  <option value="todo">To Do</option>
                  <option value="doing">In Progress</option>
                </select>
              </div>
              <div><div className="fl">Recurring</div>
                <select className="fsel" value={f.recur||'none'} onChange={e=>{ const v=e.target.value; if((f.recur&&f.recur!=='none')&&v==='none'&&!window.confirm('Stop this recurring task? Future copies will no longer be created.')) return; set('recur',v) }}>
                  {RECUR_OPTIONS.map(r=><option key={r} value={r}>{r.charAt(0).toUpperCase()+r.slice(1)}</option>)}
                </select>
              </div>
            </div>
            <div>
              <div className="fl">Snooze</div>
              <div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:6}}>
                {[{l:'30m',m:30},{l:'1hr',m:60},{l:'3hr',m:180}].map(s=>(
                  <button key={s.l} className="sbtn" style={{padding:'5px 12px',fontSize:11,color:'#a78bfa',borderColor:'rgba(167,139,250,.3)',background:'var(--pg)'}}
                    onClick={()=>{ const d=new Date(); d.setMinutes(d.getMinutes()+s.m); set('snoozedUntil',d.toISOString()); set('colId','snoozed') }}>{s.l}</button>
                ))}
                {[
                  {l:'Tonight 8pm',fn:()=>{ const d=new Date(); d.setHours(20,0,0,0); if(d<new Date()) d.setDate(d.getDate()+1); return d.toISOString() }},
                  {l:'Tomorrow 9am',fn:()=>{ const d=new Date(); d.setDate(d.getDate()+1); d.setHours(9,0,0,0); return d.toISOString() }},
                  {l:'3 days',fn:()=>{ const d=new Date(); d.setDate(d.getDate()+3); d.setHours(9,0,0,0); return d.toISOString() }},
                  {l:'1 week',fn:()=>{ const d=new Date(); d.setDate(d.getDate()+7); d.setHours(9,0,0,0); return d.toISOString() }},
                ].map(s=>(
                  <button key={s.l} className="sbtn" style={{padding:'5px 12px',fontSize:11,color:'#a78bfa',borderColor:'rgba(167,139,250,.3)',background:'var(--pg)'}}
                    onClick={()=>{ set('snoozedUntil',s.fn()); set('colId','snoozed') }}>{s.l}</button>
                ))}
              </div>
              <input type="datetime-local" className="fsel" style={{colorScheme:'dark'}}
                value={toLocalDT(f.snoozedUntil)}
                onChange={e=>{ set('snoozedUntil',e.target.value?new Date(e.target.value).toISOString():null); if(e.target.value) set('colId','snoozed') }}/>
              {f.snoozedUntil&&<button style={{marginTop:6,fontSize:11,background:'transparent',border:'none',color:'var(--t3)',cursor:'pointer'}}
                onClick={()=>{ set('snoozedUntil',null); if(f.colId==='snoozed') set('colId',f.preSnoozeColId||'inbox') }}>✕ Clear snooze</button>}
            </div>
            <div><div className="fl">Due Date</div>
              <input type="date" className="fsel" style={{colorScheme:'dark'}} value={f.dueDate||''}
                onChange={e=>{
                  const d=e.target.value||null
                  set('dueDate',d)
                  const snooze=dueDateToSnooze(d)
                  if(snooze){ set('snoozedUntil',snooze); set('colId','snoozed'); set('preSnoozeColId',f.colId!=='snoozed'?f.colId:(f.preSnoozeColId||'inbox')) }
                  else if(!d){ set('snoozedUntil',null) }
                }}/>
            </div>
            <div><div className="fl">Link / URL</div>
              <input className="fi" value={f.url||''} onChange={e=>set('url',e.target.value)} placeholder="https://…"/>
              {f.url&&<a href={f.url} target="_blank" rel="noreferrer" style={{fontSize:11,color:'var(--amber)',display:'block',marginTop:5,wordBreak:'break-all'}}>Open ↗</a>}
            </div>
            <div style={{background:'var(--s2)',border:'1px solid var(--b1)',borderRadius:8,padding:'11px 13px'}}>
              <div className="fl" style={{marginBottom:7}}>Task info</div>
              {f.createdAt&&<div style={{fontSize:11,color:'var(--t3)',marginBottom:3}}>Created: <span style={{color:'var(--t2)'}}>{fmtDate(f.createdAt)}</span></div>}
              <div style={{fontSize:11,color:'var(--t3)',marginBottom:3}}>Last saved: <span style={{color:saveState==='err'?'#fca5a5':saveState==='saving'?'var(--amber)':'var(--green)'}}>{saveState==='saving'?'Saving…':saveState==='err'?'Save failed':lastSavedAt?fmtDT(lastSavedAt.toISOString()):'Saved'}</span></div>
              {f.completedAt&&<div style={{fontSize:11,color:'var(--t3)',marginBottom:3}}>Completed: <span style={{color:'var(--green)'}}>{fmtDate(f.completedAt)}</span></div>}
              <div style={{fontSize:11,color:'var(--t3)'}}>ID: <span style={{fontFamily:"'DM Mono',monospace",fontSize:10}}>{f.id}</span></div>
            </div>
          </>}
          {tab==='checklist'&&<>
            <div className="cl-list">
              {f.checklist.length===0&&<div style={{textAlign:'center',color:'var(--t3)',fontSize:12,padding:'14px 0'}}>No items yet</div>}
              {f.checklist.map(item=>(
                <div key={item.id} className="cl-item">
                  <input type="checkbox" className="cl-cb" checked={item.checked} onChange={()=>set('checklist',f.checklist.map(i=>i.id===item.id?{...i,checked:!i.checked}:i))}/>
                  <input className="cl-ti" value={item.text} style={{textDecoration:item.checked?'line-through':'none',color:item.checked?'var(--t3)':'var(--t1)'}} onChange={e=>set('checklist',f.checklist.map(i=>i.id===item.id?{...i,text:e.target.value}:i))}/>
                  <button className="cl-del" onClick={()=>set('checklist',f.checklist.filter(i=>i.id!==item.id))}>✕</button>
                </div>
              ))}
            </div>
            <div className="cl-add">
              <input ref={clInputRef} className="cl-new" placeholder="Add item…" value={newItem}
                onChange={e=>setNew(e.target.value)}
                onKeyDown={e=>{ if(e.key==='Enter'||(e.key==='Enter'&&e.ctrlKey)) addItem() }}/>
              <button className="cl-addbtn" onClick={addItem}>+ Add</button>
            </div>
          </>}
          {tab==='more'&&<>
            <div className="frow">
              <div><div className="fl">Due Date</div><input type="date" className="fsel" value={f.dueDate||''} onChange={e=>set('dueDate',e.target.value||null)} style={{colorScheme:'dark'}}/></div>
              <div><div className="fl">Due Time</div><input type="time" className="fsel" value={f.dueTime||''} onChange={e=>set('dueTime',e.target.value||'')} style={{colorScheme:'dark'}}/></div>
            </div>
            <div className="frow">
              <div><div className="fl">Reminder</div>
                <select className="fsel" value={f.reminder||''} onChange={e=>set('reminder',e.target.value)}>
                  <option value="">None</option><option value="onday">On due date</option><option value="1d">1 day before</option><option value="2d">2 days before</option><option value="1w">1 week before</option>
                </select>
              </div>
              <div><div className="fl">Snooze until</div>
                <input type="datetime-local" className="fsel" value={toLocalDT(f.snoozedUntil)} onChange={e=>set('snoozedUntil',e.target.value?new Date(e.target.value).toISOString():null)} style={{colorScheme:'dark'}}/>
              </div>
            </div>
            <div><div className="fl">Due Date</div>
              <input type="date" className="fsel" style={{colorScheme:'dark'}} value={f.dueDate||''}
                onChange={e=>{
                  const d=e.target.value||null
                  set('dueDate',d)
                  const snooze=dueDateToSnooze(d)
                  if(snooze){ set('snoozedUntil',snooze); set('colId','snoozed'); set('preSnoozeColId',f.colId!=='snoozed'?f.colId:(f.preSnoozeColId||'inbox')) }
                  else if(!d){ set('snoozedUntil',null) }
                }}/>
            </div>
            <div><div className="fl">Link / URL</div>
              <input className="fi" value={f.url||''} onChange={e=>set('url',e.target.value)} placeholder="https://…"/>
              {f.url&&<a href={f.url} target="_blank" rel="noreferrer" style={{fontSize:11,color:'var(--amber)',display:'block',marginTop:5,wordBreak:'break-all'}}>Open ↗</a>}
            </div>
            <div style={{background:'var(--s2)',border:'1px solid var(--b1)',borderRadius:8,padding:'11px 13px'}}>
              <div className="fl" style={{marginBottom:7}}>Task info</div>
              {f.createdAt&&<div style={{fontSize:11,color:'var(--t3)',marginBottom:3}}>Created: <span style={{color:'var(--t2)'}}>{fmtDate(f.createdAt)}</span></div>}
              <div style={{fontSize:11,color:'var(--t3)',marginBottom:3}}>Last saved: <span style={{color:saveState==='err'?'#fca5a5':saveState==='saving'?'var(--amber)':'var(--green)'}}>{saveState==='saving'?'Saving…':saveState==='err'?'Save failed':lastSavedAt?fmtDT(lastSavedAt.toISOString()):'Saved'}</span></div>
              {f.completedAt&&<div style={{fontSize:11,color:'var(--t3)',marginBottom:3}}>Completed: <span style={{color:'var(--green)'}}>{fmtDate(f.completedAt)}</span></div>}
              <div style={{fontSize:11,color:'var(--t3)'}}>ID: <span style={{fontFamily:"'DM Mono',monospace",fontSize:10}}>{f.id}</span></div>
            </div>
          </>}
          {tab==='comments'&&<>
            <div style={{display:'flex',flexDirection:'column',gap:8}}>
              {(f.comments||[]).length===0&&<div style={{textAlign:'center',color:'var(--t3)',fontSize:12,padding:'14px 0'}}>No comments yet</div>}
              {(f.comments||[]).map(cm=>(
                <div key={cm.id} className="comment">
                  <div className="c-hd">
                    <span className="c-who" style={{color:OWNER_COLOR[cm.who]||'var(--t2)'}}>
                      {cm.who?.charAt(0).toUpperCase()+cm.who?.slice(1)||'Unknown'}
                    </span>
                    <span className="c-when">{fmtDate(cm.at)}</span>
                  </div>
                  <div className="c-txt">{cm.text}</div>
                </div>
              ))}
            </div>
            <MentionInput value={newCmt} onChange={setNC} onSubmit={addCmt} currentUser={currentUser}/>
          </>}
          {tab==='deps'&&<>
            <p style={{fontSize:12,color:'var(--t3)',lineHeight:1.5}}>This task is blocked until all linked tasks are completed.</p>
            <div style={{border:'1px solid var(--b1)',background:'var(--s2)',borderRadius:8,padding:10}}>
              <div className="fl" style={{marginBottom:6}}>Create new dependency task</div>
              <div className="cl-add">
                <input className="cl-new" placeholder="New task title… (goes to Inbox)"
                  value={newDepTitle} onChange={e=>setNewDepTitle(e.target.value)}
                  onKeyDown={e=>{ if(e.key==='Enter'||(e.ctrlKey&&e.key==='Enter')){ e.preventDefault(); addDepTask() } }}/>
                <button className="cl-addbtn" onClick={addDepTask}>+ Create</button>
              </div>
              <div style={{fontSize:10,color:'var(--t3)',marginTop:5}}>New task will be added to Inbox and linked as a dependency</div>
            </div>
            <div style={{display:'flex',flexDirection:'column',gap:6}}>
              <div className="fl" style={{marginBottom:0}}>Existing tasks</div>
              {otherTasks.length===0&&safeDeps.length===0&&<div style={{textAlign:'center',color:'var(--t3)',fontSize:12,padding:'8px 0'}}>No dependencies linked yet</div>}
              {otherTasks.map(t=>{
                const sel=safeDeps.includes(t.id)
                return <div key={t.id} className="dep-item" style={{borderColor:sel?'var(--amber)':'var(--b1)',background:sel?'var(--ag)':'var(--s2)'}} onClick={()=>toggleDep(t.id)}>
                  <span style={{fontSize:14}}>{sel?'☑':'☐'}</span>
                  <span style={{flex:1}}>{t.title||'Untitled'}</span>
                  <span style={{fontSize:10,color:'var(--t3)',background:'var(--s3)',padding:'1px 5px',borderRadius:3}}>{t.colId}</span>
                </div>
              })}
            </div>
          </>}
        </div>
        <div className="m-ft">
          <div className="ft-left">
            <button className="btn-del" onClick={()=>onDelete(task.id)}>Delete</button>
            <button className="btn-arch" onClick={()=>onArchive(task.id)}>Archive</button>
          </div>
          <div className="ft-right">
            <button className="btn-sec" onClick={handleClose}>Cancel</button>
            {task.colId!=='done'&&<button className="btn-sec" disabled={isBlocked} title={isBlocked?'Complete dependencies first':'Mark done'} style={{background:'var(--gg)',color:'var(--green)',borderColor:'rgba(52,211,153,.3)'}} onClick={()=>{if(isBlocked)return;saveNow(false);const ok=onDone(f.id);if(ok!==false)onClose()}}>{isBlocked?'🔒 Blocked':'✓ Mark Done'}</button>}
            <button className="btn-sec" style={{color:'var(--purple)',borderColor:'rgba(167,139,250,.3)'}} onClick={()=>onPushNotify(f)} title="Send push to other person">📣</button>
            <button className="btn-ok" onClick={()=>saveNow(true)}>Save &amp; Close</button>
          </div>
        </div>
      </div>
    </div>
  )
}

// ─── WHO PICKER ─────────────────────────────────────────────
function WhoPicker({onPick}){
  const [sel,setSel]=useState(null)
  return (
    <div className="overlay">
      <div className="modal" style={{maxWidth:380}}>
        <div className="m-hd"><span className="m-title">Who are you?</span></div>
        <div className="m-body">
          <p style={{fontSize:13,color:'var(--t2)'}}>Pick your name — the app remembers you on this device.</p>
          <div className="who-grid">
            {Object.entries(USERS).map(([key,u])=>(
              <div key={key} className={`who-card${sel===key?' sel':''}`} onClick={()=>setSel(key)}>
                <div className="who-av" style={{background:u.color+'22',color:u.color}}>{u.label[0]}</div>
                <div className="who-nm">{u.label}</div>
              </div>
            ))}
          </div>
        </div>
        <div className="m-ft"><button className="btn-ok" disabled={!sel} onClick={()=>sel&&onPick(sel)}>Continue</button></div>
      </div>
    </div>
  )
}

// ─── SETTINGS MODAL ─────────────────────────────────────────
function SettingsModal({prefs,onSave,onClose,onExportBackup}){
  const [p,setP]=useState({...prefs})
  const set=(k,v)=>setP(x=>({...x,[k]:v}))
  const Section=({label})=><div style={{marginTop:12,marginBottom:4}}><span style={{fontSize:11,fontWeight:700,color:'var(--t3)',textTransform:'uppercase',letterSpacing:'.5px'}}>{label}</span></div>
  const Row=({label,sub,k,type='toggle',opts})=>(
    <div className="srow">
      <div><div className="slb">{label}</div>{sub&&<div className="ssb">{sub}</div>}</div>
      {type==='toggle'&&<Toggle on={!!p[k]} onChange={v=>set(k,v)}/>}
      {type==='select'&&<select className="fsel" style={{width:'auto'}} value={p[k]||''} onChange={e=>set(k,e.target.value)}>{opts.map(o=><option key={o.v} value={o.v}>{o.l}</option>)}</select>}
    </div>
  )
  return (
    <div className="overlay" onClick={e=>e.target===e.currentTarget&&onClose()}>
      <div className="modal" style={{maxWidth:460}}>
        <div className="m-hd"><span className="m-title">Settings</span><button className="m-x" onClick={onClose}>×</button></div>
        <div className="m-body">
          <Section label="Appearance"/>
          <Row label="Dark mode" sub="Switch between dark and light theme" k="dark" type="toggle"/>
          <Row label="Compact cards" sub="Smaller cards, less padding" k="compact" type="toggle"/>
          <Section label="Notifications"/>
          <Row label="Email notifications" sub="Email when tasks are assigned to you" k="emailEnabled" type="toggle"/>
          <Row label="High priority alerts" sub="Email immediately for high priority assignments" k="emailHigh" type="toggle"/>
          <Row label="Due date reminders" sub="Email when tasks are due today or overdue" k="pushDue" type="toggle"/>
          <Row label="Weekly summary" sub="Monday morning digest sent to both of you" k="weeklySummary" type="toggle"/>
          <Section label="Data safety"/>
          <div className="srow">
            <div><div className="slb">Export backup</div><div className="ssb">Download a full JSON backup before major changes or production deployments.</div></div>
            <button className="btn-sec" onClick={onExportBackup}>Export JSON</button>
          </div>
          <Section label="Board defaults"/>
          <Row label="Default priority" k="defaultPriority" type="select" opts={[{v:'high',l:'High'},{v:'medium',l:'Medium'},{v:'low',l:'Low'}]}/>
          <Row label="Auto-route from Inbox" sub="Move task to owner column when owner is set" k="autoRoute" type="toggle"/>
          <Row label="Show archived tasks" sub="Show archived tasks in Done column" k="showArchived" type="toggle"/>
        </div>
        <div className="m-ft"><button className="btn-sec" onClick={onClose}>Cancel</button><button className="btn-ok" onClick={()=>onSave(p)}>Save</button></div>
      </div>
    </div>
  )
}

// ─── SHARE MODAL ────────────────────────────────────────────
function ShareModal({onClose}){
  const url=window.location.href
  const ref=useRef()
  const [copied,setCopied]=useState(false)
  useEffect(()=>{setTimeout(()=>ref.current?.select(),100)},[])
  const copy=()=>{
    ref.current?.select()
    const ok=document.execCommand('copy')
    if(ok){setCopied(true);setTimeout(()=>setCopied(false),2500);return}
    navigator.clipboard?.writeText(url).then(()=>{setCopied(true);setTimeout(()=>setCopied(false),2500)}).catch(()=>{})
  }
  return (
    <div className="overlay" onClick={e=>e.target===e.currentTarget&&onClose()}>
      <div className="modal" style={{maxWidth:420}}>
        <div className="m-hd"><span className="m-title">Share Board</span><button className="m-x" onClick={onClose}>×</button></div>
        <div className="m-body">
          <p style={{fontSize:12,color:'var(--t2)',lineHeight:1.6}}>Copy this link and send to Khatra. You'll both see and edit the same live board.</p>
          <div className="share-box">
            <input ref={ref} className="share-url" value={url} readOnly onClick={()=>ref.current?.select()}/>
            <button className={`share-copy${copied?' done':''}`} onClick={copy}>{copied?'✓ Copied!':'Copy link'}</button>
          </div>
        </div>
        <div className="m-ft"><button className="btn-sec" onClick={onClose}>Close</button></div>
      </div>
    </div>
  )
}

// ─── MAIN APP ────────────────────────────────────────────────
export default function App(){
  const [tasks,setTasks]=useState([])
  const [ready,setReady]=useState(false)
  const [online,setOnline]=useState(true)
  const [prefs,setPrefs]=useState(()=>({dark:true,emailEnabled:true,emailHigh:true,pushDue:true,autoRoute:true,showSnoozed:false,showArchived:false,compact:false,defaultPriority:'medium',weeklySummary:false,...loadPrefs()}))
  const [currentUser,setCurrentUser]=useState(()=>loadPrefs().currentUser||null)
  const [addingTo,setAddingTo]=useState(null)
  const [editing,setEditing]=useState(null)
  const [showDone,setShowDone]=useState(true)
  const [showSnoozed,setShowSnoozed]=useState(true)
  const [sharing,setSharing]=useState(false)
  const [settings,setSettings]=useState(false)
  const [toast,setToast]=useState(null)
  const [updateAvailable,setUpdateAvailable]=useState(false)
  const [filterTag,setFilterTag]=useState(null)
  const [filterCat,setFilterCat]=useState(null)
  const [bulkSel,setBulkSel]=useState([])
  const [bulkMode,setBulkMode]=useState(false)
  const [dragOver,setDragOver]=useState(null)
  const dragId=useRef(null)
  const [search,setSearch]=useState('')
  const [statFilter,setStatFilter]=useState(null)  // 'overdue'|'open'|'done'|'snoozed'|'inbox'|null
  const [showSearch,setShowSearch]=useState(false)
  const searchRef=useRef(null)
  const [sortBy,setSortBy]=useState('priority') // 'priority' | 'due' | 'created'
  const writeLock=useRef(0)
  const editingRef=useRef(null)  // tracks open modal — pauses poll while editing

  // Keep ref in sync with editing state so poll check doesn't need state
  useEffect(()=>{ editingRef.current=editing },[editing])

  useEffect(()=>{
    document.title = ENV === 'sandbox'
      ? 'SANDBOX — Troy & Khatra Task Board'
      : 'Troy & Khatra — Task Board'
  },[])

  useEffect(()=>{
    if('serviceWorker' in navigator){
      navigator.serviceWorker.register('/sw.js').catch(()=>{})
    }
    const checkVersion=async()=>{
      try{
        const res=await fetch('/version.json?ts='+Date.now(),{cache:'no-store'})
        if(!res.ok) return
        const data=await res.json()
        if(data.version&&data.version!==APP_VERSION) setUpdateAvailable(true)
      }catch{}
    }
    checkVersion()
    const id=setInterval(checkVersion,60000)
    return ()=>clearInterval(id)
  },[])

  const refreshToLatest=async()=>{
    try{
      if('serviceWorker' in navigator){
        const regs=await navigator.serviceWorker.getRegistrations()
        await Promise.all(regs.map(r=>r.update().catch(()=>{})))
      }
    }catch{}
    window.location.reload()
  }

  useEffect(()=>{
    const unsub=subscribeTasks(remote=>{
      setReady(true);setOnline(true)
      if(Date.now()-writeLock.current>3000) setTasks(remote)
    })
    return unsub
  },[])

  useEffect(()=>{
    const on=()=>setOnline(true),off=()=>setOnline(false)
    window.addEventListener('online',on);window.addEventListener('offline',off)
    return()=>{window.removeEventListener('online',on);window.removeEventListener('offline',off)}
  },[])

  // Keyboard shortcuts: / = search, Escape = close search
  useEffect(()=>{
    const handler=(e)=>{
      if(e.key==='/'&&!e.target.matches('input,textarea,select')){
        e.preventDefault(); setShowSearch(true); setTimeout(()=>searchRef.current?.focus(),50)
      }
      if(e.key==='Escape'){ setShowSearch(false); setSearch(''); setStatFilter(null) }
      if(e.key==='n'&&!e.target.matches('input,textarea,select')){
        e.preventDefault(); setAddingTo(a=>a==='inbox'?null:'inbox')
      }
    }
    window.addEventListener('keydown',handler)
    return ()=>window.removeEventListener('keydown',handler)
  },[])

  // Wake up snoozed tasks when snooze expires and email assignee once per wake event
  useEffect(()=>{
    const id=setInterval(()=>{
      setTasks(prev=>{
        const now=new Date()
        const needsWake=prev.filter(t=>t.colId==='snoozed'&&t.snoozedUntil&&new Date(t.snoozedUntil)<=now)
        if(!needsWake.length) return prev
        const nowIso=new Date().toISOString()
        const next=prev.map(t=>needsWake.find(w=>w.id===t.id)
          ?{...t,colId:t.preSnoozeColId||OWNER_TO_COL[t.assignee]||'inbox',snoozedUntil:null,preSnoozeColId:null,wokeAt:Date.now(),snoozeNotifiedAt:t.snoozeNotifiedAt||nowIso,updatedAt:nowIso,updatedBy:'system'}
          :t)
        needsWake.forEach(t=>{
          if(t.snoozeNotifiedAt) return
          const keys=t.assignee==='both'?['troy','khatra']:(USERS[t.assignee]?[t.assignee]:[])
          keys.forEach(k=>{
            const u=USERS[k]
            sendEmail(u.email,u.label,'⏰ Snoozed task is due',
              'Hi '+u.label+', this snoozed task is now due: "'+(t.title||'Untitled')+'".'+(t.notes?'\n\nNotes: '+t.notes:'')+'\n\nOpen the task board to action it.')
          })
        })
        setTimeout(()=>saveTasks(next),0)
        return next
      })
    },30000) // check every 30s
    return ()=>clearInterval(id)
  },[])

  useEffect(()=>{
    if(currentUser&&prefs.pushEnabled) registerPushUser(currentUser)
  },[currentUser,prefs.pushEnabled])

  useEffect(()=>{
    if(!ready||!currentUser||!prefs.pushDue) return
    // Check overdue on load and then every hour — max one email per day per user
    const checkOverdue=()=>{
      const now=new Date()
      const hour=now.getHours()
      // Only remind between 8am and 8pm AEST
      if(hour<8||hour>=20) return
      const lastKey='tb_lastReminder_'+currentUser
      const last=localStorage.getItem(lastKey)
      if(last&&new Date(last).toDateString()===now.toDateString()) return // already sent today
      const due=tasks.filter(t=>t.colId===currentUser&&!isSnoozed(t)&&t.dueDate&&daysUntil(t.dueDate)<=0)
      if(due.length>0){
        const u=USERS[currentUser]
        const taskList=due.map(t=>'- '+(t.title||'Untitled')).join(', ')
        sendEmail(u.email,u.label,'⏰ Tasks overdue',
          'Hi '+u.label+', these tasks are overdue: '+taskList+'. Open the task board.')
        localStorage.setItem(lastKey,now.toISOString())
      }
    }
    checkOverdue()
    const id=setInterval(checkOverdue, 60*60*1000) // check every hour
    return ()=>clearInterval(id)
  },[ready])

  useEffect(()=>{
    if(!ready||!prefs.weeklySummary) return
    const last=loadPrefs().lastWeekly,now=new Date()
    if(now.getDay()===1&&(!last||new Date(last).toDateString()!==now.toDateString())){
      const open=tasks.filter(t=>t.colId!=='done').length
      const over=tasks.filter(t=>isOver(t.dueDate,t.colId)).length
      const doneCt=tasks.filter(t=>t.completedAt&&new Date(t.completedAt)>new Date(now-7*86400000)).length
      Object.values(USERS).forEach(u=>{
        sendEmail(u.email,u.label,'Weekly task summary',
          'Hi '+u.label+', weekly summary: '+open+' open, '+over+' overdue, '+doneCt+' done this week.')
      })
      savePrefs({...prefs,currentUser,lastWeekly:now.toISOString()})
    }
  },[ready])

  const toast_=msg=>{setToast(msg);setTimeout(()=>setToast(null),2400)}

  const write=fn=>{
    setTasks(prev=>{
      const next=fn(prev)
      writeLock.current=Date.now()
      saveTasks(next).catch(()=>toast_('⚠ Save failed'))
      return next
    })
  }

  const addTask=(task,destCol)=>{
    write(p=>[{...task,colId:destCol,updatedAt:new Date().toISOString(),updatedBy:currentUser||null},...p])
    setAddingTo(null)
    if(destCol!=='inbox'&&destCol!==currentUser&&prefs.emailEnabled!==false){
      const isHigh   = task.priority==='high'
      const recipient= USERS[destCol]
      const sender   = currentUser?USERS[currentUser].label:'Someone'
      if(recipient&&(isHigh||prefs.emailHigh!==false)){
        const subject = isHigh?`🔴 High priority task assigned to you`:`New task assigned to you`
        const message = `Hi ${recipient.label},

${sender} assigned you a task:

"${task.title||'Untitled'}"
Priority: ${task.priority}
${task.dueDate?`Due: ${task.dueDate}
`:''}${task.notes?`
Notes: ${task.notes}
`:''}${formatComments(task)}
Open the task board to view it.`
        sendEmail(recipient.email, recipient.label, subject, message)
      }
    }
  }

  const assignTask=(id,colId)=>{
    const task=tasks.find(t=>t.id===id)
    write(p=>p.map(t=>t.id===id?{...t,colId,assignee:colId==='troy'||colId==='khatra'?colId:'both',updatedAt:new Date().toISOString(),updatedBy:currentUser||null}:t))
    if(task&&colId!=='inbox'&&colId!==currentUser&&prefs.emailEnabled!==false){
      const isHigh   = task.priority==='high'
      const recipient= USERS[colId]
      const sender   = currentUser?USERS[currentUser].label:'Someone'
      if(recipient&&(isHigh||prefs.emailHigh!==false)){
        const subject = isHigh?`🔴 High priority task assigned to you`:`Task assigned to you`
        const message = `Hi ${recipient.label},

${sender} assigned you:

"${task.title||'Untitled'}"
Priority: ${task.priority}
${task.dueDate?`Due: ${task.dueDate}
`:''}${task.notes?`
Notes: ${task.notes}
`:''}${formatComments(task)}
Open the task board to view it.`
        sendEmail(recipient.email, recipient.label, subject, message)
      }
    }
  }

  const doneTask=(id,skipConfetti=false)=>{
    const task=tasks.find(t=>t.id===id)
    const blocked=(task?.dependsOn||[]).some(depId=>{
      const dep=tasks.find(t=>t.id===depId)
      return dep&&dep.colId!=='done'
    })
    if(blocked){
      toast_('Complete dependencies before marking this task done')
      return false
    }
    if(!skipConfetti) fireConfetti()
    write(p=>{
      const next=p.map(t=>t.id===id?{...t,colId:'done',snoozedUntil:null,preSnoozeColId:null,wokeAt:null,completedAt:new Date().toISOString(),updatedAt:new Date().toISOString(),updatedBy:currentUser||null}:t)
      if(task?.recur&&task.recur!=='none'){const r=nextRecur(task);if(r) return [...next,r]}
      return next
    })
    return true
  }

  const moveTask   =(id,colId)=>write(p=>p.map(t=>t.id===id?{...t,colId,completedAt:null,updatedAt:new Date().toISOString(),updatedBy:currentUser||null}:t))
  const wakeTask   =(id,colId)=>write(p=>p.map(t=>t.id===id?{...t,colId:colId||'inbox',snoozedUntil:null,preSnoozeColId:null,wokeAt:Date.now(),updatedAt:new Date().toISOString(),updatedBy:currentUser||null}:t))
  const pinTask    =(id)=>write(p=>p.map(t=>t.id===id?{...t,pinned:!t.pinned,updatedAt:new Date().toISOString(),updatedBy:currentUser||null}:t))
  const snoozeTask=(id,until)=>write(p=>p.map(t=>
    t.id===id?{...t,snoozedUntil:until,preSnoozeColId:t.colId,colId:'snoozed',snoozeNotifiedAt:null,updatedAt:new Date().toISOString(),updatedBy:currentUser||null}:t
  ))
  const archiveTask=id=>{write(p=>p.map(t=>t.id===id?{...t,archivedAt:new Date().toISOString(),colId:'done',updatedAt:new Date().toISOString(),updatedBy:currentUser||null}:t));setEditing(null);toast_('Archived')}
  const deleteTask=id=>{write(p=>p.filter(t=>t.id!==id));setEditing(null)}

  const updateTask=(f, close=true, opts={})=>{
    const u={...f,updatedAt:new Date().toISOString(),updatedBy:currentUser||f.updatedBy||null}
    // Owner drives column (unless it's done or snoozed)
    if(u.colId!=='done'&&u.colId!=='snoozed'){
      u.colId=OWNER_TO_COL[u.assignee]||'inbox'
    }
    if(u.colId==='done'){ if(!u.completedAt) u.completedAt=new Date().toISOString(); u.snoozedUntil=null; u.preSnoozeColId=null; u.wokeAt=null }
    write(p=>p.map(t=>t.id===u.id?u:t))
    if(close) setEditing(null)
    if(!opts.silent) toast_('Saved')
  }

  const exportBackup=async()=>{
    try{
      const res=await fetch(`${DB_URL}.json`)
      if(!res.ok) throw new Error(`Backup failed: ${res.status}`)
      const data=await res.json()
      const stamp=new Date().toISOString().slice(0,16).replace(/[:T]/g,'-')
      const filename=`taskboard-backup-${ENV}-${stamp}.json`
      const blob=new Blob([JSON.stringify(data||{},null,2)],{type:'application/json'})
      const url=URL.createObjectURL(blob)
      const a=document.createElement('a')
      a.href=url
      a.download=filename
      document.body.appendChild(a)
      a.click()
      a.remove()
      URL.revokeObjectURL(url)
      toast_('Backup exported')
    }catch(e){
      console.warn('backup failed',e)
      toast_('⚠ Backup failed')
    }
  }

  const updatePrefs=p=>{
    setPrefs(p);savePrefs({...p,currentUser});setSettings(false);toast_('Settings saved')
  }

  const pickUser=user=>{
    setCurrentUser(user);savePrefs({...prefs,currentUser:user})
    if(prefs.pushEnabled) registerPushUser(user)
  }

  const pushNotify=async(task)=>{
    const senderName = currentUser ? USERS[currentUser].label : 'Someone'
    const other      = Object.keys(USERS).find(k => k !== currentUser)
    if (!other) { toast_('Select who you are first'); return }
    const recipient  = USERS[other]
    const priFlag    = task.priority === 'high' ? ' 🔴 HIGH PRIORITY' : ''
    const subject    = `📣 ${senderName} flagged a task for you${priFlag}`
    const message    = `Hi ${recipient.label},

${senderName} wants your attention on:

"${task.title||'Untitled'}"
${task.notes?`
Notes: ${task.notes}
`:''}
Open the task board to view it.`
    const sent = await sendEmail(recipient.email, recipient.label, subject, message)
    toast_(sent ? `Email sent to ${recipient.label} ✉️` : 'Email failed — check console')
  }

  const bulkDone  =()=>{fireConfetti();bulkSel.forEach(id=>doneTask(id,true));setBulkSel([]);setBulkMode(false)}
  const bulkAssign=col=>{bulkSel.forEach(id=>assignTask(id,col));setBulkSel([]);setBulkMode(false)}

  const allTags=[...new Set(tasks.flatMap(t=>t.tags||[]))]
  const filterTask=t=>{
    // Search filter
    if(search){
      const q=search.toLowerCase()
      const inTitle=(t.title||'').toLowerCase().includes(q)
      const inNotes=(t.notes||'').toLowerCase().includes(q)
      const inComments=(t.comments||[]).some(c=>c.text.toLowerCase().includes(q))
      const inTags=(t.tags||[]).some(tag=>tag.includes(q))
      if(!inTitle&&!inNotes&&!inComments&&!inTags) return false
    }
    // Hide snoozed tasks from all columns except the snoozed column
    if(isSnoozed(t)&&t.colId!=='snoozed') return false
    // Stat filter — clicking a stat shows only those tasks across all columns
    if(statFilter==='overdue'&&!isOver(t.dueDate,t.colId)) return false
    if(statFilter==='open'&&(t.colId==='done'||t.colId==='snoozed')) return false
    if(statFilter==='done'&&t.colId!=='done') return false
    if(statFilter==='snoozed'&&t.colId!=='snoozed') return false
    if(statFilter==='inbox'&&t.colId!=='inbox') return false
    if(filterTag&&!(t.tags||[]).includes(filterTag)) return false
    if(filterCat&&t.category!==filterCat) return false
    if(!prefs.showArchived&&t.archivedAt) return false
    return true
  }

  const isDark=prefs.dark!==false
  const handleDragStart=(id)=>{ dragId.current=id }
  const handleDrop=(colId)=>{
    const id=dragId.current; if(!id) return
    const task=tasks.find(t=>t.id===id); if(!task||task.colId===colId) return
    dragId.current=null; setDragOver(null)
    if(colId==='done'){
      doneTask(id)
    } else if(colId==='snoozed'){
      // snooze for 1hr when dragged to snoozed
      const until=new Date(); until.setHours(until.getHours()+1)
      snoozeTask(id,until.toISOString())
    } else {
      // moving to inbox, troy, khatra, both
      const ownerMap={troy:'troy',khatra:'khatra',both:'both',inbox:'inbox'}
      const newAssignee=ownerMap[colId]||task.assignee
      write(p=>p.map(t=>t.id===id?{...t,colId,assignee:newAssignee,snoozedUntil:null,preSnoozeColId:null}:t))
    }
  }

  // Base filter — same rules as filterTask but without stat filter or search
  // so stats always match what the board shows when you click them
  const baseTasks = tasks.filter(t=>{
    if(isSnoozed(t)&&t.colId!=='snoozed') return false
    if(filterTag&&!(t.tags||[]).includes(filterTag)) return false
    if(filterCat&&t.category!==filterCat) return false
    if(!prefs.showArchived&&t.archivedAt) return false
    return true
  })
  const open     =baseTasks.filter(t=>t.colId!=='done'&&t.colId!=='snoozed').length
  const overdue  =baseTasks.filter(t=>isOver(t.dueDate,t.colId)).length
  const doneCt   =baseTasks.filter(t=>t.colId==='done').length
  const inboxCt  =baseTasks.filter(t=>t.colId==='inbox').length
  const snoozedCt=baseTasks.filter(t=>t.colId==='snoozed').length
  const u=currentUser?USERS[currentUser]:null

  if(!ready) return <><style>{makeCSS(true)}</style><div className="loading"><div className="loading-dot"/><span style={{fontSize:12,color:'var(--t3)'}}>Connecting…</span></div></>

  return (
    <>
      <style>{makeCSS(isDark)}</style>
      <div className="app">
        {updateAvailable&&<div className="update-banner">New version available <button onClick={refreshToLatest}>Refresh now</button></div>}
        {ENV === 'sandbox' && <div className="env-banner">⚠ SANDBOX MODE — SAFE TO TEST</div>}
        {!currentUser&&<WhoPicker onPick={pickUser}/>}
        <div className="hdr">
          <div className="hdr-l">
            <div className="pill">Shared</div>
            <div className="hdr-title">Troy &amp; Khatra{ENV === 'sandbox' && <span className="env-pill">Sandbox</span>}</div>
          </div>
          <div className="hdr-r">
            <div style={{display:'flex',alignItems:'center',gap:5}}>
              <div className={`sdot${online?'':' off'}`}/>
              <span className="slbl">{online?'live':'offline'}</span>
            </div>
            {u&&<div className="who-badge" style={{background:u.color+'22',color:u.color,borderColor:u.color+'44'}} onClick={()=>{setCurrentUser(null);savePrefs({...prefs,currentUser:null})}} title="Switch user">{u.label[0]}</div>}
            <button className={`hbtn${showSearch?' on':''}`} onClick={()=>{setShowSearch(s=>{if(!s)setTimeout(()=>searchRef.current?.focus(),50);return !s})}} title="Search (/)">🔍</button>
            <button className="hbtn" onClick={()=>setSharing(true)}>🔗</button>
            <button className="hbtn" onClick={()=>setSettings(true)}>⚙</button>
            <button className={`hbtn${bulkMode?' on':''}`} onClick={()=>{setBulkMode(b=>!b);setBulkSel([])}}>Bulk</button>
            <select className="hbtn" style={{cursor:'pointer'}} value={sortBy} onChange={e=>setSortBy(e.target.value)}>
              <option value="priority">↕ Priority</option>
              <option value="due">↕ Due date</option>
              <option value="created">↕ Newest</option>
            </select>
            <button className={`hbtn${showSnoozed?' on':''}`} onClick={()=>setShowSnoozed(s=>!s)}>{showSnoozed?'Hide Snoozed':'Show Snoozed'}</button>
            <button className={`hbtn${showDone?' on':''}`} onClick={()=>setShowDone(s=>!s)}>{showDone?'Hide Done':'Show Done'}</button>
          </div>
        </div>

        {showSearch&&(
          <div className="search-bar">
            <span style={{fontSize:14}}>🔍</span>
            <input ref={searchRef} className="search-in" placeholder="Search tasks…" value={search}
              autoFocus onChange={e=>setSearch(e.target.value)}
              onKeyDown={e=>{ if(e.key==='Escape'){setSearch('');setShowSearch(false)} }}/>
            {search&&<button className="search-x" onClick={()=>setSearch('')}>×</button>}
            <button className="search-x" onClick={()=>{setShowSearch(false);setSearch('')}}>✕</button>
          </div>
        )}
        {(allTags.length>0||filterCat)&&(
          <div className="fbar">
            <span style={{fontSize:10,color:'var(--t3)',textTransform:'uppercase',letterSpacing:'.5px',marginRight:2}}>Filter:</span>
            {allTags.map(tag=><button key={tag} className={`ftag${filterTag===tag?' act':''}`} onClick={()=>setFilterTag(filterTag===tag?null:tag)} style={filterTag===tag?{background:tagColor(tag)+'22',color:tagColor(tag),borderColor:tagColor(tag)+'44'}:{}}>{tag}</button>)}
            {CATEGORIES.filter(c=>c.id).map(c=><button key={c.id} className={`ftag${filterCat===c.id?' act':''}`} onClick={()=>setFilterCat(filterCat===c.id?null:c.id)}>{c.label}</button>)}
            {(filterTag||filterCat)&&<button className="ftag" style={{color:'var(--red)'}} onClick={()=>{setFilterTag(null);setFilterCat(null)}}>✕ Clear</button>}
          </div>
        )}

        {statFilter&&(
          <div className="fbar" style={{background:'rgba(245,158,11,.05)',borderColor:'rgba(245,158,11,.15)'}}>
            <span style={{fontSize:11,color:'var(--amber)'}}>Showing: <b>{statFilter}</b> tasks only</span>
            <button className="ftag" style={{color:'var(--t3)',marginLeft:4}} onClick={()=>setStatFilter(null)}>✕ Clear filter</button>
          </div>
        )}
        {bulkMode&&bulkSel.length>0&&(
          <div className="fbar" style={{background:'var(--ag)',borderColor:'rgba(245,158,11,.2)'}}>
            <span style={{fontSize:12,color:'var(--amber)',fontWeight:600}}>{bulkSel.length} selected</span>
            <button className="sbtn st" onClick={()=>bulkAssign('troy')}>→ Troy</button>
            <button className="sbtn sk" onClick={()=>bulkAssign('khatra')}>→ Khatra</button>
            <button className="sbtn" style={{color:'var(--green)'}} onClick={bulkDone}>✓ Done all</button>
            <button className="sbtn" onClick={()=>{setBulkSel([]);setBulkMode(false)}}>Cancel</button>
          </div>
        )}

        <div className="stats">
          {inboxCt>0&&<button className={`stat-btn${statFilter==='inbox'?' active-stat':''}`} onClick={()=>setStatFilter(f=>f==='inbox'?null:'inbox')}>
            <span className="stn" style={{color:'var(--amber)'}}>{inboxCt}</span><span className="stl">To allocate</span>
          </button>}
          <button className={`stat-btn${statFilter==='open'?' active-stat':''}`} onClick={()=>setStatFilter(f=>f==='open'?null:'open')}>
            <span className="stn" style={{color:'#06b6d4'}}>{open}</span><span className="stl">Open</span>
          </button>
          <button className={`stat-btn${statFilter==='overdue'?' active-stat':''}`} onClick={()=>setStatFilter(f=>f==='overdue'?null:'overdue')}>
            <span className="stn" style={{color:overdue>0?'#fca5a5':'var(--t1)'}}>{overdue}</span><span className="stl">Overdue</span>
          </button>
          {snoozedCt>0&&<button className={`stat-btn${statFilter==='snoozed'?' active-stat':''}`} onClick={()=>setStatFilter(f=>f==='snoozed'?null:'snoozed')}>
            <span className="stn" style={{color:'#a78bfa'}}>{snoozedCt}</span><span className="stl">Snoozed</span>
          </button>}
          <button className={`stat-btn${statFilter==='done'?' active-stat':''}`} onClick={()=>setStatFilter(f=>f==='done'?null:'done')}>
            <span className="stn" style={{color:'var(--green)'}}>{doneCt}</span><span className="stl">Done</span>
          </button>
          {bulkMode&&<div className="stat"><span style={{fontSize:11,color:'var(--amber)'}}>Tap cards to select</span></div>}
        </div>

        <div className="board">
          {COLS.map(col=>{
            if(col.id==='done'&&!showDone) return null
            if(col.id==='snoozed'&&!showSnoozed) return null
            const PRI_ORDER={high:0,medium:1,low:2}
            const colTasks=tasks
              .filter(t=>t.colId===col.id&&filterTask(t))
              .sort((a,b)=>{
                // Pinned always first
                if(a.pinned&&!b.pinned) return -1
                if(!a.pinned&&b.pinned) return 1
                if(sortBy==='priority'){
                  const pd=(PRI_ORDER[a.priority]??1)-(PRI_ORDER[b.priority]??1)
                  if(pd!==0) return pd
                  if(!a.dueDate&&!b.dueDate) return 0
                  if(!a.dueDate) return 1
                  if(!b.dueDate) return -1
                  return new Date(a.dueDate)-new Date(b.dueDate)
                }
                if(sortBy==='due'){
                  if(!a.dueDate&&!b.dueDate) return (PRI_ORDER[a.priority]??1)-(PRI_ORDER[b.priority]??1)
                  if(!a.dueDate) return 1
                  if(!b.dueDate) return -1
                  return new Date(a.dueDate)-new Date(b.dueDate)
                }
                return new Date(b.createdAt)-new Date(a.createdAt)
              })
            return (
              <div key={col.id} className={`col${dragOver===col.id?' drag-over':''}`}
                onDragOver={e=>{e.preventDefault();setDragOver(col.id)}}
                onDragLeave={()=>setDragOver(null)}
                onDrop={e=>{e.preventDefault();handleDrop(col.id)}}>
                <div className="col-hd">
                  <div className="col-tg">
                    <div className="col-stripe" style={{background:col.accent}}/>
                    <div><div className="col-nm" style={{color:col.accent}}>{col.label}</div><div className="col-sb">{col.sub}</div></div>
                  </div>
                  <div style={{display:'flex',alignItems:'center',gap:6}}>
                    <span className="col-cnt">{colTasks.length}</span>
                    {col.id!=='done'&&<button className="addcol" onClick={()=>setAddingTo(a=>a===col.id?null:col.id)}>+</button>}
                  </div>
                </div>
                {addingTo===col.id&&<QuickAdd colId={col.id} onSave={addTask} onCancel={()=>setAddingTo(null)}/>}
                <div className="cards" style={prefs.compact?{gap:4}:{}}>
                  {colTasks.length===0&&addingTo!==col.id&&(
                    <div className="empty">
                      <div className="empty-ico">{col.id==='done'?'✓':col.id==='inbox'?'📥':'○'}</div>
                      <div className="empty-txt">{col.id==='inbox'?'Dump tasks here':col.id==='done'?'Nothing completed yet':'No active tasks'}</div>
                    </div>
                  )}
                  {col.id==='done' ? (() => {
                    const grp=(t)=>{ if(!t.completedAt||isNaN(new Date(t.completedAt))) return 'Earlier'; const d=new Date(t.completedAt); const now=new Date(); if(d.toDateString()===now.toDateString()) return 'Today'; const yw=new Date(now); yw.setDate(now.getDate()-7); return d>yw?'This week':'Earlier' }
                    const groups=[['Today',colTasks.filter(t=>grp(t)==='Today')],['This week',colTasks.filter(t=>grp(t)==='This week')],['Earlier',colTasks.filter(t=>grp(t)==='Earlier')]]
                    return groups.filter(([,ts])=>ts.length>0).map(([label,ts])=>(
                      <div key={label}>
                        <div className="date-group">{label}</div>
                        {ts.map(task=>(
                          <TaskCard key={task.id} task={task} colId={col.id} allTasks={tasks} onEdit={setEditing} onAssign={assignTask} onDone={doneTask} onMove={moveTask} onWake={wakeTask} onSnooze={snoozeTask} onPin={pinTask} onDragStart={handleDragStart} onDragEnd={()=>{dragId.current=null;setDragOver(null)}}/>
                        ))}
                      </div>
                    ))
                  })() : colTasks.map(task=>{
                    if(bulkMode) return (
                      <div key={task.id} className="card" style={bulkSel.includes(task.id)?{borderColor:'var(--amber)',background:'var(--ag)'}:{}} onClick={()=>setBulkSel(s=>s.includes(task.id)?s.filter(i=>i!==task.id):[...s,task.id])}>
                        <div className="card-top"><span style={{fontSize:16,marginRight:6}}>{bulkSel.includes(task.id)?'☑':'☐'}</span><div className="card-title">{task.title||'Untitled'}</div></div>
                      </div>
                    )
                    return <TaskCard key={task.id} task={task} colId={col.id} allTasks={tasks} onEdit={setEditing} onAssign={assignTask} onDone={doneTask} onMove={moveTask} onWake={wakeTask} onSnooze={snoozeTask} onPin={pinTask} onDragStart={handleDragStart} onDragEnd={()=>{dragId.current=null;setDragOver(null)}}/>
                  })}
                </div>
              </div>
            )
          })}
        </div>

        {editing &&<EditModal task={editing} allTasks={tasks} currentUser={currentUser} onSave={updateTask} onDelete={deleteTask} onArchive={archiveTask} onDone={doneTask} onPushNotify={pushNotify} onAddDep={task=>{write(p=>[task,...p])}} onClose={()=>setEditing(null)}/>}
        {sharing &&<ShareModal onClose={()=>setSharing(false)}/>}
        {settings&&<SettingsModal prefs={prefs} onSave={updatePrefs} onExportBackup={exportBackup} onClose={()=>setSettings(false)}/>}
        {toast   &&<div className="toast">{toast}</div>}
      </div>
    </>
  )
}


ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)
