// VYB LIFE — Detail Views B (Ideas, Tasks, Mood, Overview)

// Composer order (user-facing). Backward-compatible: legacy tags still resolve
// via TAG_META so old ideas keep their pill style.
const TAGS = ['Business','Content','Video','Reel','Thread','Product','Personal','Newsletter','Podcast','Essay','Other'];

// Per-tag color treatment. Subtle, dark-mode friendly, no loud hues.
const TAG_META = {
  Business:   { color:'#6FAE8A', faint:'rgba(111,174,138,0.13)', text:'#9CCBB1' },
  Content:    { color:'#A08A56', faint:'rgba(160,138,86,0.15)',  text:'#C7AA6F' },
  Video:      { color:'#A08A56', faint:'rgba(160,138,86,0.15)',  text:'#C7AA6F' },
  Reel:       { color:'#8E7AB5', faint:'rgba(142,122,181,0.13)', text:'#B6A2D6' },
  Thread:     { color:'#6F8FAE', faint:'rgba(111,143,174,0.13)', text:'#9CB7CE' },
  Product:    { color:'#5E8B96', faint:'rgba(94,139,150,0.14)',  text:'#8DB1B9' },
  Personal:   { color:'#C9B79C', faint:'rgba(201,183,156,0.12)', text:'#D7C7AC' },
  Newsletter: { color:'#5E7558', faint:'rgba(94,117,88,0.15)',   text:'#9DB097' },
  Podcast:    { color:'#B8643C', faint:'rgba(184,100,60,0.14)',  text:'#D0825A' },
  Essay:      { color:'#C49555', faint:'rgba(196,149,85,0.14)',  text:'#D6AE7A' },
  Short:      { color:'#7E8E9E', faint:'rgba(126,142,158,0.13)', text:'#A4B1BD' },
  Other:      { color:'#6E665F', faint:'rgba(110,102,95,0.13)',  text:'#9C9189' },
};
const tagMeta = (t) => TAG_META[t] || TAG_META.Other;

// ─── Project name normalization ───────────────────────────────
// Treat "VYB", "vyb", " VYB " as the same project. Used to dedupe
// project pickers, group the board, and prevent duplicate groups.
// Display label keeps the user's latest casing (canonical name).
const normalizeProjectKey = (name) => String(name || '')
  .trim()
  .replace(/\s+/g, ' ')
  .toLowerCase();

// Build { normalizedKey -> canonicalDisplayName } from a list of names,
// where canonical = the LAST occurrence wins (so newest casing sticks).
const buildProjectCanonicalMap = (names) => {
  const map = new Map();
  for (const n of names) {
    const k = normalizeProjectKey(n);
    if (!k) continue;
    map.set(k, String(n).trim().replace(/\s+/g, ' '));
  }
  return map;
};

// ─── Option usage metadata (composer recency/frequency) ──────
// Tracks per-option { label, count, lastUsedAt } in localStorage so the
// composer can sort options by usage and hide stale ones (>4 days).
// Two independent stores: tasks projects + ideas custom categories.
const STALE_DAYS = 4;
const STALE_MS = STALE_DAYS * 24 * 60 * 60 * 1000;
const USAGE_KEYS = {
  tasks: 'vyb-task-project-usage',
  ideas: 'vyb-idea-category-usage',
};
const loadUsageMap = (kind) => {
  try {
    const raw = localStorage.getItem(USAGE_KEYS[kind]);
    if (!raw) return {};
    const parsed = JSON.parse(raw);
    return (parsed && typeof parsed === 'object') ? parsed : {};
  } catch { return {}; }
};
const saveUsageMap = (kind, map) => {
  try { localStorage.setItem(USAGE_KEYS[kind], JSON.stringify(map)); } catch {}
};
const bumpUsage = (kind, label) => {
  const v = String(label || '').trim().replace(/\s+/g,' ');
  if (!v) return;
  const k = v.toLowerCase();
  const map = loadUsageMap(kind);
  const prev = map[k] || { label: v, count: 0, lastUsedAt: null };
  map[k] = { label: v, count: (prev.count || 0) + 1, lastUsedAt: new Date().toISOString() };
  saveUsageMap(kind, map);
};
const isFreshUsage = (lastUsedAt) => {
  if (!lastUsedAt) return true;
  const t = new Date(lastUsedAt).getTime();
  if (!isFinite(t)) return true;
  return (Date.now() - t) <= STALE_MS;
};
// Sort labels by usage: recent first, then count, then alpha. Labels with no
// metadata sort last (alpha among themselves) so they don't shadow recents.
const sortByUsage = (labels, kind) => {
  const map = loadUsageMap(kind);
  return [...labels].sort((a, b) => {
    const ka = String(a).trim().replace(/\s+/g,' ').toLowerCase();
    const kb = String(b).trim().replace(/\s+/g,' ').toLowerCase();
    const ua = map[ka]; const ub = map[kb];
    const ta = ua && ua.lastUsedAt ? new Date(ua.lastUsedAt).getTime() : 0;
    const tb = ub && ub.lastUsedAt ? new Date(ub.lastUsedAt).getTime() : 0;
    if (tb !== ta) return tb - ta;
    const ca = (ua && ua.count) || 0;
    const cb = (ub && ub.count) || 0;
    if (cb !== ca) return cb - ca;
    return String(a).localeCompare(String(b));
  });
};
// Filter out labels whose usage metadata is older than STALE_DAYS.
// Labels without metadata are kept (no usage signal yet → don't hide).
// `protectKey(label)` returning true keeps the label regardless (used for
// the currently-selected one so it never disappears mid-flow).
const filterFreshOptions = (labels, kind, protectKey) => {
  const map = loadUsageMap(kind);
  return labels.filter(l => {
    if (protectKey && protectKey(l)) return true;
    const k = String(l).trim().replace(/\s+/g,' ').toLowerCase();
    const u = map[k];
    if (!u) return true;
    return isFreshUsage(u.lastUsedAt);
  });
};

// ─── Idea preset categories ───────────────────────────────────
// Saved in `idea.tag` as the localized label string (no DB change).
// Cross-language matching: a saved tag is "preset" if its normalized
// key matches any preset's en or es label.
const IDEA_PRESETS = [
  { key:'business', emoji:'💼', en:'Business', es:'Negocio' },
  { key:'content',  emoji:'🎬', en:'Content',  es:'Contenido' },
  { key:'general',  emoji:'✨', en:'General',  es:'General' },
  { key:'web_app',  emoji:'🛠', en:'Web/App',  es:'Web/App' },
  { key:'learning', emoji:'📚', en:'Learning', es:'Aprendizaje' },
  { key:'personal', emoji:'🌱', en:'Personal', es:'Personal' },
];
const IDEA_PRESET_KEYS = new Set();
for (const p of IDEA_PRESETS) {
  IDEA_PRESET_KEYS.add(String(p.en).trim().replace(/\s+/g,' ').toLowerCase());
  IDEA_PRESET_KEYS.add(String(p.es).trim().replace(/\s+/g,' ').toLowerCase());
}
const isPresetTag = (name) => IDEA_PRESET_KEYS.has(
  String(name || '').trim().replace(/\s+/g,' ').toLowerCase()
);
const IDEA_DEFAULTS_KEY = 'vyb-idea-composer-defaults';
const loadIdeaDefaults = () => {
  try {
    const raw = localStorage.getItem(IDEA_DEFAULTS_KEY);
    if (!raw) return { categoryLabel: '' };
    const parsed = JSON.parse(raw);
    return { categoryLabel: typeof parsed.categoryLabel === 'string' ? parsed.categoryLabel : '' };
  } catch { return { categoryLabel: '' }; }
};
const saveIdeaDefaults = (defaults) => {
  try {
    localStorage.setItem(IDEA_DEFAULTS_KEY, JSON.stringify({
      categoryLabel: defaults.categoryLabel || '',
    }));
  } catch {}
};

// ─── Composer defaults (last-used project + priority) ─────────
const COMPOSER_DEFAULTS_KEY = 'vyb-task-composer-defaults';
const loadComposerDefaults = () => {
  try {
    const raw = localStorage.getItem(COMPOSER_DEFAULTS_KEY);
    if (!raw) return { projectName: '', priority: 'important' };
    const parsed = JSON.parse(raw);
    const priority = ['urgent','important','later'].includes(parsed?.priority) ? parsed.priority : 'important';
    const projectName = typeof parsed?.projectName === 'string' ? parsed.projectName : '';
    return { projectName, priority };
  } catch {
    return { projectName: '', priority: 'important' };
  }
};
const saveComposerDefaults = (defaults) => {
  try {
    localStorage.setItem(COMPOSER_DEFAULTS_KEY, JSON.stringify({
      projectName: defaults.projectName || '',
      priority: ['urgent','important','later'].includes(defaults.priority) ? defaults.priority : 'important',
    }));
  } catch {}
};

// ─── Shared Composer ───────────────────────────────────────────
// Bottom-anchored chat-style input. Used by both Tasks and Ideas.
// Children render the secondary controls row below the textarea.
const Composer = ({ value, onChange, onSubmit, placeholder, error, children, minRows=1, inputRef, recordingNode, actions, leadingAction, aboveBubble }) => {
  const innerRef = React.useRef(null);
  const ref = inputRef || innerRef;
  const autoResize = (el) => {
    if (!el) return;
    el.style.height = 'auto';
    const max = 180;
    el.style.height = Math.min(el.scrollHeight, max) + 'px';
    el.style.overflowY = el.scrollHeight > max ? 'auto' : 'hidden';
  };
  React.useEffect(() => { autoResize(ref.current); }, [value]);
  const onKeyDown = (e) => {
    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onSubmit(); }
  };
  // Portal to <body> so the composer's position:fixed is never broken by
  // ancestor transforms (.view-fade slideUp would otherwise make the bubble
  // render inside the animated wrapper and snap on animation end).
  return ReactDOM.createPortal(
    <div className="vyb-composer-fixed">
      <div className="vyb-composer-dock" aria-hidden="true"/>
      <div className="vyb-composer-fade" aria-hidden="true"/>
      {aboveBubble ? (
        <div className="vyb-composer-above" style={{marginBottom:8}}>{aboveBubble}</div>
      ) : null}
      <div className="vyb-composer-bubble">
      {recordingNode ? recordingNode : (
        <>
          {actions ? (
            <div className="vyb-composer-inputrow"
              style={{display:'flex',alignItems:'center',gap:8,width:'100%'}}>
              {leadingAction ? (
                <span className="vyb-composer-leading"
                  style={{display:'inline-flex',alignItems:'center',flexShrink:0,alignSelf:'center'}}>
                  {leadingAction}
                </span>
              ) : null}
              <textarea ref={ref} value={value}
                onChange={e=>{ onChange(e.target.value); autoResize(e.target); }}
                onKeyDown={onKeyDown}
                rows={minRows}
                placeholder={placeholder}
                autoComplete="off" autoCorrect="off" autoCapitalize="sentences"
                spellCheck={true} inputMode="text" enterKeyHint="send"
                style={{flex:1,minWidth:0,background:'transparent',border:'none',outline:'none',resize:'none',
                  color:C.textPrimary,fontFamily:'Montserrat,sans-serif',fontSize:14,lineHeight:1.5,
                  padding:'4px 2px',minHeight: minRows*22+4}}/>
              <span className="vyb-composer-actions"
                style={{display:'inline-flex',alignItems:'center',gap:6,flexShrink:0,alignSelf:'center'}}>
                {actions}
              </span>
            </div>
          ) : (
            <textarea ref={ref} value={value}
              onChange={e=>{ onChange(e.target.value); autoResize(e.target); }}
              onKeyDown={onKeyDown}
              rows={minRows}
              placeholder={placeholder}
              autoComplete="off" autoCorrect="off" autoCapitalize="sentences"
              spellCheck={true} inputMode="text" enterKeyHint="send"
              style={{width:'100%',background:'transparent',border:'none',outline:'none',resize:'none',
                color:C.textPrimary,fontFamily:'Montserrat,sans-serif',fontSize:14,lineHeight:1.5,
                padding:'4px 2px',minHeight: minRows*22+4}}/>
          )}
          {children && (
            <div style={{display:'flex',alignItems:'center',gap:10,flexWrap:'wrap',marginTop:8}}>
              {children}
            </div>
          )}
          {error && (
            <div style={{marginTop:8,fontSize:11,color:C.sandLight,fontStyle:'italic'}}>{error}</div>
          )}
        </>
      )}
    </div>
    </div>,
    document.body
  );
};

// Mobile leading "+" button. Opens a contextual sheet for project/priority/category.
// Active prop highlights the button while the sheet is open. Dot prop shows a tiny
// indicator when a value is already set behind the sheet.
const ComposerPlusButton = ({ onClick, active=false, dot=false, label='Options' }) => (
  <button onClick={onClick} title={label} aria-expanded={active} aria-label={label}
    style={{position:'relative',width:34,height:34,borderRadius:9999,aspectRatio:'1 / 1',
      border:`1px solid ${active ? C.sand : C.border}`,
      background: active ? C.sandFaint : 'transparent',
      color: active ? C.sandLight : C.textMuted,
      cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center',
      transition:'all 0.15s', flexShrink:0, padding:0}}>
    <Icon name={active ? 'x' : 'plus'} size={15}
      color={active ? C.sandLight : C.textMuted} sw={2.2}/>
    {dot && !active && (
      <span aria-hidden="true" style={{position:'absolute',top:4,right:4,width:7,height:7,
        borderRadius:999,background:C.sand,boxShadow:`0 0 0 2px ${C.bg}`}}/>
    )}
  </button>
);

const ComposerSendButton = ({ onClick, disabled, label='Add', inline=false }) => (
  <button onClick={onClick} disabled={disabled}
    title={label}
    style={{...(inline ? {} : {marginLeft:'auto'}),
      width:34,height:34,borderRadius:9999,aspectRatio:'1 / 1',border:'none',
      background: disabled ? 'rgba(160,138,86,0.25)' : C.sand,
      color: C.bg, cursor: disabled ? 'not-allowed' : 'pointer',
      display:'flex',alignItems:'center',justifyContent:'center',
      transition:'all 0.15s', flexShrink:0, padding:0}}>
    <Icon name="arrow-up" size={16} color={C.bg} sw={2.5}/>
  </button>
);

// ─── Dictation (Web Speech API) ────────────────────────────────
// Free MVP using window.SpeechRecognition. No paid services, no backend.
// Returns { supported, listening, toggle, stop }.
// onAppend(chunk) is called with each finalized chunk; the consumer decides
// how to merge it into the composer text. Interim results are ignored to
// keep the textarea calm — only finalized phrases land.
const useDictation = ({ onAppend, lang = 'es-ES', inputRef } = {}) => {
  const SR = (typeof window !== 'undefined') &&
    (window.SpeechRecognition || window.webkitSpeechRecognition);
  const supported = !!SR;
  const [listening, setListening] = React.useState(false);
  const [transcript, setTranscript] = React.useState('');
  const [error, setError] = React.useState(''); // 'not-allowed' | 'no-speech' | 'audio-capture' | 'aborted' | other
  const transcriptRef = React.useRef('');
  const recRef = React.useRef(null);
  const userActionRef = React.useRef(null); // 'confirm' | 'cancel' | null
  const onAppendRef = React.useRef(onAppend);
  React.useEffect(() => { onAppendRef.current = onAppend; }, [onAppend]);

  // Live audio level (0..1). UI components read via levelRef inside their own
  // RAF — avoids re-rendering the composer 60×/sec.
  const levelRef = React.useRef(0);
  const audioRef = React.useRef({ ctx:null, stream:null, src:null, analyser:null, raf:0 });

  const tearDownAudio = React.useCallback(() => {
    const a = audioRef.current;
    if (a.raf) { cancelAnimationFrame(a.raf); a.raf = 0; }
    try { a.src && a.src.disconnect(); } catch (_) {}
    try { a.analyser && a.analyser.disconnect(); } catch (_) {}
    if (a.stream) { try { a.stream.getTracks().forEach(t => t.stop()); } catch (_) {} }
    if (a.ctx && a.ctx.state !== 'closed') { try { a.ctx.close(); } catch (_) {} }
    audioRef.current = { ctx:null, stream:null, src:null, analyser:null, raf:0 };
    levelRef.current = 0;
  }, []);

  const setUpAudio = React.useCallback(async () => {
    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) return;
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      const Ctx = window.AudioContext || window.webkitAudioContext;
      if (!Ctx) { stream.getTracks().forEach(t => t.stop()); return; }
      const ctx = new Ctx();
      const src = ctx.createMediaStreamSource(stream);
      const analyser = ctx.createAnalyser();
      analyser.fftSize = 512;
      analyser.smoothingTimeConstant = 0.6;
      src.connect(analyser);
      const buf = new Uint8Array(analyser.fftSize);
      const tick = () => {
        analyser.getByteTimeDomainData(buf);
        let sum = 0;
        for (let i = 0; i < buf.length; i++) { const v = (buf[i] - 128) / 128; sum += v*v; }
        const rms = Math.sqrt(sum / buf.length);
        // Mild compression so soft speech still moves the bars.
        levelRef.current = Math.min(1, rms * 2.4);
        audioRef.current.raf = requestAnimationFrame(tick);
      };
      audioRef.current = { ctx, stream, src, analyser, raf: requestAnimationFrame(tick) };
    } catch (e) {
      console.warn('[dictation] audio level unavailable', e);
    }
  }, []);

  const focusInput = React.useCallback(() => {
    const el = inputRef && inputRef.current;
    if (!el) return;
    // Defer to next frame so React state updates settle first.
    requestAnimationFrame(() => {
      try {
        el.focus({ preventScroll: true });
        const len = el.value.length;
        el.setSelectionRange(len, len);
      } catch (_) { try { el.focus(); } catch (__) {} }
    });
  }, [inputRef]);

  // Internal stop: just tells the SR engine to halt. The decision of what
  // to do with the buffered transcript happens in `onend`, gated by
  // userActionRef ('confirm' | 'cancel' | null=auto-end).
  const haltSR = React.useCallback(() => {
    const r = recRef.current;
    if (!r) return false;
    try { r.stop(); } catch (_) {}
    return true;
  }, []);

  const confirm = React.useCallback(() => {
    userActionRef.current = 'confirm';
    if (!haltSR()) {
      // No live SR — flush whatever's buffered and reset.
      const txt = transcriptRef.current.trim();
      if (txt && onAppendRef.current) onAppendRef.current(txt);
      transcriptRef.current = ''; setTranscript('');
      tearDownAudio(); setListening(false); focusInput();
    }
  }, [haltSR, tearDownAudio, focusInput]);

  const cancel = React.useCallback(() => {
    userActionRef.current = 'cancel';
    transcriptRef.current = ''; setTranscript('');
    if (!haltSR()) { tearDownAudio(); setListening(false); focusInput(); }
  }, [haltSR, tearDownAudio, focusInput]);

  // Back-compat alias.
  const stop = confirm;

  const start = React.useCallback(() => {
    if (!supported) return;
    if (recRef.current) { try { recRef.current.stop(); } catch (_) {} }
    transcriptRef.current = '';
    setTranscript('');
    setError('');
    userActionRef.current = null;
    const r = new SR();
    r.lang = lang;
    r.interimResults = true;
    r.continuous = true;
    r.onresult = (ev) => {
      let finalChunk = '';
      let interim = '';
      for (let i = ev.resultIndex; i < ev.results.length; i++) {
        const res = ev.results[i];
        if (res.isFinal) finalChunk += res[0].transcript;
        else interim += res[0].transcript;
      }
      if (finalChunk) {
        transcriptRef.current = appendDictated(transcriptRef.current, finalChunk.trim());
      }
      const display = interim
        ? appendDictated(transcriptRef.current, interim.trim())
        : transcriptRef.current;
      setTranscript(display);
    };
    r.onerror = (e) => {
      console.warn('[dictation]', e.error);
      setError(e.error || 'unknown');
      // Treat as cancel on hard error.
      userActionRef.current = userActionRef.current || 'cancel';
    };
    r.onend = () => {
      const action = userActionRef.current; // null = auto-end
      const txt = transcriptRef.current.trim();
      if (action !== 'cancel' && txt && onAppendRef.current) {
        onAppendRef.current(txt);
      }
      transcriptRef.current = '';
      setTranscript('');
      userActionRef.current = null;
      recRef.current = null;
      tearDownAudio();
      setListening(false);
      focusInput();
    };
    recRef.current = r;
    try {
      r.start();
      setListening(true);
      setUpAudio();
    } catch (e) { console.warn('[dictation] start', e); }
  }, [SR, supported, lang, setUpAudio, tearDownAudio, focusInput]);

  const toggle = React.useCallback(() => { listening ? confirm() : start(); }, [listening, start, confirm]);

  React.useEffect(() => () => {
    userActionRef.current = 'cancel';
    try { recRef.current && recRef.current.stop(); } catch (_) {}
    tearDownAudio();
  }, [tearDownAudio]);

  return { supported, listening, toggle, stop, start, confirm, cancel, transcript, levelRef, error, clearError: () => setError('') };
};

// Live volume bars driven by the dictation analyser. Reads levelRef inside
// its own RAF so the parent composer doesn't re-render. Honors
// prefers-reduced-motion: static bars at half height.
const DictationWaveform = ({ levelRef, active, bars=5, height=14, width=2.5, gap=3 }) => {
  const reduced = typeof window !== 'undefined' && window.matchMedia
    && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const refs = React.useRef([]);
  React.useEffect(() => {
    if (!active || reduced) return;
    let raf = 0;
    const phases = Array.from({length:bars}, (_,i) => i * 0.55);
    const tick = (t) => {
      const lvl = levelRef && levelRef.current ? levelRef.current : 0;
      for (let i = 0; i < bars; i++) {
        // Center bars react more strongly to give a natural "beam" feel.
        const center = (bars - 1) / 2;
        const focus = 1 - Math.abs(i - center) / (center + 0.5) * 0.45;
        const wob = 0.55 + 0.45 * Math.sin(t/170 + phases[i]);
        const h = Math.max(0.12, Math.min(1, lvl * wob * focus * 1.4 + 0.10));
        const el = refs.current[i];
        if (el) el.style.transform = `scaleY(${h.toFixed(3)})`;
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [active, reduced, levelRef, bars]);
  if (!active) return null;
  return (
    <div aria-hidden="true"
      style={{display:'inline-flex',alignItems:'center',justifyContent:'center',gap,height}}>
      {Array.from({length:bars}).map((_,i) => (
        <span key={i} ref={el => (refs.current[i] = el)}
          style={{display:'inline-block',width,height,borderRadius:width,
            background:`linear-gradient(to top, ${C.sand}, ${C.sandLight})`,
            transformOrigin:'center', transform: reduced ? 'scaleY(0.5)' : 'scaleY(0.15)',
            transition: reduced ? 'none' : 'transform 60ms linear',
            opacity: reduced ? 0.55 : 0.95}}/>
      ))}
    </div>
  );
};

// Full recording-mode panel that takes over the composer-bubble while the
// user is dictating. Shows a wide waveform, listening status, optional
// interim transcript, and Cancel / Confirm controls.
const RecordingComposer = ({ levelRef, transcript, onCancel, onConfirm }) => {
  const reduced = typeof window !== 'undefined' && window.matchMedia
    && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  return (
    <div role="group" aria-label="Recording dictation"
      style={{display:'flex',flexDirection:'column',gap:10,padding:'2px 2px'}}>
      <div style={{display:'flex',alignItems:'center',justifyContent:'center',
        height:48,borderRadius:12,
        background:'linear-gradient(180deg, rgba(160,138,86,0.05), rgba(160,138,86,0.01))',
        border:`1px solid rgba(160,138,86,0.18)`,padding:'0 14px'}}>
        <DictationWaveform levelRef={levelRef} active={true}
          bars={32} height={32} width={3} gap={4}/>
      </div>
      {transcript && (
        <div aria-live="polite"
          style={{fontSize:12,fontWeight:300,color:C.textSecondary,lineHeight:1.5,
            maxHeight:54,overflowY:'auto',padding:'0 4px',fontStyle:'italic',opacity:0.85}}>
          {transcript}
        </div>
      )}
      <div style={{display:'flex',alignItems:'center',gap:10}}>
        <span aria-hidden="true" style={{
          width:8,height:8,borderRadius:999,background:C.sand,
          boxShadow:`0 0 0 4px rgba(160,138,86,0.18)`,
          animation: reduced ? 'none' : 'vyb-ripple 1.4s ease-out infinite',
          flexShrink:0}}/>
        <div style={{fontSize:10,fontWeight:600,letterSpacing:'0.18em',color:C.sandLight,textTransform:'uppercase'}}>
          Listening…
        </div>
        <div style={{marginLeft:'auto',display:'flex',gap:8}}>
          <button onClick={onCancel} aria-label="Cancel dictation"
            onMouseDown={e=>e.preventDefault()}
            title="Cancel"
            style={{width:36,height:36,borderRadius:999,
              background:'transparent',border:`1px solid ${C.border}`,
              color:C.textMuted,cursor:'pointer',
              display:'inline-flex',alignItems:'center',justifyContent:'center',
              transition:'all 0.15s'}}>
            <Icon name="x" size={15} color={C.textMuted} sw={2}/>
          </button>
          <button onClick={onConfirm} aria-label="Confirm dictation"
            onMouseDown={e=>e.preventDefault()}
            title="Confirm"
            style={{width:36,height:36,borderRadius:999,
              background:C.sand,border:'none',color:C.bg,cursor:'pointer',
              display:'inline-flex',alignItems:'center',justifyContent:'center',
              transition:'all 0.15s',
              boxShadow:'0 6px 18px rgba(160,138,86,0.35)'}}>
            <Icon name="check" size={16} color={C.bg} sw={2.5}/>
          </button>
        </div>
      </div>
    </div>
  );
};

// Append a fresh dictated chunk to existing text with sane spacing.
const appendDictated = (current, chunk) => {
  if (!current) return chunk;
  const sep = /[\s\n]$/.test(current) ? '' : ' ';
  return current + sep + chunk;
};

const DictationButton = ({ supported, listening, onClick, title='Dictate' }) => {
  const disabledTitle = 'Dictation not supported in this browser';
  const ariaLabel = !supported ? disabledTitle
    : listening ? 'Stop dictation' : 'Start dictation';
  const [hover, setHover] = React.useState(false);
  const reduced = typeof window !== 'undefined' && window.matchMedia
    && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const iconColor = listening ? C.sandLight
    : (!supported ? C.textFaint : (hover ? C.sandLight : C.textMuted));
  const borderColor = listening ? C.sand
    : (hover && supported ? 'rgba(160,138,86,0.45)' : C.border);
  const bg = listening ? 'rgba(160,138,86,0.14)'
    : (hover && supported ? 'rgba(160,138,86,0.06)' : 'transparent');
  return (
    <button onClick={supported ? onClick : undefined}
      onMouseDown={e => e.preventDefault()}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      title={supported ? (listening ? 'Stop dictation' : title) : disabledTitle}
      aria-label={ariaLabel}
      aria-pressed={listening}
      disabled={!supported}
      style={{
        position:'relative',
        display:'inline-flex',alignItems:'center',justifyContent:'center',
        width:34,height:34,aspectRatio:'1 / 1',padding:0,borderRadius:9999,
        border:`1px solid ${borderColor}`,
        background: bg,
        cursor: supported ? 'pointer' : 'not-allowed',
        transition:'all 0.18s ease', opacity: supported ? 1 : 0.5,
        flexShrink:0,
      }}>
      <Icon name="mic" size={14} color={iconColor} sw={2}/>
      {listening && !reduced && (
        <span aria-hidden="true" style={{position:'absolute',inset:-2,borderRadius:999,
          border:`1.5px solid ${C.sand}`,opacity:0.55,pointerEvents:'none',
          animation:'vyb-ripple 1.4s ease-out infinite'}}/>
      )}
    </button>
  );
};

// ─── IDEAS VIEW ────────────────────────────────────────────────
const IdeasView = ({ store }) => {
  const tr = useT();
  const _initialIdeaDefaults = React.useMemo(() => loadIdeaDefaults(), []);
  const _ideaLang = (window.getLang && window.getLang()) || 'en';
  const _defaultPresetLabel = (IDEA_PRESETS.find(p => p.key === 'general') || IDEA_PRESETS[0])[_ideaLang === 'es' ? 'es' : 'en'];
  const [input, setInput] = React.useState('');
  const [project, setProject] = React.useState(_initialIdeaDefaults.categoryLabel || _defaultPresetLabel);
  const [selectorMode, setSelectorMode] = React.useState(null); // null | 'category' | 'newCategory'
  const [sheetNewProjectName, setSheetNewProjectName] = React.useState('');
  const newProjectInlineRef = React.useRef(null);
  React.useEffect(() => {
    if (selectorMode === 'newCategory' && newProjectInlineRef.current) {
      try { newProjectInlineRef.current.focus({ preventScroll: true }); } catch {}
    }
  }, [selectorMode]);
  const [error, setError] = React.useState('');
  const [filter, setFilter] = React.useState('active'); // active | all | archived
  const [filterTag, setFilterTag] = React.useState('All');
  const [editing, setEditing] = React.useState(null);
  const [editText, setEditText] = React.useState('');
  const [editProject, setEditProject] = React.useState('');
  // Convert-to-task state, scoped per idea card.
  const [converting, setConverting] = React.useState(null); // idea.id
  const [convertTitle, setConvertTitle] = React.useState('');
  const [convertPri, setConvertPri] = React.useState('important');
  const [converted, setConverted] = React.useState(null); // { ideaId } — transient success
  const convertedTimerRef = React.useRef(null);
  React.useEffect(() => () => { if (convertedTimerRef.current) clearTimeout(convertedTimerRef.current); }, []);

  const startConvert = (idea) => {
    setConverting(idea.id);
    setConvertTitle(generateTaskTitleFromIdea(idea.text));
    setConvertPri(ideaPriorityFromText(idea.text));
  };
  const cancelConvert = () => { setConverting(null); setConvertTitle(''); };
  const commitConvert = (idea) => {
    const title = convertTitle.trim();
    if (!title) return;
    store.addTask({ text: title, pri: convertPri, project: idea.tag || null });
    setConverting(null);
    setConvertTitle('');
    setConverted({ ideaId: idea.id });
    if (convertedTimerRef.current) clearTimeout(convertedTimerRef.current);
    convertedTimerRef.current = setTimeout(() => setConverted(null), 5000);
  };
  const goToTasks = () => {
    try {
      if (window.location.pathname !== '/tasks') {
        history.pushState(null, '', '/tasks');
        window.dispatchEvent(new PopStateEvent('popstate'));
      }
    } catch (_) {}
  };

  // ── Project (free-form, like Tasks) ─────────────────────────
  const allIdeas = store.state.ideas;
  const knownProjects = React.useMemo(() => {
    const set = new Set();
    for (const i of allIdeas) if (i.tag) set.add(i.tag);
    return Array.from(set).sort((a,b) => a.localeCompare(b));
  }, [allIdeas]);

  const [projectMRU, setProjectMRU] = React.useState(() => {
    try {
      const saved = JSON.parse(localStorage.getItem('vyb_idea_proj_mru') || '[]');
      if (Array.isArray(saved) && saved.length) return saved;
    } catch {}
    const seen = new Set(); const out = [];
    for (const i of [...allIdeas].reverse()) {
      if (!i.tag || seen.has(i.tag)) continue;
      seen.add(i.tag); out.push(i.tag);
    }
    return out.reverse();
  });
  const bumpProject = React.useCallback((name) => {
    if (!name) return;
    setProjectMRU(prev => {
      const next = [...prev.filter(p => p !== name), name].slice(-20);
      try { localStorage.setItem('vyb_idea_proj_mru', JSON.stringify(next)); } catch {}
      return next;
    });
  }, []);
  const recentProjects = React.useMemo(() => projectMRU.slice(-3), [projectMRU]);
  // Suggestions shown in the expanded selector when there are no recent ones.
  // Pulled from i18n; merged with known categories so users see a quick starter set.
  const categorySuggestions = React.useMemo(() => {
    const defaults = (tr('ideas.defaultCategories') || []);
    const fromData = knownProjects;
    const seen = new Set();
    const out = [];
    for (const n of [...recentProjects, ...fromData, ...(Array.isArray(defaults) ? defaults : [])]) {
      if (!n) continue;
      const key = String(n);
      if (seen.has(key)) continue;
      seen.add(key); out.push(key);
    }
    return out.slice(0, 6);
  }, [recentProjects, knownProjects, tr]);

  const [showProjects, setShowProjects] = React.useState(false);
  const [addingProject, setAddingProject] = React.useState(false);
  const [newProjectDraft, setNewProjectDraft] = React.useState('');
  const newProjectRef = React.useRef(null);
  // Mobile-only ChatGPT-style sheet: hides inline category pill until user taps "+".
  const [mobileSheetOpen, setMobileSheetOpen] = React.useState(false);
  const isMobile = useOverviewIsMobile();
  React.useEffect(() => { if (addingProject && newProjectRef.current) newProjectRef.current.focus(); }, [addingProject]);
  const addProjectPhrases = React.useMemo(() => {
    const lang = (window.getLang && window.getLang()) || 'en';
    return lang === 'es'
      ? ['Añadir categoría', 'Añadir contexto', 'Añadir cliente', 'Añadir área', 'Añadir tema']
      : ['Add category', 'Add context', 'Add client', 'Add area', 'Add topic'];
  }, []);
  const addProjectFallback = React.useMemo(() => {
    const lang = (window.getLang && window.getLang()) || 'en';
    return lang === 'es' ? 'Añadir categoría o contexto' : 'Add category or context';
  }, []);
  const addProjectPlaceholder = useAnimatedPlaceholder(
    addProjectPhrases, addingProject && !newProjectDraft, addProjectFallback,
  );
  const MAX_PROJECT_LEN = 20;
  const commitNewProject = () => {
    const v = newProjectDraft.trim();
    if (!v) { setAddingProject(false); setNewProjectDraft(''); return; }
    if (v.length > MAX_PROJECT_LEN) return;
    setProject(v); bumpProject(v);
    setNewProjectDraft(''); setAddingProject(false); setShowProjects(false); setMobileSheetOpen(false);
    if (error) setError('');
    requestAnimationFrame(() => { try { ideasInputRef.current && ideasInputRef.current.focus(); } catch (_) {} });
  };

  const ideasInputRef = React.useRef(null);
  const add = async () => {
    const v = input.trim();
    if (!v) { setError(tr('ideas.emptyError')); return; }
    setError('');
    const proj = project.trim() || null;
    if (proj) {
      bumpProject(proj);
      // Only track usage for custom categories; presets are always shown.
      if (!isPresetTag(proj)) bumpUsage('ideas', proj);
    }
    setInput('');
    setSelectorMode(null);
    saveIdeaDefaults({ categoryLabel: proj || '' });
    const newId = await store.addIdea(v, proj);
    if (newId) {
      requestAnimationFrame(() => setTimeout(() => {
        try {
          const el = document.querySelector(`[data-idea-id="${newId}"]`);
          if (!el) return;
          el.classList.add('vyb-task-new');
          setTimeout(() => { try { el.classList.remove('vyb-task-new'); } catch {} }, 2200);
        } catch {}
      }, 120));
    }
  };

  const dictation = useDictation({
    onAppend: (chunk) => { setInput(v => appendDictated(v, chunk)); if (error) setError(''); },
    inputRef: ideasInputRef,
  });

  // Focus the idea composer when arriving from the Overview "Capture Idea"
  // quick action.
  React.useEffect(() => {
    const flag = (typeof window !== 'undefined') ? window.__vybQuickAction : null;
    if (!flag || flag.target !== 'ideas') return;
    if (Date.now() - (flag.ts || 0) > 4000) { try { window.__vybQuickAction = null; } catch {} return; }
    try { window.__vybQuickAction = null; } catch {}
    requestAnimationFrame(() => setTimeout(() => {
      try { ideasInputRef.current && ideasInputRef.current.focus({ preventScroll: true }); } catch {}
    }, 60));
  }, []); // mount only

  const ideas = allIdeas.filter(i => {
    if (filter==='active' && i.archived) return false;
    if (filter==='archived' && !i.archived) return false;
    if (filterTag!=='All') {
      const key = i.tag || '__inbox__';
      if (key !== filterTag) return false;
    }
    return true;
  });

  // Dynamic project chips: distinct tag values across active ideas, plus Inbox bucket if any null.
  const projectCounts = React.useMemo(() => {
    const m = {};
    for (const i of allIdeas) {
      if (i.archived) continue;
      const k = i.tag || '__inbox__';
      m[k] = (m[k] || 0) + 1;
    }
    return m;
  }, [allIdeas]);
  const filterChips = React.useMemo(() => {
    const keys = Object.keys(projectCounts).sort((a,b) => {
      if (a === '__inbox__') return -1;
      if (b === '__inbox__') return 1;
      return a.localeCompare(b);
    });
    return keys;
  }, [projectCounts]);

  return (
    <div style={{paddingBottom: 65}}>
      <ViewHeader label={tr('ideas.label')} title={tr('ideas.title')}
        subtitle={tr('ideas.subtitle')} />

      <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:20,flexWrap:'wrap',gap:12}}>
        <div style={{display:'flex',gap:6}}>
          {['active','archived','all'].map(f=>(
            <div key={f} onClick={()=>setFilter(f)}
              style={{padding:'6px 14px',borderRadius:999,fontSize:10,fontWeight:600,letterSpacing:'0.12em',textTransform:'uppercase',cursor:'pointer',
                border:`1px solid ${filter===f?C.borderStrong:C.border}`,background:filter===f?'rgba(250,250,248,0.06)':'transparent',color:filter===f?C.textPrimary:C.textMuted}}>
              {tr('ideas.filter'+f.charAt(0).toUpperCase()+f.slice(1))}
            </div>
          ))}
        </div>
        <div style={{display:'flex',gap:6,flexWrap:'wrap'}}>
          <div onClick={()=>setFilterTag('All')}
            style={{padding:'4px 10px',borderRadius:999,fontSize:9,fontWeight:600,letterSpacing:'0.12em',textTransform:'uppercase',cursor:'pointer',
              border:`1px solid ${filterTag==='All'?C.sand:C.border}`,background:filterTag==='All'?C.sandFaint:'transparent',color:filterTag==='All'?C.sandLight:C.textFaint}}>
            {tr('ideas.filterAll')}
          </div>
          {filterChips.map(k=>{
            const isInbox = k === '__inbox__';
            const label = isInbox ? tr('ideas.inbox') : k;
            const m = tagMeta(k);
            const sel = filterTag===k;
            return (
              <div key={k} onClick={()=>setFilterTag(k)} title={label}
                style={{padding:'4px 10px',borderRadius:999,fontSize:9,fontWeight:600,letterSpacing:'0.04em',cursor:'pointer',
                  border:`1px solid ${sel?m.color:C.border}`,background:sel?m.faint:'transparent',color:sel?m.text:C.textFaint,
                  display:'flex',gap:6,alignItems:'center',maxWidth:180,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>
                <span style={{overflow:'hidden',textOverflow:'ellipsis'}}>{label}</span>
                <span style={{opacity:0.6,flex:'0 0 auto'}}>{projectCounts[k]}</span>
              </div>
            );
          })}
        </div>
      </div>

      {ideas.length === 0 ? (
        <Card><Empty icon="lightbulb" title={filter==='archived'?tr('ideas.noArchived'):tr('ideas.noIdeasYet')} sub={tr('ideas.captureBelow')}/></Card>
      ) : (
        <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(280px,1fr))',gap:14}}>
          {ideas.map(idea=>{
            const projLabel = idea.tag || tr('ideas.inbox');
            const m = tagMeta(idea.tag || 'Other');
            return (
            <Card key={idea.id} data-idea-id={idea.id} style={{padding:20,opacity:idea.archived?0.5:1,
              display:'flex',flexDirection:'column',minHeight:160,height:'100%'}}>
              {editing===idea.id ? (
                <div className="vyb-card-edit-in" style={{display:'flex',flexDirection:'column',flex:1,minHeight:0}}>
                  <textarea value={editText} onChange={e=>setEditText(e.target.value)} autoFocus
                    style={{width:'100%',minHeight:80,background:'rgba(250,250,248,0.03)',border:`1px solid ${C.sand}`,borderRadius:8,padding:'10px 12px',fontSize:13,color:C.textPrimary,outline:'none',fontFamily:'Montserrat,sans-serif',resize:'vertical',lineHeight:1.5,flex:1}}/>
                  <div style={{display:'flex',alignItems:'center',gap:8,marginTop:10,flexWrap:'wrap'}}>
                    <span style={{fontSize:9,fontWeight:600,letterSpacing:'0.14em',color:C.textFaint,textTransform:'uppercase'}}>
                      {tr('tasks.projectLabel') || 'Project'}
                    </span>
                    <input value={editProject} onChange={e=>setEditProject(e.target.value.slice(0, 20))}
                      maxLength={20} placeholder={tr('ideas.inbox')}
                      style={{flex:'1 1 140px',minWidth:120,maxWidth:240,
                        padding:'5px 12px',borderRadius:999,fontSize:11,fontWeight:500,
                        background:C.sandFaint,color:C.textPrimary,outline:'none',
                        border:`1px solid ${C.sand}`,fontFamily:'Montserrat,sans-serif'}}/>
                  </div>
                  <div style={{display:'flex',gap:6,marginTop:12,justifyContent:'flex-end'}}>
                    <Button variant="ghost" size="sm" onClick={()=>setEditing(null)}>{tr('common.cancel')}</Button>
                    <Button size="sm" onClick={()=>{
                      const t = editProject.trim();
                      store.updateIdea(idea.id,{text:editText, tag: t || null});
                      if (t) bumpProject(t);
                      setEditing(null);
                    }}>{tr('common.save')}</Button>
                  </div>
                </div>
              ) : (
                <>
                  <div style={{display:'inline-flex',alignSelf:'flex-start',padding:'3px 10px',borderRadius:999,background:m.faint,
                    border:`1px solid ${m.color}33`,marginBottom:12,
                    fontSize:9,fontWeight:600,letterSpacing:'0.04em',color:m.text,
                    maxWidth:'100%',whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>
                    {projLabel}
                  </div>
                  <div style={{fontSize:13,fontWeight:400,color:C.textPrimary,lineHeight:1.5,
                    flex:1,wordBreak:'break-word',overflowWrap:'anywhere'}}>{idea.text}</div>
                  {converting === idea.id ? (
                    <div style={{marginTop:14,paddingTop:14,borderTop:`1px solid ${C.border}`,display:'flex',flexDirection:'column',gap:10}}>
                      <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.18em',color:C.textFaint,textTransform:'uppercase'}}>
                        {tr('ideas.convertEditTitle')}
                      </div>
                      <textarea value={convertTitle} onChange={e=>setConvertTitle(e.target.value)} autoFocus
                        rows={2}
                        onKeyDown={e=>{ if(e.key==='Enter' && (e.metaKey||e.ctrlKey)){ e.preventDefault(); commitConvert(idea); } if(e.key==='Escape'){ cancelConvert(); } }}
                        style={{width:'100%',background:'rgba(250,250,248,0.03)',border:`1px solid ${C.sand}`,borderRadius:8,
                          padding:'10px 12px',fontSize:13,color:C.textPrimary,outline:'none',
                          fontFamily:'Montserrat,sans-serif',resize:'vertical',lineHeight:1.45}}/>
                      <div style={{display:'flex',gap:6,flexWrap:'wrap'}}>
                        {TASK_PRIS.map(p => (
                          <div key={p.key} onClick={()=>setConvertPri(p.key)}
                            style={{padding:'4px 10px',borderRadius:999,fontSize:9,fontWeight:600,letterSpacing:'0.12em',textTransform:'uppercase',cursor:'pointer',display:'inline-flex',alignItems:'center',gap:6,
                              border:`1px solid ${convertPri===p.key?p.color:C.border}`,background:convertPri===p.key?p.faint:'transparent',color:convertPri===p.key?C.textPrimary:C.textMuted,transition:'all 0.15s'}}>
                            <span style={{width:5,height:5,borderRadius:999,background:p.color}}/>
                            {window.dl ? window.dl('priority', p.key) : p.label}
                          </div>
                        ))}
                      </div>
                      <div style={{display:'flex',gap:6,justifyContent:'flex-end'}}>
                        <Button variant="ghost" size="sm" onClick={cancelConvert}>{tr('common.cancel')}</Button>
                        <Button size="sm" icon="check" onClick={()=>commitConvert(idea)} style={!convertTitle.trim()?{opacity:0.5,cursor:'not-allowed'}:{}}>{tr('ideas.convertCreate')}</Button>
                      </div>
                    </div>
                  ) : converted && converted.ideaId === idea.id ? (
                    <div style={{marginTop:14,paddingTop:12,borderTop:`1px solid ${C.border}`,
                      display:'flex',alignItems:'center',justifyContent:'space-between',gap:10,flexWrap:'wrap'}}>
                      <div style={{display:'inline-flex',alignItems:'center',gap:8,
                        padding:'6px 12px',borderRadius:999,
                        background:C.sandFaint,border:`1px solid rgba(160,138,86,0.35)`}}>
                        <Icon name="check" size={11} color={C.sandLight} sw={2}/>
                        <span style={{fontSize:10,fontWeight:600,color:C.sandLight,letterSpacing:'0.04em'}}>
                          {tr('ideas.convertSuccess')}
                        </span>
                      </div>
                      <button onClick={goToTasks} style={{background:'transparent',border:'none',cursor:'pointer',
                        color:C.textSecondary,fontSize:10,fontWeight:600,letterSpacing:'0.12em',textTransform:'uppercase',
                        fontFamily:'Montserrat,sans-serif',textDecoration:'underline',textUnderlineOffset:'3px',padding:'4px 6px'}}>
                        {tr('ideas.convertViewTask')}
                      </button>
                    </div>
                  ) : null}
                  <CardActions>
                    <IconButton icon="list-todo" onClick={()=>startConvert(idea)} title={tr('ideas.convertToTask')}/>
                    <IconButton icon="pencil" onClick={()=>{setEditing(idea.id);setEditText(idea.text);setEditProject(idea.tag||'');}} title={tr('common.edit')}/>
                    <IconButton icon={idea.archived?'archive-restore':'archive'} onClick={()=>store.updateIdea(idea.id,{archived:!idea.archived})} title={idea.archived?tr('ideas.restore'):tr('common.archive')}/>
                    <IconButton icon="trash-2" variant="danger" onClick={()=>store.deleteIdea(idea.id)} title={tr('common.delete')}/>
                  </CardActions>
                </>
              )}
            </Card>
            );
          })}
        </div>
      )}

      <Composer value={input}
        onChange={(v)=>{ setInput(v); if(error) setError(''); }}
        onSubmit={add}
        placeholder={tr('ideas.composerPlaceholder')}
        error={error} inputRef={ideasInputRef}
        recordingNode={dictation.listening ? (
          <RecordingComposer levelRef={dictation.levelRef} transcript={dictation.transcript}
            onCancel={dictation.cancel} onConfirm={dictation.confirm}/>
        ) : null}
        leadingAction={null}
        aboveBubble={(() => {
          // Same V2 inline state machine as Tasks composer.
          // [+ Add category | Custom] | [💼 Business] [🎬 Content] [✨ General] ...
          const lang = (window.getLang && window.getLang()) || 'en';
          const isEs = lang === 'es';
          const pillBase = {
            display:'inline-flex',alignItems:'center',gap:4,flex:'0 0 auto',
            height:24,padding:'0 8px',borderRadius:999,
            background:'rgba(20,19,17,0.96)',
            border:'1px solid rgba(255,255,255,0.12)',
            color:'rgba(255,255,255,0.68)',cursor:'pointer',
            fontFamily:'Montserrat,sans-serif',fontSize:11,fontWeight:600,
            lineHeight:1,letterSpacing:'0.02em',
            minWidth:0,whiteSpace:'nowrap',
            boxShadow:'0 4px 14px rgba(0,0,0,0.18)',
            transition:'background 0.15s, border-color 0.15s, color 0.15s',
          };
          const rowStyle = {display:'flex',gap:5,alignItems:'center',
            justifyContent:'flex-start',width:'100%',
            flexWrap:'nowrap',overflowX:'auto',WebkitOverflowScrolling:'touch',
            paddingBottom:2,scrollbarWidth:'none'};
          const dividerStyle = {flex:'0 0 auto',width:1,height:14,
            background:'rgba(255,255,255,0.10)',margin:'0 3px'};

          const projNk = normalizeProjectKey(project);
          const activePresetKey = (() => {
            for (const p of IDEA_PRESETS) {
              if (normalizeProjectKey(p.en) === projNk) return p.key;
              if (normalizeProjectKey(p.es) === projNk) return p.key;
            }
            return null;
          })();
          const isCustomSelected = projNk && !activePresetKey;

          // Custom categories: known tags + MRU, excluding presets.
          // Sorted by usage; stale (>4 days) hidden — but currently-selected
          // is always protected so it never disappears mid-flow.
          const customSeen = new Map();
          for (const p of knownProjects) {
            const nk = normalizeProjectKey(p);
            if (nk && !isPresetTag(p) && !customSeen.has(nk)) customSeen.set(nk, p);
          }
          for (const p of projectMRU) {
            const nk = normalizeProjectKey(p);
            if (nk && !isPresetTag(p) && !customSeen.has(nk)) customSeen.set(nk, p);
          }
          if (isCustomSelected && !customSeen.has(projNk)) customSeen.set(projNk, project);
          const customListAll = Array.from(customSeen.values());
          const customListSorted = sortByUsage(customListAll, 'ideas');
          const customList = filterFreshOptions(customListSorted, 'ideas',
            (l) => normalizeProjectKey(l) === projNk);

          const pickCategory = (val) => {
            const next = val || (IDEA_PRESETS.find(p => p.key === 'general') || IDEA_PRESETS[0])[isEs ? 'es' : 'en'];
            setProject(next);
            if (val) bumpProject(val);
            setSelectorMode(null);
            if (error) setError('');
          };
          const submitNewCategory = () => {
            const v = sheetNewProjectName.trim().replace(/\s+/g,' ');
            if (!v) return;
            const nk = normalizeProjectKey(v);
            if (isPresetTag(v)) {
              const preset = IDEA_PRESETS.find(p =>
                normalizeProjectKey(p.en) === nk || normalizeProjectKey(p.es) === nk);
              pickCategory(preset ? preset[isEs ? 'es' : 'en'] : v);
            } else {
              const existing = customSeen.get(nk);
              pickCategory(existing || v);
            }
            setSheetNewProjectName('');
          };

          if (selectorMode === 'newCategory') {
            const canAdd = sheetNewProjectName.trim().length > 0
              && sheetNewProjectName.length <= MAX_PROJECT_LEN;
            return (
              <div className="vyb-composer-sheet" style={rowStyle} onMouseDown={(e)=>{ if (e.target && e.target.closest && e.target.closest('button')) e.preventDefault(); }}>
                <input ref={newProjectInlineRef} value={sheetNewProjectName}
                  onChange={e=>setSheetNewProjectName(e.target.value.slice(0, MAX_PROJECT_LEN))}
                  onKeyDown={e=>{
                    if (e.key === 'Enter') { e.preventDefault(); if (canAdd) submitNewCategory(); }
                    else if (e.key === 'Escape') { setSelectorMode('category'); setSheetNewProjectName(''); }
                  }}
                  placeholder={tr('ideas.categoryNamePlaceholder')}
                  maxLength={MAX_PROJECT_LEN}
                  style={{flex:'1 1 140px',minWidth:120,maxWidth:240,
                    height:24,padding:'0 10px',borderRadius:999,
                    background:'rgba(20,19,17,0.96)',
                    border:`1px solid ${C.borderMid}`,
                    color:C.textPrimary,outline:'none',
                    fontFamily:'Montserrat,sans-serif',fontSize:11,fontWeight:600,
                    letterSpacing:'0.02em'}}/>
                <button onClick={submitNewCategory} disabled={!canAdd}
                  style={{...pillBase,
                    background: canAdd ? C.sand : 'rgba(160,138,86,0.25)',
                    border:'none', color:C.bg,
                    cursor: canAdd ? 'pointer' : 'not-allowed',
                    fontWeight:700,letterSpacing:'0.10em',textTransform:'uppercase'}}>
                  {tr('tasks.addAction')}
                </button>
                <button onClick={()=>{ setSelectorMode('category'); setSheetNewProjectName(''); }}
                  style={{...pillBase,color:C.textMuted}}>
                  {tr('tasks.cancelAction')}
                </button>
              </div>
            );
          }

          if (selectorMode === 'category') {
            return (
              <div className="vyb-composer-sheet" style={rowStyle} onMouseDown={(e)=>{ if (e.target && e.target.closest && e.target.closest('button')) e.preventDefault(); }}>
                {customList.map(p => {
                  const sel = normalizeProjectKey(p) === projNk;
                  return (
                    <button key={p}
                      onClick={()=> sel ? pickCategory('') : pickCategory(p)}
                      style={{...pillBase,
                        background: sel ? 'rgba(42,36,24,0.98)' : pillBase.background,
                        border:`1px solid ${sel ? 'rgba(160,138,86,0.42)' : 'rgba(255,255,255,0.12)'}`,
                        color: sel ? 'rgba(255,255,255,0.92)' : 'rgba(255,255,255,0.78)',
                        maxWidth:160,overflow:'hidden',textOverflow:'ellipsis'}}>
                      {p}
                    </button>
                  );
                })}
                <button onClick={()=>{ setSheetNewProjectName(''); setSelectorMode('newCategory'); }}
                  style={{...pillBase,
                    border:'1px dashed rgba(255,255,255,0.18)',color:C.textMuted,
                    background:'rgba(20,19,17,0.96)'}}>
                  {tr('ideas.newCategory')}
                </button>
              </div>
            );
          }

          return (
            <div className="vyb-composer-sheet" style={rowStyle} onMouseDown={(e)=>{ if (e.target && e.target.closest && e.target.closest('button')) e.preventDefault(); }}>
              <button onClick={()=>setSelectorMode('category')}
                aria-label={tr('ideas.addCategoryContext')}
                style={{...pillBase,
                  ...(isCustomSelected ? {} : { border:'1px dashed rgba(255,255,255,0.18)', color:C.textMuted })}}>
                <span style={{minWidth:0,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',maxWidth:120,
                  color: isCustomSelected ? 'rgba(255,255,255,0.92)' : C.textMuted}}>
                  {isCustomSelected ? project : tr('ideas.addCategory')}
                </span>
              </button>
              <span style={dividerStyle} aria-hidden="true"/>
              {IDEA_PRESETS.map(p => {
                const label = isEs ? p.es : p.en;
                const sel = activePresetKey === p.key;
                return (
                  <button key={p.key}
                    onClick={()=>{
                      if (sel) {
                        if (p.key === 'general') return;
                        pickCategory((IDEA_PRESETS.find(x => x.key === 'general'))[isEs ? 'es' : 'en']);
                        return;
                      }
                      setProject(label);
                      if (error) setError('');
                    }}
                    aria-pressed={sel}
                    style={{...pillBase,
                      background: sel ? 'rgba(42,36,24,0.98)' : pillBase.background,
                      border:`1px solid ${sel ? 'rgba(160,138,86,0.42)' : 'rgba(255,255,255,0.12)'}`,
                      color: sel ? 'rgba(255,255,255,0.92)' : 'rgba(255,255,255,0.68)'}}>
                    <span style={{fontSize:12,lineHeight:1}}>{p.emoji}</span>
                    {label}
                  </button>
                );
              })}
            </div>
          );
        })()}
        actions={(
          <>
            <DictationButton supported={dictation.supported} listening={dictation.listening} onClick={dictation.toggle} levelRef={dictation.levelRef}/>
            <ComposerSendButton onClick={add} disabled={!input.trim()} label={tr('ideas.addIdea')} inline/>
          </>
        )}>
        {null}
      </Composer>
    </div>
  );
};

// ─── TASKS VIEW ────────────────────────────────────────────────
const TASK_PRIS = [
  {key:'urgent',    label:'Urgent',    color:C.clay,      faint:C.clayFaint,            desc:'Do first. Time-sensitive.'},
  {key:'important', label:'Important', color:C.sand,      faint:C.sandFaint,            desc:'Core work. High leverage.'},
  {key:'later',     label:'Later',     color:C.textMuted, faint:'rgba(102,94,88,0.15)', desc:'When time allows.'},
];
const TASK_PRI_KEYS = TASK_PRIS.map(p => p.key);
const TASK_PRI_BY_KEY = Object.fromEntries(TASK_PRIS.map(p => [p.key, p]));
const NO_PROJECT = '__none__';
const PROJECT_LABEL = (key) => key === NO_PROJECT ? (window.t ? window.t('tasks.generalProject') : 'General') : key;
const startOfToday = () => { const d = new Date(); d.setHours(0,0,0,0); return d.getTime(); };

// ─── Smart Task Parser ─────────────────────────────────────────
// Local, client-side natural-language parser. No API calls.
// Detects priority, due date, and project from the raw input
// (English + Spanish) and returns a cleaned title.
//
// Flag: temporarily disabled. The parser stays in the codebase so we can
// reintroduce it later (e.g. behind a setting), but the composer treats
// every keystroke as plain text — project/priority must be set manually.
const ENABLE_TASK_TEXT_DETECTION = false;
const parseTaskInput = (raw, opts = {}) => {
  const projects = (Array.isArray(opts.projects) ? opts.projects : []).filter(Boolean);
  const lang = opts.language || (window.getLang ? window.getLang() : 'en');
  const now  = opts.now instanceof Date ? opts.now : new Date();
  const empty = { cleanTitle:'', priority:'important', projectId:null, projectName:null,
                  dueDate:null, dueKind:null, detected:{ priority:false, project:false, date:false } };
  const text = String(raw || '');
  if (!text.trim()) return empty;
  if (!ENABLE_TASK_TEXT_DETECTION) {
    // Pass-through mode: keep the raw title, never auto-pick anything.
    return { ...empty, cleanTitle: text.trim() };
  }

  const compress = s => s.replace(/\s{2,}/g, ' ');
  let working = ' ' + text + ' ';

  // Priority keywords (Urgent > Later > Important).
  const RE_URGENT = /\b(urgente?|asap|hoy|today|ahora|now|importante hoy|para hoy)\b/gi;
  const RE_LATER  = /\b(later|despu[eé]s|luego|someday|alg[uú]n d[ií]a)\b/gi;
  const RE_IMP    = /\b(importante?|priority|prioridad|focus|enfoque)\b/gi;

  let priority = null, detPri = false;
  if (RE_URGENT.test(working)) { priority='urgent'; detPri=true; working = working.replace(RE_URGENT,' '); }
  else if (RE_LATER.test(working)) { priority='later'; detPri=true; working = working.replace(RE_LATER,' '); }
  if (!priority && RE_IMP.test(working)) { priority='important'; detPri=true; working = working.replace(RE_IMP,' '); }
  // Strip "important" word even when urgent already won (e.g. "importante hoy").
  if (priority === 'urgent') working = working.replace(RE_IMP, ' ');
  working = compress(working);

  // Date detection.
  const pad = n => String(n).padStart(2,'0');
  const fmt = d => `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`;
  const startOfDay = d => { const x=new Date(d); x.setHours(0,0,0,0); return x; };
  const addDays = (d,n) => { const x=new Date(d); x.setDate(x.getDate()+n); return x; };
  const today = startOfDay(now);

  const DOW = {
    sunday:0, monday:1, tuesday:2, wednesday:3, thursday:4, friday:5, saturday:6,
    domingo:0, lunes:1, martes:2, miercoles:3, jueves:4, viernes:5, sabado:6,
  };

  let dueDate=null, dueKind=null, detDate=false;
  const tryDate = (re, kind, fn) => {
    if (detDate) return;
    if (re.test(working)) {
      dueDate = fmt(fn()); dueKind = kind; detDate = true;
      working = compress(working.replace(re, ' '));
    }
  };
  tryDate(/\bpasado ma[ñn]ana\b/gi, 'dayAfter', () => addDays(today,2));
  tryDate(/\b(ma[ñn]ana|tomorrow)\b/gi, 'tomorrow', () => addDays(today,1));
  tryDate(/\b(hoy|today)\b/gi, 'today', () => today);
  tryDate(/\bnext week\b/gi, 'nextWeek', () => addDays(today,7));
  tryDate(/\b(esta semana|this week)\b/gi, 'thisWeek', () => today);

  if (!detDate) {
    const dowRe = /\b(?:el |next |pr[oó]xim[oa] )?(sunday|monday|tuesday|wednesday|thursday|friday|saturday|domingo|lunes|martes|mi[eé]rcoles|jueves|viernes|s[aá]bado)\b/i;
    const m = working.match(dowRe);
    if (m) {
      const key = m[1].toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'');
      const target = DOW[key];
      if (target != null) {
        const cur = today.getDay();
        let diff = (target - cur + 7) % 7;
        if (diff === 0) diff = 7;
        dueDate = fmt(addDays(today, diff));
        dueKind = `dow:${target}`;
        detDate = true;
        working = compress(working.replace(dowRe, ' '));
      }
    }
  }

  // Project detection: existing names only, longest match wins, case-insensitive.
  let projectName = null, detProj = false;
  const escape = s => s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
  const sortedProjects = [...new Set(projects)].sort((a,b) => b.length - a.length);
  for (const p of sortedProjects) {
    const esc = escape(p);
    const wrapRe = new RegExp(`\\b(?:para|for|en|in|de|of)\\s+${esc}\\b`, 'i');
    const bareRe = new RegExp(`\\b${esc}\\b`, 'i');
    if (wrapRe.test(working)) {
      projectName = p; detProj = true;
      working = compress(working.replace(wrapRe, ' '));
      break;
    }
    if (bareRe.test(working)) {
      projectName = p; detProj = true;
      working = compress(working.replace(bareRe, ' '));
      break;
    }
  }

  // Trim leading/trailing connector words and stray punctuation.
  let cleanTitle = working
    .replace(/\b(el|la|los|las|the|para|for|en|in|de|of)\b\s*$/i, '')
    .replace(/^\s*\b(el|la|los|las|the|para|for)\b\s+/i, '')
    .replace(/\s{2,}/g, ' ')
    .trim()
    .replace(/[\s,;:.!\-]+$/, '')
    .trim();
  if (!cleanTitle) cleanTitle = text.trim();

  return {
    cleanTitle,
    priority: priority || 'important',
    projectId: projectName,
    projectName,
    dueDate,
    dueKind,
    detected: { priority: detPri, project: detProj, date: detDate },
  };
};

// Human-friendly label for a parsed due date.
const formatDueLabel = (kind, dateStr, lang) => {
  if (!dateStr) return '';
  const isEs = lang === 'es';
  if (kind === 'today')    return isEs ? 'Hoy' : 'Today';
  if (kind === 'tomorrow') return isEs ? 'Mañana' : 'Tomorrow';
  if (kind === 'dayAfter') return isEs ? 'Pasado mañana' : 'Day after tomorrow';
  if (kind === 'thisWeek') return isEs ? 'Esta semana' : 'This week';
  if (kind === 'nextWeek') return isEs ? 'Próxima semana' : 'Next week';
  try {
    const d = new Date(dateStr + 'T00:00:00');
    const loc = isEs ? 'es-ES' : 'en-US';
    const wd = d.toLocaleDateString(loc, { weekday: 'long' });
    return wd.charAt(0).toUpperCase() + wd.slice(1);
  } catch { return dateStr; }
};

Object.assign(window, { parseTaskInput, formatDueLabel });

// ─── Idea → Task title generator ───────────────────────────────
// Keeps it short, action-oriented, and faithful. No external API.
const generateTaskTitleFromIdea = (raw) => {
  let s = String(raw || '').trim();
  if (!s) return '';

  // 1. Strip leading "Idea: " prefix (EN/ES).
  s = s.replace(/^\s*idea\s*[:\-–]\s*/i, '');

  // 2. Take only the first sentence/clause if the idea is multi-sentence.
  //    Prefer a hard sentence break; fall back to the first comma if very long.
  const firstPeriod = s.search(/[.!?](\s|$)/);
  if (firstPeriod > 24) s = s.slice(0, firstPeriod);
  if (s.length > 100) {
    const firstComma = s.indexOf(',');
    if (firstComma > 24) s = s.slice(0, firstComma);
  }

  // 3. Strip filler / hedge phrases.
  const fillers = [
    /\bser[ií]a (?:bueno|genial|interesante)\s*/gi,
    /\bme gustar[ií]a\s*/gi,
    /\bcreo que\s*/gi,
    /\btal vez\s*/gi,
    /\bquiz[aá]s?\s*/gi,
    /\bpodr[ií]a(?:mos|n)?\s*/gi,
    /\bhay que\s*/gi,
    /\bdeber[ií]amos?\s*/gi,
    /\bnecesito\s*/gi,
    /\bquiero\s*/gi,
    /\bmaybe\s*/gi,
    /\bperhaps\s*/gi,
    /\bi think\s*/gi,
    /\bi want to\s*/gi,
    /\bi'?d like to\s*/gi,
    /\bwe should\s*/gi,
    /\bit would be (?:good|nice|cool|interesting) to\s*/gi,
  ];
  for (const re of fillers) s = s.replace(re, '');
  s = s.replace(/\s{2,}/g, ' ').trim();

  // 4. Drop a leading connector if it survived ("que", "to", "para").
  s = s.replace(/^(?:que|to|para|de|of|the|a|an|el|la|los|las)\s+/i, '');

  // 5. Hard cap ~70 chars at a word boundary.
  const MAX = 70;
  if (s.length > MAX) {
    const slice = s.slice(0, MAX);
    const lastSpace = slice.lastIndexOf(' ');
    s = (lastSpace > 30 ? slice.slice(0, lastSpace) : slice).replace(/[\s,;:.!\-]+$/, '') + '…';
  }

  // 6. Capitalize first letter.
  if (s) s = s.charAt(0).toUpperCase() + s.slice(1);
  return s.replace(/[\s,;:.!\-]+$/, '');
};

// Detect urgent intent in an idea (subset of parser rules) for default priority.
const ideaPriorityFromText = (text) => {
  const t = ' ' + String(text || '') + ' ';
  return /\b(urgente?|asap|hoy|today|ahora|now)\b/i.test(t) ? 'urgent' : 'important';
};

Object.assign(window, { generateTaskTitleFromIdea, ideaPriorityFromText });

// Animated typewriter placeholder. Cycles through phrases with type → pause →
// erase → next. Disabled (returns the static fallback) when the user has
// `prefers-reduced-motion: reduce` set, or when `active` is false.
const useAnimatedPlaceholder = (phrases, active, fallback = '') => {
  const reduced = typeof window !== 'undefined' && window.matchMedia
    && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const [text, setText] = React.useState(() => (phrases && phrases[0]) || fallback);
  React.useEffect(() => {
    if (!active || reduced || !phrases || phrases.length === 0) return;
    let i = 0, j = 0, mode = 'typing';
    let timer = null;
    const tick = () => {
      const phrase = phrases[i % phrases.length];
      if (mode === 'typing') {
        j += 1;
        setText(phrase.slice(0, j));
        if (j >= phrase.length) { mode = 'pause'; timer = setTimeout(tick, 1100); return; }
        timer = setTimeout(tick, 55);
      } else if (mode === 'pause') {
        mode = 'erasing';
        timer = setTimeout(tick, 30);
      } else { // erasing
        j -= 1;
        setText(phrase.slice(0, Math.max(0, j)));
        if (j <= 0) { mode = 'typing'; i += 1; timer = setTimeout(tick, 220); return; }
        timer = setTimeout(tick, 32);
      }
    };
    setText(''); j = 0; mode = 'typing';
    timer = setTimeout(tick, 240);
    return () => { if (timer) clearTimeout(timer); };
  }, [active, reduced, phrases && phrases.join('|')]);
  if (reduced) return fallback;
  if (!active) return fallback;
  return text || ' ';
};

// Module-level pointer used to hand off "freshly moved" highlight between
// the project-change handler and the next TaskRow mount in the destination
// group. Intentionally a plain ref object — no re-renders needed.
const __recentlyMovedTask = { id: null };

// One row in a project group. Renders compactly, supports inline rename,
// project switching via a subtle popover, and two-step delete confirm.
const TaskRow = ({ task, pri, store, projectOptions=[] }) => {
  const [editing, setEditing] = React.useState(false);
  const [text, setText] = React.useState(task.text);
  const [completing, setCompleting] = React.useState(false);
  const [menuOpen, setMenuOpen] = React.useState(false);
  const [moveOpen, setMoveOpen] = React.useState(false);
  const [confirmDelete, setConfirmDelete] = React.useState(false);
  const [highlight, setHighlight] = React.useState(false);
  const completeTimerRef = React.useRef(null);
  const confirmTimerRef = React.useRef(null);
  const highlightTimerRef = React.useRef(null);
  const rowRef = React.useRef(null);
  const menuRef = React.useRef(null);
  const moveRef = React.useRef(null);
  const isMobile = (typeof window !== 'undefined') && !!(window.matchMedia && window.matchMedia('(max-width: 768px)').matches);
  const isEs = ((window.getLang && window.getLang()) === 'es');

  React.useEffect(() => () => {
    if (completeTimerRef.current)  clearTimeout(completeTimerRef.current);
    if (confirmTimerRef.current)   clearTimeout(confirmTimerRef.current);
    if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current);
  }, []);

  // On mount: if this row is the freshly-moved task, glow + scroll into view.
  React.useEffect(() => {
    if (__recentlyMovedTask.id !== task.id) return;
    __recentlyMovedTask.id = null;
    setHighlight(true);
    try { rowRef.current && rowRef.current.scrollIntoView({behavior:'smooth', block:'center'}); } catch {}
    highlightTimerRef.current = setTimeout(() => setHighlight(false), 1800);
  }, []);

  // Close project menu on outside click.
  React.useEffect(() => {
    if (!menuOpen) return;
    const onDoc = (e) => { if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('touchstart', onDoc);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('touchstart', onDoc);
    };
  }, [menuOpen]);

  // Close move menu on outside click.
  React.useEffect(() => {
    if (!moveOpen) return;
    const onDoc = (e) => { if (moveRef.current && !moveRef.current.contains(e.target)) setMoveOpen(false); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('touchstart', onDoc);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('touchstart', onDoc);
    };
  }, [moveOpen]);

  // Move task to a different priority board. Project is preserved — the
  // destination board renders the project group automatically.
  const moveTo = (newPri) => {
    setMoveOpen(false);
    if (!newPri || newPri === task.pri) return;
    __recentlyMovedTask.id = task.id;
    store.updateTask(task.id, { pri: newPri });
  };

  // Coordinate two-step delete across rows: only one row in confirm state.
  React.useEffect(() => {
    const onConfirm = (e) => {
      if (!e || !e.detail) return;
      if (e.detail.id !== task.id && confirmDelete) {
        setConfirmDelete(false);
        if (confirmTimerRef.current) { clearTimeout(confirmTimerRef.current); confirmTimerRef.current = null; }
      }
    };
    window.addEventListener('vyb-task-confirm-delete', onConfirm);
    return () => window.removeEventListener('vyb-task-confirm-delete', onConfirm);
  }, [task.id, confirmDelete]);

  const handleCheckClick = () => {
    if (task.done) { store.toggleTask(task.id); return; }
    if (completing) return;
    const reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduced) { store.toggleTask(task.id); return; }
    setCompleting(true);
    completeTimerRef.current = setTimeout(() => {
      try { store.toggleTask(task.id); } catch {}
    }, 780);
  };
  const showAsDone = task.done || completing;
  const commit = () => {
    const v = text.trim();
    if (v && v !== task.text) store.updateTask(task.id, { text: v });
    else setText(task.text);
    setEditing(false);
  };
  const opts = React.useMemo(() => {
    const set = new Set(projectOptions);
    if (task.project) set.add(task.project);
    return Array.from(set).sort((a,b) => a.localeCompare(b));
  }, [projectOptions, task.project]);
  const pickProject = (v) => {
    setMenuOpen(false);
    if (!v || v === task.project) return;
    __recentlyMovedTask.id = task.id;
    store.updateTask(task.id, { project: v });
  };

  const handleDeleteClick = () => {
    if (confirmDelete) {
      if (confirmTimerRef.current) { clearTimeout(confirmTimerRef.current); confirmTimerRef.current = null; }
      try { store.deleteTask(task.id); } catch {}
      return;
    }
    setConfirmDelete(true);
    try { window.dispatchEvent(new CustomEvent('vyb-task-confirm-delete', { detail: { id: task.id } })); } catch {}
    confirmTimerRef.current = setTimeout(() => {
      setConfirmDelete(false);
      confirmTimerRef.current = null;
    }, 3000);
  };

  // Folder is a secondary utility action, not a primary indicator —
  // muted by default, brighter on hover (handled inline via state below).
  // Match the trash IconButton baseline so both feel like the same tier.
  const [folderHover, setFolderHover] = React.useState(false);
  const projectBtnColor = (folderHover || menuOpen) ? C.textPrimary : C.textMuted;
  const deleteLabel = isEs ? 'Borrar' : 'Delete';

  return (
    <div ref={rowRef} data-task-id={task.id}
      className={"vyb-task-row" + (completing ? " vyb-task-completing" : "") + (highlight ? " vyb-task-moved" : "")}
      style={{display:'flex',alignItems:'center',gap:10,padding:'7px 10px',borderRadius:6,
        background:'transparent',transition:'background 0.15s',opacity:task.done?0.45:1,minHeight:34,position:'relative'}}>
      <div className="vyb-task-handle" title="Drag to reorder or change priority"
        style={{opacity:0.35,transition:'opacity 0.15s',color:C.textFaint,display:'flex',alignItems:'center',cursor:'grab',flexShrink:0}}>
        <Icon name="grip-vertical" size={14} color={C.textFaint}/>
      </div>
      <div onClick={handleCheckClick}
        className={completing ? "vyb-task-check-anim" : ""}
        style={{width:16,height:16,borderRadius:999,border:`1.5px solid ${showAsDone?pri.color:C.borderMid}`,
          background:showAsDone?pri.color:'transparent',display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0,cursor:'pointer',transition:'all 0.2s ease-out'}}>
        {showAsDone && <Icon name="check" size={9} color={C.bg} sw={3}/>}
      </div>
      {editing ? (
        <input value={text} onChange={e=>setText(e.target.value)} autoFocus
          onKeyDown={e=>{ if(e.key==='Enter') commit(); if(e.key==='Escape'){ setText(task.text); setEditing(false); } }}
          onBlur={commit}
          style={{flex:1,background:'transparent',border:`1px solid ${C.sand}`,borderRadius:4,padding:'3px 6px',fontSize:12,color:C.textPrimary,outline:'none',fontFamily:'Montserrat,sans-serif'}}/>
      ) : (
        <span onClick={()=>{ if (completing) return; setText(task.text); setEditing(true); }}
          className={completing ? "vyb-task-title-completing" : ""}
          style={{flex:1,fontSize:12,fontWeight:400,color:C.textPrimary,lineHeight:1.35,textDecoration:showAsDone?'line-through':'none',cursor:'text',transition:'color 0.3s ease-out'}}>
          {task.text}
        </span>
      )}
      {/* Mobile: explicit Move button. Drag/drop is unreliable on touch,
          so a small popover with Urgent / Important / Later is the
          primary path for changing a task's board on mobile. */}
      {isMobile && (
        <div ref={moveRef} style={{position:'relative',flexShrink:0}}>
          <button title={isEs?'Mover a otro tablero':'Move to another board'}
            aria-label={isEs?'Mover tarea':'Move task'}
            onClick={()=>setMoveOpen(o=>!o)}
            style={{width:30,height:30,borderRadius:8,
              background: moveOpen ? 'rgba(250,250,248,0.05)' : 'transparent',
              border:'none',cursor:'pointer',display:'inline-grid',placeItems:'center',
              opacity: moveOpen ? 1 : 0.72,
              padding:0,transition:'all 0.18s ease-out',
              WebkitTapHighlightColor:'transparent'}}>
            <Icon name="move-vertical" size={12}
              color={moveOpen ? C.textPrimary : C.textMuted} sw={1.8}/>
          </button>
          {moveOpen && (
            <div style={{position:'absolute',top:'calc(100% + 4px)',right:0,zIndex:50,
              minWidth:160,background:C.card,border:`1px solid ${C.borderMid}`,borderRadius:10,
              boxShadow:'0 12px 32px rgba(0,0,0,0.45)',padding:4}}>
              <div style={{padding:'6px 10px 4px',fontSize:9,fontWeight:700,
                letterSpacing:'0.18em',color:C.textFaint,textTransform:'uppercase'}}>
                {isEs?'Mover a':'Move to'}
              </div>
              {TASK_PRIS.map(p => {
                const active = p.key === task.pri;
                return (
                  <div key={p.key} onClick={()=>moveTo(p.key)}
                    onMouseEnter={e=>e.currentTarget.style.background='rgba(250,250,248,0.05)'}
                    onMouseLeave={e=>e.currentTarget.style.background='transparent'}
                    style={{padding:'8px 10px',fontSize:11,fontWeight:600,
                      color: active ? C.sandLight : C.textSecondary,
                      letterSpacing:'0.06em',borderRadius:6,cursor:'pointer',
                      display:'flex',alignItems:'center',gap:8,transition:'background 0.12s',
                      textTransform:'uppercase'}}>
                    <span style={{width:6,height:6,borderRadius:999,background:p.color,flexShrink:0}}/>
                    <span>{window.dl ? window.dl('priority', p.key) : p.label}</span>
                    {active && (
                      <span style={{marginLeft:'auto',fontSize:9,color:C.textFaint,fontStyle:'italic'}}>
                        {isEs?'actual':'current'}
                      </span>
                    )}
                  </div>
                );
              })}
            </div>
          )}
        </div>
      )}
      {/* Subtle change-project button + popover. Sized + tinted to match
          the trash IconButton so both icons feel like the same tier. */}
      <div ref={menuRef} style={{position:'relative',flexShrink:0}}>
        <button title={isEs?'Cambiar proyecto':'Change project'}
          aria-label={isEs?'Cambiar proyecto':'Change project'}
          onClick={()=>setMenuOpen(o=>!o)}
          onMouseEnter={()=>setFolderHover(true)} onMouseLeave={()=>setFolderHover(false)}
          style={{width:30,height:30,borderRadius:8,
            background: (menuOpen || folderHover) ? 'rgba(250,250,248,0.05)' : 'transparent',
            border:'none',cursor:'pointer',display:'inline-grid',placeItems:'center',
            opacity: (menuOpen || folderHover) ? 1 : 0.72,
            padding:0,transition:'all 0.18s ease-out',
            WebkitTapHighlightColor:'transparent'}}>
          <Icon name="folder" size={12} color={projectBtnColor} sw={1.8}/>
        </button>
        {menuOpen && (
          <div style={{position:'absolute',top:'calc(100% + 4px)',right:0,zIndex:50,
            minWidth:160,maxWidth:240,maxHeight:240,overflowY:'auto',
            background:C.card,border:`1px solid ${C.borderMid}`,borderRadius:10,
            boxShadow:'0 12px 32px rgba(0,0,0,0.45)',padding:4}}>
            {opts.length === 0 && (
              <div style={{padding:'8px 10px',fontSize:11,color:C.textFaint,fontStyle:'italic'}}>
                {isEs?'Sin proyectos':'No projects'}
              </div>
            )}
            {opts.map(p => {
              const active = p === task.project;
              return (
                <div key={p} onClick={()=>pickProject(p)}
                  onMouseEnter={e=>e.currentTarget.style.background='rgba(250,250,248,0.05)'}
                  onMouseLeave={e=>e.currentTarget.style.background='transparent'}
                  style={{padding:'7px 10px',fontSize:11,fontWeight:500,
                    color: active ? C.sandLight : C.textSecondary,
                    letterSpacing:'0.04em',borderRadius:6,cursor:'pointer',
                    display:'flex',alignItems:'center',gap:8,transition:'background 0.12s'}}>
                  <span style={{width:6,height:6,borderRadius:999,background:active?C.sand:'transparent',flexShrink:0}}/>
                  <span style={{overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',minWidth:0}}>{p}</span>
                </div>
              );
            })}
          </div>
        )}
      </div>
      {/* Two-step delete: trash icon → red "Delete/Borrar" pill on first tap */}
      {confirmDelete ? (
        <button onClick={handleDeleteClick}
          aria-label={isEs?'Confirmar borrar tarea':'Confirm delete task'}
          className="vyb-task-delete-confirm"
          style={{display:'inline-flex',alignItems:'center',gap:5,height:26,
            padding:'0 10px',borderRadius:999,
            background:'rgba(184,100,60,0.16)',border:'1px solid rgba(184,100,60,0.55)',
            color:'#D0825A',fontFamily:'Montserrat,sans-serif',
            fontSize:9,fontWeight:700,letterSpacing:'0.14em',textTransform:'uppercase',
            cursor:'pointer',whiteSpace:'nowrap',flexShrink:0,transition:'all 0.18s'}}>
          <Icon name="trash-2" size={11} color="#D0825A" sw={1.8}/>
          {deleteLabel}
        </button>
      ) : (
        <IconButton icon="trash-2" onClick={handleDeleteClick} size={12}
          title={isEs?'Borrar tarea':'Delete task'}/>
      )}
    </div>
  );
};

// Project group inside one priority section.
// Each list is a SortableJS instance sharing group "vyb-tasks" so items
// can be dragged across project groups AND across priority sections.
// On drop we read the destination container's data-pri to patch priority
// only — project is preserved verbatim and changed only via the row's
// project <select>.
const ProjectGroup = ({ projectKey, priKey, items, pri, store, projectOptions }) => {
  const tr = useT();
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!ref.current || !window.Sortable) return;
    const inst = window.Sortable.create(ref.current, {
      group: 'vyb-tasks',
      handle: '.vyb-task-handle',
      animation: 160,
      forceFallback: true,
      fallbackTolerance: 4,
      emptyInsertThreshold: 18,
      ghostClass: 'vyb-task-ghost',
      chosenClass: 'vyb-task-chosen',
      dragClass: 'vyb-task-drag',
      onStart: () => { document.body.classList.add('vyb-dragging'); },
      onEnd: (evt) => {
        document.body.classList.remove('vyb-dragging');
        try {
          const taskId = Number(evt.item.getAttribute('data-task-id'));
          if (!taskId) return;

          const fromEl = evt.from;
          const toEl   = evt.to;
          const oldIndex = evt.oldIndex;
          const newIndex = evt.newIndex;
          const fromPri      = fromEl.getAttribute('data-pri');
          const fromProjAttr = fromEl.getAttribute('data-project') || NO_PROJECT;
          const toPri        = toEl.getAttribute('data-pri');
          const toProjAttr   = toEl.getAttribute('data-project') || NO_PROJECT;
          const sameContainer = fromEl === toEl;
          const noChange = sameContainer && oldIndex === newIndex;
          const crossProject = toProjAttr !== fromProjAttr;

          // Snapshot the destination DOM order Sortable produced
          // (includes the dragged item at newIndex). Used to renumber sort_order
          // for valid same-project drops only.
          const destDomIds = !crossProject
            ? Array.from(toEl.children).map(el => Number(el.getAttribute('data-task-id'))).filter(Boolean)
            : [];

          // Capture the visual end-rect (where the user dropped) BEFORE
          // we revert. Used by the cross-project FLIP animation to glide
          // the card back from where it was dropped to its original slot.
          const endRect = crossProject ? evt.item.getBoundingClientRect() : null;

          // Revert Sortable's DOM mutation BEFORE dispatching state.
          // SortableJS edits DOM that React owns; if we leave it edited,
          // React's reconciler crashes on next render (the "black screen").
          // After revert, React re-renders from new state and owns the DOM.
          if (evt.item.parentNode) evt.item.parentNode.removeChild(evt.item);
          const refEl = fromEl.children[oldIndex] || null;
          fromEl.insertBefore(evt.item, refEl);

          // Cross-project + cross-priority drops ARE allowed:
          // moving a task to another board (Urgent/Important/Later) keeps
          // its own project — the destination board renders that group
          // automatically because the task now lives there. Same-priority
          // cross-project drops remain rejected (user dropped on a wrong
          // project chip inside the same board).
          //
          // The destination may be a column-wide "catch-all" zone — drop
          // anywhere in another board → just change the priority, no
          // rejection animation even if it's the same column (no-op).
          const isCatchAll = toEl && toEl.getAttribute('data-catchall') === '1';
          if (crossProject) {
            if (toPri && toPri !== fromPri) {
              __recentlyMovedTask.id = taskId;
              store.updateTask(taskId, { pri: toPri });
              return;
            }
            if (isCatchAll) return; // same-column catch-all drop → no-op
            const node = evt.item;
            const startRect = node.getBoundingClientRect();
            const dx = endRect.left - startRect.left;
            const dy = endRect.top  - startRect.top;
            if (dx || dy) {
              node.classList.add('vyb-task-rejected');
              node.style.transition = 'none';
              node.style.transform  = `translate(${dx}px, ${dy}px)`;
              node.style.willChange = 'transform';
              // Force reflow so the next frame animates from the offset.
              void node.getBoundingClientRect();
              requestAnimationFrame(() => {
                node.style.transition = 'transform 280ms cubic-bezier(0.22, 1, 0.36, 1)';
                node.style.transform  = 'translate(0, 0)';
              });
              const cleanup = () => {
                node.style.transition = '';
                node.style.transform  = '';
                node.style.willChange = '';
                node.classList.remove('vyb-task-rejected');
                node.removeEventListener('transitionend', cleanup);
              };
              node.addEventListener('transitionend', cleanup);
              setTimeout(cleanup, 600); // safety
            }
            return;
          }

          if (noChange) return;

          // Drag within the same project: priority change is the only
          // mutation. Sort order is renumbered from the destination DOM.
          const patch = {};
          if (toPri !== fromPri) patch.pri = toPri;

          if (Object.keys(patch).length) store.updateTask(taskId, patch);
          if (destDomIds.length) store.reorderTasks(destDomIds);
        } catch (err) {
          console.error('[tasks-drag] failed', err);
        }
      },
    });
    return () => { document.body.classList.remove('vyb-dragging'); inst.destroy(); };
  }, [items.length, priKey, projectKey]);

  // Project is required for new tasks, but legacy rows may still have
  // none — render them under a faint "General" heading. For an empty
  // group (only used as a drop target in an empty priority section),
  // suppress the heading and show a faint "Nothing here." caption.
  const isEmpty = items.length === 0;
  return (
    <div style={{marginBottom: isEmpty ? 0 : 14}}>
      {!isEmpty && (
        <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.16em',textTransform:'uppercase',
          color: projectKey === NO_PROJECT ? C.textFaint : C.sandLight, padding:'6px 10px 4px'}}>
          {PROJECT_LABEL(projectKey)}
        </div>
      )}
      <div ref={ref} data-pri={priKey} data-project={projectKey}
        style={{minHeight: isEmpty ? 32 : 0,
          display: isEmpty ? 'flex' : 'block', alignItems:'center'}}>
        {isEmpty
          ? <div style={{fontSize:11,color:C.textFaint,fontStyle:'italic',padding:'0 10px'}}>{tr('tasks.nothingHere')}</div>
          : items.map(t => (
              <TaskRow key={t.id} task={t} pri={pri} store={store} projectOptions={projectOptions}/>
            ))
        }
      </div>
    </div>
  );
};

// ── Column-wide catch-all drop zone ───────────────────────────
// Lives at the bottom of each priority column with at least one task.
// Lets the user drop anywhere inside the column (not just over an
// existing project group) to move a task into that board. The dragged
// task keeps its own project — the destination board renders the new
// group automatically because the task now lives there.
const BoardCatchAll = ({ priKey, stretch=false, emptyLabel=null }) => {
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!ref.current || !window.Sortable) return;
    const inst = window.Sortable.create(ref.current, {
      group: 'vyb-tasks',
      handle: '.vyb-task-handle',
      animation: 0,
      forceFallback: true,
      emptyInsertThreshold: 36,
      // No onEnd here — the drop is handled by the SOURCE ProjectGroup's
      // onEnd handler, which inspects toEl's data-pri / data-catchall.
    });
    return () => { try { inst.destroy(); } catch {} };
  }, [priKey]);
  return (
    <div ref={ref} data-pri={priKey} data-project="" data-catchall="1"
      style={{
        flex: stretch ? '1 1 auto' : '0 0 auto',
        minHeight: stretch ? 120 : 56,
        marginTop: stretch ? 0 : 6,
        borderRadius:10,
        border:`1px dashed transparent`,
        transition:'border-color 0.15s, background 0.15s',
        display:'flex', alignItems:'center', justifyContent:'center',
      }}>
      {emptyLabel ? (
        <span style={{fontSize:11, color:C.textFaint, fontStyle:'italic',
          padding:'0 10px', pointerEvents:'none'}}>{emptyLabel}</span>
      ) : null}
    </div>
  );
};
Object.assign(window, { BoardCatchAll });

// Highlight the catch-all (and the whole column body) while a drag is in
// flight — visual signal that "drop anywhere in this column" is valid.
// Hooked into the existing .vyb-dragging body class set by ProjectGroup.

// ── Daily Task Load summary ───────────────────────────────────
// Compact card at the top of Tasks. Computes load level from open
// task count, shows board breakdown, urgent overload warning, and
// a Top 3 preview. Reads existing data only — no schema change.
const TASK_LOAD_LEVELS = [
  { key:'empty',      max:0,        msg:'emptyMsg',      label:'empty' },
  { key:'light',      max:2,        msg:'lightMsg',      label:'light' },
  { key:'focused',    max:5,        msg:'focusedMsg',    label:'focused' },
  { key:'active',     max:8,        msg:'activeMsg',     label:'active' },
  { key:'heavy',      max:12,       msg:'heavyMsg',      label:'heavy' },
  { key:'overloaded', max:Infinity, msg:'overloadedMsg', label:'overloaded' },
];
const pickLoadLevel = (n) => TASK_LOAD_LEVELS.find(l => n <= l.max) || TASK_LOAD_LEVELS[0];

const DailyLoadCard = ({ tasks, store }) => {
  const tr = useT();
  const open = React.useMemo(() => tasks.filter(t => !t.done), [tasks]);
  const counts = React.useMemo(() => {
    const c = { urgent: 0, important: 0, later: 0 };
    for (const t of open) {
      const k = TASK_PRI_KEYS.includes(t.pri) ? t.pri : 'important';
      c[k] += 1;
    }
    return c;
  }, [open]);
  const total = open.length;
  const level = pickLoadLevel(total);

  // Top 3: urgent first (by sortOrder), then important, then later.
  const top3 = React.useMemo(() => {
    const byPri = (k) => open
      .filter(t => (TASK_PRI_KEYS.includes(t.pri) ? t.pri : 'important') === k)
      .sort((a,b) => (a.sortOrder||0) - (b.sortOrder||0) || (a.id - b.id));
    const out = [];
    for (const k of ['urgent','important','later']) {
      for (const t of byPri(k)) {
        if (out.length >= 3) break;
        out.push(t);
      }
      if (out.length >= 3) break;
    }
    return out;
  }, [open]);

  const warn =
      counts.urgent >= 8 ? { kind:'hi', text: tr('tasks.load.urgentWarnHi') }
    : counts.urgent >= 5 ? { kind:'lo', text: tr('tasks.load.urgentWarn') }
    : null;

  // Level color: warm sand for healthy ranges, slightly warmer for heavy/overloaded.
  const levelColor =
      level.key === 'empty'      ? C.textFaint
    : level.key === 'overloaded' ? (C.clayLight || '#D0825A')
    : level.key === 'heavy'      ? (C.clayLight || '#D0825A')
    : C.sandLight;
  const levelBg =
      level.key === 'empty'      ? 'transparent'
    : level.key === 'overloaded' ? 'rgba(184,100,60,0.14)'
    : level.key === 'heavy'      ? 'rgba(184,100,60,0.10)'
    : C.sandFaint;
  const levelBorder =
      level.key === 'empty'      ? C.border
    : level.key === 'overloaded' ? 'rgba(184,100,60,0.55)'
    : level.key === 'heavy'      ? 'rgba(184,100,60,0.40)'
    : 'rgba(160,138,86,0.45)';

  // Segmented meter (13 cells; clamp display).
  const SEG = 13;
  const fill = Math.min(SEG, total);
  const segColor = (i) => {
    if (i >= fill) return 'rgba(250,250,248,0.06)';
    if (level.key === 'overloaded' || level.key === 'heavy') return 'rgba(195,123,90,0.65)';
    if (level.key === 'active')   return 'rgba(160,138,86,0.85)';
    if (level.key === 'focused')  return 'rgba(160,138,86,0.65)';
    if (level.key === 'light')    return 'rgba(160,138,86,0.45)';
    return 'rgba(160,138,86,0.30)';
  };

  return (
    <Card style={{padding:'14px 18px',marginBottom:14}} className="vyb-load-card">
      <div className="vyb-load-head" style={{display:'flex',alignItems:'center',gap:14,flexWrap:'wrap'}}>
        <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.18em',
          color:C.textFaint,textTransform:'uppercase'}}>
          {tr('tasks.load.title')}
        </div>
        <div style={{display:'flex',alignItems:'baseline',gap:6}}>
          <div style={{fontSize:26,fontWeight:800,fontStyle:'italic',color:C.textPrimary,lineHeight:1,letterSpacing:'-0.02em'}}>
            {total}
          </div>
          <div style={{fontSize:10,fontWeight:500,color:C.textMuted,letterSpacing:'0.04em'}}>
            {tr('tasks.load.activeTasks')}
          </div>
        </div>
        <div style={{padding:'4px 10px',borderRadius:999,
          background:levelBg,border:`1px solid ${levelBorder}`,
          fontSize:9,fontWeight:700,letterSpacing:'0.16em',textTransform:'uppercase',color:levelColor,
          whiteSpace:'nowrap'}}>
          {tr('tasks.load.' + level.label)}
        </div>
        {/* Segmented meter pushes right on desktop, wraps on mobile */}
        <div className="vyb-load-meter" style={{marginLeft:'auto',display:'flex',gap:3,flex:'0 0 auto'}}>
          {Array.from({length: SEG}).map((_,i) => (
            <div key={i} style={{width:8,height:6,borderRadius:2,background:segColor(i),transition:'background 0.25s'}}/>
          ))}
        </div>
      </div>

      <div style={{fontSize:12,fontWeight:300,fontStyle:'italic',color:C.textMuted,
        marginTop:8,lineHeight:1.5}}>
        {tr('tasks.load.' + level.msg)}
      </div>

      {/* Board breakdown */}
      {total > 0 && (
        <div className="vyb-load-breakdown" style={{display:'flex',alignItems:'center',gap:12,
          flexWrap:'wrap',marginTop:10,fontSize:10,fontWeight:600,letterSpacing:'0.06em'}}>
          {[
            { k:'urgent',    color:C.clay,                  count:counts.urgent },
            { k:'important', color:C.sand,                  count:counts.important },
            { k:'later',     color:C.textMuted,             count:counts.later },
          ].map((row,i) => (
            <React.Fragment key={row.k}>
              {i > 0 && <span style={{color:C.textFaint,opacity:0.5}}>·</span>}
              <span style={{display:'inline-flex',alignItems:'center',gap:6,color:C.textSecondary}}>
                <span style={{width:6,height:6,borderRadius:999,background:row.color}}/>
                <span style={{color:C.textPrimary,fontWeight:700}}>{row.count}</span>
                <span style={{color:C.textMuted,textTransform:'uppercase',letterSpacing:'0.12em',fontSize:9}}>
                  {tr('tasks.load.' + row.k)}
                </span>
              </span>
            </React.Fragment>
          ))}
        </div>
      )}

      {/* Urgent overload warning */}
      {warn && (
        <div style={{marginTop:12,padding:'8px 12px',borderRadius:10,
          background: warn.kind==='hi' ? 'rgba(184,100,60,0.10)' : 'rgba(160,138,86,0.08)',
          border: `1px solid ${warn.kind==='hi' ? 'rgba(184,100,60,0.35)' : 'rgba(160,138,86,0.30)'}`,
          display:'flex',alignItems:'flex-start',gap:8}}>
          <Icon name="alert-circle" size={12}
            color={warn.kind==='hi' ? '#D0825A' : C.sandLight} sw={1.8}/>
          <div style={{fontSize:11,fontWeight:500,lineHeight:1.5,
            color: warn.kind==='hi' ? '#D0825A' : C.sandLight,letterSpacing:'0.01em'}}>
            {warn.text}
          </div>
        </div>
      )}

      {/* Top 3 preview */}
      {top3.length > 0 && (
        <div style={{marginTop:14,paddingTop:12,borderTop:`1px solid ${C.border}`}}>
          <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.18em',
            color:C.textFaint,textTransform:'uppercase',marginBottom:8}}>
            {tr('tasks.load.top3Title')}
          </div>
          <div style={{display:'flex',flexDirection:'column',gap:4}}>
            {top3.map((t,idx) => {
              const p = TASK_PRI_BY_KEY[t.pri] || TASK_PRI_BY_KEY.important;
              return (
                <div key={t.id} className="vyb-load-top3-row"
                  style={{display:'flex',alignItems:'center',gap:10,padding:'6px 8px',borderRadius:6,
                    transition:'background 0.15s'}}>
                  <span style={{fontSize:9,fontWeight:700,color:C.textFaint,fontVariantNumeric:'tabular-nums',width:14,textAlign:'right',letterSpacing:'0.04em'}}>
                    {idx+1}
                  </span>
                  <div onClick={()=>store.toggleTask(t.id)} title="Mark done"
                    style={{width:14,height:14,borderRadius:999,border:`1.5px solid ${C.borderMid}`,
                      background:'transparent',display:'flex',alignItems:'center',justifyContent:'center',
                      flexShrink:0,cursor:'pointer',transition:'all 0.18s'}}>
                  </div>
                  <span style={{flex:1,minWidth:0,fontSize:12,color:C.textPrimary,
                    overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
                    {t.text}
                  </span>
                  <span style={{display:'inline-flex',alignItems:'center',gap:5,
                    padding:'2px 8px',borderRadius:999,
                    background: p.faint, border:`1px solid ${p.color}33`,
                    fontSize:9,fontWeight:600,letterSpacing:'0.12em',textTransform:'uppercase',
                    color: p.key === 'urgent' ? (C.clayLight || p.color)
                         : p.key === 'important' ? C.sandLight
                         : C.textMuted,
                    flexShrink:0,whiteSpace:'nowrap'}}>
                    <span style={{width:5,height:5,borderRadius:999,background:p.color}}/>
                    {window.dl ? window.dl('priority', p.key) : p.label}
                  </span>
                </div>
              );
            })}
          </div>
        </div>
      )}
    </Card>
  );
};

const TasksView = ({ store }) => {
  const tr = useT();
  const tasks = store.state.tasks;
  const [input, setInput]     = React.useState('');
  // Seed from last-used defaults so the next task inherits the previous
  // project + priority. First-ever use → '' / 'important'.
  const _initialDefaults = React.useMemo(() => loadComposerDefaults(), []);
  const [pri, setPri]         = React.useState(_initialDefaults.priority);
  const [project, setProject] = React.useState(_initialDefaults.projectName);
  const [error, setError]     = React.useState('');
  // Manual-override flags. The parser only fills priority/project when the
  // user hasn't touched those controls in the current composer session.
  // Pre-touched if defaults supplied them so the desktop parser doesn't
  // silently override the user's last-used choice.
  const [priTouched, setPriTouched]         = React.useState(true);
  const [projectTouched, setProjectTouched] = React.useState(!!_initialDefaults.projectName);
  const [showDone, setShowDone] = React.useState(false);
  const isMobile = useOverviewIsMobile();
  // 'all' | 'urgent' | 'important' | 'later' — drives which columns render.
  // On mobile, only one priority section is open at a time and selection
  // is persisted in localStorage. 'all' is desktop-only.
  const [viewFilter, setViewFilter] = React.useState(() => {
    const isM = typeof window !== 'undefined' && window.matchMedia
      && window.matchMedia('(max-width: 768px)').matches;
    if (isM) {
      try {
        const v = localStorage.getItem('vyb-tasks-mobile-filter');
        if (['all','urgent','important','later'].includes(v)) return v;
      } catch {}
      return 'all';
    }
    return 'all';
  });
  React.useEffect(() => {
    if (!isMobile) return;
    try { localStorage.setItem('vyb-tasks-mobile-filter', viewFilter); } catch {}
  }, [isMobile, viewFilter]);
  // Mobile All-view: independent open state per priority section. Multiple
  // can be open at once. Persisted as JSON.
  const [mobileExpandedSections, setMobileExpandedSections] = React.useState(() => {
    try {
      const raw = localStorage.getItem('vyb-tasks-mobile-expanded-sections');
      if (raw) {
        const parsed = JSON.parse(raw);
        if (parsed && typeof parsed === 'object') {
          return {
            urgent:    !!parsed.urgent,
            important: parsed.important !== false,
            later:     !!parsed.later,
          };
        }
      }
    } catch {}
    return { urgent: true, important: true, later: true };
  });
  React.useEffect(() => {
    try { localStorage.setItem('vyb-tasks-mobile-expanded-sections', JSON.stringify(mobileExpandedSections)); } catch {}
  }, [mobileExpandedSections]);
  const toggleSection = (key) => setMobileExpandedSections(s => ({ ...s, [key]: !s[key] }));
  const setAllExpanded = (val) => setMobileExpandedSections({ urgent: val, important: val, later: val });
  // Custom filter handler that implements the All-tap toggle behavior.
  const onFilterTap = (key) => {
    if (key === 'all') {
      if (viewFilter !== 'all') {
        setViewFilter('all');
        setAllExpanded(true);
        return;
      }
      const anyOpen = mobileExpandedSections.urgent || mobileExpandedSections.important || mobileExpandedSections.later;
      setAllExpanded(!anyOpen);
      return;
    }
    setViewFilter(key);
    // Make sure the chosen section is open if user later flips to All.
    setMobileExpandedSections(s => ({ ...s, [key]: true }));
  };
  // Mobile composer: priority pill collapses to just the selected one;
  // tap reveals the other two. Project chips stay always-visible (compact).
  const [mobileShowPriorities, setMobileShowPriorities] = React.useState(false);
  // Compact composer: project chips hidden behind a single "Project" pill until expanded.
  const [showProjects, setShowProjects] = React.useState(false);
  // Mobile-only ChatGPT-style sheet: hides inline pills until user taps "+".
  const [mobileSheetOpen, setMobileSheetOpen] = React.useState(false);
  // Mobile inline selector state machine. Replaces popup/dropdown/sheet —
  // the project + priority controls expand inline in the same row.
  // Modes: null | 'project' | 'priority' | 'newProject'
  const [selectorMode, setSelectorMode] = React.useState(null);
  const [sheetNewProjectName, setSheetNewProjectName] = React.useState('');
  const newProjectInlineRef = React.useRef(null);
  React.useEffect(() => {
    if (selectorMode === 'newProject' && newProjectInlineRef.current) {
      try { newProjectInlineRef.current.focus({ preventScroll: true }); } catch {}
    }
  }, [selectorMode]);
  // Priority defaulting is now driven by last-used localStorage defaults
  // (loaded into state at mount). We deliberately do NOT auto-rewrite the
  // selected priority based on viewFilter — the user's last choice wins.

  // Distinct project names from existing tasks (for autocomplete + row dropdowns).
  // Deduped by normalized key so casing variants ("VYB" vs "vyb") render once.
  // Latest-used casing wins as the display label.
  const knownProjects = React.useMemo(() => {
    const sorted = [...tasks].sort((a,b) =>
      String(a.createdAt || '').localeCompare(String(b.createdAt || ''))
      || (a.id - b.id)
    );
    const map = new Map(); // normalizedKey -> displayName (latest wins)
    for (const t of sorted) {
      if (!t.project) continue;
      const k = normalizeProjectKey(t.project);
      if (!k) continue;
      map.set(k, String(t.project).trim().replace(/\s+/g, ' '));
    }
    return Array.from(map.values()).sort((a,b) => a.localeCompare(b));
  }, [tasks]);

  // MRU project queue (persisted). Most recent at the END.
  // Backfills from existing task history on first render.
  // MRU shape: [{ name: string, lastUsedAt: ISO string }]
  // Stored under 'vyb_proj_mru'. Legacy format (string[]) is migrated on read.
  const RECENT_PROJ_TTL_MS = 3 * 24 * 60 * 60 * 1000; // 3 days
  const RECENT_PROJ_MAX = 5;
  const [projectMRU, setProjectMRU] = React.useState(() => {
    const nowIso = new Date().toISOString();
    try {
      const saved = JSON.parse(localStorage.getItem('vyb_proj_mru') || '[]');
      if (Array.isArray(saved) && saved.length) {
        // Migrate legacy string[] → object[]; backdate so 3-day TTL still works.
        if (typeof saved[0] === 'string') {
          return saved.map(name => ({ name, lastUsedAt: nowIso }));
        }
        return saved.filter(e => e && e.name).map(e => ({
          name: String(e.name),
          lastUsedAt: e.lastUsedAt || nowIso,
        }));
      }
    } catch {}
    // Backfill from tasks: per-project last createdAt timestamp.
    const lastByName = new Map();
    for (const t of tasks) {
      const n = t.project; if (!n) continue;
      const ts = new Date(t.createdAt || t.created_at || 0).getTime() || 0;
      const cur = lastByName.get(n) || 0;
      if (ts > cur) lastByName.set(n, ts);
    }
    return Array.from(lastByName.entries())
      .sort((a,b) => a[1] - b[1])
      .map(([name, ts]) => ({ name, lastUsedAt: new Date(ts || Date.now()).toISOString() }));
  });
  const bumpProject = React.useCallback((name) => {
    if (!name) return;
    const nowIso = new Date().toISOString();
    setProjectMRU(prev => {
      const filtered = prev.filter(e => e.name !== name);
      const next = [...filtered, { name, lastUsedAt: nowIso }].slice(-20);
      try { localStorage.setItem('vyb_proj_mru', JSON.stringify(next)); } catch {}
      return next;
    });
  }, []);
  // Visible: most-recent-5, filtered by 3-day TTL. Newest on the right
  // (matches previous chip ordering). Stale entries stay in storage so
  // a single re-use within the next 17 days revives the chip.
  const recentProjects = React.useMemo(() => {
    const cutoff = Date.now() - RECENT_PROJ_TTL_MS;
    return projectMRU
      .filter(e => new Date(e.lastUsedAt).getTime() >= cutoff)
      .slice(-RECENT_PROJ_MAX)
      .map(e => e.name);
  }, [projectMRU]);

  // Inline "new project" input toggle for the composer.
  const [addingProject, setAddingProject] = React.useState(false);
  const [newProjectDraft, setNewProjectDraft] = React.useState('');
  const newProjectRef = React.useRef(null);
  React.useEffect(() => { if (addingProject && newProjectRef.current) newProjectRef.current.focus(); }, [addingProject]);
  // Animated placeholder examples — teaches that "project" can be a class,
  // client, area, or goal depending on context.
  const addProjectPhrases = React.useMemo(() => {
    const lang = (window.getLang && window.getLang()) || 'en';
    return lang === 'es'
      ? ['Añadir proyecto', 'Añadir clase', 'Añadir contexto', 'Añadir cliente', 'Añadir área', 'Añadir meta']
      : ['Add project', 'Add class', 'Add context', 'Add client', 'Add area', 'Add goal'];
  }, []);
  const addProjectFallback = React.useMemo(() => {
    const lang = (window.getLang && window.getLang()) || 'en';
    return lang === 'es' ? 'Añadir proyecto o contexto' : 'Add project or context';
  }, []);
  const addProjectPlaceholder = useAnimatedPlaceholder(
    addProjectPhrases,
    addingProject && !newProjectDraft,
    addProjectFallback,
  );

  const MAX_PROJECT_LEN = 20;
  const commitNewProject = () => {
    const v = newProjectDraft.trim();
    if (!v) { setAddingProject(false); setNewProjectDraft(''); return; }
    if (v.length > MAX_PROJECT_LEN) return; // OK is disabled in this state; guard anyway.
    setProject(v);
    setProjectTouched(true);
    bumpProject(v);
    setNewProjectDraft('');
    setAddingProject(false);
    // Mobile: keep the sheet open so the user can also pick a priority
    // before sending. Desktop: collapse the inline chip strip as before.
    if (!isMobile) {
      setShowProjects(false);
      setMobileSheetOpen(false);
    }
    if (error) setError('');
    // Return focus to the title input so user can press Enter to save.
    requestAnimationFrame(() => { try { tasksInputRef.current && tasksInputRef.current.focus({ preventScroll: true }); } catch (_) {} });
  };

  // Live parser result for preview + save. Considers all known projects
  // (existing tasks + MRU + currently-selected) so smart matching works
  // before any task in the new project exists.
  const parsed = React.useMemo(() => {
    const all = new Set(knownProjects);
    for (const p of projectMRU) all.add(p.name);
    if (project.trim()) all.add(project.trim());
    return parseTaskInput(input, { projects: Array.from(all) });
  }, [input, knownProjects, projectMRU, project]);

  const add = async () => {
    const v = input.trim();
    if (!v) { setError(tr('tasks.errAddTitle')); return; }
    setError('');
    // Mobile: NEVER auto-detect from text. The user picks project + priority
    // from the always-visible controls, so the title stays exactly what they
    // typed and the destination is never a surprise.
    // Desktop: keep the smart parser as a productivity nicety, but manual
    // choices still win.
    const useParser = !isMobile;
    const finalPri  = (!useParser || priTouched) ? pri
      : (parsed.detected.priority ? parsed.priority : pri);
    const rawProj = (!useParser || projectTouched)
      ? project.trim()
      : (parsed.detected.project ? parsed.projectName : project.trim());
    const finalText = useParser ? (parsed.cleanTitle || v) : v;
    // Canonicalize project name against existing projects so casing variants
    // ("VYB" vs "vyb") collapse into the same group. Latest casing wins so
    // future renders pick up the user's preferred display.
    const allKnown = new Set(knownProjects);
    for (const p of projectMRU) allKnown.add(p.name);
    const canonicalMap = buildProjectCanonicalMap(Array.from(allKnown));
    const projKey = normalizeProjectKey(rawProj);
    const finalProj = projKey
      ? (canonicalMap.has(projKey) ? canonicalMap.get(projKey) : rawProj.replace(/\s+/g, ' '))
      : '';
    // Reveal the destination section so the task doesn't feel lost.
    // Mobile: open accordion section. Desktop: if a single-priority filter
    // is active and the new task lands elsewhere, switch to All so the
    // user can see where the task went.
    if (isMobile) {
      if (viewFilter !== 'all' && viewFilter !== finalPri) setViewFilter('all');
      setMobileExpandedSections(s => ({ ...s, [finalPri]: true }));
    } else if (viewFilter !== 'all' && viewFilter !== finalPri) {
      setViewFilter('all');
    }
    const wasFirst = total === 0;
    // CRITICAL: do NOT clear input until the insert succeeds. Previously
    // setInput('') ran BEFORE the await, so any insert failure (RLS, network,
    // schema drift) silently dropped the draft and the task appeared to
    // "disappear". Now the draft + selections persist on failure and the
    // user sees an error.
    const payload = { text: finalText, pri: finalPri, project: finalProj };
    let newId = null;
    try {
      newId = await store.addTask(payload);
    } catch (err) {
      console.error('[tasks-add] insert threw', err, payload);
    }
    if (!newId) {
      console.error('[tasks-add] insert returned null', payload);
      setError(tr('tasks.errAddFailed'));
      return;
    }
    // Success: clear the title, but KEEP project + priority so the next
    // task inherits the same context. Persist as last-used defaults.
    if (finalProj) {
      bumpProject(finalProj);
      bumpUsage('tasks', finalProj);
      if (project !== finalProj) setProject(finalProj);
    }
    saveComposerDefaults({ projectName: finalProj || '', priority: finalPri });
    setInput('');
    setSelectorMode(null);
    // Keep priTouched/projectTouched true so the parser stays out of the way.
    if (newId) {
      // Wait one frame for the new row to mount, then a small delay so the
      // section's fade-in is partly visible before we scroll/highlight.
      // For the first-ever task, hold a bit longer so the board reveal
      // (sections cascading in) doesn't fight with the scroll/highlight.
      const settle = wasFirst ? 360 : 120;
      const reveal = () => {
        try {
          const el = document.querySelector(`[data-task-id="${newId}"]`);
          if (!el) return;
          // Only scroll if the row isn't already comfortably in view.
          const r = el.getBoundingClientRect();
          const vh = window.innerHeight || document.documentElement.clientHeight;
          const margin = 80;
          const offscreen = r.top < margin || r.bottom > (vh - margin);
          if (offscreen && el.scrollIntoView) {
            el.scrollIntoView({ behavior: 'smooth', block: 'center' });
          }
          el.classList.add('vyb-task-new');
          setTimeout(() => { try { el.classList.remove('vyb-task-new'); } catch {} }, 2200);
        } catch {}
      };
      requestAnimationFrame(() => setTimeout(reveal, settle));
    }
    // projectTouched stays — explicit user context persists across captures.
  };

  const tasksInputRef = React.useRef(null);
  const dictation = useDictation({
    onAppend: (chunk) => { setInput(v => appendDictated(v, chunk)); if (error) setError(''); },
    inputRef: tasksInputRef,
  });

  // If the user landed here via the Overview "Add Task" quick action,
  // focus the textarea so they can start typing immediately. The composer
  // is position:fixed at the bottom of the viewport, so we deliberately do
  // NOT call scrollIntoView — that would jerk the page on mobile and the
  // input is already on-screen.
  React.useEffect(() => {
    const flag = (typeof window !== 'undefined') ? window.__vybQuickAction : null;
    if (!flag || flag.target !== 'tasks') return;
    if (Date.now() - (flag.ts || 0) > 4000) { try { window.__vybQuickAction = null; } catch {} return; }
    try { window.__vybQuickAction = null; } catch {}
    if (isMobile) setMobileSheetOpen(false);
    requestAnimationFrame(() => setTimeout(() => {
      try { tasksInputRef.current && tasksInputRef.current.focus({ preventScroll: true }); } catch {}
    }, 60));
  }, []); // mount only

  // Group: pri → project → [tasks], sorted by sortOrder then createdAt.
  // Project keys are normalized (case + space insensitive) so "VYB" and
  // "vyb" collapse into the same group. The displayed label uses the
  // canonical (latest) casing — user's most recent choice wins.
  const grouped = React.useMemo(() => {
    const out = {};
    const labels = {}; // normalizedKey -> latest display name
    for (const k of TASK_PRI_KEYS) out[k] = {};
    // First pass: build canonical labels by createdAt order (latest wins).
    const sortedByCreated = [...tasks].sort((a,b) =>
      String(a.createdAt || '').localeCompare(String(b.createdAt || ''))
      || (a.id - b.id)
    );
    for (const t of sortedByCreated) {
      if (!t.project) continue;
      const nk = normalizeProjectKey(t.project);
      if (!nk) continue;
      labels[nk] = String(t.project).trim().replace(/\s+/g, ' ');
    }
    for (const t of tasks) {
      if (t.done) continue;
      const k = TASK_PRI_KEYS.includes(t.pri) ? t.pri : 'important';
      let pk;
      if (!t.project) pk = NO_PROJECT;
      else {
        const nk = normalizeProjectKey(t.project);
        pk = labels[nk] || t.project;
      }
      (out[k][pk] ||= []).push(t);
    }
    for (const k of TASK_PRI_KEYS) {
      for (const pk of Object.keys(out[k])) {
        out[k][pk].sort((a,b) => (a.sortOrder||0) - (b.sortOrder||0) || (a.id - b.id));
      }
    }
    return out;
  }, [tasks]);

  // Completed today (rolled-up at the bottom).
  const completedToday = React.useMemo(() => {
    const cutoff = startOfToday();
    return tasks
      .filter(t => t.done && t.completedAt && new Date(t.completedAt).getTime() >= cutoff)
      .sort((a,b) => new Date(b.completedAt) - new Date(a.completedAt));
  }, [tasks]);

  const total = tasks.length;
  const openTotal = tasks.filter(t => !t.done).length;

  return (
    <div style={{paddingBottom: 65}}>
      <style>{`
        .vyb-task-row:hover { background: rgba(255,255,255,0.025); }
        .vyb-task-row:hover .vyb-task-handle { opacity: 1 !important; }
        .vyb-task-ghost { opacity: 0.35; border: 1px dashed ${C.border} !important; }
        .vyb-task-chosen { cursor: grabbing !important; }
        .vyb-task-drag { background: rgba(160,138,86,0.08) !important; box-shadow: 0 6px 20px rgba(160,138,86,0.18); border-radius: 6px; transform: rotate(-0.5deg); }
        .vyb-task-rejected { box-shadow: 0 0 0 1px rgba(160,138,86,0.45), 0 6px 18px rgba(160,138,86,0.10); border-radius: 6px; }
        body.vyb-dragging, body.vyb-dragging * { user-select: none !important; cursor: grabbing !important; }
        /* Reveal the column-wide catch-all only while a task is being
           dragged — gives the user a clear "drop anywhere here" hint. */
        body.vyb-dragging [data-catchall="1"] { border-color: rgba(160,138,86,0.30) !important;
          background: rgba(160,138,86,0.04); }
        @keyframes vybTaskColIn {
          from { opacity: 0; transform: translateY(6px) scale(0.985); }
          to   { opacity: 1; transform: translateY(0) scale(1); }
        }
        @keyframes vybPriIn {
          from { opacity: 0; transform: translateX(-4px); }
          to   { opacity: 1; transform: translateX(0); }
        }
        @keyframes vybTaskSavedRing {
          0%   { transform: scale(0.985); box-shadow: 0 0 0 0 rgba(160,138,86,0.55); background: rgba(160,138,86,0.18); }
          50%  { transform: scale(1);     box-shadow: 0 0 0 8px rgba(160,138,86,0.05); background: rgba(160,138,86,0.10); }
          100% { transform: scale(1);     box-shadow: 0 0 0 0 rgba(160,138,86,0);   background: transparent; }
        }
        .vyb-task-saved { border-radius: 8px; animation: vybTaskSavedRing 1.9s cubic-bezier(0.22,1,0.36,1) 1; transform-origin: left center; }
        @keyframes vybTaskGridIn {
          from { opacity: 0; transform: translateY(4px); }
          to   { opacity: 1; transform: translateY(0); }
        }
        @keyframes vybTaskNewIn {
          0%   { opacity: 0; transform: translateY(8px) scale(0.985); }
          60%  { opacity: 1; transform: translateY(0)   scale(1.005); }
          100% { opacity: 1; transform: translateY(0)   scale(1); }
        }
        @keyframes vybTaskNewGlow {
          0%   { background: rgba(160,138,86,0.00); box-shadow: 0 0 0 0 rgba(160,138,86,0.55); }
          18%  { background: rgba(160,138,86,0.20); box-shadow: 0 0 0 6px rgba(160,138,86,0.10); }
          55%  { background: rgba(160,138,86,0.10); box-shadow: 0 0 0 3px rgba(160,138,86,0.04); }
          100% { background: rgba(160,138,86,0.00); box-shadow: 0 0 0 0 rgba(160,138,86,0); }
        }
        .vyb-task-new {
          border-radius: 8px;
          animation:
            vybTaskNewIn 380ms cubic-bezier(0.22,1,0.36,1) both,
            vybTaskNewGlow 2000ms cubic-bezier(0.22,1,0.36,1) 1;
          transform-origin: left center;
        }
        @media (prefers-reduced-motion: reduce) {
          .vyb-task-new { animation: none; box-shadow: 0 0 0 1px rgba(160,138,86,0.45); }
        }
        .vyb-composer-actions > button { aspect-ratio: 1 / 1; border-radius: 9999px; padding: 0; flex: 0 0 auto; margin-left: 0 !important; }
        @keyframes vybTaskCompleting {
          0%   { transform: translateY(0) scale(1);     opacity: 1;    background: rgba(160,138,86,0.00); box-shadow: 0 0 0 0 rgba(160,138,86,0); }
          18%  { transform: translateY(0) scale(1.005); opacity: 1;    background: rgba(160,138,86,0.14); box-shadow: 0 0 0 6px rgba(160,138,86,0.08); }
          55%  { transform: translateY(0) scale(1);     opacity: 0.95; background: rgba(160,138,86,0.10); box-shadow: 0 0 0 4px rgba(160,138,86,0.04); }
          100% { transform: translateY(6px) scale(0.985); opacity: 0;  background: rgba(160,138,86,0.00); box-shadow: 0 0 0 0 rgba(160,138,86,0); }
        }
        @keyframes vybTaskCheckPop {
          0%   { transform: scale(0.85); }
          45%  { transform: scale(1.18); }
          100% { transform: scale(1); }
        }
        @keyframes vybTaskTitleFade {
          0%   { color: var(--c-text-primary, #2a2a2a); }
          100% { color: var(--c-text-faint, #999); }
        }
        .vyb-task-completing {
          border-radius: 8px;
          animation: vybTaskCompleting 780ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
          pointer-events: none;
          transform-origin: left center;
        }
        .vyb-task-check-anim { animation: vybTaskCheckPop 360ms cubic-bezier(0.22, 1, 0.36, 1); }
        .vyb-task-title-completing { animation: vybTaskTitleFade 600ms ease-out forwards; }
        @keyframes vybTaskMovedGlow {
          0%   { background: rgba(160,138,86,0.00); box-shadow: 0 0 0 0 rgba(160,138,86,0.55); }
          18%  { background: rgba(160,138,86,0.22); box-shadow: 0 0 0 6px rgba(160,138,86,0.10); }
          55%  { background: rgba(160,138,86,0.12); box-shadow: 0 0 0 3px rgba(160,138,86,0.04); }
          100% { background: rgba(160,138,86,0.00); box-shadow: 0 0 0 0 rgba(160,138,86,0); }
        }
        .vyb-task-moved {
          border-radius: 8px;
          animation: vybTaskMovedGlow 1800ms cubic-bezier(0.22,1,0.36,1) 1;
          transform-origin: left center;
        }
        @keyframes vybTaskDelConfirmIn {
          from { opacity: 0; transform: translateX(4px) scale(0.94); }
          to   { opacity: 1; transform: translateX(0)   scale(1); }
        }
        .vyb-task-delete-confirm {
          animation: vybTaskDelConfirmIn 180ms cubic-bezier(0.22,1,0.36,1) both;
        }
        .vyb-task-delete-confirm:hover {
          background: rgba(184,100,60,0.24) !important;
          border-color: rgba(184,100,60,0.75) !important;
        }
        @media (prefers-reduced-motion: reduce) {
          .vyb-task-saved { animation: none; box-shadow: 0 0 0 1px rgba(160,138,86,0.45); }
          .vyb-task-completing, .vyb-task-check-anim, .vyb-task-title-completing { animation: none; }
          .vyb-task-moved, .vyb-task-delete-confirm { animation: none; }
        }
        @media (max-width: 1024px) {
          .vyb-tasks-grid[data-filter="all"] { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
        }
        @media (max-width: 768px) {
          /* Force vertical stack — beat inline display:grid with !important. */
          .vyb-tasks-grid {
            display: flex !important;
            flex-direction: column !important;
            width: 100% !important;
            max-width: 100% !important;
            gap: 12px !important;
            overflow-x: hidden !important;
          }
          .vyb-tasks-grid > .vyb-card {
            min-height: 0 !important;
            width: 100% !important;
            max-width: 100% !important;
            min-width: 0 !important;
            padding: 12px 14px !important;
            box-sizing: border-box !important;
          }
          .vyb-tasks-grid .vyb-card { min-width: 0; }
          /* Task rows: ensure title wraps, project pills don't overflow. */
          .vyb-task-row { min-width: 0 !important; }
          .vyb-task-row > * { min-width: 0; }
          .vyb-task-row > span { overflow-wrap: anywhere !important; word-break: break-word; }
          .vyb-task-row > select { max-width: 110px !important; }
          .vyb-task-handle { display: none !important; }
          /* Hide secondary helper text — keep the chip rows breathable. */
          .vyb-tasks-helper, .vyb-tasks-hint { display: none !important; }
          /* Drop the inline vertical separator in the composer. */
          .vyb-composer-sep { display: none !important; }
          /* Tighter composer bubble. */
          .vyb-composer-bubble { padding: 10px 12px 10px !important; border-radius: 14px !important; }
          .vyb-composer-bubble textarea { font-size: 14px !important; }
          /* Parser preview chips: smaller + horizontal scroll fallback. */
          .vyb-task-parse-preview { flex-wrap: wrap !important; gap: 5px !important; margin-top: 8px !important; }
          .vyb-task-parse-preview > * { max-width: 100%; }
          /* Filter bar: hide "View" label on mobile, keep all 4 chips
             (All / Urgent / Important / Later) compact + scrollable. */
          .vyb-tasks-viewlabel { display: none !important; }
          .vyb-tasks-filterchips { width: 100%; gap: 5px !important; flex-wrap: nowrap !important; overflow-x: auto; -webkit-overflow-scrolling: touch; padding-bottom: 2px; }
          .vyb-tasks-filterchips::-webkit-scrollbar { display: none; }
          .vyb-tasks-filterchips > div { padding: 5px 10px !important; font-size: 9px !important; flex: 0 0 auto; }
          /* Mobile project chips: scrollable so long names never push priority below. */
          .vyb-tasks-composer-projects { flex-wrap: nowrap !important; overflow-x: auto; -webkit-overflow-scrolling: touch; min-width: 0 !important; padding-bottom: 2px; }
          .vyb-tasks-composer-projects::-webkit-scrollbar { display: none; }
          .vyb-tasks-composer-projects input { width: auto !important; min-width: 110px !important; max-width: 180px !important; }
          .vyb-tasks-composer-priorities { flex-wrap: nowrap !important; flex: 0 0 auto !important; }
          /* Mic + send sit on the input row as perfect circles. */
          .vyb-composer-actions { flex: 0 0 auto !important; align-self: center !important; }
          .vyb-composer-actions > button {
            aspect-ratio: 1 / 1 !important;
            flex: 0 0 auto !important;
            width: 40px !important; height: 40px !important;
            border-radius: 9999px !important;
            margin-left: 0 !important;
            padding: 0 !important;
          }
          .vyb-composer-inputrow { gap: 8px !important; align-items: center !important; }
          .vyb-composer-inputrow textarea { padding-top: 8px !important; padding-bottom: 8px !important; }
          /* Leading "+" button: same circle size as mic/send for visual balance. */
          .vyb-composer-leading > button {
            width: 40px !important; height: 40px !important;
            aspect-ratio: 1 / 1 !important; border-radius: 9999px !important;
            padding: 0 !important; flex: 0 0 auto !important;
          }
        }
        @media (prefers-reduced-motion: reduce) {
          .vyb-tasks-grid > * { animation: none !important; }
        }
      `}</style>

      <ViewHeader label={tr('tasks.label')} title={tr('tasks.title')}
        subtitle={total
          ? tr('tasks.ofTotalComplete')
              .replace('{done}', String(total - openTotal))
              .replace('{total}', String(total))
              .replace('{open}', String(openTotal))
          : tr('tasks.empty')} />

      {/* Filter chips + helper line. Filter all = 3-column grid; single
          priority = expanded full-width column. */}
      <div className="vyb-tasks-filterbar" style={{display:'flex',alignItems:'center',justifyContent:'space-between',gap:12,flexWrap:'wrap',marginBottom:14}}>
        <div className="vyb-tasks-filterchips" style={{display:'flex',gap:6,flexWrap:'wrap',alignItems:'center'}}>
          <span className="vyb-tasks-viewlabel" style={{fontSize:9,fontWeight:700,letterSpacing:'0.18em',color:C.textFaint,textTransform:'uppercase',marginRight:4}}>{tr('tasks.viewLabel')}</span>
          {[{key:'all'}, ...TASK_PRIS].map(opt => {
            const sel = viewFilter === opt.key;
            const dot = opt.key === 'all' ? null : opt.color;
            const label = opt.key === 'all'
              ? tr('tasks.filterAll')
              : (window.dl ? window.dl('priority', opt.key) : opt.label);
            return (
              <div key={opt.key}
                className={opt.key === 'all' ? 'vyb-tasks-filter-all' : 'vyb-tasks-filter-pri'}
                onClick={()=> isMobile ? onFilterTap(opt.key) : setViewFilter(opt.key)}
                style={{padding:'6px 12px',borderRadius:999,fontSize:9,fontWeight:600,letterSpacing:'0.14em',
                  textTransform:'uppercase',cursor:'pointer',display:'inline-flex',alignItems:'center',gap:7,
                  border:`1px solid ${sel ? (dot || C.sand) : C.border}`,
                  background: sel ? (opt.key==='urgent' ? 'rgba(195,123,90,0.12)'
                                    : opt.key==='important' ? C.sandFaint
                                    : opt.key==='later' ? 'rgba(102,94,88,0.12)'
                                    : C.sandFaint) : 'transparent',
                  color: sel ? C.textPrimary : C.textMuted, transition:'all 0.18s ease-out'}}>
                {dot && <span style={{width:5,height:5,borderRadius:999,background:dot}}/>}
                {label}
              </div>
            );
          })}
        </div>
        <div className="vyb-tasks-helper" style={{fontSize:11,fontWeight:300,fontStyle:'italic',color:C.textMuted,letterSpacing:'0.01em'}}>
          {tr('tasks.keepShort')}
        </div>
      </div>

      {total === 0 ? (
        <Card><Empty icon="list-todo" title={tr('tasks.capturedNeedsDoing')} sub={tr('tasks.useComposerBelow')}/></Card>
      ) : (
        <div className="vyb-tasks-grid" data-filter={viewFilter}
          style={{display:'grid',gap:18,
            gridTemplateColumns: viewFilter === 'all'
              ? 'repeat(3, minmax(0, 1fr))'
              : 'minmax(0, 1fr)',
            animation: 'vybTaskGridIn 0.28s cubic-bezier(0.22,1,0.36,1) both'}}>
          {TASK_PRIS.filter(p => viewFilter === 'all' || viewFilter === p.key).map((p, idx) => {
            const groups = grouped[p.key] || {};
            const projectKeys = Object.keys(groups).sort((a,b) => {
              if (a === NO_PROJECT) return -1;
              if (b === NO_PROJECT) return 1;
              return a.localeCompare(b);
            });
            const open = projectKeys.reduce((sum,pk) => sum + groups[pk].length, 0);
            // Per-priority visual hierarchy. Urgent gets a warm border + soft
            // glow so it reads as "this is what matters now" without looking
            // like an error state.
            const isUrgent    = p.key === 'urgent';
            const isImportant = p.key === 'important';
            const cardStyle = {
              padding:'16px 18px',
              borderLeft:`${isUrgent ? 3 : 2}px solid ${p.color}`,
              // Urgent gets its themed gradient via .vyb-task-urgent CSS class.
              // Important/Later use the standard themed card surface (C.card).
              background: isUrgent ? undefined : C.card,
              borderColor: isUrgent
                ? 'rgba(195,123,90,0.36)'
                : isImportant
                  ? C.sandGlow
                  : C.border,
              boxShadow: isUrgent
                ? '0 0 0 1px rgba(195,123,90,0.10), 0 14px 38px rgba(195,123,90,0.12)'
                : 'none',
              minHeight: viewFilter === 'all' && !isMobile ? 260 : undefined,
              animation: `vybTaskColIn 0.32s cubic-bezier(0.22,1,0.36,1) ${idx * 0.07}s both`,
              // Flex column so the BoardCatchAll can stretch and turn the
              // entire card body into a valid drop zone.
              display:'flex', flexDirection:'column',
            };
            const headerColor    = isUrgent ? C.clayLight || C.clay : C.textPrimary;
            const headerWeight   = isUrgent ? 800 : 700;
            const headerLetter   = isUrgent ? '0.20em' : '0.18em';
            // On mobile All-view, cards are accordion-style: each section
            // toggles independently. Filtered views always show the body.
            const mobileAccordion = isMobile && viewFilter === 'all';
            const expanded = !mobileAccordion || !!mobileExpandedSections[p.key];
            const onHeaderClick = mobileAccordion
              ? () => toggleSection(p.key)
              : undefined;
            const headerCompact = mobileAccordion && !expanded;
            return (
              <Card key={p.key}
                style={{...cardStyle, padding: headerCompact ? '12px 14px' : cardStyle.padding}}
                className={isUrgent ? 'vyb-task-urgent' : ''}>
                <div onClick={onHeaderClick}
                  style={{display:'flex',alignItems:'center',gap:10,
                    marginBottom: expanded ? 10 : 0, paddingBottom: expanded ? 8 : 0,
                    borderBottom: expanded ? `1px solid ${isUrgent ? 'rgba(195,123,90,0.18)' : C.border}` : 'none',
                    flexWrap:'wrap',
                    cursor: onHeaderClick ? 'pointer' : 'default',
                    userSelect: onHeaderClick ? 'none' : 'auto'}}>
                  {isUrgent ? (
                    <Icon name="zap" size={13} color={p.color} sw={2.4}/>
                  ) : (
                    <div style={{width:8,height:8,borderRadius:999,background:p.color,flexShrink:0,
                      boxShadow: isImportant ? `0 0 0 3px ${C.sandFaint}` : 'none'}}/>
                  )}
                  <div style={{fontSize: isUrgent ? 12 : 11, fontWeight: headerWeight, letterSpacing: headerLetter,
                    color: headerColor, textTransform:'uppercase'}}>
                    {window.dl ? window.dl('priority', p.key) : p.label}
                  </div>
                  <div style={{fontSize:10,fontWeight:300,color:C.textFaint}}>{open} {tr('tasks.open')}</div>
                  <div style={{marginLeft:'auto',fontSize:10,fontWeight:300,color:C.textFaint,fontStyle:'italic',
                    display:'flex',alignItems:'center',gap:8}}>
                    {!isMobile && (window.dl ? window.dl('priorityDesc', p.key) : p.desc)}
                    {mobileAccordion && (
                      <span style={{display:'inline-flex',transition:'transform 0.2s ease-out',
                        transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)'}}>
                        <Icon name="chevron-down" size={14} color={C.textMuted}/>
                      </span>
                    )}
                  </div>
                  {mobileAccordion && !expanded && (
                    <div style={{flexBasis:'100%',fontSize:10,fontWeight:300,color:C.textFaint,fontStyle:'italic',marginTop:2}}>
                      {window.dl ? window.dl('priorityDesc', p.key) : p.desc}
                    </div>
                  )}
                </div>
                {/* Subtle board-level overload warning. Calm amber tone, not red.
                    Urgent: ≥5 → soft, ≥8 → firmer. Important: ≥10 → soft.
                    Later is intentionally never warned. */}
                {expanded && (() => {
                  let warnKey = null;
                  if (p.key === 'urgent' && open >= 8)         warnKey = 'tasks.boardWarn.urgent8';
                  else if (p.key === 'urgent' && open >= 5)    warnKey = 'tasks.boardWarn.urgent5';
                  else if (p.key === 'important' && open >= 10) warnKey = 'tasks.boardWarn.important10';
                  if (!warnKey) return null;
                  const strong = warnKey === 'tasks.boardWarn.urgent8';
                  return (
                    <div style={{
                      display:'flex', alignItems:'flex-start', gap:8,
                      margin:'0 0 10px', padding:'7px 10px', borderRadius:8,
                      background: strong ? 'rgba(195,123,90,0.10)' : 'rgba(160,138,86,0.08)',
                      border: `1px solid ${strong ? 'rgba(195,123,90,0.30)' : 'rgba(160,138,86,0.26)'}`,
                    }}>
                      <Icon name="alert-triangle" size={11}
                        color={strong ? (C.clayLight || C.clay) : C.sandLight} sw={2}/>
                      <div style={{fontSize:10.5, fontWeight:400, lineHeight:1.5,
                        color: strong ? (C.clayLight || C.sandLight) : C.sandLight,
                        letterSpacing:'0.01em'}}>
                        {tr(warnKey)}
                      </div>
                    </div>
                  );
                })()}
                {expanded && (open === 0 ? (
                  // Empty section: the entire card body IS the drop zone.
                  // Stretches to fill so any drop inside the card lands here.
                  <BoardCatchAll priKey={p.key} stretch
                    emptyLabel={tr('tasks.nothingHere')}/>
                ) : (
                  <>
                    {projectKeys.map(pk => (
                      <ProjectGroup key={pk} projectKey={pk} priKey={p.key}
                        items={groups[pk] || []} pri={p} store={store} projectOptions={knownProjects}/>
                    ))}
                    {/* Full-column catch-all stretches to fill the rest of
                        the card so the user can drop anywhere in the board
                        (not just over an existing project group). */}
                    <BoardCatchAll priKey={p.key} stretch/>
                  </>
                ))}
              </Card>
            );
          })}

          {completedToday.length > 0 && (
            <div style={{marginTop:8,gridColumn:'1 / -1'}}>
              <div onClick={()=>setShowDone(!showDone)}
                style={{display:'flex',alignItems:'center',gap:8,cursor:'pointer',padding:'8px 10px',borderTop:`1px solid ${C.border}`,color:C.textMuted}}>
                <Icon name={showDone?'chevron-down':'chevron-right'} size={12} color={C.textMuted}/>
                <span style={{fontSize:10,fontWeight:600,letterSpacing:'0.14em',textTransform:'uppercase'}}>{tr('tasks.completedToday')}</span>
                <span style={{fontSize:10,color:C.textFaint}}>{completedToday.length}</span>
              </div>
              {showDone && (
                <div style={{paddingLeft:6}}>
                  {completedToday.map(t => (
                    <div key={t.id} style={{display:'flex',alignItems:'center',gap:10,padding:'6px 10px',opacity:0.55}}>
                      <div onClick={()=>store.toggleTask(t.id)}
                        style={{width:16,height:16,borderRadius:999,border:`1.5px solid ${C.sand}`,background:C.sand,display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0,cursor:'pointer'}}>
                        <Icon name="check" size={9} color={C.bg} sw={3}/>
                      </div>
                      <span style={{flex:1,fontSize:12,color:C.textMuted,textDecoration:'line-through'}}>{t.text}</span>
                      {t.project && (
                        <span style={{fontSize:9,fontWeight:600,letterSpacing:'0.12em',textTransform:'uppercase',color:C.textFaint}}>{t.project}</span>
                      )}
                      <div onClick={()=>store.toggleTask(t.id)}
                        style={{fontSize:9,fontWeight:600,letterSpacing:'0.12em',textTransform:'uppercase',color:C.sandLight,cursor:'pointer'}}>
                        {tr('tasks.undo')}
                      </div>
                    </div>
                  ))}
                </div>
              )}
            </div>
          )}
        </div>
      )}

      {(() => {
      const _isEsT = ((window.getLang && window.getLang()) === 'es');
      // On mobile we now ALWAYS render the project + priority panel above
      // the input bubble (no hidden + button) so the user can always see
      // where the task will land before adding it. _inMobileSheet keeps
      // the older "expanded" treatment (section labels, all 3 priority
      // pills shown) since that's what we want in the always-visible panel.
      const _inMobileSheet = isMobile;
      // Section labels are meaningful inside the always-visible mobile panel,
      // where Project + Priority are stacked vertically.
      const SectionLabel = ({ children }) => _inMobileSheet ? (
        <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.20em',
          color:C.textFaint,textTransform:'uppercase',marginBottom:6,paddingLeft:2}}>
          {children}
        </div>
      ) : null;
      const tasksComposerExtras = (<>
        <SectionLabel>{tr('tasks.projectLabel')}</SectionLabel>
        {/* Project control: compact pill by default, expands to chips +
            add-project on tap. The project area scrolls horizontally so
            long names never push the priority pill below. Add-project
            mode hides the priority pill to keep the composer compact. */}
        <div className="vyb-tasks-composer-projects"
          style={{display:'flex',alignItems:'center',gap:6,
            flexWrap:'nowrap',
            flex: (showProjects || addingProject) ? '1 1 auto' : '0 1 auto',
            minWidth:0,
            overflowX: showProjects ? 'auto' : 'visible',
            WebkitOverflowScrolling:'touch'}}>
          {!showProjects && !addingProject && !project && (
            <div onClick={()=>{ setShowProjects(true); setMobileShowPriorities(false); }} title={tr('tasks.addProjectContext')}
              style={{display:'inline-flex',alignItems:'center',gap:6,flex:'0 0 auto',
                height:26,padding:'0 11px',borderRadius:999,fontSize:10,fontWeight:600,
                lineHeight:1,letterSpacing:'0.04em',cursor:'pointer',transition:'all 0.15s',
                border:`1px dashed ${C.border}`,
                background:'transparent',color:C.textMuted,
                whiteSpace:'nowrap'}}>
              <Icon name="folder-plus" size={11} color={C.textMuted} sw={2}/>
              <span>{tr('tasks.addProjectShort')}</span>
            </div>
          )}
          {!showProjects && !addingProject && project && (
            <div onClick={()=>{ setShowProjects(true); setMobileShowPriorities(false); }} title={project}
              style={{display:'inline-flex',alignItems:'center',flex:'0 1 auto',minWidth:0,
                height:26,padding:'0 11px',borderRadius:999,fontSize:10,fontWeight:600,
                lineHeight:1,letterSpacing:'0.04em',cursor:'pointer',transition:'all 0.15s',
                border:`1px solid ${C.sand}`,
                background:C.sandFaint,color:C.sandLight,
                maxWidth:'100%',whiteSpace:'nowrap',
                overflow:'hidden',textOverflow:'ellipsis'}}>
              <span style={{overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',maxWidth:180}}>
                {project}
              </span>
            </div>
          )}
          {showProjects && !addingProject && (() => {
            // Show selected project first, then the rest (de-duped, in MRU order).
            const others = recentProjects.filter(p => p !== project);
            const pills = project ? [project, ...others] : [...recentProjects];
            return (
              <>
                {pills.map(p => {
                  const sel = project === p;
                  return (
                    <div key={p} onClick={()=>{
                        setProject(sel?'':p); setProjectTouched(true);
                        if(!sel) bumpProject(p);
                        if(error) setError('');
                        // On mobile keep the sheet open so the user can also
                        // pick a priority. On desktop, collapse the inline
                        // chip strip after the choice so the bar stays tidy.
                        if (!isMobile) {
                          if (!sel) setShowProjects(false);
                          setMobileShowPriorities(false);
                        }
                      }}
                      style={{flex:'0 0 auto',height:26,padding:'0 11px',borderRadius:999,fontSize:10,fontWeight:600,lineHeight:1,cursor:'pointer',transition:'all 0.15s',display:'inline-flex',alignItems:'center',
                        whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis',maxWidth:160,
                        border:`1px solid ${sel?C.sand:C.border}`,background:sel?C.sandFaint:'transparent',color:sel?C.sandLight:C.textMuted}}>
                      {p}
                    </div>
                  );
                })}
                <div onClick={()=>{ setAddingProject(true); setMobileShowPriorities(false); }} title={tr('tasks.addProjectContext')}
                  style={{display:'inline-flex',alignItems:'center',gap:4,flex:'0 0 auto',
                    height:26,padding:'0 11px',borderRadius:999,lineHeight:1,
                    border:`1px dashed ${C.border}`,cursor:'pointer',color:C.textMuted,
                    fontSize:10,fontWeight:600,whiteSpace:'nowrap',transition:'all 0.15s'}}>
                  <span>{tr('tasks.addProjectShort')}</span>
                </div>
                <div onClick={()=>setShowProjects(false)} title="Close"
                  style={{width:22,height:22,borderRadius:999,display:'inline-flex',flex:'0 0 auto',
                    alignItems:'center',justifyContent:'center',cursor:'pointer',
                    color:C.textFaint,marginLeft:2}}>
                  <Icon name="x" size={11} color={C.textFaint} sw={2}/>
                </div>
              </>
            );
          })()}
          {addingProject && (() => {
            const trimmed = newProjectDraft.trim();
            const len = newProjectDraft.length;
            const overLimit = len > MAX_PROJECT_LEN;
            const okEnabled = trimmed.length > 0 && !overLimit;
            const lang = (window.getLang && window.getLang()) || 'en';
            const isEs = lang === 'es';
            return (
              <>
                <input ref={newProjectRef} value={newProjectDraft}
                  onChange={e=>setNewProjectDraft(e.target.value.slice(0, MAX_PROJECT_LEN))}
                  onKeyDown={e=>{
                    if(e.key==='Enter'){ e.preventDefault(); if(okEnabled) commitNewProject(); }
                    else if(e.key==='Escape'){ setAddingProject(false); setNewProjectDraft(''); }
                  }}
                  placeholder={addProjectPlaceholder}
                  maxLength={MAX_PROJECT_LEN}
                  style={{padding:'5px 12px',borderRadius:999,fontSize:10,fontWeight:600,
                    background:C.sandFaint,color:C.textPrimary,outline:'none',
                    border:`1px solid ${overLimit ? (C.clay || C.sand) : C.sand}`,
                    fontFamily:'Montserrat,sans-serif',
                    flex:'1 1 140px',minWidth:120,maxWidth:220,
                    letterSpacing:'0.04em'}}/>
                <span style={{flex:'0 0 auto',fontSize:9,fontVariantNumeric:'tabular-nums',
                  color: overLimit ? (C.clayLight || C.sandLight) : C.textFaint,
                  letterSpacing:'0.04em'}}>
                  {len}/{MAX_PROJECT_LEN}
                </span>
                <div onClick={()=>{ if (okEnabled) commitNewProject(); }} title="OK"
                  style={{padding:'5px 11px',borderRadius:999,fontSize:9,fontWeight:700,
                    letterSpacing:'0.14em',textTransform:'uppercase',
                    cursor: okEnabled ? 'pointer' : 'not-allowed',
                    border:`1px solid ${okEnabled ? C.sand : C.border}`,
                    background: okEnabled ? C.sandFaint : 'transparent',
                    color: okEnabled ? C.sandLight : C.textFaint,
                    opacity: okEnabled ? 1 : 0.55,
                    transition:'all 0.15s'}}>
                  OK
                </div>
                <div onClick={()=>{ setAddingProject(false); setNewProjectDraft(''); }} title="Cancel"
                  style={{padding:'5px 11px',borderRadius:999,fontSize:9,fontWeight:600,
                    letterSpacing:'0.14em',textTransform:'uppercase',cursor:'pointer',
                    border:`1px solid ${C.border}`,background:'transparent',color:C.textMuted,
                    transition:'all 0.15s'}}>
                  {isEs ? 'Cancelar' : 'Cancel'}
                </div>
                {overLimit && (
                  <div style={{flexBasis:'100%',marginTop:4,fontSize:10,fontStyle:'italic',
                    color: C.clayLight || C.sandLight, letterSpacing:'0.02em'}}>
                    {isEs ? 'Máximo 20 caracteres. Usa un nombre corto.'
                          : 'Max 20 characters. Use a short name.'}
                  </div>
                )}
              </>
            );
          })()}
        </div>

        {/* Priority compact pill: shows only the selected priority by
            default. Tapping reveals the other two. Hidden while
            project options/add-project are active to keep things compact —
            but always visible inside the mobile bottom-sheet so the user
            can pick project AND priority without reopening anything. */}
        {!addingProject && (!showProjects || _inMobileSheet) && (
          <>
          <SectionLabel>{tr('tasks.priorityLabel')}</SectionLabel>
          <div className="vyb-tasks-composer-priorities"
            style={{display:'flex',gap:6,flexWrap:'nowrap',alignItems:'center',flex:'0 0 auto'}}>
            {(() => {
              // Always render priorities in fixed order (urgent → important → later).
              // Compact (mobile collapsed) state still shows only the selected one.
              return TASK_PRIS
                .filter(p => _inMobileSheet || mobileShowPriorities || p.key === pri)
                .map(p => {
                  const sel = pri === p.key;
                  return (
                    <div key={p.key}
                      onClick={()=>{
                        if (sel) {
                          // Tapping the active pill toggles the rest visible
                          // (mobile compact mode). Don't close the sheet.
                          setMobileShowPriorities(v => {
                            const next = !v;
                            if (next && !isMobile) { setShowProjects(false); }
                            return next;
                          });
                          return;
                        }
                        setPri(p.key); setPriTouched(true);
                        // Mobile: keep the sheet open so user can also pick
                        // a project (or correct their priority). Desktop:
                        // collapse the inline pills back to compact form.
                        if (!isMobile) {
                          setMobileShowPriorities(false);
                          setShowProjects(false);
                        }
                      }}
                      style={{height:26,padding:'0 11px',borderRadius:999,fontSize:9,fontWeight:600,lineHeight:1,letterSpacing:'0.12em',textTransform:'uppercase',cursor:'pointer',display:'inline-flex',gap:6,alignItems:'center',justifyContent:'center',
                        border:`1px solid ${sel?p.color:C.border}`,background:sel?p.faint:'transparent',color:sel?C.textPrimary:C.textMuted,transition:'all 0.15s ease-out',
                        animation: mobileShowPriorities && !sel ? 'vybPriIn 0.18s ease-out both' : undefined}}>
                      <div style={{width:5,height:5,borderRadius:999,background:p.color}}/>
                      {window.dl ? window.dl('priority', p.key) : p.label}
                    </div>
                  );
                });
            })()}
          </div>
          </>
        )}
      </>);
      const _summaryParts = [];
      if (project) _summaryParts.push(project);
      if (pri && pri !== 'important') {
        const _p = TASK_PRI_BY_KEY[pri];
        if (_p) _summaryParts.push(window.dl ? window.dl('priority', pri) : _p.label);
      }
      const _hasMeta = _summaryParts.length > 0;
      const _summaryLine = _hasMeta && isMobile && !mobileSheetOpen ? (
        <div onClick={()=>setMobileSheetOpen(true)}
          style={{margin:'0 0 6px 2px',fontSize:10,color:C.textMuted,letterSpacing:'0.04em',
            cursor:'pointer',display:'flex',alignItems:'center',gap:6,flexWrap:'wrap'}}>
          {_summaryParts.map((s,i) => (
            <React.Fragment key={i}>
              {i>0 && <span style={{color:C.textFaint}}>•</span>}
              <span>{s}</span>
            </React.Fragment>
          ))}
        </div>
      ) : null;
      return (
      <Composer value={input}
        onChange={(v)=>{ setInput(v); if(error) setError(''); }}
        onSubmit={add}
        placeholder={tr('tasks.mobilePlaceholder')}
        error={error} inputRef={tasksInputRef}
        recordingNode={dictation.listening ? (
          <RecordingComposer levelRef={dictation.levelRef} transcript={dictation.transcript}
            onCancel={dictation.cancel} onConfirm={dictation.confirm}/>
        ) : null}
        leadingAction={null}
        aboveBubble={(() => {
          // V2 inline selector state machine. No popups, no modals.
          // Tapping a pill expands inline; tapping the selected option
          // closes (or deselects, in the project case).
          // Used on BOTH mobile and desktop — same component, same UX.
          const priMeta = TASK_PRI_BY_KEY[pri] || TASK_PRI_BY_KEY.important;
          const priLabel = window.dl ? window.dl('priority', pri) : priMeta.label;
          const priColor = priMeta.color;
          const projHasValue = !!project;
          // Shared compact floating pill — solid surface, never transparent.
          const pillBase = {
            display:'inline-flex',alignItems:'center',gap:4,flex:'0 0 auto',
            height:24,padding:'0 8px',borderRadius:999,
            background:'rgba(20,19,17,0.96)',
            border:'1px solid rgba(255,255,255,0.12)',
            color:'rgba(255,255,255,0.68)',cursor:'pointer',
            fontFamily:'Montserrat,sans-serif',fontSize:11,fontWeight:600,
            lineHeight:1,letterSpacing:'0.02em',
            minWidth:0,whiteSpace:'nowrap',
            boxShadow:'0 4px 14px rgba(0,0,0,0.18)',
            transition:'background 0.15s, border-color 0.15s, color 0.15s',
          };
          const rowStyle = {display:'flex',gap:5,alignItems:'center',
            justifyContent:'flex-start',width:'100%',
            flexWrap:'nowrap',overflowX:'auto',WebkitOverflowScrolling:'touch',
            paddingBottom:2,scrollbarWidth:'none'};
          const dividerStyle = {flex:'0 0 auto',width:1,height:14,
            background:'rgba(255,255,255,0.10)',margin:'0 3px'};
          // Build deduped project list (canonical casing), then sort by usage,
          // then drop stale (>4d) entries — but always keep the current selection.
          const seen = new Map();
          for (const p of knownProjects) {
            const nk = normalizeProjectKey(p);
            if (nk && !seen.has(nk)) seen.set(nk, p);
          }
          for (const p of projectMRU) {
            const nk = normalizeProjectKey(p.name);
            if (nk && !seen.has(nk)) seen.set(nk, p.name);
          }
          if (project) {
            const nk = normalizeProjectKey(project);
            if (nk && !seen.has(nk)) seen.set(nk, project);
          }
          const _projectListAll = Array.from(seen.values());
          const _projectListSorted = sortByUsage(_projectListAll, 'tasks');
          const projectList = filterFreshOptions(_projectListSorted, 'tasks',
            (l) => normalizeProjectKey(l) === normalizeProjectKey(project));

          const pickProject = (val) => {
            // val === '' deselects.
            setProject(val || '');
            setProjectTouched(true);
            if (val) bumpProject(val);
            setSelectorMode(null);
            if (error) setError('');
          };
          const submitNewProject = () => {
            const v = sheetNewProjectName.trim().replace(/\s+/g, ' ');
            if (!v) return;
            const nk = normalizeProjectKey(v);
            const existing = seen.get(nk);
            pickProject(existing || v);
            setSheetNewProjectName('');
          };

          // ─── Mode: newProject ────────────────────────────
          if (selectorMode === 'newProject') {
            const canAdd = sheetNewProjectName.trim().length > 0
              && sheetNewProjectName.length <= MAX_PROJECT_LEN;
            return (
              <div className="vyb-composer-sheet" style={rowStyle} onMouseDown={(e)=>{ if (e.target && e.target.closest && e.target.closest('button')) e.preventDefault(); }}>
                <input ref={newProjectInlineRef} value={sheetNewProjectName}
                  onChange={e=>setSheetNewProjectName(e.target.value.slice(0, MAX_PROJECT_LEN))}
                  onKeyDown={e=>{
                    if (e.key === 'Enter') { e.preventDefault(); if (canAdd) submitNewProject(); }
                    else if (e.key === 'Escape') { setSelectorMode('project'); setSheetNewProjectName(''); }
                  }}
                  placeholder={tr('tasks.projectNamePlaceholder')}
                  maxLength={MAX_PROJECT_LEN}
                  style={{flex:'1 1 140px',minWidth:120,maxWidth:240,
                    height:28,padding:'0 11px',borderRadius:999,
                    background:'rgba(250,250,248,0.04)',
                    border:`1px solid ${C.borderMid}`,
                    color:C.textPrimary,outline:'none',
                    fontFamily:'Montserrat,sans-serif',fontSize:11,fontWeight:600,
                    letterSpacing:'0.02em'}}/>
                <button onClick={submitNewProject} disabled={!canAdd}
                  style={{...pillBase,
                    background: canAdd ? C.sand : 'rgba(160,138,86,0.25)',
                    border:'none', color:C.bg,
                    cursor: canAdd ? 'pointer' : 'not-allowed',
                    fontWeight:700,letterSpacing:'0.10em',textTransform:'uppercase'}}>
                  {tr('tasks.addAction')}
                </button>
                <button onClick={()=>{ setSelectorMode('project'); setSheetNewProjectName(''); }}
                  style={{...pillBase,color:C.textMuted}}>
                  {tr('tasks.cancelAction')}
                </button>
              </div>
            );
          }

          // ─── Mode: project ───────────────────────────────
          if (selectorMode === 'project') {
            return (
              <div className="vyb-composer-sheet" style={rowStyle} onMouseDown={(e)=>{ if (e.target && e.target.closest && e.target.closest('button')) e.preventDefault(); }}>
                {projectList.map(p => {
                  const sel = normalizeProjectKey(p) === normalizeProjectKey(project);
                  return (
                    <button key={p}
                      onClick={()=> sel ? pickProject('') : pickProject(p)}
                      style={{...pillBase,
                        background: sel ? 'rgba(42,36,24,0.98)' : pillBase.background,
                        border:`1px solid ${sel ? 'rgba(160,138,86,0.42)' : 'rgba(255,255,255,0.12)'}`,
                        color: sel ? 'rgba(255,255,255,0.92)' : 'rgba(255,255,255,0.78)',
                        maxWidth:160,overflow:'hidden',textOverflow:'ellipsis'}}>
                      {p}
                    </button>
                  );
                })}
                <button onClick={()=>{ setSheetNewProjectName(''); setSelectorMode('newProject'); }}
                  style={{...pillBase,
                    border:'1px dashed rgba(255,255,255,0.18)',color:C.textMuted,
                    background:'rgba(20,19,17,0.96)'}}>
                  {tr('tasks.newShort')}
                </button>
              </div>
            );
          }

          // ─── Mode: null (normal) ─────────────────────────
          // Project pill | divider | Urgent | Important | Later
          // All three priorities always visible — tap to select.
          return (
            <div className="vyb-composer-sheet" style={rowStyle} onMouseDown={(e)=>{ if (e.target && e.target.closest && e.target.closest('button')) e.preventDefault(); }}>
              <button onClick={()=>setSelectorMode('project')}
                aria-label={tr('tasks.chooseProject')}
                style={{...pillBase,
                  ...(projHasValue ? {} : { border:'1px dashed rgba(255,255,255,0.18)', color:C.textMuted })}}>
                <span style={{minWidth:0,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',maxWidth:110,
                  color: projHasValue ? 'rgba(255,255,255,0.92)' : C.textMuted}}>
                  {projHasValue ? project : tr('tasks.addProject')}
                </span>
              </button>
              <span style={dividerStyle} aria-hidden="true"/>
              {TASK_PRIS.map(p => {
                const sel = pri === p.key;
                const label = window.dl ? window.dl('priority', p.key) : p.label;
                // Solid per-priority active surface (no transparency).
                const selBg = p.key === 'urgent'    ? 'rgba(45,24,22,0.98)'
                            : p.key === 'important' ? 'rgba(42,36,24,0.98)'
                            :                         'rgba(30,30,29,0.98)';
                const selBorder = p.key === 'urgent'    ? 'rgba(208,130,90,0.45)'
                                : p.key === 'important' ? 'rgba(160,138,86,0.42)'
                                :                         'rgba(255,255,255,0.18)';
                return (
                  <button key={p.key}
                    onClick={()=>{
                      setPri(p.key); setPriTouched(true);
                      if (error) setError('');
                    }}
                    aria-pressed={sel}
                    style={{...pillBase,
                      background: sel ? selBg : pillBase.background,
                      border:`1px solid ${sel ? selBorder : 'rgba(255,255,255,0.12)'}`,
                      color: sel ? 'rgba(255,255,255,0.92)' : 'rgba(255,255,255,0.68)'}}>
                    <span style={{width:5,height:5,borderRadius:999,background:p.color,flexShrink:0,
                      opacity: sel ? 1 : 0.85}}/>
                    {label}
                  </button>
                );
              })}
            </div>
          );
        })()}
        actions={(
          <>
            <DictationButton supported={dictation.supported} listening={dictation.listening} onClick={dictation.toggle} levelRef={dictation.levelRef}/>
            <ComposerSendButton onClick={add} disabled={!input.trim()} label={tr('tasks.addTask')} inline/>
          </>
        )}>
        {null}
      </Composer>
      );
      })()}


      {/* Smart parse preview — desktop only. Mobile disables auto-detect
          per spec, so the preview is suppressed there. */}
      {!isMobile && input.trim() && (parsed.detected.priority || parsed.detected.project || parsed.detected.date) && (() => {
        const lang = (window.getLang && window.getLang()) || 'en';
        const isEs = lang === 'es';
        const previewPri = priTouched ? pri : (parsed.detected.priority ? parsed.priority : pri);
        const priInfo    = TASK_PRI_BY_KEY[previewPri] || TASK_PRI_BY_KEY.important;
        const previewProj = projectTouched ? project.trim()
                            : (parsed.detected.project ? parsed.projectName : project.trim());
        const dueLabel = parsed.dueDate ? formatDueLabel(parsed.dueKind, parsed.dueDate, lang) : '';
        const chipBase = {
          padding:'4px 10px', borderRadius:999, fontSize:10, fontWeight:600,
          letterSpacing:'0.04em', display:'inline-flex', alignItems:'center', gap:6,
          border:`1px solid ${C.border}`, background:'rgba(160,138,86,0.05)',
          color:C.textSecondary, fontFamily:'Montserrat,sans-serif',
        };
        return (
          <div className="vyb-task-parse-preview" style={{marginTop:10,display:'flex',flexWrap:'wrap',gap:6,alignItems:'center'}}>
            <span style={{fontSize:9,fontWeight:700,letterSpacing:'0.18em',color:C.textFaint,textTransform:'uppercase'}}>
              {isEs ? 'Detectado' : 'Detected'}
            </span>
            {parsed.cleanTitle && parsed.cleanTitle !== input.trim() && (
              <span style={{...chipBase, background:'transparent', color:C.textPrimary, fontStyle:'italic', maxWidth:'100%', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>
                {parsed.cleanTitle}
              </span>
            )}
            <span style={{...chipBase, borderColor: priInfo.color, background: priInfo.faint, color: C.textPrimary, textTransform:'uppercase', fontSize:9, letterSpacing:'0.12em'}}>
              <span style={{width:5,height:5,borderRadius:999,background:priInfo.color}}/>
              {window.dl ? window.dl('priority', priInfo.key) : priInfo.label}
            </span>
            <span style={{...chipBase, color: previewProj ? C.sandLight : C.textFaint, borderColor: previewProj ? 'rgba(160,138,86,0.35)' : C.border, background: previewProj ? C.sandFaint : 'transparent'}}>
              <Icon name="folder" size={10} color={previewProj ? C.sandLight : C.textFaint} sw={2}/>
              {previewProj || (isEs ? 'Sin proyecto' : 'No project')}
            </span>
            {dueLabel && (
              <span style={{...chipBase, color:C.sandLight, borderColor:'rgba(160,138,86,0.35)', background:C.sandFaint}}>
                <Icon name="calendar" size={10} color={C.sandLight} sw={2}/>
                {dueLabel}
              </span>
            )}
          </div>
        );
      })()}

      <div className="vyb-tasks-hint" style={{marginTop:8,fontSize:11,fontWeight:300,fontStyle:'italic',color:C.textFaint,letterSpacing:'0.01em',textAlign:'center'}}>
        {tr('tasks.writeItNowHint')}
      </div>
    </div>
  );
};

// ─── MOOD VIEW ─────────────────────────────────────────────────
const MoodView = ({ store }) => {
  const today = todayKey();
  const m = store.state.mood[today] || {};
  const wk = weekKeys();
  const tIdx = todayDow();

  // 14-day trend
  const trend = Array.from({length:14},(_,i)=>{
    const k = dayKey(-(13-i));
    return { key:k, data:store.state.mood[k] };
  });

  const moods = [
    {v:1,icon:'cloud-rain',label:'Low'},
    {v:2,icon:'meh',label:'Meh'},
    {v:3,icon:'smile',label:'Okay'},
    {v:4,icon:'sun',label:'Good'},
    {v:5,icon:'zap',label:'High'},
  ];

  const avg = (field) => {
    const vals = Object.values(store.state.mood).map(d=>d[field]).filter(v=>v);
    return vals.length ? (vals.reduce((a,b)=>a+b,0)/vals.length).toFixed(1) : '—';
  };
  const loggedDays = Object.keys(store.state.mood).length;

  return (
    <div>
      <ViewHeader label="Daily Check-in" title="Mood & Energy"
        subtitle="A record of how you actually feel. Reflection, not performance." />

      <div style={{display:'grid',gridTemplateColumns:'2fr 1fr 1fr',gap:20,marginBottom:20}}>
        <Card>
          <Label>Today · {new Date().toLocaleDateString('en-US',{month:'long',day:'numeric'})}</Label>
          <div style={{marginBottom:24}}>
            <div style={{fontSize:10,fontWeight:200,letterSpacing:'0.16em',color:C.textFaint,textTransform:'uppercase',marginBottom:12}}>Mood</div>
            <div style={{display:'flex',gap:8}}>
              {moods.map(mo=>(
                <div key={mo.v} onClick={()=>store.setMoodDay(today,{mood:mo.v})}
                  style={{flex:1,display:'flex',flexDirection:'column',alignItems:'center',gap:8,padding:'14px 4px',borderRadius:12,cursor:'pointer',
                    border:`1px solid ${m.mood===mo.v?C.sand:C.border}`,background:m.mood===mo.v?C.sandFaint:'transparent',transition:'all 0.2s'}}>
                  <Icon name={mo.icon} size={22} color={m.mood===mo.v?C.sand:C.textFaint} />
                  <span style={{fontSize:9,letterSpacing:'0.14em',color:m.mood===mo.v?C.sandLight:C.textFaint,fontWeight:m.mood===mo.v?600:300,textTransform:'uppercase'}}>{mo.label}</span>
                </div>
              ))}
            </div>
          </div>
          <div>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
              <div style={{fontSize:10,fontWeight:200,letterSpacing:'0.16em',color:C.textFaint,textTransform:'uppercase'}}>Energy level</div>
              <div style={{fontSize:13,fontWeight:700,color:C.sage}}>{m.energy||'—'}<span style={{color:C.textFaint}}>/5</span></div>
            </div>
            <div style={{display:'flex',gap:8}}>
              {[1,2,3,4,5].map(n=>(
                <div key={n} onClick={()=>store.setMoodDay(today,{energy:n})}
                  style={{flex:1,height:36,borderRadius:8,cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center',
                    border:`1px solid ${n<=(m.energy||0)?C.sage:C.border}`,background:n<=(m.energy||0)?C.sageFaint:'transparent',transition:'all 0.2s'}}>
                  <span style={{fontSize:11,fontWeight:700,color:n<=(m.energy||0)?C.sageLight:C.textFaint}}>{n}</span>
                </div>
              ))}
            </div>
          </div>
        </Card>
        <Card>
          <Label>Avg Mood</Label>
          <div style={{fontSize:44,fontWeight:800,fontStyle:'italic',color:C.sand,lineHeight:1}}>{avg('mood')}</div>
          <div style={{fontSize:10,fontWeight:300,color:C.textMuted,marginTop:10,letterSpacing:'0.08em'}}>across {loggedDays} day{loggedDays===1?'':'s'}</div>
        </Card>
        <Card>
          <Label>Avg Energy</Label>
          <div style={{fontSize:44,fontWeight:800,fontStyle:'italic',color:C.sage,lineHeight:1}}>{avg('energy')}</div>
          <div style={{fontSize:10,fontWeight:300,color:C.textMuted,marginTop:10,letterSpacing:'0.08em'}}>across {loggedDays} day{loggedDays===1?'':'s'}</div>
        </Card>
      </div>

      <Card>
        <Label mb={20}>14-Day Trend</Label>
        <div style={{display:'flex',gap:4,alignItems:'flex-end',height:140,paddingBottom:24,borderBottom:`1px solid ${C.border}`}}>
          {trend.map((d,i)=>{
            const mv = d.data?.mood;
            const ev = d.data?.energy;
            const [y,mn,dd] = d.key.split('-');
            const dow = new Date(+y,+mn-1,+dd).getDay();
            const label = ['S','M','T','W','T','F','S'][dow];
            const isToday = d.key === today;
            return (
              <div key={i} style={{flex:1,display:'flex',flexDirection:'column',alignItems:'center',gap:6,position:'relative'}}>
                <div style={{display:'flex',gap:2,alignItems:'flex-end',height:100}}>
                  <div style={{width:8,borderRadius:2,background:mv?`linear-gradient(to top, ${C.sand}, ${C.sandLight})`:'rgba(255,255,255,0.04)',height:mv?(mv/5)*90:4,transition:'height 0.4s'}} title={mv?`Mood ${mv}/5`:''}/>
                  <div style={{width:8,borderRadius:2,background:ev?`linear-gradient(to top, ${C.sage}, ${C.sageLight})`:'rgba(255,255,255,0.04)',height:ev?(ev/5)*90:4,transition:'height 0.4s'}} title={ev?`Energy ${ev}/5`:''}/>
                </div>
                <div style={{fontSize:9,fontWeight:isToday?700:300,color:isToday?C.sand:C.textFaint,letterSpacing:'0.08em'}}>{label}</div>
              </div>
            );
          })}
        </div>
        <div style={{display:'flex',gap:20,marginTop:16}}>
          <div style={{display:'flex',alignItems:'center',gap:8}}>
            <div style={{width:10,height:10,borderRadius:2,background:C.sand}}/>
            <span style={{fontSize:10,fontWeight:200,letterSpacing:'0.16em',color:C.textMuted,textTransform:'uppercase'}}>Mood</span>
          </div>
          <div style={{display:'flex',alignItems:'center',gap:8}}>
            <div style={{width:10,height:10,borderRadius:2,background:C.sage}}/>
            <span style={{fontSize:10,fontWeight:200,letterSpacing:'0.16em',color:C.textMuted,textTransform:'uppercase'}}>Energy</span>
          </div>
        </div>
      </Card>
    </div>
  );
};

// ─── OVERVIEW PAGE ─────────────────────────────────────────────
// Greeting + date. Compact, pulls profile name if set.
const OverviewHeader = ({ store }) => {
  const tr = useT();
  const lang = getLang();
  const now = new Date();
  const h = now.getHours();
  const greet = h < 6 ? tr('overview.greetEvening','Good evening')
    : h < 12 ? tr('overview.greetMorning','Good morning')
    : h < 18 ? tr('overview.greetAfternoon','Good afternoon')
    : tr('overview.greetEvening','Good evening');
  const name = store.state.profile?.name || '';
  const dateLine = now.toLocaleDateString(lang==='es'?'es-ES':'en-US',
    {weekday:'long',month:'long',day:'numeric'});
  return (
    <div style={{marginBottom:4}}>
      <div style={{fontSize:34,fontWeight:300,color:C.textPrimary,letterSpacing:'-0.02em',lineHeight:1.1}}>
        {greet}{name?`, ${name}`:''}
      </div>
      <div style={{fontSize:13,fontWeight:300,color:C.textMuted,marginTop:6,letterSpacing:'0.04em',textTransform:'capitalize'}}>
        {dateLine}
      </div>
    </div>
  );
};

// Today's direction hero — one-line status + CTA into Momentum.
const TodayDirectionCard = ({ store, goTo }) => {
  const tr = useT();
  const calc = window.calculateMomentum || (() => null);
  const m = React.useMemo(() => calc({
    habits: store.state.habits, tasks: store.state.tasks,
    ideas:  store.state.ideas,  books: store.state.books,
  }), [store.state.habits, store.state.tasks, store.state.ideas, store.state.books]);
  if (!m) return null;
  const copy = (window.getMomentumCopy && window.getMomentumCopy(m)) || {};
  const isComplete = m.status.key === 'complete';
  const accent = isComplete ? C.sage : C.sand;
  const accentLight = isComplete ? C.sageLight : C.sandLight;
  const headline = copy.headline || copy.title || m.status.label;
  const sub = copy.subline || copy.message || '';
  return (
    <Card style={{padding:28, borderColor: isComplete ? 'rgba(94,117,88,0.32)' : C.border}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',gap:20,flexWrap:'wrap'}}>
        <div style={{flex:'1 1 280px',minWidth:0}}>
          <div style={{display:'flex',alignItems:'center',gap:10,marginBottom:10}}>
            <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.20em',color:C.textFaint,textTransform:'uppercase'}}>
              {tr('overview.todayDirection',"Today's direction")}
            </div>
            <div style={{padding:'3px 10px',borderRadius:999,
              border:`1px solid ${accent}`,color:accentLight,
              fontFamily:'Montserrat,sans-serif',fontSize:8,fontWeight:700,
              letterSpacing:'0.18em',textTransform:'uppercase'}}>
              {m.status.label}
            </div>
          </div>
          <div style={{fontSize:22,fontWeight:400,color:C.textPrimary,letterSpacing:'-0.01em',lineHeight:1.3}}>
            {headline}
          </div>
          {sub && (
            <div style={{fontSize:13,fontWeight:300,color:C.textMuted,marginTop:8,lineHeight:1.5}}>
              {sub}
            </div>
          )}
        </div>
        <button onClick={()=>goTo('momentum')}
          style={{background:'transparent',border:`1px solid ${accent}`,borderRadius:10,
            padding:'10px 16px',color:accent,cursor:'pointer',
            fontFamily:'Montserrat,sans-serif',fontSize:10,fontWeight:600,
            letterSpacing:'0.16em',textTransform:'uppercase',
            display:'inline-flex',alignItems:'center',gap:8,flexShrink:0,transition:'all 0.15s'}}
          onMouseEnter={e=>{e.currentTarget.style.color=accentLight;e.currentTarget.style.borderColor=accentLight;}}
          onMouseLeave={e=>{e.currentTarget.style.color=accent;e.currentTarget.style.borderColor=accent;}}>
          {tr('overview.openMomentum','Open Momentum')}
          <Icon name="arrow-up-right" size={12} color="currentColor"/>
        </button>
      </div>
    </Card>
  );
};

// Quick actions — 4 buttons, responsive grid.
// Routes the user into daily-execution surfaces. Momentum is intentionally
// not in this row (it has its own section + a compact preview lower down).
const QuickActionsRow = ({ goTo }) => {
  const tr = useT();
  const items = [
    {key:'tasks',   icon:'plus',      label:tr('overview.qaAddTask','Add Task'),           short:tr('overview.qaShortTask','Task'),   primary:true},
    {key:'habits',  icon:'target',    label:tr('overview.qaGoToHabits','Go to Habits'),    short:tr('overview.qaShortHabits','Habits')},
    {key:'ideas',   icon:'lightbulb', label:tr('overview.qaCaptureIdea','Capture Idea'),   short:tr('overview.qaShortIdea','Idea')},
    {key:'reading', icon:'book-open', label:tr('overview.qaOpenReading','Open Reading'),   short:tr('overview.qaShortRead','Read')},
  ];
  return (
    <div className="vyb-quick-actions vyb-dash-grid">
      <style>{`
        .vyb-qa-btn { transition: background 0.18s, border-color 0.18s, box-shadow 0.18s, transform 0.18s ease-out; }
        /* Pointer / hover-capable devices (desktop): primary starts neutral,
           gains gold accent only on hover. Non-primary lifts subtly. */
        @media (hover: hover) and (pointer: fine) {
          .vyb-qa-btn:hover { background: rgba(255,255,255,0.05); border-color: ${C.borderMid};
            transform: translateY(-1px) scale(1.015);
            box-shadow: 0 8px 22px rgba(0,0,0,0.22); }
          .vyb-qa-btn:hover .vyb-qa-icon { border-color: ${C.borderMid}; background: rgba(255,255,255,0.04); }
          .vyb-qa-btn:hover .vyb-qa-icon svg { transform: translateX(1px); }
          .vyb-qa-icon svg { transition: transform 0.18s ease-out, stroke 0.18s; }
          .vyb-qa-primary { background: rgba(255,255,255,0.02) !important; border-color: ${C.border} !important; box-shadow: none !important; }
          .vyb-qa-primary .vyb-qa-icon { background: transparent !important; border-color: ${C.border} !important; }
          .vyb-qa-primary .vyb-qa-icon svg { stroke: ${C.sandLight} !important; }
          .vyb-qa-primary:hover { background: ${C.sandFaint} !important; border-color: ${C.sand} !important;
            transform: translateY(-1px) scale(1.02);
            box-shadow: 0 0 0 1px rgba(160,138,86,0.12), 0 10px 26px rgba(160,138,86,0.18) !important; }
          .vyb-qa-primary:hover .vyb-qa-icon { background: rgba(160,138,86,0.18) !important; border-color: ${C.sandGlow} !important; }
          .vyb-qa-primary:hover .vyb-qa-icon svg { stroke: ${C.sand} !important; stroke-width: 2.4 !important; }
        }
        /* Touch / no-hover devices (mobile): primary stays gold by default. */
        @media (hover: none), (pointer: coarse) {
          .vyb-qa-btn:active { background: rgba(255,255,255,0.06); transform: scale(0.985); }
          .vyb-qa-primary:active { background: rgba(160,138,86,0.22) !important; }
        }
      `}</style>
      {items.map(it => {
        const primary = !!it.primary;
        // Default ("mobile" / inline) styling keeps the primary highlighted —
        // the @media (hover: hover) block above strips it back on desktop.
        const baseBg = primary ? C.sandFaint : 'rgba(255,255,255,0.02)';
        const baseBorder = primary ? C.sandGlow : C.border;
        return (
          <button key={it.key} onClick={()=>{
              // Signal intent to the destination view so it can focus its
              // composer immediately. Cleared by the consumer.
              if (it.key === 'tasks' || it.key === 'ideas') {
                try { window.__vybQuickAction = { target: it.key, ts: Date.now() }; } catch {}
              }
              goTo(it.key);
            }} aria-label={it.label}
            className={`vyb-qa-btn${primary ? ' vyb-qa-primary' : ''}`}
            style={{background:baseBg,border:`1px solid ${baseBorder}`,borderRadius:12,
              padding:'14px 14px',color:C.textPrimary,cursor:'pointer',textAlign:'left',
              display:'flex',alignItems:'center',gap:10,minWidth:0,
              boxShadow: primary ? '0 0 0 1px rgba(160,138,86,0.10), 0 8px 24px rgba(160,138,86,0.10)' : 'none'}}>
            <div className="vyb-qa-icon" style={{width:32,height:32,borderRadius:8,
              border:`1px solid ${primary ? C.sandGlow : C.border}`,
              background: primary ? 'rgba(160,138,86,0.18)' : 'transparent',
              display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0,
              transition:'background 0.18s, border-color 0.18s'}}>
              <Icon name={it.icon} size={14} color={primary ? C.sand : C.sandLight} sw={primary ? 2.4 : 1.8}/>
            </div>
            <span className="vyb-qa-label vyb-qa-label-full" style={{fontSize:12,fontWeight:primary?700:500,
              color:C.textPrimary,letterSpacing:'0.01em',
              minWidth:0,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
              {it.label}
            </span>
            <span className="vyb-qa-label vyb-qa-label-short" style={{display:'none',fontSize:11,fontWeight:primary?700:500,
              color:C.textPrimary,letterSpacing:'0.01em',
              minWidth:0,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
              {it.short}
            </span>
          </button>
        );
      })}
    </div>
  );
};

// Today Habits — weekly grid for Overview.
// Table-like layout: Habit (name + area) | Mon..Sun day cells | Streak.
// Header row aligns weekday labels to the cells below. Rows are separated
// by a hairline. Today's cell is the only interactive marker (uses
// store.toggleHabit — same path as the Habits page). Past/future/rest are
// display-only. Mobile: the day grid scrolls horizontally inside the card
// while name + streak stay pinned visually outside the scroll region.
const OverviewHabitsTimeline = ({ store, onOpen }) => {
  const tr = useT();
  const { habits, habitCategories=[] } = store.state;
  const today = todayKey();
  const wk = weekKeys();
  const tIdx = todayDow();
  const isSched = window.isHabitScheduledOn || (() => true);
  const lang = getLang();
  const dayLabels = lang==='es'
    ? ['Lun','Mar','Mié','Jue','Vie','Sáb','Dom']
    : ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];

  const areaOf = React.useCallback((id) => {
    return habitCategories.find(c => c.id === id) || null;
  }, [habitCategories]);
  const areaName = React.useCallback((id) => {
    const c = areaOf(id);
    return c ? c.name : tr('overview.areaOther','Other');
  }, [areaOf, tr]);
  const areaIcon = React.useCallback((id) => {
    const c = areaOf(id);
    return (c && c.icon) ? c.icon : 'circle';
  }, [areaOf]);

  // Only habits scheduled for today appear here (Overview is a "today"
  // surface). Weekly columns stay so the user keeps the weekly context
  // for each visible habit.
  const todayDate = React.useMemo(() => new Date(), []);
  const scheduledToday = habits.filter(h => isSched(h, todayDate));
  const MAX_ROWS = 7;
  const rows = scheduledToday.slice(0, MAX_ROWS);
  const overflow = Math.max(0, scheduledToday.length - MAX_ROWS);

  // Wider weekday area: name col is now ~42% (minmax + 1fr), grid uses
  // 1fr per day so columns expand to fill the rest. Min day width 28px
  // protects the layout when the card is narrow.
  const GRID = 'minmax(180px, 0.9fr) repeat(7, minmax(28px, 1fr))';

  // Circular day cell — pure visual derived from sched/done/relative position.
  const renderCell = (h, dk, i) => {
    const d = new Date(dk + 'T00:00:00');
    const sched = isSched(h, d);
    const done = !!h.checkIns[dk];
    const isPast = i < tIdx;
    const isToday = i === tIdx;
    const isFuture = i > tIdx;
    // Past + today scheduled cells are editable; future stays inert.
    const interactive = sched && !isFuture;

    let bg = 'transparent';
    let border = `1px solid ${C.border}`;
    let inner = null;
    let opacity = 1;
    if (!sched) {
      // Non-scheduled days: completely blank.
      border = 'none';
      bg = 'transparent';
      inner = null;
    } else if (done) {
      bg = isToday ? C.sand : 'rgba(160,138,86,0.22)';
      border = `1px solid ${isToday ? C.sand : 'rgba(160,138,86,0.45)'}`;
      inner = <Icon name="check" size={12} color={isToday?C.bg:C.sandLight} sw={2.6}/>;
    } else if (isToday) {
      border = `1.5px solid ${C.sand}`;
      bg = 'rgba(160,138,86,0.08)';
      inner = null;
    } else if (isPast) {
      // Past scheduled but missed — soft outline; clickable to backfill.
      border = `1px solid ${C.border}`;
      inner = null;
    } else if (isFuture) {
      // Future scheduled — visible but faded and inert.
      border = `1px solid ${C.border}`;
      opacity = 0.35;
      inner = null;
    }

    return (
      <div key={dk} className="vyb-habit-day-cell" style={{display:'flex',alignItems:'center',justifyContent:'center'}}>
        <div
          role={interactive?'button':undefined}
          tabIndex={interactive?0:-1}
          aria-pressed={interactive?done:undefined}
          aria-label={`${h.name} — ${dayLabels[i]}`}
          onClick={interactive?()=>store.toggleHabit(h.id, dk):undefined}
          onKeyDown={interactive?(e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();store.toggleHabit(h.id, dk);}}):undefined}
          className="vyb-habit-day-marker"
          style={{
            width:26,height:26,minWidth:26,minHeight:26,maxWidth:26,maxHeight:26,
            aspectRatio:'1 / 1',borderRadius:'50%',border,background:bg,
            display:'flex',alignItems:'center',justifyContent:'center',opacity,flex:'0 0 auto',
            cursor:interactive?'pointer':'default',transition:'all 0.15s',
          }}>
          {inner}
        </div>
      </div>
    );
  };

  return (
    <Card style={{padding:24}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',gap:12,marginBottom:18,flexWrap:'wrap'}}>
        <div>
          <Label mb={6}>{tr('overview.today','Today')}</Label>
          <Title mb={0}>{tr('overview.todayHabits','Today Habits')}</Title>
          <div style={{fontSize:11,fontWeight:300,color:C.textMuted,marginTop:8,letterSpacing:'0.02em'}}>
            {tr('overview.todayHabitsSub','Track today inside your week.')}
          </div>
        </div>
        <button onClick={onOpen} title={tr('overview.openHabits','Open Habits')}
          style={{background:'transparent',border:`1px solid ${C.border}`,borderRadius:8,
            padding:'6px 10px',color:C.textMuted,cursor:'pointer',
            fontFamily:'Montserrat,sans-serif',fontSize:9,fontWeight:600,letterSpacing:'0.14em',textTransform:'uppercase',
            display:'inline-flex',alignItems:'center',gap:6,transition:'all 0.15s'}}
          onMouseEnter={e=>{e.currentTarget.style.color=C.textPrimary;e.currentTarget.style.borderColor=C.borderMid;}}
          onMouseLeave={e=>{e.currentTarget.style.color=C.textMuted;e.currentTarget.style.borderColor=C.border;}}>
          {tr('overview.viewAll','View all')} <Icon name="arrow-up-right" size={11} color="currentColor"/>
        </button>
      </div>

      {rows.length === 0 ? (
        <div style={{padding:'28px 4px',textAlign:'center'}}>
          <div style={{fontSize:13,fontWeight:400,color:C.textSecondary,marginBottom:6}}>
            {habits.length === 0
              ? tr('overview.noHabitsYet','No habits yet.')
              : tr('overview.noHabitsToday','No habits planned for today.')}
          </div>
          <div style={{fontSize:11,fontWeight:300,color:C.textMuted,marginBottom:14}}>
            {habits.length === 0
              ? tr('overview.startBuildingSystem','Start building your system.')
              : tr('overview.nextHabitsHint','Your next scheduled habits will appear here.')}
          </div>
          <button onClick={onOpen}
            style={{background:'transparent',border:`1px solid ${C.borderMid}`,borderRadius:8,
              padding:'8px 14px',color:C.textPrimary,cursor:'pointer',
              fontFamily:'Montserrat,sans-serif',fontSize:10,fontWeight:600,
              letterSpacing:'0.14em',textTransform:'uppercase'}}>
            {tr('overview.qaGoToHabits','Go to Habits')}
          </button>
        </div>
      ) : (
        <div style={{overflowX:'auto',marginInline:-4}}>
          <div style={{minWidth:440,paddingInline:4}}>
            {/* Header row */}
            <div style={{display:'grid',gridTemplateColumns:GRID,
              alignItems:'center',gap:8,paddingBottom:10,
              borderBottom:`1px solid ${C.border}`}}>
              <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.20em',
                color:C.textFaint,textTransform:'uppercase'}}>
                {tr('overview.habitColLabel','Habit')}
              </div>
              {dayLabels.map((d,i)=>{
                const isToday = i===tIdx;
                return (
                  <div key={i} style={{fontSize:9,fontWeight:700,
                    letterSpacing:'0.14em',
                    color:isToday?C.sand:C.textFaint,textTransform:'uppercase',
                    textAlign:'center'}}>
                    {d.slice(0,3)}
                  </div>
                );
              })}
            </div>

            {/* Habit rows — no edit/delete; click cells to toggle */}
            {rows.map((h, idx) => {
              return (
                <div key={h.id} className="vyb-habit-row"
                  style={{display:'grid',gridTemplateColumns:GRID,
                    alignItems:'center',gap:8,padding:'14px 0',
                    borderBottom: idx === rows.length-1 ? 'none' : `1px solid ${C.border}`,
                    transition:'background 0.15s'}}
                  onMouseEnter={e=>{e.currentTarget.style.background='rgba(255,255,255,0.015)';}}
                  onMouseLeave={e=>{e.currentTarget.style.background='transparent';}}>
                  {/* Icon chip + name + area */}
                  <div style={{display:'flex',alignItems:'center',gap:10,minWidth:0,paddingRight:8}}>
                    <div style={{width:28,height:28,borderRadius:'50%',
                      background:'rgba(160,138,86,0.10)',
                      border:`1px solid ${C.border}`,
                      display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0}}>
                      <Icon name={areaIcon(h.categoryId)} size={13} color={C.sandLight} sw={1.6}/>
                    </div>
                    <div style={{minWidth:0}}>
                      <div style={{fontSize:13,fontWeight:500,color:C.textPrimary,
                        whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>
                        {h.name}
                      </div>
                      <div style={{fontSize:10,fontWeight:600,letterSpacing:'0.14em',
                        textTransform:'uppercase',color:C.textFaint,marginTop:3,
                        whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>
                        {areaName(h.categoryId)}
                      </div>
                    </div>
                  </div>
                  {/* 7 circular day cells */}
                  {wk.map((dk, i) => renderCell(h, dk, i))}
                </div>
              );
            })}

            {overflow > 0 && (
              <div style={{paddingTop:12,textAlign:'right'}}>
                <button onClick={onOpen}
                  style={{background:'transparent',border:'none',color:C.textMuted,cursor:'pointer',
                    fontFamily:'Montserrat,sans-serif',fontSize:10,fontWeight:600,
                    letterSpacing:'0.14em',textTransform:'uppercase',padding:0,
                    display:'inline-flex',alignItems:'center',gap:6}}>
                  {tr('overview.viewAllHabits','View all habits')} (+{overflow})
                  <Icon name="arrow-up-right" size={11} color="currentColor"/>
                </button>
              </div>
            )}
          </div>
        </div>
      )}
    </Card>
  );
};

// Quick Notes — local-only multi-note list for Overview.
// Storage: vyb-quick-notes (JSON array of {id, text, createdAt}).
// Intentionally separate from store.notes (which is the legacy single-string
// scratchpad). Keep this small — no tags, no folders, no rich text.
const OverviewQuickNotes = () => {
  const tr = useT();
  const STORAGE_KEY = 'vyb-quick-notes';
  const [notes, setNotes] = React.useState(() => {
    try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); }
    catch { return []; }
  });
  const [draft, setDraft] = React.useState('');
  const taRef = React.useRef(null);

  // Persist on every change.
  React.useEffect(() => {
    try { localStorage.setItem(STORAGE_KEY, JSON.stringify(notes)); } catch {}
  }, [notes]);

  const add = () => {
    const text = draft.trim();
    if (!text) return;
    const note = { id: Date.now().toString(36) + Math.random().toString(36).slice(2,6),
                   text, createdAt: new Date().toISOString() };
    setNotes(n => [note, ...n]);
    setDraft('');
    if (taRef.current) taRef.current.focus();
  };
  const remove = (id) => setNotes(n => n.filter(x => x.id !== id));

  const onKey = (e) => {
    // Enter saves, Shift+Enter newline.
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      add();
    }
  };

  const fmt = (iso) => {
    try {
      const d = new Date(iso);
      return d.toLocaleDateString(getLang()==='es'?'es-ES':'en-US',
        {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});
    } catch { return ''; }
  };

  const visible = notes.slice(0, 5);

  return (
    <Card>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',marginBottom:14}}>
        <div>
          <Label>{tr('overview.reflectionsReminders','Reflections & Reminders')}</Label>
          <Title mb={0}>{tr('overview.quickNotes','Quick Notes')}</Title>
        </div>
      </div>

      <textarea ref={taRef} value={draft}
        onChange={e=>setDraft(e.target.value)} onKeyDown={onKey}
        placeholder={tr('overview.quickNotesPlaceholder','Reflection, reminder, or quick thought...')}
        rows={2}
        style={{width:'100%',background:'rgba(250,250,248,0.03)',
          border:`1px solid ${C.border}`,borderRadius:10,
          padding:'10px 12px',fontSize:13,color:C.textPrimary,outline:'none',
          fontFamily:'inherit',resize:'vertical',lineHeight:1.45,
          transition:'all 0.2s'}}
        onFocus={e=>{e.target.style.borderColor=C.sand;e.target.style.background='rgba(250,250,248,0.05)';}}
        onBlur={e=>{e.target.style.borderColor=C.border;e.target.style.background='rgba(250,250,248,0.03)';}}/>

      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',gap:10,marginTop:8}}>
        <span style={{fontSize:10,fontWeight:300,color:C.textFaint,letterSpacing:'0.04em'}}>
          {tr('overview.quickNotesHint','Enter to save · Shift+Enter for new line')}
        </span>
        <button onClick={add} disabled={!draft.trim()}
          style={{background:draft.trim()?C.sand:'transparent',
            color:draft.trim()?C.bg:C.textFaint,
            border:draft.trim()?'none':`1px solid ${C.border}`,
            borderRadius:8,padding:'7px 14px',
            cursor:draft.trim()?'pointer':'default',
            fontFamily:'Montserrat,sans-serif',fontSize:10,fontWeight:700,
            letterSpacing:'0.14em',textTransform:'uppercase',transition:'all 0.15s'}}>
          {tr('overview.addNote','Add Note')}
        </button>
      </div>

      <div style={{marginTop:16,display:'flex',flexDirection:'column',gap:8}}>
        {visible.length === 0 ? (
          <div style={{padding:'14px 4px',textAlign:'center',
            fontSize:12,fontWeight:300,color:C.textMuted,fontStyle:'italic'}}>
            {tr('overview.noQuickNotes','No quick notes yet.')}
          </div>
        ) : (
          visible.map(n => (
            <div key={n.id} style={{display:'flex',gap:10,alignItems:'flex-start',
              padding:'10px 12px',borderRadius:10,
              background:'rgba(255,255,255,0.02)',border:`1px solid ${C.border}`}}>
              <div style={{flex:1,minWidth:0}}>
                <div style={{fontSize:12,fontWeight:400,color:C.textPrimary,
                  lineHeight:1.5,whiteSpace:'pre-wrap',wordBreak:'break-word'}}>
                  {n.text}
                </div>
                <div style={{fontSize:9,fontWeight:600,letterSpacing:'0.14em',
                  color:C.textFaint,textTransform:'uppercase',marginTop:6}}>
                  {fmt(n.createdAt)}
                </div>
              </div>
              <IconButton icon="x" size={12}
                title={tr('overview.deleteNote','Delete note')}
                onClick={()=>remove(n.id)}/>
            </div>
          ))
        )}
      </div>
    </Card>
  );
};

// Overview = main daily dashboard.
// In-content greeting/header is intentionally absent — the AppShell title
// bar already shows "Panel / This Week / <date>", so duplicating it here
// would feel redundant.
// Order (top → bottom):
//   1. Daily completion seal (self-gates, only renders if today done)
//   2. Quick actions row
//   3. Top Priorities | Mood & Energy        (equal height)
//   4. Today Habits                          (full width, weekly grid)
//   5. Current Reading | Deep Work Timer     (equal height)
//   6. Quick Notes                           (full width)
//   7. Content Ideas | Today Momentum        (equal height)
// Components are kept as separate widgets so a future customization phase
// can show/hide/reorder them via a config array without restructuring.
// Local mobile detector — keeps OverviewPage self-contained.
const useOverviewIsMobile = () => {
  const [m, setM] = React.useState(() =>
    typeof window !== 'undefined' && window.matchMedia
      ? window.matchMedia('(max-width: 768px)').matches : false);
  React.useEffect(() => {
    if (typeof window === 'undefined' || !window.matchMedia) return;
    const mq = window.matchMedia('(max-width: 768px)');
    const h = (e) => setM(e.matches);
    mq.addEventListener ? mq.addEventListener('change', h) : mq.addListener(h);
    return () => mq.removeEventListener ? mq.removeEventListener('change', h) : mq.removeListener(h);
  }, []);
  return m;
};

const OverviewPage = ({ store, goTo }) => {
  const isMobile = useOverviewIsMobile();
  const { isVisible } = (window.useOverviewPrefs ? window.useOverviewPrefs() : { isVisible: () => true });

  // Helpers for the auto-fit pair grids: only render the wrapper when at
  // least one child is visible, so hiding both doesn't leave an empty row.
  const renderPair = (a, b) => {
    if (!a && !b) return null;
    return (
      <div style={{display:'grid',gap:20,
        gridTemplateColumns:'repeat(auto-fit, minmax(320px, 1fr))',alignItems:'stretch'}}>
        {a}{b}
      </div>
    );
  };

  if (isMobile) {
    return (
      <div style={{display:'flex',flexDirection:'column',gap:16}}>
        <DashboardCustomize/>
        <DailyCompletionSeal store={store}/>
        <QuickActionsRow goTo={goTo}/>
        {isVisible('dailyCatchup')   && <DailyCatchupCard store={store} goTo={goTo}/>}
        {isVisible('topPriorities')  && <PrioritiesWidget store={store} onOpen={()=>goTo('tasks')}/>}
        <div className="vyb-dash-grid" style={{alignItems:'stretch'}}>
          <div className="vyb-span-2"><HabitsTodayMini store={store} onOpen={()=>goTo('habits')}/></div>
          <div className="vyb-span-2"><PendingTasksMini store={store} onOpen={()=>goTo('tasks')}/></div>
        </div>
        {isVisible('todayHabits')    && <OverviewHabitsTimeline store={store} onOpen={()=>goTo('habits')}/>}
        {isVisible('deepWork')       && <FocusWidget />}
        {isVisible('quickNotes')     && <OverviewQuickNotes />}
        {isVisible('contentIdeas')   && <IdeasWidget store={store} onOpen={()=>goTo('ideas')}/>}
        {isVisible('todayMomentum')  && <TodayMomentumWidget store={store} onOpen={()=>goTo('momentum')}/>}
        {isVisible('currentReading') && <ReadingWidget store={store} onOpen={()=>goTo('reading')}/>}
        {isVisible('moodEnergy')     && <MoodWidget store={store} onOpen={()=>goTo('mood')}/>}
      </div>
    );
  }

  return (
    <div style={{display:'flex',flexDirection:'column',gap:20}}>
      <DashboardCustomize/>
      <DailyCompletionSeal store={store}/>
      <QuickActionsRow goTo={goTo}/>
      {isVisible('dailyCatchup') && <DailyCatchupCard store={store} goTo={goTo}/>}
      {/* Row 2: aligned to the same 4-col grid as quick actions above.
          Top Priorities spans cols 1–2; the two KPI minis take cols 3 & 4. */}
      <div className="vyb-dash-grid" style={{alignItems:'stretch'}}>
        {isVisible('topPriorities') && (
          <div className="vyb-span-2">
            <PrioritiesWidget store={store} onOpen={()=>goTo('tasks')}/>
          </div>
        )}
        <HabitsTodayMini store={store} onOpen={()=>goTo('habits')}/>
        <PendingTasksMini store={store} onOpen={()=>goTo('tasks')}/>
      </div>
      {isVisible('todayHabits') && <OverviewHabitsTimeline store={store} onOpen={()=>goTo('habits')}/>}
      {renderPair(
        isVisible('currentReading') && <ReadingWidget key="r" store={store} onOpen={()=>goTo('reading')}/>,
        isVisible('deepWork')       && <FocusWidget key="f" />
      )}
      {isVisible('quickNotes') && <OverviewQuickNotes />}
      {renderPair(
        isVisible('contentIdeas')  && <IdeasWidget key="i" store={store} onOpen={()=>goTo('ideas')}/>,
        isVisible('todayMomentum') && <TodayMomentumWidget key="m" store={store} onOpen={()=>goTo('momentum')}/>
      )}
      {isVisible('moodEnergy') && <MoodWidget store={store} onOpen={()=>goTo('mood')}/>}
    </div>
  );
};

// ─── MOMENTUM VIEW ─────────────────────────────────────────────
// Detailed progress section. Reuses existing widgets (Today's Momentum,
// Streak, Weekly, Seal) and adds a per-area breakdown plus a placeholder
// for the upcoming Milestones step. All numbers come from calculateMomentum.
const MomentumStat = ({ label, value, hint, accent = C.sandLight }) => (
  <div style={{
    background:'rgba(255,255,255,0.02)',border:`1px solid ${C.border}`,borderRadius:12,
    padding:'14px 16px',display:'flex',flexDirection:'column',gap:4}}>
    <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.20em',color:C.textFaint,textTransform:'uppercase'}}>{label}</div>
    <div style={{fontFamily:'Montserrat,sans-serif',fontSize:22,fontWeight:800,fontStyle:'italic',color:accent,letterSpacing:'-0.01em',lineHeight:1.1}}>
      {value}
    </div>
    {hint && (
      <div style={{fontSize:10,fontWeight:300,color:C.textMuted,letterSpacing:'0.02em'}}>{hint}</div>
    )}
  </div>
);

// Tight, dense breakdown row. Lives inside a single zero-padding Card so all
// rows share one container with subtle dividers between them.
const BreakdownTightRow = ({ icon, name, stats, accent = C.sand, actionLabel, onAction, last }) => (
  <div style={{display:'flex',alignItems:'center',gap:12,padding:'12px 16px',
    borderBottom: last ? 'none' : `1px solid ${C.border}`}}>
    <div style={{width:28,height:28,borderRadius:8,flexShrink:0,
      background:`rgba(${accent===C.sage?'94,117,88':'160,138,86'},0.10)`,
      border:`1px solid rgba(${accent===C.sage?'94,117,88':'160,138,86'},0.28)`,
      display:'inline-flex',alignItems:'center',justifyContent:'center'}}>
      <Icon name={icon} size={13} color={accent} sw={2}/>
    </div>
    <div style={{flex:1,minWidth:0}}>
      <div style={{fontSize:12,fontWeight:600,color:C.textPrimary,letterSpacing:'0.01em'}}>{name}</div>
      <div style={{fontSize:11,fontWeight:300,color:C.textMuted,letterSpacing:'0.02em',
        overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{stats}</div>
    </div>
    {onAction && (
      <button onClick={onAction}
        style={{background:'transparent',border:'none',color:C.textFaint,cursor:'pointer',
          fontFamily:'Montserrat,sans-serif',fontSize:9,fontWeight:600,letterSpacing:'0.14em',textTransform:'uppercase',
          padding:'4px 6px',transition:'color 0.15s',whiteSpace:'nowrap'}}
        onMouseEnter={e=>{e.currentTarget.style.color=C.textPrimary;}}
        onMouseLeave={e=>{e.currentTarget.style.color=C.textFaint;}}>
        {actionLabel} →
      </button>
    )}
  </div>
);

// Subtle, dismiss-not-needed recommendation card. Only renders when the
// helper produced a recommendation object (rule-based, no AI).
const SystemRecommendation = ({ rec, goTo }) => {
  if (!rec) return null;
  return (
    <Card style={{
      borderColor:'rgba(160,138,86,0.32)',
      background:'linear-gradient(180deg, rgba(160,138,86,0.06) 0%, rgba(13,12,11,0) 100%)',
      padding:'14px 18px'}}>
      <div style={{display:'flex',alignItems:'center',gap:14,flexWrap:'wrap'}}>
        <div style={{
          width:34,height:34,borderRadius:10,flexShrink:0,
          background:'rgba(160,138,86,0.10)',border:'1px solid rgba(160,138,86,0.28)',
          display:'inline-flex',alignItems:'center',justifyContent:'center',color:C.sandLight}}>
          <Icon name="lightbulb" size={15} sw={2}/>
        </div>
        <div style={{flex:1,minWidth:0}}>
          <div style={{fontSize:13,fontWeight:600,color:C.textPrimary,letterSpacing:'-0.005em',marginBottom:3}}>
            {rec.title}
          </div>
          <div style={{fontSize:11,fontWeight:300,color:C.textMuted,letterSpacing:'0.02em',lineHeight:1.5}}>
            {rec.body}
          </div>
        </div>
        {rec.cta && (
          <ActionLink label={rec.cta.label} onClick={()=>goTo && goTo(rec.cta.route)}/>
        )}
      </div>
    </Card>
  );
};

// Per-habit weekly consistency. Renders only when there are scheduled habits.
const WeeklyConsistencyCard = ({ rows, goTo }) => {
  if (!rows || rows.length === 0) return null;
  const visible = rows.filter(r => (r.scheduledThisWeek || 0) > 0);
  if (visible.length === 0) return null;
  return (
    <Card style={{padding:0}}>
      <div style={{padding:'14px 16px',borderBottom:`1px solid ${C.border}`,
        display:'flex',justifyContent:'space-between',alignItems:'baseline',gap:12,flexWrap:'wrap'}}>
        <div>
          <Label>This week</Label>
          <Title mb={0}>Weekly consistency</Title>
        </div>
        <ActionLink label="Open Habits" onClick={()=>goTo && goTo('habits')}/>
      </div>
      {visible.map((r, i) => {
        const target = r.weeklyTarget || r.scheduledThisWeek || 1;
        const done   = r.completedAnyThisWeek != null ? r.completedAnyThisWeek : r.completedThisWeek;
        const pct    = target ? Math.round((done / target) * 100) : 0;
        const accent = r.targetMet ? C.sage : C.sand;
        const accentLight = r.targetMet ? C.sageLight : C.sandLight;

        // Build a contextual one-line summary: "1 due today · 1 upcoming · 1 missed".
        const segs = [];
        if (r.dueTodayThisWeek)  segs.push(`${r.dueTodayThisWeek} due today`);
        if (r.upcomingThisWeek)  segs.push(`${r.upcomingThisWeek} upcoming`);
        if (r.missedThisWeek)    segs.push(`${r.missedThisWeek} missed`);
        let contextLine;
        if (r.targetMet) contextLine = 'weekly goal complete';
        else if (r.recoverable) contextLine = segs.length ? `${segs.join(' · ')} · still recoverable` : 'still recoverable';
        else contextLine = segs.length ? segs.join(' · ') : 'all scheduled days complete';

        // Friction signal (gentle, not negative).
        let friction = null;
        if (r.missedThisWeek >= 2) {
          friction = 'This habit may be too heavy right now. Try 2 days next week.';
        } else if (r.scheduledThisWeek >= 2 && r.completedThisWeek === 0 && r.missedThisWeek >= 1) {
          friction = 'Try making this easier to repeat.';
        }

        return (
          <div key={r.id}
            style={{display:'flex',flexDirection:'column',gap:8,padding:'12px 16px',
              borderBottom: i === visible.length - 1 ? 'none' : `1px solid ${C.border}`}}>
            <div style={{display:'flex',alignItems:'center',gap:12}}>
              <div style={{width:26,height:26,borderRadius:8,flexShrink:0,
                background:'rgba(160,138,86,0.08)',border:`1px solid ${C.border}`,
                display:'inline-flex',alignItems:'center',justifyContent:'center',color:C.textMuted}}>
                <Icon name={r.icon || 'target'} size={12} sw={2}/>
              </div>
              <div style={{flex:1,minWidth:0}}>
                <div style={{fontSize:12,fontWeight:600,color:C.textPrimary,letterSpacing:'0.01em',
                  overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{r.name}</div>
                <div style={{position:'relative',marginTop:6,width:'100%',height:4,borderRadius:999,
                  background:'rgba(250,250,248,0.05)',overflow:'hidden'}}>
                  <div style={{position:'absolute',inset:0,width:`${Math.min(100,pct)}%`,
                    background:`linear-gradient(90deg, ${accent}, ${accentLight})`,borderRadius:999,
                    transition:'width 0.5s cubic-bezier(0.16,1,0.3,1)'}}/>
                </div>
              </div>
              <div style={{fontFamily:'Montserrat,sans-serif',fontSize:12,fontWeight:600,
                color: r.targetMet ? C.sageLight : C.textPrimary,letterSpacing:'-0.01em',
                minWidth:42,textAlign:'right',whiteSpace:'nowrap'}}>
                {done}/{target}
              </div>
            </div>

            {/* Per-day chips */}
            <div style={{display:'flex',gap:5,flexWrap:'wrap',marginLeft:38}}>
              {r.days.map(d => {
                const palette = {
                  completed: { fg: C.sageLight, bd: 'rgba(94,117,88,0.45)',  bg: 'rgba(94,117,88,0.12)' },
                  due_today: { fg: C.sandLight, bd: 'rgba(160,138,86,0.55)', bg: 'rgba(160,138,86,0.14)' },
                  upcoming:  { fg: C.textMuted, bd: C.border,                bg: 'transparent' },
                  missed:    { fg: '#C9A26B',   bd: 'rgba(201,162,107,0.40)',bg: 'rgba(201,162,107,0.08)' },
                }[d.status];
                return (
                  <span key={d.date}
                    title={`${d.label} · ${d.status.replace('_',' ')}`}
                    style={{
                      fontFamily:'Montserrat,sans-serif',fontSize:9,fontWeight:700,
                      letterSpacing:'0.10em',textTransform:'uppercase',
                      padding:'3px 7px',borderRadius:6,
                      color:palette.fg,border:`1px solid ${palette.bd}`,background:palette.bg}}>
                    {d.label}{d.status === 'completed' ? ' ✓' : ''}
                  </span>
                );
              })}
              <span style={{marginLeft:'auto',fontSize:10,fontWeight:300,
                color:C.textFaint,letterSpacing:'0.02em',fontStyle:'italic'}}>
                {contextLine}
              </span>
            </div>

            {/* Friction signal — gentle, not judgmental */}
            {friction && (
              <div style={{marginLeft:38,padding:'8px 10px',borderRadius:6,
                border:'1px solid rgba(201,162,107,0.30)',background:'rgba(201,162,107,0.06)',
                fontSize:10.5,fontWeight:300,color:C.textSecondary,letterSpacing:'0.02em',
                display:'flex',alignItems:'center',gap:8}}>
                <Icon name="alert-circle" size={12} color="#C9A26B" sw={2}/>
                <span style={{flex:1,minWidth:0}}>
                  <span style={{fontWeight:600,color:'#C9A26B',fontSize:9,letterSpacing:'0.14em',
                    textTransform:'uppercase',marginRight:6}}>Friction signal</span>
                  {friction}
                </span>
              </div>
            )}
          </div>
        );
      })}
    </Card>
  );
};

// Horizontal 4-column activity strip with subtle vertical dividers.
// Neutral record of today, not a score. Smaller/lighter than Daily Habits.
const ActivityStrip = ({ today, goTo }) => {
  const tr = useT();
  const allZero = !(Number(today.completedTasks) || 0)
    && !(Number(today.ideasCaptured)  || 0)
    && !(Number(today.readingSessions)|| 0)
    && !(Number(today.focusSessions)  || 0);
  const reading = Number(today.readingSessions) || 0;
  const items = [
    { display: String(Number(today.completedTasks) || 0), label: tr('momentum.tasksCompleted'), dim: !(Number(today.completedTasks) || 0) },
    { display: String(Number(today.ideasCaptured)  || 0), label: tr('momentum.ideasCaptured'),  dim: !(Number(today.ideasCaptured)  || 0) },
    { display: reading > 0 ? tr('momentum.yes') : tr('momentum.no'), label: tr('momentum.readingToday'), dim: reading === 0 },
    { display: String(Number(today.focusSessions)  || 0), label: tr('momentum.focusSessions'),  dim: !(Number(today.focusSessions)  || 0) },
  ];
  return (
    <Card style={{padding:'14px 0 12px'}}>
      <div style={{padding:'0 16px 10px',fontSize:9,fontWeight:700,letterSpacing:'0.22em',color:C.textFaint,textTransform:'uppercase'}}>
        {tr('momentum.activityToday')}
      </div>
      {allZero ? (
        <div style={{padding:'4px 16px 4px',display:'flex',flexDirection:'column',gap:10,alignItems:'center'}}>
          <div style={{fontSize:12,fontWeight:300,fontStyle:'italic',color:C.textMuted,letterSpacing:'0.02em'}}>
            {tr('momentum.noActivityYet')}
          </div>
          <div style={{display:'flex',gap:14,flexWrap:'wrap',justifyContent:'center'}}>
            <ActionLink label={tr('momentum.addTaskShort')}     onClick={()=>goTo && goTo('tasks')}/>
            <ActionLink label={tr('momentum.captureIdeaShort')} onClick={()=>goTo && goTo('ideas')}/>
            <ActionLink label={tr('momentum.openReadingShort')} onClick={()=>goTo && goTo('reading')}/>
          </div>
        </div>
      ) : (
        <div style={{display:'grid',gridTemplateColumns:'repeat(4, minmax(0,1fr))'}}>
          {items.map((it, i) => (
            <div key={it.label}
              style={{padding:'2px 16px',textAlign:'center',
                borderLeft: i === 0 ? 'none' : `1px solid ${C.border}`}}>
              <div style={{fontFamily:'Montserrat,sans-serif',
                fontSize:20,fontWeight:300,letterSpacing:'-0.01em',
                color: it.dim ? C.textFaint : C.textSecondary,lineHeight:1.15,marginBottom:4}}>
                {it.display}
              </div>
              <div style={{fontSize:9,fontWeight:500,letterSpacing:'0.14em',textTransform:'uppercase',
                color: it.dim ? C.textFaint : C.textMuted}}>
                {it.label}
              </div>
            </div>
          ))}
        </div>
      )}
    </Card>
  );
};

// Decide which section the Next Action CTA should jump to. Mirrors the
// cascade in getMomentumCopy.nextAction (habits → tasks → ideas → reading
// → focus → overview).
const nextActionTarget = (today) => {
  const t = today || {};
  if ((t.totalHabits || 0) === 0)        return { label: 'Go to Habits',  route: 'habits' };
  if (!t.isDailyHabitsComplete)          return { label: 'Go to Habits',  route: 'habits' };
  if ((t.completedTasks || 0) === 0
      && (t.activeTasks || 0) > 0)       return { label: 'Go to Tasks',   route: 'tasks' };
  if ((t.ideasCaptured || 0) === 0)      return { label: 'Go to Ideas',   route: 'ideas' };
  if ((t.readingSessions || 0) === 0)    return { label: 'Go to Reading', route: 'reading' };
  if ((t.focusSessions || 0) === 0)      return { label: 'Start Focus',   route: 'overview' };
  return { label: 'Open Overview', route: 'overview' };
};

const BreakdownRow = ({ icon, name, stats, accent = C.sand }) => (
  <div style={{display:'flex',alignItems:'center',gap:14,padding:'14px 16px',
    border:`1px solid ${C.border}`,borderRadius:12,background:'rgba(255,255,255,0.015)'}}>
    <div style={{width:34,height:34,borderRadius:999,flexShrink:0,
      background:`rgba(${accent===C.sage?'94,117,88':'160,138,86'},0.10)`,
      border:`1px solid rgba(${accent===C.sage?'94,117,88':'160,138,86'},0.28)`,
      display:'inline-flex',alignItems:'center',justifyContent:'center'}}>
      <Icon name={icon} size={14} color={accent} sw={2}/>
    </div>
    <div style={{flex:1,minWidth:0}}>
      <div style={{fontSize:13,fontWeight:500,color:C.textPrimary,letterSpacing:'0.01em',marginBottom:3}}>{name}</div>
      <div style={{fontSize:11,fontWeight:300,color:C.textMuted,letterSpacing:'0.02em'}}>{stats}</div>
    </div>
  </div>
);

// Milestone config — pure derivation from momentum totals. Each entry is
// independent and degrades safely if a metric is missing (treated as 0).
// `dataReady=false` keeps a milestone visibly locked when its underlying
// data is not tracked yet (e.g. focus sessions before persistence lands).
const buildMilestones = (m) => {
  const t = (m && m.totals) || {};
  const today = (m && m.today) || {};
  const weekly = (m && m.weekly) || {};
  const streak = (m && m.streak) || {};

  const make = (id, title, description, current, goal, dataReady = true) => {
    const safeCurrent = Math.max(0, Number(current) || 0);
    const reached = dataReady && safeCurrent >= goal;
    const pct = dataReady ? Math.min(100, Math.round((safeCurrent / goal) * 100)) : 0;
    return {
      id, title, description,
      unlocked: reached,
      progressLabel: dataReady ? `${Math.min(safeCurrent, goal)}/${goal}` : 'Coming soon',
      progressPercent: pct,
      dataReady,
    };
  };

  return [
    { ...make('first_signal',   'First Signal',     'Complete your first full habit day.', t.fullHabitDays || 0, 1), icon: 'sparkles' },
    // "Momentum Started" uses today as best-effort if no historical peak.
    // TODO: once we persist daily peaks, swap to a true historical max.
    { ...make('momentum_started','Momentum Started','Complete 3 habits in one day.',
        Math.max(today.completedHabits || 0, t.maxHabitsInOneDay || 0), 3), icon: 'zap' },
    { ...make('rhythm_builder', 'Rhythm Builder',   'Complete 3 full habit days in a week.', weekly.completeHabitDays || 0, 3), icon: 'activity' },
    { ...make('locked_in',      'Locked In',        'Complete 7 full habit days.',
        Math.max(streak.current || 0, t.fullHabitDays || 0), 7), icon: 'lock' },
    { ...make('idea_collector', 'Idea Collector',   'Capture 10 ideas.', t.ideas || 0, 10), icon: 'lightbulb' },
    // Tasks: best-effort with the all-time completed total.
    { ...make('task_closer',    'Task Closer',      'Complete 10 tasks.', t.completedTasks || 0, 10), icon: 'check-circle' },
    { ...make('reader_mode',    'Reader Mode',      'Log 3 reading sessions.', t.readingSessions || 0, 3), icon: 'book-open' },
    // Focus persistence not implemented yet — keep locked safely.
    // TODO: flip dataReady once focus session history is stored.
    { ...make('deep_work_init', 'Deep Work Initiate','Complete your first focus session.', t.focusSessions || 0, 1, (t.focusSessions || 0) > 0), icon: 'timer' },
  ];
};

const MilestoneCard = ({ ms }) => {
  const { unlocked, dataReady } = ms;
  const accent = unlocked ? C.sage : C.sand;
  const accentLight = unlocked ? C.sageLight : C.sandLight;
  const borderColor = unlocked ? 'rgba(94,117,88,0.32)'
    : (dataReady && ms.progressPercent > 0 ? 'rgba(160,138,86,0.22)' : C.border);
  const titleColor = unlocked ? C.textPrimary : (dataReady ? C.textPrimary : C.textSecondary);

  return (
    <div style={{
      position:'relative',
      background: unlocked
        ? 'linear-gradient(135deg, rgba(94,117,88,0.08), rgba(94,117,88,0.02))'
        : 'rgba(255,255,255,0.015)',
      border:`1px solid ${borderColor}`,borderRadius:14,
      padding:16,display:'flex',flexDirection:'column',gap:10,
      boxShadow: unlocked ? '0 0 0 1px rgba(94,117,88,0.06), 0 16px 36px rgba(94,117,88,0.08)' : 'none',
      transition:'all 0.25s ease',
      opacity: !dataReady ? 0.7 : 1,
    }}>
      <div style={{display:'flex',alignItems:'flex-start',justifyContent:'space-between',gap:10}}>
        <div aria-hidden="true" style={{
          width:34,height:34,borderRadius:10,flexShrink:0,
          background: unlocked ? `rgba(94,117,88,0.14)` : `rgba(160,138,86,0.06)`,
          border:`1px solid ${unlocked ? 'rgba(94,117,88,0.32)' : C.border}`,
          display:'inline-flex',alignItems:'center',justifyContent:'center'}}>
          <Icon name={unlocked ? ms.icon : 'lock'} size={14}
            color={unlocked ? accentLight : C.textFaint} sw={2}/>
        </div>
        <div style={{padding:'4px 8px',borderRadius:999,
          border:`1px solid ${unlocked ? 'rgba(94,117,88,0.40)' : C.border}`,
          background: unlocked ? 'rgba(94,117,88,0.12)' : 'transparent',
          color: unlocked ? C.sageLight : C.textFaint,
          fontFamily:'Montserrat,sans-serif',fontSize:8,fontWeight:700,
          letterSpacing:'0.18em',textTransform:'uppercase'}}>
          {unlocked ? 'Unlocked' : (dataReady ? 'In progress' : 'Coming soon')}
        </div>
      </div>

      <div>
        <div style={{fontSize:13,fontWeight:600,color:titleColor,letterSpacing:'0.01em',marginBottom:4}}>
          {ms.title}
        </div>
        <div style={{fontSize:11,fontWeight:300,color:C.textMuted,letterSpacing:'0.02em',lineHeight:1.5}}>
          {ms.description}
        </div>
      </div>

      <div style={{marginTop:'auto'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',gap:8,marginBottom:6}}>
          <span style={{fontSize:9,fontWeight:700,letterSpacing:'0.18em',
            color: unlocked ? accentLight : C.textFaint,textTransform:'uppercase'}}>
            {ms.progressLabel}
          </span>
          {dataReady && (
            <span style={{fontSize:9,fontWeight:300,color:C.textFaint,letterSpacing:'0.04em'}}>
              {ms.progressPercent}%
            </span>
          )}
        </div>
        <div style={{position:'relative',width:'100%',height:3,borderRadius:999,
          background:'rgba(250,250,248,0.05)',overflow:'hidden'}}>
          <div style={{position:'absolute',inset:0,width:`${ms.progressPercent}%`,
            background: unlocked
              ? `linear-gradient(90deg, ${accent}, ${accentLight})`
              : `linear-gradient(90deg, rgba(160,138,86,0.5), rgba(204,186,147,0.7))`,
            borderRadius:999,
            transition:'width 0.6s cubic-bezier(0.16,1,0.3,1)'}}/>
        </div>
      </div>
    </div>
  );
};

// Premium "milestone unlocked" toast. Renders via portal so it sits above
// every layout. Auto-dismisses after ~3s; respects prefers-reduced-motion
// (CSS strips the shimmer + scale via the .vyb-milestone-toast hook).
const MilestoneUnlockToast = ({ ms, onDismiss }) => {
  React.useEffect(() => {
    if (!ms) return;
    const t = setTimeout(onDismiss, 3200);
    return () => clearTimeout(t);
  }, [ms, onDismiss]);

  if (!ms) return null;

  return ReactDOM.createPortal(
    <div className="vyb-milestone-toast" role="status" aria-live="polite"
      style={{
        position:'fixed',top:24,left:'50%',transform:'translateX(-50%)',
        zIndex:1500,maxWidth:'min(92vw, 420px)',width:'100%',pointerEvents:'none',
        animation:'vyb-toast-in 0.5s cubic-bezier(0.16,1,0.3,1) both, vyb-toast-out 0.4s ease 2.7s both',
      }}>
      <div style={{
        position:'relative',overflow:'hidden',borderRadius:14,
        background:'linear-gradient(135deg, rgba(28,25,21,0.96), rgba(20,18,15,0.96))',
        border:'1px solid rgba(160,138,86,0.42)',
        boxShadow:'0 0 0 1px rgba(160,138,86,0.12), 0 24px 60px rgba(0,0,0,0.55), 0 0 60px rgba(160,138,86,0.18)',
        padding:'14px 16px',backdropFilter:'blur(14px)',
      }}>
        {/* Soft golden radial wash */}
        <div aria-hidden="true" style={{
          position:'absolute',top:-50,left:-30,width:220,height:220,pointerEvents:'none',
          background:'radial-gradient(circle, rgba(160,138,86,0.22), transparent 65%)'}}/>
        {/* Shimmer sweep */}
        <div aria-hidden="true" className="vyb-shimmer" style={{
          position:'absolute',top:0,left:0,bottom:0,width:'40%',pointerEvents:'none',
          background:'linear-gradient(100deg, transparent, rgba(212,189,138,0.18), transparent)',
          animation:'vyb-shimmer 1.6s ease-out 0.25s 1 both'}}/>

        <div style={{position:'relative',display:'flex',alignItems:'center',gap:14}}>
          <div aria-hidden="true" style={{
            width:42,height:42,borderRadius:12,flexShrink:0,
            background:'radial-gradient(circle at 30% 30%, rgba(212,189,138,0.95), rgba(160,138,86,0.85) 60%, rgba(120,98,58,0.95))',
            border:'1px solid rgba(212,189,138,0.55)',
            boxShadow:'inset 0 1px 0 rgba(255,235,200,0.30), 0 8px 22px rgba(160,138,86,0.30)',
            display:'inline-flex',alignItems:'center',justifyContent:'center'}}>
            <Icon name={ms.icon || 'sparkles'} size={18} color={C.bg} sw={2.2}/>
          </div>
          <div style={{flex:1,minWidth:0}}>
            <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',
              color:C.sandLight,textTransform:'uppercase',marginBottom:4}}>
              Milestone Unlocked
            </div>
            <div style={{fontSize:14,fontWeight:600,color:C.textPrimary,letterSpacing:'0.005em',lineHeight:1.2}}>
              {ms.title}
            </div>
            <div style={{marginTop:3,fontSize:11,fontWeight:300,color:C.textMuted,letterSpacing:'0.02em',lineHeight:1.4}}>
              {ms.description}
            </div>
          </div>
        </div>
      </div>
    </div>,
    document.body
  );
};

const MilestonesGrid = ({ momentum }) => {
  const [filter, setFilter] = React.useState('all'); // all | unlocked | progress
  const milestones = React.useMemo(() => buildMilestones(momentum), [momentum]);
  const unlockedCount = milestones.filter(m => m.unlocked).length;

  // Unlock detection — seed prev set on first mount so previously-unlocked
  // milestones never re-fire on page load. Subsequent recalculations only
  // surface true locked → unlocked transitions. Queue keeps simultaneous
  // unlocks single-file (no stacked overlays).
  const prevUnlockedRef = React.useRef(null);
  const [queue, setQueue] = React.useState([]);
  const [active, setActive] = React.useState(null);

  React.useEffect(() => {
    if (prevUnlockedRef.current === null) {
      prevUnlockedRef.current = new Set(milestones.filter(m => m.unlocked).map(m => m.id));
      return;
    }
    const newly = milestones.filter(m => m.unlocked && !prevUnlockedRef.current.has(m.id));
    if (newly.length > 0) {
      setQueue(q => [...q, ...newly]);
      newly.forEach(m => prevUnlockedRef.current.add(m.id));
    }
    // Handle the rare lock-back case (e.g. data recompute) so it can re-fire later.
    milestones.filter(m => !m.unlocked && prevUnlockedRef.current.has(m.id))
      .forEach(m => prevUnlockedRef.current.delete(m.id));
  }, [milestones]);

  // Drain queue one at a time.
  React.useEffect(() => {
    if (active || queue.length === 0) return;
    setActive(queue[0]);
    setQueue(q => q.slice(1));
  }, [active, queue]);

  const visible = milestones.filter(m =>
    filter === 'unlocked' ? m.unlocked
    : filter === 'progress' ? !m.unlocked
    : true);

  const FilterPill = ({ id, label, count }) => {
    const on = filter === id;
    return (
      <button onClick={()=>setFilter(id)}
        style={{padding:'6px 12px',borderRadius:999,cursor:'pointer',
          border:`1px solid ${on?C.sand:C.border}`,
          background: on ? 'rgba(160,138,86,0.10)' : 'transparent',
          color: on ? C.sandLight : C.textMuted,
          fontFamily:'Montserrat,sans-serif',fontSize:9,fontWeight:600,
          letterSpacing:'0.16em',textTransform:'uppercase',transition:'all 0.15s'}}>
        {label} <span style={{opacity:0.6,marginLeft:4}}>{count}</span>
      </button>
    );
  };

  return (
    <Card>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',gap:16,marginBottom:18,flexWrap:'wrap'}}>
        <div style={{display:'flex',gap:8,flexWrap:'wrap'}}>
          <FilterPill id="all" label="All" count={milestones.length}/>
          <FilterPill id="unlocked" label="Unlocked" count={unlockedCount}/>
          <FilterPill id="progress" label="In progress" count={milestones.length - unlockedCount}/>
        </div>
        <div style={{display:'flex',alignItems:'baseline',gap:6}}>
          <span style={{fontFamily:'Montserrat,sans-serif',fontSize:18,fontWeight:800,
            fontStyle:'italic',color:unlockedCount?C.sageLight:C.textMuted,letterSpacing:'-0.01em'}}>
            {unlockedCount}<span style={{color:C.textFaint,fontWeight:300}}>/{milestones.length}</span>
          </span>
          <span style={{fontSize:9,fontWeight:200,letterSpacing:'0.16em',color:C.textFaint,textTransform:'uppercase'}}>unlocked</span>
        </div>
      </div>

      <div className="vyb-milestones-grid"
        style={{display:'grid',gridTemplateColumns:'repeat(auto-fill, minmax(220px, 1fr))',gap:12}}>
        {visible.map(ms => <MilestoneCard key={ms.id} ms={ms}/>)}
      </div>

      <div style={{marginTop:18,fontSize:11,fontWeight:300,fontStyle:'italic',color:C.textFaint,letterSpacing:'0.03em'}}>
        Built through repetition. Small wins. Real momentum.
      </div>

      <MilestoneUnlockToast ms={active} onDismiss={() => setActive(null)}/>
    </Card>
  );
};

// Small typographic divider that anchors each Momentum section. Keeps the
// page scannable without inflating it with extra cards.
const MomentumSectionHeader = ({ label, title, hint }) => (
  <div style={{display:'flex',alignItems:'flex-end',justifyContent:'space-between',gap:16,
    marginTop:8,marginBottom:-4,flexWrap:'wrap'}}>
    <div>
      <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.24em',color:C.textFaint,textTransform:'uppercase',marginBottom:6}}>
        {label}
      </div>
      <div style={{fontSize:18,fontWeight:600,fontStyle:'italic',color:C.textPrimary,letterSpacing:'-0.005em'}}>
        {title}
      </div>
    </div>
    {hint && (
      <div style={{fontSize:11,fontWeight:300,fontStyle:'italic',color:C.textFaint,letterSpacing:'0.03em'}}>
        {hint}
      </div>
    )}
  </div>
);

const ActionLink = ({ label, onClick }) => (
  <button onClick={onClick}
    style={{background:'transparent',border:`1px solid ${C.border}`,borderRadius:8,
      padding:'6px 10px',color:C.textMuted,cursor:'pointer',
      fontFamily:'Montserrat,sans-serif',fontSize:9,fontWeight:600,letterSpacing:'0.14em',textTransform:'uppercase',
      display:'inline-flex',alignItems:'center',gap:6,transition:'all 0.15s'}}
    onMouseEnter={e=>{e.currentTarget.style.color=C.textPrimary;e.currentTarget.style.borderColor=C.borderMid;}}
    onMouseLeave={e=>{e.currentTarget.style.color=C.textMuted;e.currentTarget.style.borderColor=C.border;}}>
    {label} <Icon name="arrow-up-right" size={11} color="currentColor"/>
  </button>
);

const ActivityActionRow = ({ label, value, actionLabel, onAction }) => {
  const n = Number(value) || 0;
  const dim = n === 0;
  return (
    <div style={{
      display:'flex',alignItems:'center',justifyContent:'space-between',gap:10,
      padding:'10px 12px',borderRadius:8,
      border:`1px solid ${C.border}`,background:'rgba(250,250,248,0.02)'}}>
      <div style={{display:'flex',alignItems:'baseline',gap:10,minWidth:0,flex:1}}>
        <div style={{fontFamily:'Montserrat,sans-serif',fontSize:18,fontWeight:600,
          color:dim?C.textFaint:C.textPrimary,letterSpacing:'-0.01em',minWidth:22}}>
          {n}
        </div>
        <div style={{fontSize:11,fontWeight:300,color:dim?C.textFaint:C.textSecondary,letterSpacing:'0.02em',
          overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
          {label}
        </div>
      </div>
      {onAction && (
        <button onClick={onAction}
          style={{background:'transparent',border:'none',color:C.textFaint,cursor:'pointer',
            fontFamily:'Montserrat,sans-serif',fontSize:9,fontWeight:600,letterSpacing:'0.12em',textTransform:'uppercase',
            padding:'4px 6px',transition:'color 0.15s',whiteSpace:'nowrap'}}
          onMouseEnter={e=>{e.currentTarget.style.color=C.textPrimary;}}
          onMouseLeave={e=>{e.currentTarget.style.color=C.textFaint;}}>
          {actionLabel} →
        </button>
      )}
    </div>
  );
};

const ActivityRow = ({ label, value }) => {
  const n = Number(value) || 0;
  const dim = n === 0;
  return (
    <div style={{
      display:'flex',alignItems:'baseline',justifyContent:'space-between',gap:10,
      padding:'10px 12px',borderRadius:8,
      border:`1px solid ${C.border}`,
      background:'rgba(250,250,248,0.02)'}}>
      <div style={{fontSize:11,fontWeight:300,color:dim?C.textFaint:C.textSecondary,letterSpacing:'0.02em'}}>
        {label}
      </div>
      <div style={{fontFamily:'Montserrat,sans-serif',fontSize:18,fontWeight:600,
        color:dim?C.textFaint:C.textPrimary,letterSpacing:'-0.01em'}}>
        {n}
      </div>
    </div>
  );
};

const AIReflectionPlaceholder = () => (
  <Card style={{
    border:`1px dashed ${C.border}`,
    background:'linear-gradient(180deg, rgba(160,138,86,0.04) 0%, rgba(13,12,11,0) 100%)',
    position:'relative',
  }}>
    <div style={{position:'absolute',top:14,right:14,
      fontSize:9,fontWeight:600,letterSpacing:'0.18em',textTransform:'uppercase',
      color:C.sandLight,border:'1px solid rgba(160,138,86,0.35)',
      background:'rgba(160,138,86,0.08)',padding:'4px 10px',borderRadius:999}}>
      Premium · Coming soon
    </div>
    <div style={{display:'flex',gap:18,alignItems:'flex-start'}}>
      <div style={{
        width:44,height:44,borderRadius:12,flexShrink:0,
        display:'flex',alignItems:'center',justifyContent:'center',
        background:'rgba(160,138,86,0.10)',border:'1px solid rgba(160,138,86,0.25)',
        color:C.sandLight}}>
        <Icon name="sparkles" size={20}/>
      </div>
      <div style={{flex:1,minWidth:0}}>
        <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.24em',
          color:C.textFaint,textTransform:'uppercase',marginBottom:6}}>
          AI Reflection
        </div>
        <div style={{fontSize:18,fontWeight:600,fontStyle:'italic',
          color:C.textPrimary,letterSpacing:'-0.005em',marginBottom:12}}>
          Coming soon
        </div>
        <div style={{fontSize:13,fontWeight:300,lineHeight:1.65,color:C.textSecondary,
          marginBottom:8,maxWidth:560}}>
          Soon, VYB will help you read the signals behind your habits, focus, ideas, and progress.
        </div>
        <div style={{fontSize:11,fontWeight:300,fontStyle:'italic',
          color:C.textFaint,letterSpacing:'0.02em',maxWidth:560}}>
          Your system is collecting the proof. AI insights will help you understand it.
        </div>
        <button disabled style={{
          marginTop:18,padding:'9px 18px',
          fontSize:11,fontWeight:600,letterSpacing:'0.12em',textTransform:'uppercase',
          color:C.textFaint,background:'transparent',
          border:`1px solid ${C.border}`,borderRadius:8,
          cursor:'not-allowed',opacity:0.6}}>
          Unlock later
        </button>
      </div>
    </div>
  </Card>
);

// Empty-account starter card — shown when the user has essentially no signals
// yet. Replaces the noisy 0/0 metric layout with a clear "do this first" guide.
const MomentumStarter = ({ counts, goTo }) => {
  const steps = [
    { n:'01', icon:'check-circle', title:'Create 2–3 habits', copy:'Choose what days each habit should happen.', cta:'Create habits', route:'habits', done: counts.habits >= 2 },
    { n:'02', icon:'list-todo',    title:'Add one task',      copy:'Capture what needs doing today.',           cta:'Add task',      route:'tasks',  done: counts.tasks  >= 1 },
    { n:'03', icon:'lightbulb',    title:'Capture one idea',  copy:'Notes, signals, or sparks — anything.',     cta:'Capture idea',  route:'ideas',  done: counts.ideas  >= 1 },
    { n:'04', icon:'book-open',    title:'Start one book',    copy:'Track progress, notes, and insights.',      cta:'Add book',      route:'reading',done: counts.books  >= 1 },
  ];
  return (
    <Card style={{padding:24}}>
      <div style={{marginBottom:16}}>
        <Label mb={4}>Start your system</Label>
        <div style={{fontSize:18,fontWeight:700,fontStyle:'italic',color:C.textPrimary,letterSpacing:'-0.01em',marginBottom:6}}>
          Momentum needs a few signals before it can show progress.
        </div>
        <div style={{fontSize:12,fontWeight:300,color:C.textMuted,letterSpacing:'0.02em'}}>
          Add a little of each. The system fills in as you use it.
        </div>
      </div>
      <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill, minmax(220px, 1fr))',gap:12}}>
        {steps.map(s => (
          <div key={s.n} style={{padding:14,borderRadius:12,
            border:`1px solid ${s.done?'rgba(94,117,88,0.32)':C.border}`,
            background: s.done ? 'rgba(94,117,88,0.06)' : 'rgba(255,255,255,0.02)',
            display:'flex',flexDirection:'column',gap:8}}>
            <div style={{display:'flex',alignItems:'center',gap:10}}>
              <div style={{width:28,height:28,borderRadius:8,
                background: s.done ? 'rgba(94,117,88,0.14)' : C.sandFaint,
                display:'flex',alignItems:'center',justifyContent:'center'}}>
                <Icon name={s.done?'check':s.icon} size={13} color={s.done?C.sageLight:C.sand} sw={2}/>
              </div>
              <div style={{fontSize:9,fontWeight:300,letterSpacing:'0.22em',color:C.textFaint}}>{s.n}</div>
            </div>
            <div style={{fontSize:13,fontWeight:700,color:C.textPrimary,letterSpacing:'-0.005em'}}>{s.title}</div>
            <div style={{fontSize:11,fontWeight:300,color:C.textMuted,lineHeight:1.5,flex:1}}>{s.copy}</div>
            <ActionLink label={s.done?'Open':s.cta} onClick={()=>goTo && goTo(s.route)}/>
          </div>
        ))}
      </div>
    </Card>
  );
};

// Momentum "How it works" — compact, visual, dismissible. Persists collapse
// state in localStorage so power users aren't pestered.
const MomentumHowItWorks = () => {
  const tr = useT();
  const [open, setOpen] = React.useState(() => {
    // Collapsed by default. Open only if the user explicitly opened it before.
    try { return localStorage.getItem('vyb-momentum-howto') === '1'; } catch { return false; }
  });
  const toggle = () => {
    const next = !open;
    setOpen(next);
    try { localStorage.setItem('vyb-momentum-howto', next ? '1' : '0'); } catch {}
  };
  const steps = [
    { n:'1', icon:'calendar-check', title:tr('momentumHIW.plan'),     copy:tr('momentumHIW.planCopy') },
    { n:'2', icon:'check-circle',   title:tr('momentumHIW.complete'), copy:tr('momentumHIW.completeCopy') },
    { n:'3', icon:'activity',       title:tr('momentumHIW.track'),    copy:tr('momentumHIW.trackCopy') },
    { n:'4', icon:'sliders',        title:tr('momentumHIW.improve'),  copy:tr('momentumHIW.improveCopy') },
  ];
  return (
    <Card style={{padding:0}}>
      <div onClick={toggle} style={{display:'flex',alignItems:'center',gap:10,padding:'14px 18px',cursor:'pointer',
        borderBottom: open ? `1px solid ${C.border}` : 'none'}}>
        <Icon name="info" size={13} color={C.sand}/>
        <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',color:C.textPrimary,textTransform:'uppercase'}}>
          {tr('momentum.howItWorks')}
        </div>
        <div style={{marginLeft:'auto',fontSize:10,fontWeight:300,color:C.textFaint,fontStyle:'italic'}}>
          {open ? tr('momentum.hide') : tr('momentum.show')}
        </div>
        <Icon name={open?'chevron-up':'chevron-down'} size={12} color={C.textFaint}/>
      </div>
      {open && (
        <div style={{padding:14}}>
          <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit, minmax(160px, 1fr))',gap:10}}>
            {steps.map((s, i) => (
              <div key={s.n} style={{padding:'12px 14px',borderRadius:10,border:`1px solid ${C.border}`,
                background:'rgba(255,255,255,0.015)',display:'flex',flexDirection:'column',gap:6}}>
                <div style={{display:'flex',alignItems:'center',gap:8}}>
                  <div style={{width:22,height:22,borderRadius:6,background:C.sandFaint,
                    display:'flex',alignItems:'center',justifyContent:'center'}}>
                    <Icon name={s.icon} size={11} color={C.sand} sw={2}/>
                  </div>
                  <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',color:C.textFaint}}>{s.n}</div>
                </div>
                <div style={{fontSize:12,fontWeight:700,color:C.textPrimary,letterSpacing:'-0.005em'}}>{s.title}</div>
                <div style={{fontSize:10.5,fontWeight:300,color:C.textMuted,lineHeight:1.45}}>{s.copy}</div>
              </div>
            ))}
          </div>
        </div>
      )}
    </Card>
  );
};

// ─── Paths & Artifacts (Phase 1, English-only) ──────────────────
// A premium progression layer that lives inside Momentum. Replaces the
// previous "Milestones / Proof of Progress" surface (PART 10 — Option A).
// Persistence is localStorage-only for MVP. No Supabase changes.
//
// Storage keys:
//   vyb-unlocked-artifacts   → string[] of artifact ids ever unlocked
//   vyb-active-artifact      → string | null (id of the one shown as active)
//   vyb-path-stage-progress  → reserved for future per-user stage state.
//
// Tasks are pure predicates over a `ctx` derived from momentum + store.
// If a data field is not yet reliable (e.g. confirmed page deltas), we
// fall back safely to the closest signal and add a TODO.

const ARTIFACTS = {
  first_signal:    { id:'first_signal',    title:'First Signal',    description:'Unlocked by completing your first System Builder stage.', rarity:'Signal',        sourcePath:'system_builder', iconType:'ring' },
  rhythm_builder:  { id:'rhythm_builder',  title:'Rhythm Builder',  description:'Unlocked by completing 3 planned habit days.',           rarity:'Rare Signal',   sourcePath:'system_builder', iconType:'pulse' },
  identity_ring:   { id:'identity_ring',   title:'Identity Ring',   description:'Unlocked by completing 7 planned habit days.',           rarity:'Prime Signal',  sourcePath:'system_builder', iconType:'ring-double' },
  reader_spark:    { id:'reader_spark',    title:'Reader Spark',    description:'Unlocked by starting your reading system.',              rarity:'Signal',        sourcePath:'reader_path',    iconType:'spark' },
  reader_flame:    { id:'reader_flame',    title:'Reader Flame',    description:'Unlocked by building reading momentum.',                 rarity:'Rare Signal',   sourcePath:'reader_path',    iconType:'flame' },
  idea_spark:      { id:'idea_spark',      title:'Idea Spark',      description:'Unlocked by capturing your first idea signals.',         rarity:'Signal',        sourcePath:'creator_path',   iconType:'spark' },
  idea_vault:      { id:'idea_vault',      title:'Idea Vault',      description:'Unlocked by building your idea bank.',                   rarity:'Rare Signal',   sourcePath:'creator_path',   iconType:'vault' },
};

const RARITY_STYLE = {
  'Signal':         { color:'#A08A56', glow:'rgba(160,138,86,0.18)', border:'rgba(160,138,86,0.45)' },
  'Rare Signal':    { color:'#C7AA6F', glow:'rgba(199,170,111,0.22)', border:'rgba(199,170,111,0.55)' },
  'Prime Signal':   { color:'#9CCBB1', glow:'rgba(156,203,177,0.22)', border:'rgba(156,203,177,0.55)' },
  'Legacy Signal':  { color:'#B6A2D6', glow:'rgba(182,162,214,0.22)', border:'rgba(182,162,214,0.55)' },
  'Mythic Signal':  { color:'#D7C7AC', glow:'rgba(215,199,172,0.30)', border:'rgba(215,199,172,0.75)' },
};

// Stage tasks are { label, check(ctx) → boolean, progress(ctx) → optional "x/y" }
// The first 3 stages of each path use real data. Stages 4–8 are locked.
const PATHS = [
  {
    id:'system_builder', title:'System Builder', description:'Build your habit foundation.',
    stages: [
      { id:'sb_1', title:'First Signal', reward:'first_signal', tasks:[
        { label:'Create 3 habits',          check:c => c.totalHabitsCreated >= 3,         progress:c => `${Math.min(c.totalHabitsCreated,3)}/3` },
        { label:'Complete today\u2019s system', check:c => c.todaySystemComplete === true },
        { label:'Capture 1 idea',           check:c => c.totalIdeas >= 1 },
      ] },
      { id:'sb_2', title:'Rhythm Begins', reward:'rhythm_builder', tasks:[
        { label:'Complete 3 planned habit days', check:c => c.totalPlannedHabitDaysCompleted >= 3, progress:c => `${Math.min(c.totalPlannedHabitDaysCompleted,3)}/3` },
        { label:'Complete 5 tasks',              check:c => c.totalCompletedTasks >= 5,            progress:c => `${Math.min(c.totalCompletedTasks,5)}/5` },
        { label:'Add your first book',           check:c => c.totalBooks >= 1 },
      ] },
      { id:'sb_3', title:'Identity in Motion', reward:'identity_ring', tasks:[
        { label:'Complete 7 planned habit days', check:c => c.totalPlannedHabitDaysCompleted >= 7, progress:c => `${Math.min(c.totalPlannedHabitDaysCompleted,7)}/7` },
        { label:'Capture 10 ideas',              check:c => c.totalIdeas >= 10,                    progress:c => `${Math.min(c.totalIdeas,10)}/10` },
        // TODO: switch to confirmed pages-read once "Done for Today" captures page deltas.
        { label:'Complete 3 reading sessions',   check:c => c.totalReadingSessions >= 3,           progress:c => `${Math.min(c.totalReadingSessions,3)}/3` },
      ] },
      { id:'sb_4', title:'Coming soon', locked:true },
      { id:'sb_5', title:'Coming soon', locked:true },
      { id:'sb_6', title:'Coming soon', locked:true },
      { id:'sb_7', title:'Coming soon', locked:true },
      { id:'sb_8', title:'Coming soon', locked:true },
    ],
  },
  {
    id:'reader_path', title:'Reader Path', description:'Build a reading rhythm.',
    stages: [
      { id:'rp_1', title:'Open the Book', reward:'reader_spark', tasks:[
        { label:'Add your first book',                     check:c => c.totalBooks >= 1 },
        { label:'Log your first reading session',          check:c => c.totalReadingSessions >= 1 },
        { label:'Save one note, quote, or insight',        check:c => c.totalReadingEntries >= 1 },
      ] },
      { id:'rp_2', title:'Pages Moving', reward:'reader_flame', tasks:[
        // TODO: prefer confirmed pages-read total once tracked.
        { label:'Complete 3 reading sessions',             check:c => c.totalReadingSessions >= 3, progress:c => `${Math.min(c.totalReadingSessions,3)}/3` },
        { label:'Reach 25% progress in one book',          check:c => c.maxBookProgressPercent >= 25 },
        { label:'Save 3 reading entries',                  check:c => c.totalReadingEntries >= 3,  progress:c => `${Math.min(c.totalReadingEntries,3)}/3` },
      ] },
      { id:'rp_3', title:'Coming soon', locked:true },
      { id:'rp_4', title:'Coming soon', locked:true },
      { id:'rp_5', title:'Coming soon', locked:true },
      { id:'rp_6', title:'Coming soon', locked:true },
      { id:'rp_7', title:'Coming soon', locked:true },
      { id:'rp_8', title:'Coming soon', locked:true },
    ],
  },
  {
    id:'creator_path', title:'Creator Path', description:'Capture and develop ideas.',
    stages: [
      { id:'cp_1', title:'Idea Spark', reward:'idea_spark', tasks:[
        { label:'Capture 5 ideas',                        check:c => c.totalIdeas >= 5,                  progress:c => `${Math.min(c.totalIdeas,5)}/5` },
        { label:'Create ideas in 2 different categories', check:c => Object.keys(c.ideasByCategory).length >= 2 },
        { label:'Edit or archive 1 idea',                 check:c => c.archivedOrEditedIdeas >= 1 },
      ] },
      { id:'cp_2', title:'Idea Bank', reward:'idea_vault', tasks:[
        { label:'Capture 15 ideas',          check:c => c.totalIdeas >= 15,              progress:c => `${Math.min(c.totalIdeas,15)}/15` },
        { label:'Capture 3 business ideas',  check:c => (c.ideasByCategory.Business || 0) >= 3, progress:c => `${Math.min(c.ideasByCategory.Business || 0,3)}/3` },
        { label:'Capture 3 content ideas',   check:c => (c.ideasByCategory.Content  || 0) >= 3, progress:c => `${Math.min(c.ideasByCategory.Content  || 0,3)}/3` },
      ] },
      { id:'cp_3', title:'Coming soon', locked:true },
      { id:'cp_4', title:'Coming soon', locked:true },
      { id:'cp_5', title:'Coming soon', locked:true },
      { id:'cp_6', title:'Coming soon', locked:true },
      { id:'cp_7', title:'Coming soon', locked:true },
      { id:'cp_8', title:'Coming soon', locked:true },
    ],
  },
];

// Build the read-only context that path predicates run against.
const buildPathsContext = (m, state) => {
  const habits = state.habits || [];
  const tasks  = state.tasks  || [];
  const ideas  = (state.ideas || []).filter(i => i && !i.archived);
  const allIdeas = state.ideas || [];
  const books  = state.books  || [];
  const entries = books.flatMap(b => Array.isArray(b && b.entries) ? b.entries : []);
  const ideasByCategory = ideas.reduce((acc, i) => {
    const k = i.tag || 'Other';
    acc[k] = (acc[k] || 0) + 1;
    return acc;
  }, {});
  const maxBookProgressPercent = books.reduce((mx, b) => {
    if (!b || !b.totalPages) return mx;
    const p = Math.round(((b.currentPage || 0) / b.totalPages) * 100);
    return Math.max(mx, p);
  }, 0);
  // "edited or archived" — archived flag is reliable; we don't track edits per
  // se, so any archived idea satisfies it. Counts archived ideas only.
  const archivedOrEditedIdeas = allIdeas.filter(i => i && i.archived).length;
  return {
    totalHabitsCreated: habits.length,
    todaySystemComplete: !!(m && m.today && m.today.isDailyHabitsComplete),
    totalPlannedHabitDaysCompleted: (m && m.totals && m.totals.fullHabitDays) || 0,
    totalCompletedTasks: tasks.filter(t => t && t.done).length,
    totalIdeas: ideas.length,
    ideasByCategory,
    archivedOrEditedIdeas,
    totalBooks: books.length,
    totalReadingSessions: entries.length,
    totalReadingEntries: entries.length,
    maxBookProgressPercent,
    // TODO: pagesRead from confirmed page deltas on Done for Today.
  };
};

// Compute per-path active stage + items + completeness from data.
// Pure — no localStorage reads. Returns a list of unlocked artifact ids
// (qualified by data, regardless of stickiness from prior sessions).
const computePathsState = (m, state) => {
  const ctx = buildPathsContext(m, state);
  const qualified = new Set(); // artifact ids the user has earned by data
  const out = PATHS.map(p => {
    const stages = p.stages.map((s) => {
      if (s.locked) return { ...s, items:[], complete:false, locked:true };
      const items = (s.tasks || []).map(t => ({
        label: t.label,
        done: !!t.check(ctx),
        progress: t.progress ? t.progress(ctx) : null,
      }));
      const total = items.length;
      const done = items.filter(i => i.done).length;
      const complete = total > 0 && done === total;
      if (complete && s.reward) qualified.add(s.reward);
      return { ...s, items, done, total, complete };
    });
    // Active stage = first non-locked, non-complete stage; or last unlocked.
    let activeIdx = stages.findIndex(s => !s.locked && !s.complete);
    if (activeIdx === -1) {
      // All non-locked stages are complete → active = last completed unlocked
      const lastUnlocked = [...stages].reverse().find(s => !s.locked);
      activeIdx = stages.indexOf(lastUnlocked);
    }
    const totalStages = stages.length;
    return { ...p, stages, activeIdx, totalStages };
  });
  return { paths: out, qualifiedArtifacts: qualified, ctx };
};

// localStorage helpers — backward compatible.
// New keys: vyb-unlocked-aura / vyb-active-aura.
// Old keys: vyb-unlocked-artifacts / vyb-active-artifact (migrated on first read).
// We write to BOTH so older code paths keep working until fully renamed.
const LS_NEW_UNLOCKED = 'vyb-unlocked-aura';
const LS_OLD_UNLOCKED = 'vyb-unlocked-artifacts';
const LS_NEW_ACTIVE   = 'vyb-active-aura';
const LS_OLD_ACTIVE   = 'vyb-active-artifact';
const readUnlockedArtifacts = () => {
  try {
    let raw = localStorage.getItem(LS_NEW_UNLOCKED);
    if (!raw) {
      raw = localStorage.getItem(LS_OLD_UNLOCKED);
      if (raw) localStorage.setItem(LS_NEW_UNLOCKED, raw); // migrate
    }
    if (!raw) return new Set();
    const arr = JSON.parse(raw);
    return new Set(Array.isArray(arr) ? arr : []);
  } catch { return new Set(); }
};
const writeUnlockedArtifacts = (set) => {
  const json = JSON.stringify(Array.from(set));
  try { localStorage.setItem(LS_NEW_UNLOCKED, json); } catch {}
  try { localStorage.setItem(LS_OLD_UNLOCKED, json); } catch {} // back-compat
};
const readActiveArtifact = () => {
  try {
    let id = localStorage.getItem(LS_NEW_ACTIVE);
    if (!id) {
      id = localStorage.getItem(LS_OLD_ACTIVE);
      if (id) localStorage.setItem(LS_NEW_ACTIVE, id); // migrate
    }
    return id || null;
  } catch { return null; }
};
const writeActiveArtifact = (id) => {
  try {
    if (id) {
      localStorage.setItem(LS_NEW_ACTIVE, id);
      localStorage.setItem(LS_OLD_ACTIVE, id); // back-compat
    } else {
      localStorage.removeItem(LS_NEW_ACTIVE);
      localStorage.removeItem(LS_OLD_ACTIVE);
    }
  } catch {}
};

// Subtle premium icon for an artifact. Uses a small SVG sigil family driven
// by the artifact's iconType. Color comes from rarity. No emojis.
const ArtifactSigil = ({ artifact, size=42, dim=false, glow=false }) => {
  const r = RARITY_STYLE[artifact.rarity] || RARITY_STYLE.Signal;
  const stroke = dim ? 'rgba(255,255,255,0.18)' : r.color;
  const fill = dim ? 'transparent' : r.glow;
  const s = size;
  // Per iconType: a different glyph family.
  const glyph = (() => {
    switch (artifact.iconType) {
      case 'ring-double':
        return <>
          <circle cx={s/2} cy={s/2} r={s*0.36} fill="none" stroke={stroke} strokeWidth="1.4"/>
          <circle cx={s/2} cy={s/2} r={s*0.22} fill="none" stroke={stroke} strokeWidth="1.4"/>
        </>;
      case 'pulse':
        return <>
          <circle cx={s/2} cy={s/2} r={s*0.36} fill="none" stroke={stroke} strokeWidth="1.4"/>
          <path d={`M ${s*0.22} ${s/2} L ${s*0.38} ${s/2} L ${s*0.46} ${s*0.34} L ${s*0.54} ${s*0.66} L ${s*0.62} ${s/2} L ${s*0.78} ${s/2}`}
            fill="none" stroke={stroke} strokeWidth="1.4" strokeLinejoin="round" strokeLinecap="round"/>
        </>;
      case 'spark':
        return <>
          <circle cx={s/2} cy={s/2} r={s*0.36} fill="none" stroke={stroke} strokeWidth="1.4"/>
          <path d={`M ${s/2} ${s*0.28} L ${s*0.56} ${s*0.46} L ${s*0.72} ${s/2} L ${s*0.56} ${s*0.54} L ${s/2} ${s*0.72} L ${s*0.44} ${s*0.54} L ${s*0.28} ${s/2} L ${s*0.44} ${s*0.46} Z`}
            fill={fill} stroke={stroke} strokeWidth="1"/>
        </>;
      case 'flame':
        return <>
          <circle cx={s/2} cy={s/2} r={s*0.36} fill="none" stroke={stroke} strokeWidth="1.4"/>
          <path d={`M ${s/2} ${s*0.30} C ${s*0.62} ${s*0.42}, ${s*0.66} ${s*0.55}, ${s/2} ${s*0.72}
            C ${s*0.34} ${s*0.55}, ${s*0.38} ${s*0.42}, ${s/2} ${s*0.30} Z`}
            fill={fill} stroke={stroke} strokeWidth="1.2"/>
        </>;
      case 'vault':
        return <>
          <rect x={s*0.24} y={s*0.26} width={s*0.52} height={s*0.48} rx="3" fill="none" stroke={stroke} strokeWidth="1.4"/>
          <circle cx={s/2} cy={s/2} r={s*0.10} fill="none" stroke={stroke} strokeWidth="1.4"/>
          <line x1={s/2} y1={s*0.50} x2={s/2} y2={s*0.66} stroke={stroke} strokeWidth="1.4"/>
        </>;
      case 'ring':
      default:
        return <>
          <circle cx={s/2} cy={s/2} r={s*0.36} fill="none" stroke={stroke} strokeWidth="1.4"/>
          <circle cx={s/2} cy={s/2} r={s*0.04} fill={stroke}/>
        </>;
    }
  })();
  return (
    <div style={{
      width:s, height:s, borderRadius:s/2,
      display:'inline-flex', alignItems:'center', justifyContent:'center',
      background: dim ? 'rgba(255,255,255,0.02)' : `radial-gradient(circle at 50% 50%, ${r.glow}, transparent 70%)`,
      boxShadow: glow && !dim ? `0 0 18px ${r.glow}` : 'none',
      border: `1px solid ${dim ? C.border : r.border}`,
      flexShrink: 0,
    }}>
      <svg width={s} height={s} viewBox={`0 0 ${s} ${s}`} style={{display:'block'}}>{glyph}</svg>
    </div>
  );
};

// Display rarity using the Aura naming. Internal keys stay "Rare Signal" etc
// to avoid breaking existing data; only the visible label changes.
const RARITY_DISPLAY = {
  'Signal':         'Signal Aura',
  'Rare Signal':    'Rare Aura',
  'Prime Signal':   'Prime Aura',
  'Legacy Signal':  'Legacy Aura',
  'Mythic Signal':  'Mythic Aura',
};
const displayRarity = (r) => RARITY_DISPLAY[r] || (r ? `${r} Aura` : '');

const RarityPill = ({ rarity, dim }) => {
  const r = RARITY_STYLE[rarity] || RARITY_STYLE.Signal;
  return (
    <span style={{
      padding:'2px 8px', borderRadius:999,
      border:`1px solid ${dim ? C.border : r.border}`,
      background: dim ? 'transparent' : `linear-gradient(135deg, ${r.glow}, transparent)`,
      color: dim ? C.textFaint : r.color,
      fontFamily:'Montserrat, sans-serif', fontSize:9, fontWeight:700,
      letterSpacing:'0.18em', textTransform:'uppercase', whiteSpace:'nowrap',
    }}>{displayRarity(rarity)}</span>
  );
};

// Subtle premium toast for newly unlocked artifacts. Stacks vertically.
// Auto-dismisses after a short hold; click to dismiss early.
const ArtifactToastStack = ({ items, onDismiss }) => {
  if (!items || !items.length) return null;
  return (
    <div style={{
      position:'fixed', right:20, bottom:24, zIndex:9999,
      display:'flex', flexDirection:'column', gap:10, pointerEvents:'none',
    }}>
      {items.map(it => {
        const a = ARTIFACTS[it.id];
        if (!a) return null;
        const r = RARITY_STYLE[a.rarity] || RARITY_STYLE.Signal;
        return (
          <div key={it.key} onClick={() => onDismiss(it.key)} style={{
            pointerEvents:'auto', cursor:'pointer',
            minWidth:240, maxWidth:320,
            padding:'12px 14px', borderRadius:14,
            background:'linear-gradient(135deg, rgba(20,18,16,0.96), rgba(20,18,16,0.92))',
            border:`1px solid ${r.border}`,
            boxShadow:`0 0 0 1px ${r.glow}, 0 18px 40px rgba(0,0,0,0.45), 0 0 24px ${r.glow}`,
            display:'flex', alignItems:'center', gap:12,
            animation:'vyb-toast-in 0.45s cubic-bezier(0.16,1,0.3,1) both',
          }}>
            <ArtifactSigil artifact={a} size={40} glow/>
            <div style={{minWidth:0}}>
              <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',color:r.color,textTransform:'uppercase'}}>
                Aura unlocked
              </div>
              <div style={{fontSize:14,fontWeight:500,color:C.textPrimary,letterSpacing:'-0.005em'}}>
                {a.title}
              </div>
              <div style={{fontSize:10,color:C.textMuted,fontStyle:'italic'}}>{displayRarity(a.rarity)}</div>
            </div>
          </div>
        );
      })}
      {/* keyframes injected once */}
      <style>{`@keyframes vyb-toast-in {
        from { opacity:0; transform: translateY(8px) scale(0.98); }
        to   { opacity:1; transform: translateY(0)   scale(1); }
      }`}</style>
    </div>
  );
};

// Active path card — picks the path with the most-advanced active stage.
const ActivePathCard = ({ paths }) => {
  // Heuristic: most completed-stage-count, then most-progressed active stage.
  const ranked = [...paths].sort((a, b) => {
    const aDone = a.stages.filter(s => s.complete).length;
    const bDone = b.stages.filter(s => s.complete).length;
    if (bDone !== aDone) return bDone - aDone;
    const aPct = a.stages[a.activeIdx] ? (a.stages[a.activeIdx].done / Math.max(1, a.stages[a.activeIdx].total)) : 0;
    const bPct = b.stages[b.activeIdx] ? (b.stages[b.activeIdx].done / Math.max(1, b.stages[b.activeIdx].total)) : 0;
    return bPct - aPct;
  });
  const p = ranked[0];
  if (!p) return null;
  const s = p.stages[p.activeIdx];
  const reward = s && s.reward ? ARTIFACTS[s.reward] : null;
  return (
    <Card style={{padding:18}}>
      <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',color:C.textFaint,textTransform:'uppercase',marginBottom:6}}>
        Active path
      </div>
      <div style={{fontSize:16,fontWeight:500,color:C.textPrimary,letterSpacing:'-0.005em'}}>{p.title}</div>
      <div style={{fontSize:11,color:C.textMuted,fontStyle:'italic',marginTop:2}}>{p.description}</div>
      {s && (
        <div style={{marginTop:12,display:'flex',alignItems:'center',gap:12}}>
          {reward && <ArtifactSigil artifact={reward} size={36} dim={!s.complete}/>}
          <div style={{flex:1,minWidth:0}}>
            <div style={{fontSize:11,color:C.textMuted}}>
              Stage {p.activeIdx + 1}/{p.totalStages} — {s.title}
            </div>
            <div style={{fontSize:11,color:C.textPrimary,marginTop:2}}>
              {s.locked ? 'Locked' : `${s.done || 0}/${s.total || 0} complete`}
            </div>
          </div>
        </div>
      )}
    </Card>
  );
};

// Active artifact card — show selected artifact, or the empty state.
const ActiveArtifactCard = ({ activeId, unlocked, onClear }) => {
  const a = activeId ? ARTIFACTS[activeId] : null;
  const isUnlocked = a && unlocked.has(a.id);
  return (
    <Card style={{padding:18}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',gap:8,marginBottom:6}}>
        <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',color:C.textFaint,textTransform:'uppercase'}}>
          Active Aura
        </div>
        {a && isUnlocked && <ActionLink label="Clear" onClick={onClear}/>}
      </div>
      <div style={{fontSize:11,color:C.textMuted,fontStyle:'italic',marginBottom:10}}>
        Choose the aura you want to show.
      </div>
      {!a || !isUnlocked ? (
        <div style={{fontSize:12,color:C.textFaint,fontStyle:'italic'}}>
          No active aura yet. Complete a stage to unlock your first aura signal.
        </div>
      ) : (
        <div style={{display:'flex',alignItems:'center',gap:12}}>
          <ArtifactSigil artifact={a} size={48} glow/>
          <div style={{minWidth:0,flex:1}}>
            <div style={{display:'flex',alignItems:'center',gap:8,flexWrap:'wrap'}}>
              <div style={{fontSize:14,fontWeight:500,color:C.textPrimary}}>{a.title}</div>
              <RarityPill rarity={a.rarity}/>
              <span style={{
                fontSize:9,fontWeight:700,letterSpacing:'0.18em',textTransform:'uppercase',
                color:'#9CCBB1', padding:'2px 8px', borderRadius:999,
                border:'1px solid rgba(156,203,177,0.45)', background:'rgba(94,117,88,0.10)',
              }}>Active</span>
            </div>
            <div style={{fontSize:11,color:C.textMuted,marginTop:4}}>{a.description}</div>
          </div>
        </div>
      )}
    </Card>
  );
};

// Stage task list — checked / unchecked rows.
const StageTaskList = ({ items }) => (
  <div style={{display:'flex',flexDirection:'column',gap:6,marginTop:10}}>
    {items.map((it, i) => (
      <div key={i} style={{display:'flex',alignItems:'center',gap:10,fontSize:12.5,color: it.done ? C.textPrimary : C.textMuted}}>
        {it.done ? (
          <Icon name="check" size={13} color={C.sage} sw={2.5}/>
        ) : (
          <span style={{width:13,height:13,borderRadius:999,border:`1px solid ${C.borderMid}`,display:'inline-block',flexShrink:0}}/>
        )}
        <span style={{flex:1,minWidth:0,textDecoration: it.done ? 'none' : 'none'}}>{it.label}</span>
        {it.progress && (
          <span style={{fontSize:10.5,color:C.textFaint,fontVariantNumeric:'tabular-nums'}}>{it.progress}</span>
        )}
      </div>
    ))}
  </div>
);

// Single Path card — title, description, active stage, tasks, reward preview.
const PathCard = ({ path }) => {
  const s = path.stages[path.activeIdx];
  if (!s) return null;
  const reward = s.reward ? ARTIFACTS[s.reward] : null;
  const pct = s.total ? Math.round((s.done / s.total) * 100) : 0;
  return (
    <Card style={{padding:18,display:'flex',flexDirection:'column',gap:4}}>
      <div style={{fontSize:14,fontWeight:500,color:C.textPrimary,letterSpacing:'-0.005em'}}>{path.title}</div>
      <div style={{fontSize:11,color:C.textMuted,fontStyle:'italic'}}>{path.description}</div>
      <div style={{marginTop:10,display:'flex',justifyContent:'space-between',alignItems:'baseline',gap:8}}>
        <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.18em',color:C.textFaint,textTransform:'uppercase'}}>
          Stage {path.activeIdx + 1}/{path.totalStages} — {s.title}
        </div>
        <div style={{fontSize:11,color:C.textMuted,fontVariantNumeric:'tabular-nums'}}>
          {s.locked ? 'Locked' : `${s.done}/${s.total} complete`}
        </div>
      </div>
      {!s.locked && (
        <div style={{height:3,borderRadius:999,background:'rgba(255,255,255,0.05)',overflow:'hidden',marginTop:6}}>
          <div style={{width:`${pct}%`,height:'100%',background: s.complete ? 'var(--c-sage)' : 'var(--c-sand)',borderRadius:999,transition:'width 0.5s ease'}}/>
        </div>
      )}
      {!s.locked && <StageTaskList items={s.items}/>}
      {reward && (
        <div style={{marginTop:12,paddingTop:12,borderTop:`1px solid ${C.border}`,
          display:'flex',alignItems:'center',gap:10}}>
          <ArtifactSigil artifact={reward} size={28} dim={!s.complete}/>
          <div style={{flex:1,minWidth:0}}>
            <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',color:C.textFaint,textTransform:'uppercase'}}>
              Reward
            </div>
            <div style={{fontSize:12.5,fontWeight:500,color: s.complete ? C.textPrimary : C.textMuted}}>
              {reward.title}
            </div>
          </div>
          <RarityPill rarity={reward.rarity} dim={!s.complete}/>
        </div>
      )}
    </Card>
  );
};

// Compact artifact tile used in the artifacts grid.
const ArtifactTile = ({ artifact, unlocked, isActive, onSetActive }) => {
  const r = RARITY_STYLE[artifact.rarity] || RARITY_STYLE.Signal;
  return (
    <div style={{
      padding:14, borderRadius:14,
      border:`1px solid ${unlocked ? r.border : C.border}`,
      background: unlocked ? `linear-gradient(135deg, ${r.glow}, transparent 60%)` : 'rgba(255,255,255,0.015)',
      display:'flex', alignItems:'center', gap:12,
      opacity: unlocked ? 1 : 0.55,
      boxShadow: isActive ? `0 0 0 1px ${r.border}, 0 0 18px ${r.glow}` : 'none',
    }}>
      <ArtifactSigil artifact={artifact} size={40} dim={!unlocked} glow={isActive}/>
      <div style={{flex:1,minWidth:0}}>
        <div style={{display:'flex',alignItems:'center',gap:6,flexWrap:'wrap'}}>
          <div style={{fontSize:12.5,fontWeight:500,color:unlocked?C.textPrimary:C.textMuted,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>
            {artifact.title}
          </div>
          <RarityPill rarity={artifact.rarity} dim={!unlocked}/>
        </div>
        <div style={{fontSize:10.5,color:C.textMuted,marginTop:3,lineHeight:1.4}}>
          {unlocked ? artifact.description : 'Locked.'}
        </div>
      </div>
      {unlocked && (
        isActive ? (
          <span style={{
            fontSize:9,fontWeight:700,letterSpacing:'0.18em',textTransform:'uppercase',
            color:'#9CCBB1', padding:'4px 10px', borderRadius:999,
            border:'1px solid rgba(156,203,177,0.45)', background:'rgba(94,117,88,0.10)',
            whiteSpace:'nowrap',
          }}>Active</span>
        ) : (
          <button onClick={onSetActive} style={{
            fontSize:9,fontWeight:700,letterSpacing:'0.18em',textTransform:'uppercase',
            color:C.sandLight, padding:'4px 10px', borderRadius:999,
            border:`1px solid ${r.border}`, background:'transparent',
            cursor:'pointer', whiteSpace:'nowrap',
          }}>Set as Active Aura</button>
        )
      )}
    </div>
  );
};

// Top-level Paths & Artifacts section. Owns localStorage state, unlock
// detection, and the toast queue.
const PathsAndArtifactsSection = ({ momentum, store }) => {
  const state = computePathsState(momentum, store.state);
  const [unlocked, setUnlocked] = React.useState(() => readUnlockedArtifacts());
  const [activeId, setActiveId] = React.useState(() => readActiveArtifact());
  const [toasts, setToasts] = React.useState([]);
  const [showAll, setShowAll] = React.useState(false);
  // Track first-mount so we don't toast every previously-unlocked artifact
  // when the page reloads. Only NEW ids in the current session toast.
  const firstMountRef = React.useRef(true);

  // Reconcile: any newly-qualified artifacts get added to unlocked set.
  React.useEffect(() => {
    const qualified = state.qualifiedArtifacts;
    const next = new Set(unlocked);
    const newlyAdded = [];
    qualified.forEach(id => {
      if (!next.has(id)) {
        next.add(id);
        newlyAdded.push(id);
      }
    });
    if (newlyAdded.length === 0) return;
    setUnlocked(next);
    writeUnlockedArtifacts(next);
    // Suppress toasts on first mount for ids already met by data — those
    // are seeded silently. Any id newly satisfied during this session toasts.
    if (firstMountRef.current) {
      firstMountRef.current = false;
      return;
    }
    setToasts(t => [...t, ...newlyAdded.map(id => ({ id, key: `${id}-${Date.now()}-${Math.random().toString(36).slice(2,6)}` }))]);
  }, [state.qualifiedArtifacts.size, ...Array.from(state.qualifiedArtifacts)]);

  // After mount, flip the firstMountRef even if no new ids appeared so any
  // future qualifier in this session does toast.
  React.useEffect(() => { firstMountRef.current = false; }, []);

  // Auto-dismiss toasts after a short hold.
  React.useEffect(() => {
    if (!toasts.length) return;
    const timers = toasts.map(t => setTimeout(() => {
      setToasts(arr => arr.filter(x => x.key !== t.key));
    }, 4200));
    return () => timers.forEach(clearTimeout);
  }, [toasts.length]);

  const dismissToast = (key) => setToasts(arr => arr.filter(x => x.key !== key));
  const setActive = (id) => {
    if (!unlocked.has(id)) return;
    setActiveId(id);
    writeActiveArtifact(id);
  };
  const clearActive = () => { setActiveId(null); writeActiveArtifact(null); };

  // Auto-promote: if there's no active artifact yet but unlocked has one,
  // do nothing — user opts in. Spec says user chooses.

  const ALL = Object.values(ARTIFACTS);
  const visibleArtifacts = showAll ? ALL : ALL.slice(0, 4);

  return (
    <>
      {/* TODO: Aura Pass — future staged objective system. Do NOT rename
          internal Paths to Aura Pass yet; keep Paths cards as-is for now. */}
      <MomentumSectionHeader label="Aura" title="Aura"
        hint="Build the system. Farm the aura."/>

      {/* Top row: Active Path + Active Artifact */}
      <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit, minmax(260px, 1fr))',gap:16}}>
        <ActivePathCard paths={state.paths}/>
        <ActiveArtifactCard activeId={activeId} unlocked={unlocked} onClear={clearActive}/>
      </div>

      {/* Path cards row */}
      <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit, minmax(280px, 1fr))',gap:16}}>
        {state.paths.map(p => <PathCard key={p.id} path={p}/>)}
      </div>

      {/* Artifacts grid */}
      <div>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',gap:8,marginBottom:10}}>
          <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',color:C.textFaint,textTransform:'uppercase'}}>
            Aura Collection
          </div>
          {ALL.length > 4 && (
            <ActionLink label={showAll ? 'Hide' : 'View all aura'} onClick={() => setShowAll(v => !v)}/>
          )}
        </div>
        <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit, minmax(260px, 1fr))',gap:12}}>
          {visibleArtifacts.map(a => (
            <ArtifactTile key={a.id} artifact={a}
              unlocked={unlocked.has(a.id)}
              isActive={activeId === a.id}
              onSetActive={() => setActive(a.id)}/>
          ))}
        </div>
      </div>

      <ArtifactToastStack items={toasts} onDismiss={dismissToast}/>
    </>
  );
};

// ─── Analytics Phase 1 chart components ──────────────────────────
// Inline SVG only (no chart library). Each chart is self-contained,
// uses viewBox + preserveAspectRatio so it scales fluidly inside any
// grid cell. Empty / not-enough-data states render a calm placeholder
// rather than a broken axis.

// Compact stat card used in the "Today" overview row.
// Shows a primary "x/y" or "n" value plus a small sub-line. If extras > 0
// renders "+N extra" so the denominator stays honest.
const TodayStatCard = ({ icon, label, primary, sub, extras, accent=C.sand, onClick, dim }) => (
  <div onClick={onClick} style={{
    flex:'1 1 140px', minWidth:120, padding:'14px 16px', borderRadius:14,
    border:`1px solid ${C.border}`, background:'rgba(255,255,255,0.015)',
    display:'flex', flexDirection:'column', gap:6, cursor: onClick?'pointer':'default',
    opacity: dim ? 0.55 : 1,
  }}>
    <div style={{display:'flex',alignItems:'center',gap:7}}>
      <div style={{width:22,height:22,borderRadius:6,background:'rgba(255,255,255,0.03)',
        display:'flex',alignItems:'center',justifyContent:'center'}}>
        <Icon name={icon} size={12} color={accent} sw={2}/>
      </div>
      <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',color:C.textFaint,textTransform:'uppercase'}}>{label}</div>
    </div>
    <div style={{fontSize:22,fontWeight:300,color:C.textPrimary,letterSpacing:'-0.01em',lineHeight:1.05}}>
      {primary}
    </div>
    <div style={{fontSize:10.5,fontWeight:300,color:C.textMuted,letterSpacing:'0.02em',minHeight:14}}>
      {sub || ''}
      {extras > 0 && (
        <span style={{marginLeft:6,color:C.sandLight}}>+{extras} {(window.t && window.t('momentum.extra')) || 'extra'}</span>
      )}
    </div>
  </div>
);

const TodayOverview = ({ today, counts, weekTotals, goTo }) => {
  const tr = useT();
  // Habits primary "done/scheduled". Extras = habits checked today that
  // weren't scheduled today (catch-up). We don't currently track that here
  // so it stays 0 until calculateMomentum exposes it.
  const sched = today.scheduledHabitsToday || 0;
  const habitsDone = today.completedScheduledHabitsToday || 0;
  const habitsExtra = 0;
  return (
    <div>
      <MomentumSectionHeader label={tr('momentum.todayOverview')} title={tr('momentum.todayOverview')}
        hint={tr('momentum.todayHelp')}/>
      {/* Recap cards are informational only — clear CTA lives in Next Move. */}
      <div style={{display:'flex',flexWrap:'wrap',gap:10}}>
        <TodayStatCard icon="check-circle" label={tr('momentum.statHabits')}
          primary={sched ? `${habitsDone}/${sched}` : '—'}
          sub={sched ? `${today.scheduledHabitsCompletionPercent}%` : tr('momentum.noHabitsToday')}
          extras={habitsExtra} dim={!sched}/>
        <TodayStatCard icon="list-todo" label={tr('momentum.statTasks')}
          primary={String(today.completedTasks || 0)}
          sub={today.activeTasks ? `${today.activeTasks} ${(window.getLang && window.getLang()==='es') ? 'activas' : 'active'}` : ''}
          accent={C.sage}/>
        <TodayStatCard icon="lightbulb" label={tr('momentum.statIdeas')}
          primary={String(today.ideasCaptured || 0)}
          sub={today.totalIdeas ? `${today.totalIdeas} ${(window.getLang && window.getLang()==='es') ? 'en bóveda' : 'in vault'}` : ''}/>
        <TodayStatCard icon="book-open" label={tr('momentum.statReading')}
          primary={String(today.readingSessions || 0)}
          sub={today.currentReadingProgress ? `${today.currentReadingProgress}%` : ''}
          accent={C.sage}/>
        {/* Hide focus card if no data and no historical sessions */}
        {(today.focusSessions > 0 || (weekTotals && weekTotals.focusSessions > 0)) && (
          <TodayStatCard icon="timer" label={tr('momentum.statFocus')}
            primary={String(today.focusSessions || 0)}
            sub={(weekTotals && weekTotals.focusSessions) ? `${weekTotals.focusSessions} ${(window.getLang && window.getLang()==='es') ? 'en la semana' : 'this week'}` : ''}/>
        )}
      </div>
    </div>
  );
};

// Momentum trend line chart — interactive metric switcher.
// Accepts the rich `daily` array from calculateMomentum and renders one of
// 5 metrics. Percent metrics use a 0..100 y-axis; count metrics auto-scale.
const TREND_METRICS = [
  { id:'momentumScore',  label:'Momentum Score', kind:'percent', accessor:(d)=>d.momentumScore },
  { id:'habitCompletion',label:'Habit Completion', kind:'percent', accessor:(d)=>d.habitCompletionPercent },
  { id:'tasks',          label:'Tasks',          kind:'count',   accessor:(d)=>d.isFuture?null:d.tasksCompleted },
  { id:'ideas',          label:'Ideas',          kind:'count',   accessor:(d)=>d.isFuture?null:d.ideasCaptured },
  { id:'reading',        label:'Reading',        kind:'count',   accessor:(d)=>d.isFuture?null:d.readingSessions },
];

const MomentumTrendChart = ({ daily }) => {
  const [metricId, setMetricId] = React.useState('momentumScore');
  const metric = TREND_METRICS.find(m => m.id === metricId) || TREND_METRICS[0];
  const data = Array.isArray(daily) ? daily : [];
  const W = 520, H = 200, PAD_L = 32, PAD_R = 14, PAD_T = 14, PAD_B = 28;
  const innerW = W - PAD_L - PAD_R;
  const innerH = H - PAD_T - PAD_B;
  const n = data.length || 7;
  const xFor = (i) => PAD_L + (innerW * (i / Math.max(1, n - 1)));
  // Compute y-domain. Percent metrics fixed at 0..100; counts auto-scale.
  const rawValues = data.map(d => metric.accessor(d)).filter(v => v != null && Number.isFinite(v));
  const isPercent = metric.kind === 'percent';
  const yMax = isPercent ? 100 : Math.max(1, ...rawValues, 0);
  const yFor = (v) => PAD_T + innerH * (1 - (v / yMax));
  const pts = data.map((d, i) => {
    const v = metric.accessor(d);
    return { ...d, value: v, x: xFor(i), y: v == null ? null : yFor(v) };
  });
  const known = pts.filter(p => p.y != null);
  const pathFor = (arr) => arr.map((p, i) => `${i===0?'M':'L'} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(' ');
  const knownPath = known.length ? pathFor(known) : '';
  const hasAny = known.some(p => Number(p.value) > 0);

  // Y-axis tick values.
  const ticks = isPercent
    ? [0, 50, 100]
    : (yMax <= 1 ? [0, 1] : (yMax <= 4 ? [0, Math.ceil(yMax/2), yMax] : [0, Math.round(yMax/2), yMax]));

  return (
    <Card style={{padding:18}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',gap:8,marginBottom:10,flexWrap:'wrap'}}>
        <div>
          <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',color:C.textFaint,textTransform:'uppercase'}}>
            Momentum Trend
          </div>
          <div style={{fontSize:11,fontWeight:300,fontStyle:'italic',color:C.textMuted}}>
            Switch the metric to explore your week.
          </div>
        </div>
      </div>
      {/* Metric switcher */}
      <div style={{display:'flex',flexWrap:'wrap',gap:6,marginBottom:12}}>
        {TREND_METRICS.map(m => {
          const on = m.id === metricId;
          return (
            <button key={m.id} onClick={()=>setMetricId(m.id)}
              style={{padding:'6px 10px',borderRadius:999,
                border:`1px solid ${on?'rgba(160,138,86,0.55)':C.border}`,
                background:on?'rgba(160,138,86,0.12)':'transparent',
                color:on?C.sandLight:C.textMuted,fontSize:10,fontWeight:600,
                letterSpacing:'0.08em',textTransform:'uppercase',
                cursor:'pointer',fontFamily:'Inter, sans-serif'}}>
              {m.label}
            </button>
          );
        })}
      </div>
      {!data.length || !hasAny ? (
        <div style={{height:160,display:'flex',alignItems:'center',justifyContent:'center',
          fontSize:12,color:C.textFaint,fontStyle:'italic'}}>
          No data yet for {metric.label.toLowerCase()}.
        </div>
      ) : (
        <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" width="100%" height={210} style={{display:'block'}}>
          {ticks.map(g => (
            <line key={'g'+g} x1={PAD_L} x2={W-PAD_R} y1={yFor(g)} y2={yFor(g)}
              stroke="rgba(255,255,255,0.05)" strokeWidth="1"/>
          ))}
          {ticks.map(g => (
            <text key={'l'+g} x={PAD_L-6} y={yFor(g)+3} fontSize="9" fill={C.textFaint}
              textAnchor="end" fontFamily="Inter, sans-serif">{g}</text>
          ))}
          {pts.map((p,i) => (
            <text key={'x'+i} x={p.x} y={H-8} fontSize="9" fill={p.isToday?C.sandLight:C.textFaint}
              textAnchor="middle" fontFamily="Inter, sans-serif" fontWeight={p.isToday?700:400}>
              {p.label}
            </text>
          ))}
          {known.length > 1 && (
            <path d={knownPath} fill="none" stroke="var(--c-sand)" strokeWidth="1.8"
              strokeLinecap="round" strokeLinejoin="round"/>
          )}
          {known.map((p,i) => (
            <circle key={'p'+i} cx={p.x} cy={p.y} r={p.isToday?3.8:2.6}
              fill={p.isToday?'var(--c-sandLight)':'var(--c-sand)'}>
              <title>{`${p.label} · ${isPercent?p.value+'%':p.value}`}</title>
            </circle>
          ))}
        </svg>
      )}
    </Card>
  );
};

// Weekly activity bar chart — stacked counts of habits/tasks/ideas/reading.
// Categories use distinct (calm) hues. Bars are stacked per day.
const WeeklyActivityBars = ({ totals, days }) => {
  const tr = useT();
  const W = 520, H = 170, PAD_L = 22, PAD_R = 12, PAD_T = 14, PAD_B = 28;
  const innerW = W - PAD_L - PAD_R;
  const innerH = H - PAD_T - PAD_B;
  const labels = (days || []).map(d => d.label);
  const series = [
    { key:'habits',  color:'#A08A56', label:tr('momentum.habits') },
    { key:'tasks',   color:'#6FAE8A', label:tr('momentum.tasks') },
    { key:'ideas',   color:'#C9B79C', label:tr('momentum.ideas') },
    { key:'reading', color:'#6F8FAE', label:tr('momentum.reading') },
  ];
  const byDay = (totals && totals.byDay) || {};
  const dayTotals = labels.map((_, i) =>
    series.reduce((a,s) => a + ((byDay[s.key] && byDay[s.key][i]) || 0), 0));
  const max = Math.max(1, ...dayTotals);
  const n = labels.length || 7;
  const slot = innerW / n;
  const barW = Math.min(28, slot * 0.55);
  const xFor = (i) => PAD_L + slot * (i + 0.5) - barW / 2;
  const yFor = (v) => PAD_T + innerH * (1 - v / max);
  const hasAny = dayTotals.some(v => v > 0);
  return (
    <Card style={{padding:18}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',gap:8,marginBottom:6,flexWrap:'wrap'}}>
        <div>
          <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',color:C.textFaint,textTransform:'uppercase'}}>
            {tr('momentum.weeklyActivity')}
          </div>
          <div style={{fontSize:11,fontWeight:300,fontStyle:'italic',color:C.textMuted}}>
            {tr('momentum.weeklyActivityHelp')}
          </div>
        </div>
        <div style={{display:'flex',gap:10,flexWrap:'wrap'}}>
          {series.map(s => (
            <div key={s.key} style={{display:'flex',alignItems:'center',gap:5,fontSize:10,color:C.textMuted}}>
              <span style={{width:8,height:8,borderRadius:2,background:s.color,display:'inline-block'}}/>
              {s.label}
            </div>
          ))}
        </div>
      </div>
      {!hasAny ? (
        <div style={{height:130,display:'flex',alignItems:'center',justifyContent:'center',
          fontSize:12,color:C.textFaint,fontStyle:'italic'}}>
          {tr('momentum.noDataYet')}
        </div>
      ) : (
        <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" width="100%" height={180} style={{display:'block'}}>
          <line x1={PAD_L} x2={W-PAD_R} y1={H-PAD_B} y2={H-PAD_B} stroke="rgba(255,255,255,0.08)" strokeWidth="1"/>
          {labels.map((lab, i) => {
            let stackTop = H - PAD_B;
            return (
              <g key={i}>
                {series.map(s => {
                  const v = (byDay[s.key] && byDay[s.key][i]) || 0;
                  if (!v) return null;
                  const h = (innerH * v) / max;
                  const y = stackTop - h;
                  stackTop = y;
                  return <rect key={s.key} x={xFor(i)} y={y} width={barW} height={h}
                    fill={s.color} opacity="0.85" rx="1"/>;
                })}
                <text x={xFor(i)+barW/2} y={H-10} fontSize="9"
                  fill={(days && days[i] && days[i].isToday) ? C.sandLight : C.textFaint}
                  textAnchor="middle" fontFamily="Inter, sans-serif"
                  fontWeight={(days && days[i] && days[i].isToday) ? 700 : 400}>{lab}</text>
              </g>
            );
          })}
        </svg>
      )}
    </Card>
  );
};

// Area balance radar — one axis per area, value = weekly completion %.
// 3+ areas to render; otherwise show a friendly empty state.
const AreaRadarChart = ({ areas }) => {
  const tr = useT();
  const data = Array.isArray(areas) ? areas : [];
  if (data.length < 3) {
    return (
      <Card style={{padding:18}}>
        <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',color:C.textFaint,textTransform:'uppercase',marginBottom:6}}>
          {tr('momentum.areaBalanceTitle')}
        </div>
        <div style={{height:120,display:'flex',alignItems:'center',justifyContent:'center',
          fontSize:12,color:C.textFaint,fontStyle:'italic',textAlign:'center',padding:'0 16px'}}>
          {tr('momentum.areaNoData')}
        </div>
      </Card>
    );
  }
  const SIZE = 200, CX = SIZE/2, CY = SIZE/2 + 6, R = 70;
  const n = data.length;
  const angle = (i) => (-Math.PI / 2) + (2 * Math.PI * i / n);
  const pt = (i, r) => [CX + Math.cos(angle(i)) * r, CY + Math.sin(angle(i)) * r];
  const rings = [25, 50, 75, 100];
  const polygon = (rs) => rs.map((r,i) => pt(i, (R*r)/100).join(',')).join(' ');
  const valuePoly = polygon(data.map(a => a.percent));
  return (
    <Card style={{padding:18}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',gap:8,marginBottom:6,flexWrap:'wrap'}}>
        <div>
          <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',color:C.textFaint,textTransform:'uppercase'}}>
            {tr('momentum.areaBalanceTitle')}
          </div>
          <div style={{fontSize:11,fontWeight:300,fontStyle:'italic',color:C.textMuted}}>
            {tr('momentum.areaBalanceHelp')}
          </div>
        </div>
      </div>
      <div style={{display:'flex',gap:14,alignItems:'center',flexWrap:'wrap',justifyContent:'center'}}>
        <svg viewBox={`0 0 ${SIZE} ${SIZE+18}`} width="100%" style={{maxWidth:220,display:'block'}}>
          {/* concentric rings */}
          {rings.map(r => (
            <polygon key={r} points={data.map((_,i) => pt(i, (R*r)/100).join(',')).join(' ')}
              fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth="1"/>
          ))}
          {/* axis spokes */}
          {data.map((_,i) => {
            const [x,y] = pt(i, R);
            return <line key={i} x1={CX} y1={CY} x2={x} y2={y} stroke="rgba(255,255,255,0.05)" strokeWidth="1"/>;
          })}
          {/* value polygon */}
          <polygon points={valuePoly} fill="rgba(160,138,86,0.18)" stroke="var(--c-sand)" strokeWidth="1.5" strokeLinejoin="round"/>
          {data.map((a,i) => {
            const [px,py] = pt(i, R * (a.percent/100));
            return <circle key={'p'+i} cx={px} cy={py} r="2.5" fill="var(--c-sandLight)"/>;
          })}
          {/* labels */}
          {data.map((a,i) => {
            const [lx, ly] = pt(i, R + 14);
            const anchor = lx < CX - 4 ? 'end' : (lx > CX + 4 ? 'start' : 'middle');
            return (
              <text key={'lb'+i} x={lx} y={ly+3} fontSize="9" fill={C.textMuted}
                textAnchor={anchor} fontFamily="Inter, sans-serif">
                {a.area}
              </text>
            );
          })}
        </svg>
        <div style={{minWidth:120,display:'flex',flexDirection:'column',gap:3}}>
          {data.map(a => (
            <div key={a.id} style={{display:'flex',justifyContent:'space-between',gap:8,fontSize:10.5,color:C.textMuted}}>
              <span>{a.area}</span>
              <span style={{color:C.textPrimary,fontWeight:500}}>{a.percent}%</span>
            </div>
          ))}
        </div>
      </div>
    </Card>
  );
};

// Habit consistency rings — circular progress per habit. Denominator is
// the weekly scheduled target (rest days never count as missed).
// User-created habit names are rendered exactly as written; no translation.
const HabitConsistencyCompact = ({ rows, goTo }) => {
  const tr = useT();
  const data = Array.isArray(rows) ? rows : [];
  if (!data.length) {
    return (
      <Card>
        <div style={{fontSize:11,color:C.textFaint,fontStyle:'italic'}}>{tr('momentum.consistencyEmpty')}</div>
      </Card>
    );
  }
  const SIZE = 92, STROKE = 8;
  const RAD = (SIZE - STROKE) / 2;
  const CIRC = 2 * Math.PI * RAD;
  return (
    <Card style={{padding:18}}>
      <div style={{fontSize:11,fontWeight:300,fontStyle:'italic',color:C.textMuted,marginBottom:16}}>
        How your planned habits are filling up this week.
      </div>
      <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(140px,1fr))',gap:18}}>
        {data.map(row => {
          const target = row.weeklyTarget || row.scheduledThisWeek || 0;
          const done = Math.min(row.completedAnyThisWeek || 0, target || row.completedAnyThisWeek || 0);
          const pct = target ? Math.min(100, Math.round((done / target) * 100)) : 0;
          const stroke = row.targetMet ? 'var(--c-sage)' : 'var(--c-sand)';
          const dashOffset = CIRC * (1 - pct / 100);
          return (
            <div key={row.id} style={{display:'flex',flexDirection:'column',alignItems:'center',gap:10,
              padding:'12px 6px',borderRadius:12,minWidth:0}}>
              <div style={{position:'relative',width:SIZE,height:SIZE}}>
                <svg width={SIZE} height={SIZE} viewBox={`0 0 ${SIZE} ${SIZE}`} style={{transform:'rotate(-90deg)'}}>
                  <circle cx={SIZE/2} cy={SIZE/2} r={RAD}
                    fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth={STROKE}/>
                  <circle cx={SIZE/2} cy={SIZE/2} r={RAD}
                    fill="none" stroke={stroke} strokeWidth={STROKE}
                    strokeDasharray={CIRC} strokeDashoffset={dashOffset}
                    strokeLinecap="round"
                    style={{transition:'stroke-dashoffset 0.6s ease'}}/>
                </svg>
                <div style={{position:'absolute',inset:0,display:'flex',alignItems:'center',justifyContent:'center'}}>
                  <span style={{fontSize:16,fontWeight:600,color:C.textPrimary,fontVariantNumeric:'tabular-nums'}}>{pct}%</span>
                </div>
              </div>
              <div style={{textAlign:'center',minWidth:0,width:'100%'}}>
                <div title={row.name} style={{fontSize:13,fontWeight:500,color:C.textPrimary,
                  whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{row.name}</div>
                <div style={{fontSize:11,color:C.textMuted,marginTop:3,letterSpacing:'0.04em'}}>
                  {done}/{target || 0} this week
                </div>
              </div>
            </div>
          );
        })}
      </div>
    </Card>
  );
};

// Reading signal — current book + week activity. NO faked page deltas.
// TODO (page deltas): use confirmed page deltas from "Done for Today"
//   when that captures pages read; expose week.totals.pagesRead from
//   calculateMomentum so we can prefer pages over sessions.
const ReadingSignalCard = ({ activeBook, today, weekTotals, readingByDay, days, goTo }) => {
  const tr = useT();
  const isES = window.getLang && window.getLang()==='es';
  const wk = (weekTotals && weekTotals.readingSessions) || 0;
  const td = (today && today.readingSessions) || 0;
  const sparkW = 140, sparkH = 32;
  const max = Math.max(1, ...((readingByDay || []).map(n => n)));
  return (
    <Card>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',gap:8,marginBottom:10,flexWrap:'wrap'}}>
        <div>
          <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',color:C.textFaint,textTransform:'uppercase'}}>
            {tr('momentum.readingSignalTitle')}
          </div>
          <div style={{fontSize:11,fontWeight:300,fontStyle:'italic',color:C.textMuted}}>
            {tr('momentum.readingSignalHelp')}
          </div>
        </div>
        <ActionLink label={tr('momentum.open')} onClick={()=>goTo && goTo('reading')}/>
      </div>
      {activeBook ? (
        <div style={{display:'flex',gap:14,alignItems:'center',flexWrap:'wrap'}}>
          <div style={{flex:'1 1 200px',minWidth:0}}>
            <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.18em',color:C.textFaint,textTransform:'uppercase',marginBottom:4}}>
              {tr('momentum.currentBook')}
            </div>
            <div style={{fontSize:14,fontWeight:500,color:C.textPrimary,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>
              {activeBook.title || (isES ? 'Sin título' : 'Untitled')}
            </div>
            <div style={{marginTop:8,height:4,borderRadius:999,background:'rgba(255,255,255,0.05)',overflow:'hidden'}}>
              <div style={{width:`${today.currentReadingProgress || 0}%`,height:'100%',background:'var(--c-sage)',borderRadius:999}}/>
            </div>
            <div style={{marginTop:6,fontSize:10.5,color:C.textMuted,letterSpacing:'0.02em'}}>
              {(activeBook.currentPage || 0)}/{activeBook.totalPages || '—'} {tr('momentum.pages')} · {today.currentReadingProgress || 0}%
            </div>
          </div>
          <div style={{flex:'0 0 auto',display:'flex',flexDirection:'column',alignItems:'flex-end',gap:6}}>
            <div style={{fontSize:11,color:C.textMuted}}>
              <span style={{color:C.textPrimary,fontWeight:600,fontSize:14}}>{wk}</span>{' '}
              {wk === 1 ? tr('momentum.sessionThisWeek') : tr('momentum.sessionsThisWeek')}
            </div>
            <div style={{fontSize:10.5,color:C.textFaint}}>
              {td} {tr('momentum.sessionsToday')}
            </div>
            <svg viewBox={`0 0 ${sparkW} ${sparkH}`} width={sparkW} height={sparkH}>
              {(readingByDay || []).map((n, i) => {
                const slot = sparkW / (readingByDay.length || 7);
                const barW = slot * 0.55;
                const x = slot * i + (slot - barW) / 2;
                const h = n ? Math.max(2, (sparkH - 4) * (n / max)) : 0;
                const y = sparkH - h;
                return <rect key={i} x={x} y={y} width={barW} height={h}
                  fill={(days && days[i] && days[i].isToday) ? 'var(--c-sageFaint)' : 'rgba(94,117,88,0.55)'} rx="1"/>;
              })}
            </svg>
          </div>
        </div>
      ) : (
        wk > 0 ? (
          <div style={{fontSize:12,color:C.textMuted}}>
            <span style={{color:C.textPrimary,fontWeight:600,fontSize:14}}>{wk}</span>{' '}
            {wk === 1 ? tr('momentum.sessionThisWeek') : tr('momentum.sessionsThisWeek')}
          </div>
        ) : (
          <div style={{padding:'6px 0'}}>
            <div style={{fontSize:12,fontWeight:600,color:C.textSecondary}}>No reading signal yet.</div>
            <div style={{fontSize:11,fontWeight:300,color:C.textFaint,marginTop:3,fontStyle:'italic'}}>
              Open Reading to log progress.
            </div>
          </div>
        )
      )}
    </Card>
  );
};

// Small "next move" card — replaces the larger Current Mission block.
const NextMoveCard = ({ copy, today, goTo }) => {
  const tr = useT();
  if (!copy) return null;
  const nt = nextActionTarget(today);
  return (
    <Card style={{padding:'14px 16px',
      borderColor:'rgba(160,138,86,0.28)',
      background:'linear-gradient(135deg, rgba(160,138,86,0.05), rgba(160,138,86,0.01))',
      boxShadow:'inset 3px 0 0 rgba(160,138,86,0.55)'}}>
      <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',gap:14,flexWrap:'wrap'}}>
        <div style={{display:'flex',flexDirection:'column',gap:2,minWidth:0,flex:1}}>
          <span style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',color:C.sandLight,textTransform:'uppercase'}}>
            {tr('momentum.nextMove')}
          </span>
          <span style={{fontSize:13.5,fontWeight:500,color:C.textPrimary,letterSpacing:'0.005em'}}>
            {copy}
          </span>
        </div>
        {nt && <ActionLink label={nt.label} onClick={()=>goTo && goTo(nt.route)}/>}
      </div>
    </Card>
  );
};

// Momentum Score card — large number, label, short state.
// Pure visual; no navigation; no animation beyond hover-free static layout.
const MomentumScoreCard = ({ score, state }) => {
  const s = Number(score) || 0;
  const accent = s >= 70 ? C.sageLight : (s >= 30 ? C.sandLight : C.textMuted);
  const accentBorder = s >= 70 ? 'rgba(94,117,88,0.45)' : (s >= 30 ? 'rgba(160,138,86,0.40)' : C.border);
  return (
    <Card style={{padding:20}}>
      <div style={{display:'flex',alignItems:'center',gap:18,flexWrap:'wrap'}}>
        <div style={{display:'flex',flexDirection:'column',alignItems:'flex-start',minWidth:120}}>
          <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',
            color:C.textFaint,textTransform:'uppercase'}}>Momentum Score</div>
          <div style={{fontSize:48,fontWeight:300,color:C.textPrimary,lineHeight:1,
            fontVariantNumeric:'tabular-nums',marginTop:6}}>{s}</div>
          <div style={{marginTop:8,padding:'4px 10px',borderRadius:999,
            border:`1px solid ${accentBorder}`,fontSize:10,fontWeight:600,
            letterSpacing:'0.14em',textTransform:'uppercase',color:accent}}>
            {state || 'Low Signal'}
          </div>
        </div>
        <div style={{flex:'1 1 200px',minWidth:160}}>
          <div style={{fontSize:11,fontWeight:300,color:C.textMuted,lineHeight:1.55}}>
            A simple read of today: planned habits, tasks, ideas and reading.
            Build the system — the score follows.
          </div>
          {/* Progress strip */}
          <div style={{marginTop:12,height:6,borderRadius:3,
            background:'rgba(255,255,255,0.06)',overflow:'hidden'}}>
            <div style={{width:`${Math.min(100,Math.max(0,s))}%`,height:'100%',
              background:accent,transition:'width 0.4s ease'}}/>
          </div>
        </div>
      </div>
    </Card>
  );
};

// Momentum Map — last 30 days as a heatmap grid. Subtle sand intensity.
// Each cell shows a tooltip with date, score and counts. No tap interaction.
const MomentumHeatmap = ({ heatmap }) => {
  const data = Array.isArray(heatmap) ? heatmap : [];
  // Color ramp by intensity bucket (0..4). Sand for activity, neutral for none.
  const bgFor = (i) => ([
    'rgba(255,255,255,0.04)',
    'rgba(160,138,86,0.18)',
    'rgba(160,138,86,0.34)',
    'rgba(160,138,86,0.52)',
    'rgba(160,138,86,0.78)',
  ])[Math.max(0, Math.min(4, i || 0))];
  const fmtDate = (k) => {
    try {
      const d = new Date(k + 'T00:00:00');
      return d.toLocaleDateString(undefined, { month:'short', day:'numeric' });
    } catch (e) { return k; }
  };
  return (
    <Card style={{padding:18}}>
      <div style={{marginBottom:12}}>
        <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',
          color:C.textFaint,textTransform:'uppercase'}}>Momentum Map</div>
        <div style={{fontSize:11,fontWeight:300,fontStyle:'italic',color:C.textMuted}}>
          Last 30 days. Darker = stronger signal.
        </div>
      </div>
      {!data.length ? (
        <div style={{height:80,display:'flex',alignItems:'center',justifyContent:'center',
          fontSize:12,color:C.textFaint,fontStyle:'italic'}}>No data yet.</div>
      ) : (
        <>
          <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill, minmax(20px, 1fr))',
            gap:5}}>
            {data.map(cell => {
              const tip = `${fmtDate(cell.date)} · score ${cell.momentumScore}`
                + ` · habits ${cell.habitsCompleted}/${cell.habitsScheduled}`
                + ` · tasks ${cell.tasksCompleted}`
                + ` · ideas ${cell.ideasCaptured}`
                + ` · reading ${cell.readingSessions}`;
              return (
                <div key={cell.date} title={tip} aria-label={tip}
                  style={{aspectRatio:'1 / 1',borderRadius:4,
                    background:bgFor(cell.intensity),
                    outline:cell.isToday?`1px solid ${C.sandLight}`:'none',
                    outlineOffset:cell.isToday?1:0}}/>
              );
            })}
          </div>
          {/* Legend */}
          <div style={{display:'flex',alignItems:'center',gap:8,marginTop:14,
            fontSize:10,color:C.textFaint,letterSpacing:'0.08em'}}>
            <span>Less</span>
            {[0,1,2,3,4].map(i => (
              <div key={i} style={{width:12,height:12,borderRadius:3,background:bgFor(i)}}/>
            ))}
            <span>More</span>
          </div>
        </>
      )}
    </Card>
  );
};

// Today Signals — derived from current state. If a habit is unchecked, the
// corresponding signal disappears next render. Generic labels only.
const TodaySignals = ({ signals }) => {
  const items = Array.isArray(signals) ? signals : [];
  const iconFor = (type) => {
    if (type === 'habits_system_complete' || type === 'habits_partial') return 'check-circle';
    if (type === 'tasks_completed') return 'check';
    if (type === 'ideas_captured')  return 'sparkles';
    if (type === 'reading_signal')  return 'book';
    if (type === 'focus_signal')    return 'target';
    return 'circle';
  };
  return (
    <Card style={{padding:18}}>
      <div style={{marginBottom:12}}>
        <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',
          color:C.textFaint,textTransform:'uppercase'}}>Today Signals</div>
        <div style={{fontSize:11,fontWeight:300,fontStyle:'italic',color:C.textMuted}}>
          What\u2019s actually moving today.
        </div>
      </div>
      {!items.length ? (
        <div style={{padding:'18px 0',fontSize:12,color:C.textFaint,fontStyle:'italic'}}>
          No signals yet today. Complete a habit, task, idea or reading session.
        </div>
      ) : (
        <div style={{display:'flex',flexDirection:'column',gap:8}}>
          {items.map((s, i) => (
            <div key={s.type+'-'+i} style={{display:'flex',alignItems:'center',gap:10,
              padding:'8px 10px',borderRadius:8,
              background:'rgba(250,250,248,0.02)',border:`1px solid ${C.border}`}}>
              <div style={{width:24,height:24,borderRadius:'50%',
                display:'flex',alignItems:'center',justifyContent:'center',
                background:'rgba(160,138,86,0.10)',color:C.sandLight}}>
                <Icon name={iconFor(s.type)} size={12}/>
              </div>
              <div style={{fontSize:12,color:C.textPrimary,fontWeight:400}}>{s.label}</div>
            </div>
          ))}
        </div>
      )}
    </Card>
  );
};

// Momentum guide — simple 6-slide walkthrough explaining the Momentum page.
// Plain language, large title, short body, small visual preview.
// Rendered via portal to document.body so it always centers in the viewport.
const MOMENTUM_GUIDE_KEY = 'vyb-momentum-guide-seen';

// Slides are i18n-driven. Indexes are stable; callers translate via
// momentum.guide.slides.<n>.{eyebrow,title,body}. Visual kinds are
// presentation-only.
const MOMENTUM_GUIDE_VISUALS = ['cards','kpis','week','rings','radar','heatmap','aura'];
const getMomentumGuideSlides = (tr) => MOMENTUM_GUIDE_VISUALS.map((visual, i) => ({
  visual,
  eyebrow: tr(`momentum.guide.slides.${i}.eyebrow`),
  title:   tr(`momentum.guide.slides.${i}.title`),
  body:    tr(`momentum.guide.slides.${i}.body`),
}));

// Tiny inline preview visuals — sand/sage accents, decorative only.
const GuideVisual = ({ kind }) => {
  const sand = 'var(--c-sand)';
  const sandLight = 'var(--c-sandLight)';
  const sage = 'var(--c-sage)';
  if (kind === 'cards') {
    return (
      <div style={{display:'grid',gridTemplateColumns:'repeat(3,1fr)',gap:10,width:240}}>
        {[0,1,2].map(i => (
          <div key={i} style={{height:54,borderRadius:10,
            background:'rgba(160,138,86,0.10)',border:'1px solid rgba(160,138,86,0.22)',
            display:'flex',flexDirection:'column',justifyContent:'center',alignItems:'center',gap:4}}>
            <div style={{width:24,height:6,borderRadius:3,background:'rgba(255,255,255,0.18)'}}/>
            <div style={{width:14,height:4,borderRadius:2,background:'rgba(255,255,255,0.10)'}}/>
          </div>
        ))}
      </div>
    );
  }
  if (kind === 'kpis') {
    return (
      <div style={{display:'flex',gap:10}}>
        {['72','3','5','1'].map((n, i) => (
          <div key={i} style={{width:54,height:64,borderRadius:10,
            background:'rgba(160,138,86,0.10)',border:'1px solid rgba(160,138,86,0.22)',
            display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',gap:4}}>
            <div style={{fontSize:18,fontWeight:300,color:sandLight}}>{n}</div>
            <div style={{width:20,height:3,borderRadius:2,background:'rgba(255,255,255,0.14)'}}/>
          </div>
        ))}
      </div>
    );
  }
  if (kind === 'line') {
    return (
      <svg width={260} height={90} viewBox="0 0 260 90">
        <line x1="10" x2="250" y1="78" y2="78" stroke="rgba(255,255,255,0.10)"/>
        <path d="M10 70 L50 60 L90 50 L130 35 L170 28 L210 18 L250 12"
          fill="none" stroke={sand} strokeWidth="2" strokeLinecap="round"/>
        {[10,50,90,130,170,210,250].map((x,i) => {
          const ys = [70,60,50,35,28,18,12];
          return <circle key={i} cx={x} cy={ys[i]} r="2.5" fill={sandLight}/>;
        })}
      </svg>
    );
  }
  if (kind === 'radar') {
    // Hex radar with two layers
    const cx=120, cy=70, r=52;
    const pts = (factor) => Array.from({length:6}, (_,i) => {
      const a = (Math.PI/3)*i - Math.PI/2;
      return [cx + Math.cos(a)*r*factor, cy + Math.sin(a)*r*factor].join(',');
    }).join(' ');
    return (
      <svg width={240} height={140} viewBox="0 0 240 140">
        <polygon points={pts(1)} fill="none" stroke="rgba(255,255,255,0.10)"/>
        <polygon points={pts(0.66)} fill="none" stroke="rgba(255,255,255,0.08)"/>
        <polygon points={pts(0.33)} fill="none" stroke="rgba(255,255,255,0.06)"/>
        <polygon points="120,30 168,55 162,100 90,108 70,82 82,48"
          fill="rgba(160,138,86,0.20)" stroke={sandLight} strokeWidth="1.5"/>
      </svg>
    );
  }
  if (kind === 'rings') {
    const ring = (pct, color) => {
      const RAD = 18, STROKE = 4, CIRC = 2*Math.PI*RAD;
      return (
        <svg width={48} height={48} style={{transform:'rotate(-90deg)'}}>
          <circle cx={24} cy={24} r={RAD} fill="none" stroke="rgba(255,255,255,0.08)" strokeWidth={STROKE}/>
          <circle cx={24} cy={24} r={RAD} fill="none" stroke={color} strokeWidth={STROKE}
            strokeDasharray={CIRC} strokeDashoffset={CIRC*(1-pct/100)} strokeLinecap="round"/>
        </svg>
      );
    };
    return (
      <div style={{display:'flex',gap:14}}>
        {ring(100, sage)}{ring(70, sandLight)}{ring(40, sand)}{ring(85, sandLight)}
      </div>
    );
  }
  if (kind === 'week') {
    // 7 day dots — today highlighted, two completed, rest neutral.
    const dots = [
      {fill:sandLight, ring:false}, {fill:sandLight, ring:false},
      {fill:'rgba(255,255,255,0.10)', ring:false},
      {fill:'transparent', ring:true}, // today
      {fill:'transparent', ring:false},{fill:'transparent', ring:false},{fill:'transparent', ring:false},
    ];
    return (
      <div style={{display:'flex',gap:10,alignItems:'center'}}>
        {dots.map((d,i)=>(
          <div key={i} style={{width:18,height:18,borderRadius:'50%',
            background:d.fill,
            border:d.ring?`2px solid ${sandLight}`:`1px solid rgba(255,255,255,0.12)`}}/>
        ))}
      </div>
    );
  }
  if (kind === 'heatmap') {
    // 5×7 mini month heatmap with varied intensities.
    const intensities = [
      0.2,0.6,0.0,0.4,0.8,0.3,0.0,
      0.5,0.9,0.7,0.4,0.0,0.2,0.6,
      0.8,0.5,0.3,1.0,0.6,0.4,0.0,
      0.0,0.3,0.7,0.5,0.9,0.4,0.2,
      0.6,0.4,0.0,0.3,0.0,0.0,0.0,
    ];
    return (
      <div style={{display:'grid',gridTemplateColumns:'repeat(7, 16px)',gap:4}}>
        {intensities.map((v,i)=>(
          <div key={i} style={{width:16,height:16,borderRadius:3,
            background: v === 0 ? 'rgba(255,255,255,0.05)'
              : `rgba(160,138,86,${0.18 + v*0.55})`,
            border:'1px solid rgba(255,255,255,0.04)'}}/>
        ))}
      </div>
    );
  }
  if (kind === 'aura') {
    return (
      <div style={{position:'relative',width:120,height:120,display:'flex',
        alignItems:'center',justifyContent:'center'}}>
        <div style={{position:'absolute',inset:0,borderRadius:'50%',
          background:'radial-gradient(circle, rgba(160,138,86,0.45) 0%, rgba(94,117,88,0.20) 55%, transparent 75%)'}}/>
        <div style={{position:'relative',width:54,height:54,borderRadius:'50%',
          background:'rgba(160,138,86,0.35)',border:`2px solid ${sandLight}`,
          boxShadow:'0 0 32px rgba(160,138,86,0.45)'}}/>
      </div>
    );
  }
  return null;
};

const MomentumGuideModal = ({ onClose }) => {
  const tr = useT();
  const slides = React.useMemo(() => getMomentumGuideSlides(tr), [tr]);
  const [i, setI] = React.useState(0);
  const last = slides.length - 1;

  // Lock background scroll while open. Restore on unmount.
  React.useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => { document.body.style.overflow = prev; };
  }, []);

  React.useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') onClose();
      else if (e.key === 'ArrowRight' && i < last) setI(i+1);
      else if (e.key === 'ArrowLeft' && i > 0) setI(i-1);
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [i, last, onClose]);

  const s = slides[i];
  const [isMobile, setIsMobile] = React.useState(
    () => typeof window !== 'undefined' && window.matchMedia
      && window.matchMedia('(max-width: 768px)').matches
  );
  React.useEffect(() => {
    if (typeof window === 'undefined' || !window.matchMedia) return;
    const mq = window.matchMedia('(max-width: 768px)');
    const onChange = (e) => setIsMobile(e.matches);
    if (mq.addEventListener) mq.addEventListener('change', onChange);
    else mq.addListener(onChange);
    return () => {
      if (mq.removeEventListener) mq.removeEventListener('change', onChange);
      else mq.removeListener(onChange);
    };
  }, []);

  const overlay = (
    <div onClick={onClose} role="dialog" aria-modal="true"
      style={{position:'fixed',inset:0,background:'var(--c-overlay)',
        backdropFilter:'blur(6px)',zIndex:9999,display:'flex',alignItems:'center',
        justifyContent:'center',
        padding:isMobile?'16px 16px calc(16px + env(safe-area-inset-bottom))':16}}>
      <div onClick={e=>e.stopPropagation()} className="view-fade"
        style={{
          width:isMobile?'100%':'min(900px, calc(100vw - 32px))',
          maxWidth:isMobile?420:undefined,
          height:isMobile?'auto':'min(680px, calc(100vh - 32px))',
          maxHeight:isMobile
            ? 'calc(100dvh - 32px - env(safe-area-inset-bottom))'
            : 'min(680px, calc(100vh - 32px))',
          background:C.card,border:`1px solid ${C.borderMid}`,
          borderRadius:isMobile?20:20,boxShadow:'0 40px 100px rgba(0,0,0,0.7)',
          display:'flex',flexDirection:'column',overflow:'hidden'}}>
        {/* Header bar */}
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',
          padding:isMobile?'12px 14px':'14px 18px',borderBottom:`1px solid ${C.border}`,flexShrink:0}}>
          <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
            textTransform:'uppercase',color:C.textMuted,
            overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{tr('momentum.guide.header','How Momentum Works')}</div>
          <IconButton icon="x" onClick={onClose} color={C.textMuted} title={tr('momentum.guide.close','Close')}/>
        </div>
        {/* Slide body — scrolls internally if needed */}
        <div style={{flex:1,minHeight:0,overflowY:'auto',WebkitOverflowScrolling:'touch',
          display:'flex',flexDirection:'column',alignItems:'center',justifyContent:isMobile?'flex-start':'center',
          textAlign:'center',padding:isMobile?'18px 18px 6px':'40px 32px',gap:isMobile?14:22}}>
          <div style={{fontSize:isMobile?10:11,fontWeight:700,letterSpacing:'0.28em',
            textTransform:'uppercase',color:C.sandLight}}>{s.eyebrow}</div>
          <div style={{fontSize:isMobile?20:34,fontWeight:300,color:C.textPrimary,
            lineHeight:1.2,letterSpacing:'-0.01em',maxWidth:560}}>{s.title}</div>
          <div style={{fontSize:isMobile?13:16,fontWeight:300,color:C.textSecondary,
            lineHeight:1.5,maxWidth:480}}>{s.body}</div>
          <div style={{marginTop:isMobile?2:8}}>
            <GuideVisual kind={s.visual}/>
          </div>
        </div>
        {/* Footer: dots + nav */}
        <div style={{padding:isMobile?'10px 14px 14px':'14px 18px 18px',borderTop:`1px solid ${C.border}`,
          display:'flex',flexDirection:'column',gap:isMobile?8:12,flexShrink:0}}>
          <div style={{display:'flex',justifyContent:'center',gap:8}}>
            {slides.map((_, idx) => (
              <button key={idx} onClick={()=>setI(idx)} aria-label={`Go to slide ${idx+1}`}
                style={{width:idx===i?22:6,height:6,borderRadius:3,border:'none',
                  background:idx===i?C.sandLight:'rgba(255,255,255,0.18)',cursor:'pointer',
                  padding:0,transition:'all 0.25s ease'}}/>
            ))}
          </div>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',gap:12}}>
            <button onClick={()=> i>0 && setI(i-1)} disabled={i===0}
              style={{background:'transparent',border:'none',color:i===0?C.textFaint:C.textMuted,
                fontSize:12,fontWeight:500,letterSpacing:'0.08em',textTransform:'uppercase',
                cursor:i===0?'default':'pointer',padding:'8px 4px',opacity:i===0?0.4:1}}>
              {tr('momentum.guide.back','Back')}
            </button>
            {i < last ? (
              <Button onClick={()=>setI(i+1)} variant="primary" size="md">{tr('momentum.guide.next','Next')}</Button>
            ) : (
              <Button onClick={onClose} variant="primary" size="md">{tr('momentum.guide.gotIt','Got it')}</Button>
            )}
          </div>
        </div>
      </div>
    </div>
  );

  // Portal to document.body so the modal escapes any scrolled ancestor and
  // always centers in the viewport.
  return ReactDOM.createPortal(overlay, document.body);
};

// Phase divider — used for Today / This Week / This Month section headers.
// Intentional spacing + a hairline rule to make each phase feel like a new
// chapter of the page. No CTAs, no action buttons.
// Subtle progress microcopy. Returns a short, calm line based on a
// percentage and an optional context (today/week/month/habit/area). Caller
// is responsible for hiding it when there's no meaningful target.
const getProgressMicrocopy = (percent, context = 'default', tr) => {
  const p = Math.max(0, Math.min(100, Number(percent) || 0));
  const tier =
    p >= 100 ? 'done'
    : p >= 90 ? 'almost'
    : p >= 75 ? 'strong'
    : p >= 50 ? 'half'
    : p >= 25 ? 'moving'
    : p >   0 ? 'started'
    : 'start';
  const FALLBACK = {
    today: {
      start:'Start with one habit.',  started:'You started today.',
      moving:'Today is moving.',      half:'Halfway through today.',
      strong:'Strong day so far.',    almost:'Almost done for today.',
      done:'Today is complete.',
    },
    week: {
      start:'Start the week with one check-in.', started:'The week has started.',
      moving:'Your week is moving.',  half:'Half the rhythm is built.',
      strong:'Strong week.',          almost:'Almost a full week.',
      done:'Weekly rhythm complete.',
    },
    month: {
      start:'Start the month with one move.', started:'The month is starting to show.',
      moving:'Your pattern is forming.', half:'The month has real movement.',
      strong:'Strong monthly pattern.', almost:'Almost a complete pattern.',
      done:'Month fully built.',
    },
    habit: {
      start:'Not started yet.',       started:'First check-in done.',
      moving:'Building consistency.', half:'Halfway this week.',
      strong:'Strong consistency.',   almost:'Almost complete.',
      done:'Fully consistent.',
    },
    area: {
      start:'No signal yet.',         started:'Small signal started.',
      moving:'Area is moving.',       half:'Area is building.',
      strong:'Strong area.',          almost:'Almost full.',
      done:'Area complete.',
    },
    default: {
      start:'Start with one small move.', started:'You started. Keep it moving.',
      moving:"You're building momentum.", half:'Halfway there.',
      strong:'Strong progress.',          almost:'Almost locked.',
      done:'Complete. Protect the rhythm.',
    },
  };
  const ctx = FALLBACK[context] ? context : 'default';
  const fallback = FALLBACK[ctx][tier];
  if (typeof tr === 'function') {
    return tr('momentum.micro.' + ctx + '.' + tier, fallback);
  }
  return fallback;
};

const MomentumPhaseHeader = ({ title, subtitle }) => (
  <div style={{marginTop:18}}>
    <div style={{height:1,background:C.border,marginBottom:18,opacity:0.7}}/>
    <div style={{fontSize:22,fontWeight:500,color:C.textPrimary,
      letterSpacing:'-0.01em',lineHeight:1.15,marginBottom:6}}>
      {title}
    </div>
    {subtitle && (
      <div style={{fontSize:13,fontWeight:300,color:C.textMuted,fontStyle:'italic'}}>
        {subtitle}
      </div>
    )}
  </div>
);

// Today Habits — premium redesign.
// Layout: eyebrow on top, then a two-column header (big metric left, status
// + supporting copy right), then a wide luminous progress bar below. On
// completion the card softly lights up: sage glow on the bar, gentle border
// brightening, and a subtle outer bloom. Mobile stacks the columns.
// Logic, props and computations are unchanged.
const TodayHabitsCard = ({ planned, completed, extras, isComplete }) => {
  const tr = useT();
  const total = Number(planned) || 0;
  const done  = Number(completed) || 0;
  const xtra  = Number(extras) || 0;
  const pct   = total > 0 ? Math.min(100, Math.round((done/total)*100)) : 0;
  const isEmpty = total === 0;

  // Three explicit visual states (logic unchanged — derived from same props).
  //   neutral    → empty day OR nothing completed yet (no color, no glow)
  //   inProgress → some but not all done (warm amber accents, NO glow)
  //   complete   → all planned done (gold + premium glow, mirrors seal)
  // Amber palette: rgb(196,144,68) → rgb(230,180,96) (warm honey, never green).
  const state =
    (isEmpty || done === 0) ? 'neutral'
    : (done >= total ? 'complete' : 'inProgress');
  const isComplete_  = state === 'complete';
  const isInProgress = state === 'inProgress';
  const isNeutral    = state === 'neutral';

  const statusLabel = isEmpty
    ? tr('momentum.statusOpenDay','Open Day')
    : (isComplete_ ? tr('momentum.statusComplete','Complete') : (isNeutral ? tr('momentum.statusNotStarted','Not Started') : tr('momentum.statusInProgress','In Progress')));
  const statusColor = isComplete_
    ? C.sandLight
    : (isInProgress ? 'rgb(230,180,96)' : C.textMuted);
  const statusBorder = isComplete_
    ? 'rgba(212,189,138,0.55)'
    : (isInProgress ? 'rgba(196,144,68,0.50)' : C.border);
  const statusBg = isComplete_
    ? 'linear-gradient(135deg, rgba(160,138,86,0.18), rgba(212,189,138,0.10))'
    : (isInProgress ? 'rgba(196,144,68,0.12)' : 'transparent');

  // Card chrome:
  //  • complete   → gold gradient bg + gold border + gold inset/drop glow
  //  • inProgress → subtle amber border tint, NO ambient glow, NO drop shadow
  //  • neutral    → fully default Card (no override)
  const cardStyle = {
    padding: '28px 28px 26px',
    position: 'relative',
    overflow: 'hidden',
    background: isComplete_
      ? 'linear-gradient(135deg, rgba(160,138,86,0.08), rgba(160,138,86,0.04) 60%, rgba(212,189,138,0.06))'
      : undefined,
    border: isComplete_
      ? `1px solid rgba(160,138,86,0.32)`
      : (isInProgress ? `1px solid rgba(196,144,68,0.22)` : undefined),
    boxShadow: isComplete_
      ? '0 0 0 1px rgba(160,138,86,0.12) inset, 0 14px 48px -18px rgba(160,138,86,0.45)'
      : undefined,
    transition: 'box-shadow 0.5s ease, border-color 0.5s ease, background 0.5s ease',
  };

  return (
    <Card style={cardStyle}>
      {/* Ambient wash — gold ONLY on complete (mirrors the seal).
          In-progress and neutral states intentionally have no glow. */}
      {isComplete_ && (
        <>
          <div aria-hidden="true" style={{
            position:'absolute',top:-60,left:-40,width:260,height:260,pointerEvents:'none',
            background:'radial-gradient(circle, rgba(160,138,86,0.18), transparent 65%)',
            filter:'blur(2px)',
          }}/>
          <div aria-hidden="true" style={{
            position:'absolute',inset:0,pointerEvents:'none',
            background:'radial-gradient(circle at 50% 110%, rgba(212,189,138,0.14) 0%, transparent 55%)',
          }}/>
        </>
      )}

      <div style={{position:'relative',display:'flex',flexDirection:'column',gap:22}}>
        {/* Eyebrow */}
        <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
          textTransform:'uppercase',color:C.textFaint}}>{tr('momentum.todayHabitsEyebrow','Today Habits')}</div>

        {/* Header row — big metric LEFT, supporting text + status RIGHT.
             Wraps to stacked on narrow widths via flexWrap. */}
        <div style={{display:'flex',justifyContent:'space-between',
          alignItems:'flex-end',gap:20,flexWrap:'wrap'}}>
          {/* Metric */}
          <div style={{display:'flex',alignItems:'baseline',gap:10,
            fontVariantNumeric:'tabular-nums',minWidth:0}}>
            <div style={{fontSize:64,fontWeight:600,
              color:isComplete_?C.sandLight:C.textPrimary,
              lineHeight:0.95,letterSpacing:'-0.02em',
              transition:'color 0.4s ease'}}>
              {done}
            </div>
            <div style={{fontSize:30,fontWeight:200,color:C.textFaint,lineHeight:1}}>
              / {total}
            </div>
          </div>

          {/* Right column — supporting copy + status */}
          <div style={{display:'flex',flexDirection:'column',alignItems:'flex-end',
            gap:10,minWidth:0,maxWidth:'100%'}}>
            <div style={{padding:'5px 12px',borderRadius:999,
              border:`1px solid ${statusBorder}`,background:statusBg,
              fontSize:10,fontWeight:600,letterSpacing:'0.18em',
              textTransform:'uppercase',color:statusColor,
              transition:'all 0.4s ease'}}>
              {statusLabel}
            </div>
            <div style={{fontSize:12,color:C.textMuted,fontWeight:300,
              textAlign:'right',lineHeight:1.45,maxWidth:260}}>
              {isEmpty
                ? tr('momentum.noPlannedToday','No planned habits today.')
                : (isComplete_
                    ? tr('momentum.habitsCompleteOf','{done}/{total} habits complete').replace('{done}', done).replace('{total}', total)
                    : tr('momentum.habitsCompleteOf','{done}/{total} habits complete').replace('{done}', done).replace('{total}', total))}
            </div>
          </div>
        </div>

        {/* Wide premium progress bar */}
        <div style={{position:'relative',width:'100%',height:12,borderRadius:8,
          background:'linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02))',
          border:'1px solid rgba(255,255,255,0.05)',
          overflow:'hidden'}}>
          <div style={{
            width:`${pct}%`,height:'100%',
            background: isComplete_
              ? 'linear-gradient(90deg, rgba(160,138,86,0.85), rgba(212,189,138,1))'
              : (isInProgress
                  ? 'linear-gradient(90deg, rgba(196,144,68,0.75), rgba(230,180,96,1))'
                  : 'transparent'),
            // Glow only on complete — in-progress stays calm, no shadow.
            boxShadow: isComplete_
              ? '0 0 18px rgba(212,189,138,0.50), 0 0 6px rgba(212,189,138,0.40) inset'
              : 'none',
            transition: 'width 0.6s cubic-bezier(0.22,0.61,0.36,1), background 0.5s ease, box-shadow 0.5s ease',
            borderRadius:8,
          }}/>
        </div>

        {/* Subtle progress microcopy — calm status line under the bar. */}
        <div style={{fontSize:11,color:C.textFaint,fontStyle:'italic',marginTop:-4}}>
          {isEmpty ? tr('momentum.noPlannedToday','No planned habits today.') : getProgressMicrocopy(pct, 'today', tr)}
        </div>

        {/* Extras — small secondary chip-line, never competes with the metric */}
        {xtra > 0 && (
          <div style={{display:'flex',alignItems:'center',gap:8,marginTop:-6}}>
            <div style={{padding:'3px 10px',borderRadius:999,
              border:`1px solid ${C.border}`,
              background:'rgba(160,138,86,0.06)',
              fontSize:10,fontWeight:600,letterSpacing:'0.14em',
              textTransform:'uppercase',color:C.sandLight}}>
              +{xtra} {xtra===1?tr('momentum.extraSingular','extra'):tr('momentum.extras','extras')}
            </div>
            <div style={{fontSize:11,color:C.textFaint,fontStyle:'italic'}}>
              {tr('momentum.completedBeyondPlan','completed beyond plan')}
            </div>
          </div>
        )}
      </div>
    </Card>
  );
};

// Today Snapshot — compact 3-column KPI card. Tasks / Ideas / Reading.
// Information only. Zero values render cleanly.
const TodaySnapshotCard = ({ tasks, ideas, reading, readingPages }) => {
  const tr = useT();
  // Reading display rule (confirmed sessions only — no slider data):
  //  • pages > 0  → "{pages}" with sub "pages"
  //  • else sessions > 0 → "{sessions}" with sub "session(s)"
  //  • else        → "0" with sub "reading"
  const rPages = Number(readingPages) || 0;
  const rSess  = Number(reading) || 0;
  const readingLabel = tr('momentum.reading','Reading');
  const readingCell = rPages > 0
    ? { label:readingLabel, n:rPages, sub: rPages === 1 ? tr('momentum.pageSingular','page') : tr('momentum.pages','pages') }
    : rSess > 0
      ? { label:readingLabel, n:rSess, sub: rSess === 1 ? tr('momentum.session','session') : tr('momentum.sessions','sessions') }
      : { label:readingLabel, n:0,     sub:tr('momentum.reading','Reading').toLowerCase() };
  const cells = [
    { label: tr('momentum.tasks','Tasks'),   n: Number(tasks)   || 0, sub: tr('momentum.subCompleted','completed') },
    { label: tr('momentum.ideas','Ideas'),   n: Number(ideas)   || 0, sub: tr('momentum.subCaptured','captured')  },
    readingCell,
  ];
  return (
    <Card style={{padding:'24px 20px'}}>
      <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
        textTransform:'uppercase',color:C.textFaint,textAlign:'center',marginBottom:18}}>
        {tr('momentum.todaySnapshotTitle','Today Snapshot')}
      </div>
      <div style={{display:'grid',gridTemplateColumns:'repeat(3, 1fr)',gap:14}}>
        {cells.map(c => (
          <div key={c.label} style={{display:'flex',flexDirection:'column',
            alignItems:'center',gap:6,padding:'14px 6px',borderRadius:12,
            background:'rgba(250,250,248,0.02)',border:`1px solid ${C.border}`}}>
            <div style={{fontSize:36,fontWeight:300,color:C.textPrimary,
              lineHeight:1,fontVariantNumeric:'tabular-nums'}}>{c.n}</div>
            <div style={{fontSize:11,fontWeight:600,letterSpacing:'0.16em',
              textTransform:'uppercase',color:C.textMuted}}>{c.label}</div>
            <div style={{fontSize:10,color:C.textFaint,fontStyle:'italic'}}>{c.sub}</div>
          </div>
        ))}
      </div>
    </Card>
  );
};

// ─────────────────────────────────────────────────────────────
// Momentum V2 — This Week section components
// All components are pure-presentational and read from the existing
// calculateMomentum() output. No new data fetching, no schema change.
// ─────────────────────────────────────────────────────────────

// Tiny chip used by WeeklyRhythmRow to render each day.
const _RhythmDay = ({ label, state, isToday }) => {
  // Visual states: complete | partial | incomplete | rest
  const styles = (() => {
    if (state === 'complete')  return { bg:'var(--c-sage)',          border:'rgba(94,117,88,0.65)',  fg:'#FAFAF8' };
    if (state === 'partial')   return { bg:'rgba(160,138,86,0.55)',  border:'rgba(160,138,86,0.55)', fg:'#FAFAF8' };
    if (state === 'incomplete')return { bg:'transparent',            border:'rgba(255,255,255,0.18)',fg:C.textFaint };
    return                          { bg:'transparent',            border:C.border,                fg:C.textFaint }; // rest
  })();
  const symbol = state === 'complete' ? '●'
              : state === 'partial'   ? '◐'
              : state === 'incomplete'? '○' : '–';
  return (
    <div style={{display:'flex',flexDirection:'column',alignItems:'center',gap:8,minWidth:0}}>
      <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.18em',
        textTransform:'uppercase',color:isToday?C.sandLight:C.textFaint}}>{label}</div>
      <div style={{width:32,height:32,borderRadius:'50%',
        display:'flex',alignItems:'center',justifyContent:'center',
        background:styles.bg,border:`1px solid ${styles.border}`,
        boxShadow:isToday?'0 0 0 2px rgba(160,138,86,0.25)':'none',
        color:styles.fg,fontSize:14,lineHeight:1,
        transition:'all 0.4s ease'}}>
        <span aria-hidden="true">{symbol}</span>
      </div>
    </div>
  );
};

// Combined "This Week Habits" — replaces the prior Weekly Rhythm + Weekly
// Habits Summary cards. Single card with: title, big bar + summary numbers,
// 7-day status dots, legend. Counts ONLY planned habits (extras excluded).
const WeeklyHabitsCard = ({ days, completed, target }) => {
  const data = Array.isArray(days) ? days : [];
  const t = Number(target) || 0;
  const c = Math.min(Number(completed) || 0, t);
  const pct = t > 0 ? Math.round((c/t)*100) : 0;
  const isStrong = t > 0 && pct >= 100;
  const barColor = isStrong ? 'rgba(120,150,110,1)' : 'rgba(200,178,120,0.95)';
  const barGlow  = isStrong ? '0 0 18px rgba(120,150,110,0.55)' : '0 0 10px rgba(160,138,86,0.35)';

  const stateOf = (d) => {
    if (d.isRest || d.totalHabits === 0) return 'rest';
    if (d.completedHabits >= d.totalHabits) return 'complete';
    if (d.completedHabits > 0) return 'partial';
    return 'none';
  };
  const dotStyle = (state) => {
    if (state === 'complete') return { bg:'var(--c-sage)',         border:'rgba(94,117,88,0.7)' };
    if (state === 'partial')  return { bg:'rgba(200,178,120,0.85)',border:'rgba(160,138,86,0.55)' };
    if (state === 'none')     return { bg:'rgba(255,255,255,0.06)',border:'rgba(255,255,255,0.18)' };
    return                          { bg:'transparent',            border:C.border };
  };

  return (
    <Card style={{padding:'24px 22px 20px'}}>
      <div style={{display:'flex',alignItems:'baseline',justifyContent:'space-between',
        gap:12,flexWrap:'wrap',marginBottom:18}}>
        <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
          textTransform:'uppercase',color:C.textFaint}}>This Week Habits</div>
        {t > 0 && (
          <div style={{fontSize:11,color:C.textMuted,fontWeight:300,
            fontVariantNumeric:'tabular-nums'}}>
            <span style={{color:isStrong?C.sageLight:C.textPrimary,fontWeight:600}}>{c}</span>
            {' / '}{t} planned · <span style={{color:isStrong?C.sageLight:C.sandLight,
              fontWeight:600}}>{pct}%</span>
          </div>
        )}
      </div>
      {t === 0 ? (
        <div style={{fontSize:14,color:C.textMuted,fontWeight:300,fontStyle:'italic',
          marginBottom:18}}>
          No planned habits this week.
        </div>
      ) : (
        <>
          <div style={{display:'flex',alignItems:'baseline',gap:10,
            fontVariantNumeric:'tabular-nums',marginBottom:14}}>
            <div style={{fontSize:48,fontWeight:200,
              color:isStrong?C.sageLight:C.textPrimary,
              lineHeight:0.95,letterSpacing:'-0.02em'}}>{c}</div>
            <div style={{fontSize:22,fontWeight:200,color:C.textFaint}}>/ {t}</div>
            <div style={{flex:1}}/>
            <div style={{fontSize:13,color:C.textMuted,fontWeight:300}}>
              planned habits completed
            </div>
          </div>
          <div style={{position:'relative',width:'100%',height:10,borderRadius:6,
            background:'linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02))',
            border:'1px solid rgba(255,255,255,0.05)',overflow:'hidden',marginBottom:22}}>
            <div style={{width:`${Math.min(100,pct)}%`,height:'100%',
              background:barColor,boxShadow:barGlow,borderRadius:6,
              transition:'width 0.6s cubic-bezier(0.22,0.61,0.36,1)'}}/>
          </div>
        </>
      )}
      {/* Day-by-day dot row */}
      <div style={{display:'grid',gridTemplateColumns:'repeat(7, 1fr)',gap:6}}>
        {data.map(d => {
          const s = stateOf(d);
          const ds = dotStyle(s);
          return (
            <div key={d.date} style={{display:'flex',flexDirection:'column',
              alignItems:'center',gap:8,minWidth:0,
              opacity: d.isFuture && s !== 'complete' ? 0.55 : 1}}>
              <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.18em',
                textTransform:'uppercase',
                color:d.isToday?C.sandLight:C.textFaint}}>{d.label}</div>
              <div style={{width:14,height:14,borderRadius:'50%',
                background:ds.bg,border:`1px solid ${ds.border}`,
                boxShadow:d.isToday?'0 0 0 3px rgba(160,138,86,0.18)':'none',
                transition:'all 0.4s ease'}} aria-hidden="true"/>
            </div>
          );
        })}
      </div>
      {/* Legend */}
      <div style={{display:'flex',justifyContent:'center',flexWrap:'wrap',
        gap:14,marginTop:16,fontSize:10,color:C.textFaint,letterSpacing:'0.06em'}}>
        <span style={{display:'inline-flex',alignItems:'center',gap:6}}>
          <span style={{width:8,height:8,borderRadius:'50%',background:'var(--c-sage)'}}/>
          complete
        </span>
        <span style={{display:'inline-flex',alignItems:'center',gap:6}}>
          <span style={{width:8,height:8,borderRadius:'50%',background:'rgba(200,178,120,0.85)'}}/>
          partial
        </span>
        <span style={{display:'inline-flex',alignItems:'center',gap:6}}>
          <span style={{width:8,height:8,borderRadius:'50%',background:'rgba(255,255,255,0.06)',
            border:'1px solid rgba(255,255,255,0.18)'}}/>
          none
        </span>
        <span style={{display:'inline-flex',alignItems:'center',gap:6}}>
          <span style={{width:8,height:8,borderRadius:'50%',border:`1px solid ${C.border}`}}/>
          rest
        </span>
      </div>
    </Card>
  );
};

// (Legacy) Weekly Rhythm — kept defined but no longer rendered. Replaced by
// WeeklyHabitsCard above. Safe to remove later if no other caller appears.
const WeeklyRhythmRow = ({ days }) => {
  const data = Array.isArray(days) ? days : [];
  const stateOf = (d) => {
    if (d.isRest || d.totalHabits === 0) return 'rest';
    if (d.completedHabits === d.totalHabits) return 'complete';
    if (d.completedHabits > 0) return 'partial';
    if (d.isFuture) return 'rest';
    return 'incomplete';
  };
  return (
    <Card style={{padding:'22px 18px'}}>
      <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
        textTransform:'uppercase',color:C.textFaint,marginBottom:14,textAlign:'center'}}>
        Weekly Rhythm
      </div>
      <div style={{display:'grid',gridTemplateColumns:'repeat(7, 1fr)',gap:6}}>
        {data.map(d => (
          <_RhythmDay key={d.date} label={d.label}
            state={stateOf(d)} isToday={d.isToday}/>
        ))}
      </div>
      {/* Legend */}
      <div style={{display:'flex',justifyContent:'center',gap:14,marginTop:16,
        fontSize:10,color:C.textFaint,letterSpacing:'0.06em'}}>
        <span><span style={{color:'var(--c-sage)'}}>●</span> complete</span>
        <span><span style={{color:C.sandLight}}>◐</span> partial</span>
        <span>○ none</span>
        <span>– rest</span>
      </div>
    </Card>
  );
};

// Weekly Habits — total planned-habit instances completed this week.
const WeeklyHabitsSummary = ({ completed, target, extras }) => {
  const t = Number(target) || 0;
  const c = Math.min(Number(completed) || 0, t);
  const x = Number(extras) || 0;
  const pct = t > 0 ? Math.round((c/t)*100) : 0;
  const status = t === 0 ? 'Open Week'
              : pct >= 100 ? 'Strong Week'
              : pct >= 60  ? 'Building Rhythm'
              : pct > 0    ? 'Building Rhythm' : 'Needs Support';
  const isStrong = pct >= 100;
  const barColor = isStrong ? 'rgba(120,150,110,1)' : 'rgba(200,178,120,0.95)';
  const barGlow  = isStrong ? '0 0 18px rgba(120,150,110,0.55)' : '0 0 10px rgba(160,138,86,0.35)';
  return (
    <Card style={{padding:'24px 24px 22px'}}>
      <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
        textTransform:'uppercase',color:C.textFaint,marginBottom:18}}>Weekly Habits</div>
      {t === 0 ? (
        <div style={{fontSize:14,color:C.textMuted,fontWeight:300,fontStyle:'italic'}}>
          No planned habits this week.
        </div>
      ) : (
        <>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-end',
            gap:18,flexWrap:'wrap',marginBottom:14}}>
            <div style={{display:'flex',alignItems:'baseline',gap:10,
              fontVariantNumeric:'tabular-nums'}}>
              <div style={{fontSize:54,fontWeight:200,
                color:isStrong?C.sageLight:C.textPrimary,
                lineHeight:0.95,letterSpacing:'-0.02em'}}>{c}</div>
              <div style={{fontSize:24,fontWeight:200,color:C.textFaint}}>/ {t}</div>
            </div>
            <div style={{display:'flex',flexDirection:'column',alignItems:'flex-end',gap:8}}>
              <div style={{padding:'5px 12px',borderRadius:999,
                border:`1px solid ${isStrong?'rgba(94,117,88,0.55)':'rgba(160,138,86,0.45)'}`,
                background:isStrong?'rgba(94,117,88,0.18)':'rgba(160,138,86,0.10)',
                fontSize:10,fontWeight:600,letterSpacing:'0.16em',
                textTransform:'uppercase',color:isStrong?C.sageLight:C.sandLight}}>
                {status}
              </div>
              <div style={{fontSize:12,color:C.textMuted,fontWeight:300,textAlign:'right'}}>
                planned habits completed
              </div>
            </div>
          </div>
          <div style={{position:'relative',width:'100%',height:10,borderRadius:6,
            background:'linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02))',
            border:'1px solid rgba(255,255,255,0.05)',overflow:'hidden'}}>
            <div style={{width:`${Math.min(100,pct)}%`,height:'100%',
              background:barColor,boxShadow:barGlow,borderRadius:6,
              transition:'width 0.6s cubic-bezier(0.22,0.61,0.36,1)'}}/>
          </div>
          {x > 0 && (
            <div style={{marginTop:12,display:'flex',alignItems:'center',gap:8}}>
              <div style={{padding:'3px 10px',borderRadius:999,
                border:`1px solid ${C.border}`,background:'rgba(160,138,86,0.06)',
                fontSize:10,fontWeight:600,letterSpacing:'0.14em',
                textTransform:'uppercase',color:C.sandLight}}>
                +{x} extra{x===1?'':'s'}
              </div>
              <div style={{fontSize:11,color:C.textFaint,fontStyle:'italic'}}>
                completed beyond plan
              </div>
            </div>
          )}
        </>
      )}
    </Card>
  );
};

// Weekly Progress — Mon→Sun mini bars with per-day check-in count + 4-state
// coloring (Complete/Partial/None/Rest). Adapted from the Habits page Weekly
// Progress component, tuned for the Momentum surface.
const WeeklyProgressCard = ({ days }) => {
  const tr = useT();
  const data = Array.isArray(days) ? days : [];
  const total = data.reduce((a,d) => a + (d.completedHabits || 0), 0);
  const wkScheduled = data.reduce((a,d) => a + (d.totalHabits || 0), 0);
  const wkCompleted = data.reduce((a,d) => a + (d.completedHabits || 0), 0);
  const wkPct = wkScheduled ? Math.round((wkCompleted / wkScheduled) * 100) : 0;
  const stateOf = (d) => {
    if (d.isRest || d.totalHabits === 0) return 'rest';
    if (d.completedHabits >= d.totalHabits) return 'complete';
    if (d.completedHabits > 0) return 'partial';
    return 'none';
  };
  const palette = (s) => {
    if (s === 'complete') return { fill:'linear-gradient(to top, var(--c-sage), rgba(150,180,140,0.9))',
                                    border:'rgba(94,117,88,0.55)', count:C.sageLight };
    if (s === 'partial')  return { fill:'linear-gradient(to top, #C8862C, #E0AE52)',
                                    border:'rgba(212,154,58,0.55)', count:'#E8B968' };
    if (s === 'none')     return { fill:'rgba(255,255,255,0.04)',
                                    border:'rgba(255,255,255,0.10)', count:C.textFaint };
    return                       { fill:'transparent',
                                    border:C.border,                count:C.textFaint };
  };
  return (
    <Card style={{padding:22}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',
        gap:12,flexWrap:'wrap',marginBottom:14}}>
        <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
          textTransform:'uppercase',color:C.textFaint}}>{tr('momentum.weeklyProgressEyebrow','Weekly Progress')}</div>
        <div style={{fontSize:11,fontWeight:300,color:C.textMuted,
          fontVariantNumeric:'tabular-nums'}}>
          <span style={{color:C.sand,fontWeight:700,fontStyle:'italic'}}>{total}</span>{' '}
          {total === 1 ? tr('momentum.checkInThisWeek','check-in this week') : tr('momentum.checkInsThisWeek','check-ins this week')}
        </div>
      </div>
      <div style={{display:'flex',justifyContent:'space-between',gap:8}}>
        {data.map(d => {
          const s = stateOf(d);
          const p = palette(s);
          const ratio = d.totalHabits > 0
            ? Math.min(1, (d.completedHabits || 0) / d.totalHabits)
            : 0;
          const fillPct = s === 'rest' ? 0 : (s === 'none' ? 100 : Math.round(ratio * 100));
          return (
            <div key={d.date} style={{flex:1,display:'flex',flexDirection:'column',
              alignItems:'center',gap:6,minWidth:0,
              opacity: d.isFuture && s !== 'complete' ? 0.7 : 1}}>
              <div style={{fontSize:9,fontWeight:600,letterSpacing:'0.14em',
                textTransform:'uppercase',
                color:d.isToday?C.sand:C.textFaint}}>{d.label}</div>
              <div style={{width:'100%',height:64,borderRadius:10,
                background:'rgba(255,255,255,0.03)',position:'relative',overflow:'hidden',
                border:`1px solid ${d.isToday?C.borderMid:p.border}`,
                boxShadow:d.isToday?'0 0 0 2px rgba(160,138,86,0.18)':'none',
                transition:'all 0.4s ease'}}>
                <div style={{position:'absolute',bottom:0,left:0,right:0,
                  height:`${fillPct}%`,background:p.fill,
                  transition:'height 0.5s cubic-bezier(0.22,0.61,0.36,1)'}}/>
              </div>
              <div style={{fontSize:11,fontWeight:600,color:p.count,
                fontVariantNumeric:'tabular-nums'}}>{d.completedHabits || 0}</div>
            </div>
          );
        })}
      </div>
      {/* Divider + per-day status dot row + compact legend */}
      <div style={{height:1,background:C.border,marginTop:22,marginBottom:16,
        marginLeft:-22,marginRight:-22}}/>
      <div style={{display:'grid',gridTemplateColumns:'repeat(7, 1fr)',gap:8,
        marginBottom:14}}>
        {data.map(d => {
          const s = stateOf(d);
          const dot = s === 'complete' ? { bg:'var(--c-sage)',           border:'rgba(94,117,88,0.7)' }
                   : s === 'partial'  ? { bg:'#D49A3A',                 border:'rgba(212,154,58,0.55)' }
                   : s === 'none'     ? { bg:'rgba(255,255,255,0.18)',  border:'rgba(255,255,255,0.18)' }
                   :                    { bg:'transparent',             border:C.borderMid };
          return (
            <div key={d.date} style={{display:'flex',justifyContent:'center',
              opacity: d.isFuture && s !== 'complete' ? 0.55 : 1}}>
              <span style={{width:10,height:10,borderRadius:'50%',
                background:dot.bg,border:`1px solid ${dot.border}`,
                boxShadow:d.isToday?'0 0 0 3px rgba(160,138,86,0.18)':'none',
                transition:'all 0.3s ease'}} aria-hidden="true"/>
            </div>
          );
        })}
      </div>
      {wkScheduled > 0 && (
        <div style={{fontSize:11,color:C.textFaint,fontStyle:'italic',
          textAlign:'center',marginBottom:12}}>
          {getProgressMicrocopy(wkPct, 'week', tr)}
        </div>
      )}
      <div style={{display:'flex',justifyContent:'center',alignItems:'center',
        flexWrap:'wrap',gap:'10px 22px',
        fontSize:10,fontWeight:500,letterSpacing:'0.08em',color:C.textMuted}}>
        <span style={{display:'inline-flex',alignItems:'center',gap:8}}>
          <span style={{width:9,height:9,borderRadius:'50%',background:'var(--c-sage)'}}/>
          {tr('momentum.legendComplete','Complete')}
        </span>
        <span style={{display:'inline-flex',alignItems:'center',gap:8}}>
          <span style={{width:9,height:9,borderRadius:'50%',background:'#D49A3A'}}/>
          {tr('momentum.legendPartial','Partial')}
        </span>
        <span style={{display:'inline-flex',alignItems:'center',gap:8}}>
          <span style={{width:9,height:9,borderRadius:'50%',
            background:'rgba(255,255,255,0.18)'}}/>
          {tr('momentum.legendNone','None')}
        </span>
        <span style={{display:'inline-flex',alignItems:'center',gap:8}}>
          <span style={{width:9,height:9,borderRadius:'50%',
            background:'transparent',border:`1px solid ${C.borderMid}`}}/>
          {tr('momentum.legendRest','Rest')}
        </span>
      </div>
    </Card>
  );
};

// Habit Consistency — per-habit weekly progress as small rings.
const HabitConsistencyWeek = ({ rows }) => {
  const tr = useT();
  const data = (Array.isArray(rows) ? rows : []).filter(r => (r.weeklyTarget || r.scheduledThisWeek) > 0);
  if (!data.length) {
    return (
      <Card style={{padding:22}}>
        <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
          textTransform:'uppercase',color:C.textFaint,marginBottom:10,textAlign:'center'}}>
          {tr('momentum.habitConsistencyTitle','Habit Consistency')}
        </div>
        <div style={{fontSize:12,color:C.textFaint,fontStyle:'italic',textAlign:'center'}}>
          {tr('momentum.noHabitTargetsWeek','No habit targets this week.')}
        </div>
      </Card>
    );
  }
  const SIZE = 92, STROKE = 8;
  const RAD = (SIZE - STROKE) / 2;
  const CIRC = 2 * Math.PI * RAD;
  return (
    <Card style={{padding:22}}>
      <div style={{textAlign:'center',marginBottom:18}}>
        <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
          textTransform:'uppercase',color:C.textFaint,marginBottom:6}}>
          {tr('momentum.habitConsistencyTitle','Habit Consistency')}
        </div>
        <div style={{fontSize:11,fontWeight:300,fontStyle:'italic',color:C.textMuted}}>
          {tr('momentum.habitConsistencySubtitle','How each habit is filling up this week.')}
        </div>
      </div>
      {/* Centered & evenly distributed layout: few items center; many wrap
          and remain balanced. */}
      <div style={{display:'flex',flexWrap:'wrap',justifyContent:'center',
        alignItems:'flex-start',gap:'18px 22px'}}>
        {data.map(row => {
          const target = row.weeklyTarget || row.scheduledThisWeek || 0;
          const done = Math.min(row.completedAnyThisWeek || 0, target);
          const pct = target ? Math.min(100, Math.round((done/target)*100)) : 0;
          const stroke = pct >= 100 ? 'var(--c-sage)' : 'var(--c-sand)';
          const off = CIRC * (1 - pct/100);
          return (
            <div key={row.id} style={{display:'flex',flexDirection:'column',alignItems:'center',
              gap:10,padding:'8px 4px',width:140,minWidth:0}}>
              <div style={{position:'relative',width:SIZE,height:SIZE}}>
                <svg width={SIZE} height={SIZE} style={{transform:'rotate(-90deg)'}}>
                  <circle cx={SIZE/2} cy={SIZE/2} r={RAD} fill="none"
                    stroke="rgba(255,255,255,0.06)" strokeWidth={STROKE}/>
                  <circle cx={SIZE/2} cy={SIZE/2} r={RAD} fill="none"
                    stroke={stroke} strokeWidth={STROKE}
                    strokeDasharray={CIRC} strokeDashoffset={off}
                    strokeLinecap="round"
                    style={{transition:'stroke-dashoffset 0.6s ease'}}/>
                </svg>
                <div style={{position:'absolute',inset:0,display:'flex',
                  alignItems:'center',justifyContent:'center'}}>
                  <span style={{fontSize:16,fontWeight:600,color:C.textPrimary,
                    fontVariantNumeric:'tabular-nums'}}>{done}/{target}</span>
                </div>
              </div>
              <div style={{textAlign:'center',width:'100%',minWidth:0}}>
                <div title={row.name} style={{fontSize:13,fontWeight:500,color:C.textPrimary,
                  whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{row.name}</div>
              </div>
            </div>
          );
        })}
      </div>
    </Card>
  );
};

// Weekly Snapshot — tasks/ideas/reading totals for the current week.
const WeeklySnapshot = ({ tasks, ideas, reading, readingPages }) => {
  const rPages = Number(readingPages) || 0;
  const rSess  = Number(reading) || 0;
  const readingCell = rPages > 0
    ? { label:'Reading', n:rPages, sub: rPages === 1 ? 'page'    : 'pages' }
    : rSess > 0
      ? { label:'Reading', n:rSess, sub: rSess === 1 ? 'session' : 'sessions' }
      : { label:'Reading', n:0,     sub:'reading' };
  const cells = [
    { label:'Tasks',   n:Number(tasks)   || 0, sub:'completed' },
    { label:'Ideas',   n:Number(ideas)   || 0, sub:'captured'  },
    readingCell,
  ];
  return (
    <Card style={{padding:'24px 20px'}}>
      <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
        textTransform:'uppercase',color:C.textFaint,textAlign:'center',marginBottom:18}}>
        Weekly Snapshot
      </div>
      <div style={{display:'grid',gridTemplateColumns:'repeat(3, 1fr)',gap:14}}>
        {cells.map(c => (
          <div key={c.label} style={{display:'flex',flexDirection:'column',
            alignItems:'center',gap:6,padding:'14px 6px',borderRadius:12,
            background:'rgba(250,250,248,0.02)',border:`1px solid ${C.border}`}}>
            <div style={{fontSize:36,fontWeight:300,color:C.textPrimary,
              lineHeight:1,fontVariantNumeric:'tabular-nums'}}>{c.n}</div>
            <div style={{fontSize:11,fontWeight:600,letterSpacing:'0.16em',
              textTransform:'uppercase',color:C.textMuted}}>{c.label}</div>
            <div style={{fontSize:10,color:C.textFaint,fontStyle:'italic'}}>{c.sub}</div>
          </div>
        ))}
      </div>
    </Card>
  );
};

// Weekly Area Balance — radar/spider chart that fills as habits complete.
// Uses weekly area data only. Not switchable.
const WeeklyAreaRadar = ({ areas }) => {
  const tr = useT();
  const data = Array.isArray(areas) ? areas : [];
  // Need at least 3 axes for a meaningful polygon; fall back to list.
  if (data.length < 3) {
    return (
      <Card style={{padding:18}}>
        <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
          textTransform:'uppercase',color:C.textFaint,marginBottom:6}}>{tr('momentum.areaBalanceTitle','Area Balance')}</div>
        <div style={{fontSize:11,fontWeight:300,fontStyle:'italic',color:C.textMuted,marginBottom:14}}>
          {tr('momentum.areaBalanceSubtitle','Your week by life area.')}
        </div>
        {!data.length ? (
          <div style={{height:120,display:'flex',alignItems:'center',justifyContent:'center',
            fontSize:12,color:C.textFaint,fontStyle:'italic'}}>
            {tr('momentum.areaEmpty','Add habits in different areas to see your balance.')}
          </div>
        ) : (
          <div style={{display:'flex',flexDirection:'column',gap:6}}>
            {data.map(a => (
              <div key={a.id} style={{display:'flex',justifyContent:'space-between',
                gap:8,fontSize:12,color:C.textMuted}}>
                <span>{a.area}</span>
                <span style={{color:C.textPrimary,fontWeight:500}}>{a.percent}%</span>
              </div>
            ))}
          </div>
        )}
      </Card>
    );
  }
  const SIZE = 240, CX = SIZE/2, CY = SIZE/2 + 6, R = 80;
  const n = data.length;
  const angle = (i) => (-Math.PI/2) + (2*Math.PI*i/n);
  const pt = (i, r) => [CX + Math.cos(angle(i))*r, CY + Math.sin(angle(i))*r];
  const rings = [25, 50, 75, 100];
  const valuePoly = data.map((a,i) => pt(i, (R*a.percent)/100).join(',')).join(' ');
  return (
    <Card style={{padding:18}}>
      <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
        textTransform:'uppercase',color:C.textFaint,marginBottom:6}}>{tr('momentum.areaBalanceTitle','Area Balance')}</div>
      <div style={{fontSize:11,fontWeight:300,fontStyle:'italic',color:C.textMuted,marginBottom:16}}>
        {tr('momentum.areaBalanceSubtitle','Your week by life area.')}
      </div>
      <div style={{display:'flex',gap:16,alignItems:'center',
        flexWrap:'wrap',justifyContent:'center'}}>
        <svg viewBox={`0 0 ${SIZE} ${SIZE+18}`} width="100%" style={{maxWidth:260,display:'block'}}>
          {rings.map(r => (
            <polygon key={r} points={data.map((_,i) => pt(i, (R*r)/100).join(',')).join(' ')}
              fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth="1"/>
          ))}
          {data.map((_,i) => {
            const [x,y] = pt(i, R);
            return <line key={i} x1={CX} y1={CY} x2={x} y2={y}
              stroke="rgba(255,255,255,0.05)" strokeWidth="1"/>;
          })}
          <polygon points={valuePoly}
            fill="rgba(160,138,86,0.22)" stroke="var(--c-sand)"
            strokeWidth="1.5" strokeLinejoin="round"
            style={{filter:'drop-shadow(0 0 6px rgba(160,138,86,0.35))'}}/>
          {data.map((a,i) => {
            const [px,py] = pt(i, R*(a.percent/100));
            return <circle key={'p'+i} cx={px} cy={py} r="2.8" fill="var(--c-sandLight)"/>;
          })}
          {data.map((a,i) => {
            const [lx, ly] = pt(i, R + 16);
            const anchor = lx < CX - 4 ? 'end' : (lx > CX + 4 ? 'start' : 'middle');
            return (
              <text key={'lb'+i} x={lx} y={ly+3} fontSize="10" fill={C.textMuted}
                textAnchor={anchor} fontFamily="Inter, sans-serif">
                {a.area}
              </text>
            );
          })}
        </svg>
        <div style={{minWidth:140,display:'flex',flexDirection:'column',gap:5}}>
          {data.map(a => (
            <div key={a.id} style={{display:'flex',justifyContent:'space-between',
              gap:10,fontSize:12,color:C.textMuted}}>
              <span>{a.area}</span>
              <span style={{color:C.textPrimary,fontWeight:500,
                fontVariantNumeric:'tabular-nums'}}>{a.percent}%</span>
            </div>
          ))}
        </div>
      </div>
      <div style={{marginTop:14,fontSize:11,color:C.textFaint,
        fontStyle:'italic',textAlign:'center'}}>
        {(() => {
          const valid = data.filter(a => (a.scheduled || 0) > 0);
          if (!valid.length) return tr('momentum.areaFillHint','Fill the shape by completing planned habits in each area.');
          const avg = Math.round(valid.reduce((s,a) => s + (a.percent || 0), 0) / valid.length);
          return getProgressMicrocopy(avg, 'area', tr);
        })()}
      </div>
    </Card>
  );
};

// Strongest / Needs Support — small two-cell insight from weekly habit data.
// (Legacy — kept defined for back-compat, no longer rendered. Replaced by
// BestNeedsSupportPair which works for both weekly and monthly inputs.)
const StrongestNeedsSupport = ({ consistency }) => {
  const rows = (Array.isArray(consistency) ? consistency : [])
    .filter(r => (r.weeklyTarget || r.scheduledThisWeek) > 0)
    .map(r => {
      const target = r.weeklyTarget || r.scheduledThisWeek || 0;
      const done = Math.min(r.completedAnyThisWeek || 0, target);
      return { id:r.id, name:r.name, done, target,
        percent: target ? Math.round((done/target)*100) : 0 };
    });
  if (rows.length < 1) {
    return (
      <Card style={{padding:'18px 20px'}}>
        <div style={{fontSize:12,color:C.textFaint,fontStyle:'italic'}}>
          Not enough data yet.
        </div>
      </Card>
    );
  }
  const sorted = [...rows].sort((a,b) => b.percent - a.percent);
  const strongest = sorted[0];
  const needs = sorted[sorted.length - 1];
  const sameOne = rows.length === 1 || (strongest.id === needs.id);
  const Cell = ({ eyebrow, row, accent }) => (
    <div style={{flex:'1 1 200px',minWidth:0,padding:'14px 16px',borderRadius:12,
      background:'rgba(250,250,248,0.02)',border:`1px solid ${C.border}`}}>
      <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',
        textTransform:'uppercase',color:C.textFaint,marginBottom:6}}>{eyebrow}</div>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',gap:8}}>
        <div style={{fontSize:14,fontWeight:500,color:C.textPrimary,
          whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis',minWidth:0}}>
          {row.name}
        </div>
        <div style={{fontSize:13,color:accent,fontWeight:600,
          fontVariantNumeric:'tabular-nums',whiteSpace:'nowrap'}}>
          {row.done}/{row.target} · {row.percent}%
        </div>
      </div>
    </div>
  );
  return (
    <div style={{display:'flex',gap:14,flexWrap:'wrap'}}>
      <Cell eyebrow="Strongest Habit" row={strongest} accent={C.sageLight}/>
      {!sameOne && <Cell eyebrow="Needs Support" row={needs} accent={C.sandLight}/>}
    </div>
  );
};

// Best Habit / Needs Support — generic pair used by both Weekly and Monthly.
// Accepts { rows: [{ id, name, completed, target, percent }] }.
// Empty-state copy: "Not enough habit data yet."
// One-habit case: only show Best (and Needs only if percent < 100).
const BestNeedsSupportPair = ({ rows, periodLabel }) => {
  const tr = useT();
  const data = (Array.isArray(rows) ? rows : []).filter(r => (r.target || 0) > 0);
  const Wrap = ({ children }) => (
    <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(240px,1fr))',gap:14}}>
      {children}
    </div>
  );
  const EmptyCard = ({ note }) => (
    <Card style={{padding:'18px 20px'}}>
      <div style={{fontSize:12,color:C.textFaint,fontStyle:'italic',textAlign:'center'}}>
        {note}
      </div>
    </Card>
  );
  if (!data.length) {
    return <EmptyCard note={tr('momentum.notEnoughHabitData','Not enough habit data yet.')}/>;
  }
  // ── Weighted scoring ───────────────────────────────────────
  // Best:  60% completion% + 25% completed/5 (cap) + 15% target/5 (cap)
  // Needs: 60% missed%     + 30% missed/5  (cap) + 10% target/5 (cap)
  // Tie-breakers: higher target, then higher completed, then original order.
  const enriched = data.map((r, idx) => {
    const target    = r.target    || 0;
    const completed = r.completed || 0;
    const pct       = target ? completed / target : 0;
    const missed    = Math.max(0, target - completed);
    const bestScore =
      pct * 0.6 +
      Math.min(completed / 5, 1) * 0.25 +
      Math.min(target / 5, 1)   * 0.15;
    const supportScore =
      (1 - pct) * 0.6 +
      Math.min(missed / 5, 1)  * 0.3 +
      Math.min(target / 5, 1)  * 0.1;
    return { ...r, _idx:idx, _bestScore:bestScore, _supportScore:supportScore };
  });
  const tiebreak = (a,b) => (b.target - a.target) || (b.completed - a.completed) || (a._idx - b._idx);
  const byBest    = [...enriched].sort((a,b) => (b._bestScore    - a._bestScore)    || tiebreak(a,b));
  const byNeeds   = [...enriched].sort((a,b) => (b._supportScore - a._supportScore) || tiebreak(a,b));

  // Pick best with completed > 0 when possible (avoid "best" being 0/N).
  const best  = byBest.find(r => r.completed > 0) || byBest[0];
  // Pick a needs-support that actually has missed > 0 (skip 100%-complete habits).
  const worst = byNeeds.find(r => r.percent < 100 && (r.target - r.completed) > 0);

  const isSingle  = data.length === 1;
  const allDone   = data.every(r => r.percent >= 100);
  const showBest  = !isSingle ? best && best.completed > 0 : (best && best.completed > 0);
  const showNeeds = !!worst && (!isSingle || (best && best.percent < 100 && best.completed > 0));

  const Cell = ({ eyebrow, row, tone }) => {
    const isPos = tone === 'positive';
    const accent     = isPos ? C.sageLight : C.sandLight;
    const borderCol  = isPos ? 'rgba(94,117,88,0.45)' : 'rgba(160,138,86,0.45)';
    const bgTint     = isPos ? 'rgba(94,117,88,0.06)' : 'rgba(160,138,86,0.06)';
    return (
      <Card style={{padding:'18px 20px',background:bgTint,border:`1px solid ${borderCol}`}}>
        <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',
          textTransform:'uppercase',color:accent,marginBottom:10}}>{eyebrow}</div>
        <div style={{fontSize:18,fontWeight:500,color:C.textPrimary,
          whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis',
          marginBottom:10}} title={row.name}>{row.name}</div>
        <div style={{display:'flex',alignItems:'baseline',gap:10,
          fontVariantNumeric:'tabular-nums'}}>
          <div style={{fontSize:24,fontWeight:300,color:C.textPrimary,
            letterSpacing:'-0.01em'}}>{row.completed}</div>
          <div style={{fontSize:14,fontWeight:300,color:C.textFaint}}>
            / {row.target}
          </div>
          <div style={{flex:1}}/>
          <div style={{fontSize:14,fontWeight:600,color:accent}}>{row.percent}%</div>
        </div>
      </Card>
    );
  };

  // Needs-Support empty copy: friendlier when everything is at 100%.
  const needsEmptyNote = allDone
    ? tr('momentum.noHabitNeedsSupport','No habit needs support right now.')
    : tr('momentum.notEnoughForSupport','Not enough data yet for Needs Support.');
  const bestEmptyNote = tr('momentum.notEnoughData','Not enough data yet.');

  return (
    <Wrap>
      {showBest
        ? <Cell eyebrow={tr('momentum.bestHabit','Best Habit')} row={best} tone="positive"/>
        : <EmptyCard note={bestEmptyNote}/>}
      {showNeeds
        ? <Cell eyebrow={tr('momentum.needsSupport','Needs Support')} row={worst} tone="warm"/>
        : <EmptyCard note={needsEmptyNote}/>}
    </Wrap>
  );
};

// ──────────────────────────────────────────────────────────────
// MONTHLY components — Update 3.
// All read m.month.* shapes built in dashboard-momentum.jsx.
// Tokens come from C; nothing here fetches or mutates anything.
// ──────────────────────────────────────────────────────────────

// Calendar heatmap for the current month. Aligned to Mon..Sun columns.
// Today's cell is outlined; future days render as faint placeholders.
const MonthlyMomentumMap = ({ period, days, monthlyPercent }) => {
  const tr = useT();
  const safe = Array.isArray(days) ? days : [];
  if (!safe.length || !period) {
    return (
      <Card style={{padding:'18px 20px'}}>
        <div style={{fontSize:12,color:C.textFaint,fontStyle:'italic'}}>
          {tr('momentum.notEnoughThisMonth','Not enough data yet for this month.')}
        </div>
      </Card>
    );
  }
  const dim = period.daysInMonth || safe.length;
  // Cell color by intensity bucket. 'glow' merges visually into 'complete'
  // with a subtle inner bloom — no separate legend entry.
  const completeStyle = {
    bg: 'rgba(94,117,88,0.42)',
    border: `1px solid rgba(94,117,88,0.60)`,
    glow: '0 0 10px rgba(94,117,88,0.30) inset, 0 0 6px rgba(94,117,88,0.18)',
  };
  const cellStyle = (d, isFuture) => {
    if (isFuture) return { bg: 'transparent', border: `1px dashed ${C.border}`, glow: null };
    switch (d && d.intensity) {
      case 'glow':
      case 'complete': return completeStyle;
      case 'strong':   return { bg: 'rgba(94,117,88,0.25)',  border: `1px solid rgba(94,117,88,0.35)`,  glow: null };
      case 'medium':   return { bg: 'rgba(160,138,86,0.22)', border: `1px solid rgba(160,138,86,0.35)`, glow: null };
      case 'low':      return { bg: 'rgba(160,138,86,0.10)', border: `1px solid rgba(160,138,86,0.22)`, glow: null };
      case 'rest':     return { bg: 'transparent',           border: `1px solid ${C.border}`,           glow: null };
      case 'none':
      default:         return { bg: 'transparent',           border: `1px solid ${C.border}`,           glow: null };
    }
  };

  // Build a 7-col grid. Leading offset = weekday of day 1 (Mon=0..Sun=6).
  const first = safe[0];
  const firstDow = first ? ((first.weekday + 6) % 7) : 0; // shift Sun(0)→6
  const cells = [];
  for (let i = 0; i < firstDow; i++) cells.push({ pad: true, key: 'pad-pre-'+i });
  // Map of dayNumber → record
  const byNum = new Map(safe.map(d => [d.dayNumber, d]));
  for (let n = 1; n <= dim; n++) {
    const d = byNum.get(n);
    cells.push({ pad: false, key: 'd-'+n, dayNumber: n, day: d, isFuture: !d });
  }

  const labels = ['M','T','W','T','F','S','S'];

  return (
    <Card style={{padding:'20px 22px'}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',
        marginBottom:14,gap:10,flexWrap:'wrap'}}>
        <div>
          <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
            textTransform:'uppercase',color:C.textFaint,marginBottom:4}}>
            {tr('momentum.monthlyMapTitle','Monthly Momentum Map')}
          </div>
          <div style={{fontSize:16,fontWeight:400,color:C.textPrimary,
            letterSpacing:'-0.005em'}}>{period.label}</div>
        </div>
        <div style={{fontSize:11,color:C.textFaint,fontVariantNumeric:'tabular-nums'}}>
          {tr('momentum.dayOf','Day {a} of {b}').replace('{a}', period.daysElapsed).replace('{b}', dim)}
        </div>
      </div>

      {/* Subtle progress microcopy for the month-to-date pattern. */}
      {(monthlyPercent != null) && (
        <div style={{fontSize:11,color:C.textFaint,fontStyle:'italic',marginBottom:14}}>
          {getProgressMicrocopy(monthlyPercent, 'month', tr)}
        </div>
      )}

      <div style={{display:'grid',gridTemplateColumns:'repeat(7, 1fr)',gap:6,marginBottom:8}}>
        {labels.map((l, i) => (
          <div key={'l'+i} style={{fontSize:9,fontWeight:700,letterSpacing:'0.18em',
            textTransform:'uppercase',color:C.textFaint,textAlign:'center'}}>{l}</div>
        ))}
      </div>

      <div style={{display:'grid',gridTemplateColumns:'repeat(7, 1fr)',gap:6}}>
        {cells.map(c => {
          if (c.pad) return <div key={c.key} style={{height:36}}/>;
          const s = cellStyle(c.day, c.isFuture);
          const isToday = !!(c.day && c.day.isToday);
          const ringColor = isToday ? C.sageLight : 'transparent';
          const baseShadow = s.glow || 'none';
          const shadow = isToday
            ? `${baseShadow !== 'none' ? baseShadow + ', ' : ''}0 0 0 1.5px ${ringColor} inset`
            : baseShadow;
          return (
            <div key={c.key} title={c.day ? `${c.day.date} · ${c.day.plannedHabitsCompleted}/${c.day.plannedHabitsTotal} habits · score ${c.day.score}` : ''}
              style={{
                height:36, borderRadius:8, position:'relative',
                background: s.bg, border: s.border,
                boxShadow: shadow,
                display:'flex',alignItems:'center',justifyContent:'center',
                fontSize:10, color: c.isFuture ? C.textFaint : C.textMuted,
                fontVariantNumeric:'tabular-nums',
              }}>
              {c.dayNumber}
            </div>
          );
        })}
      </div>

      {/* Legend */}
      <div style={{display:'flex',gap:14,flexWrap:'wrap',marginTop:14,
        fontSize:10,color:C.textFaint,letterSpacing:'0.06em'}}>
        {[
          { label:tr('momentum.legendNoHabits','No Habits'), bg:'transparent', border:`1px solid ${C.border}` },
          { label:tr('momentum.legendLow','Low'),       bg:'rgba(160,138,86,0.10)' },
          { label:tr('momentum.legendMedium','Medium'),    bg:'rgba(160,138,86,0.22)' },
          { label:tr('momentum.legendStrong','Strong'),    bg:'rgba(94,117,88,0.25)'  },
          { label:tr('momentum.legendComplete','Complete'),  bg:'rgba(94,117,88,0.42)', shadow:'0 0 6px rgba(94,117,88,0.35)' },
        ].map((l,i)=>(
          <div key={i} style={{display:'flex',alignItems:'center',gap:6}}>
            <div style={{width:10,height:10,borderRadius:3,background:l.bg,
              border:l.border||'1px solid transparent',
              boxShadow:l.shadow||'none'}}/>
            <span>{l.label}</span>
          </div>
        ))}
      </div>
    </Card>
  );
};

// Monthly Totals — KPI tiles. Habits show completed/target so it stays honest
// across rest days. Reading tile collapses pages into the same stat when known.
const MonthlyTotalsCard = ({ totals }) => {
  const tr = useT();
  const t = totals || {};
  const Tile = ({ eyebrow, value, sub }) => (
    <div style={{flex:'1 1 140px',minWidth:0,padding:'14px 16px',borderRadius:12,
      background:'rgba(250,250,248,0.02)',border:`1px solid ${C.border}`}}>
      <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',
        textTransform:'uppercase',color:C.textFaint,marginBottom:8}}>{eyebrow}</div>
      <div style={{fontSize:26,fontWeight:300,color:C.textPrimary,letterSpacing:'-0.01em',
        fontVariantNumeric:'tabular-nums',lineHeight:1}}>{value}</div>
      {sub && (
        <div style={{fontSize:11,color:C.textFaint,marginTop:6}}>{sub}</div>
      )}
    </div>
  );
  const habitVal = (t.habitsTarget || 0) > 0
    ? `${t.habitsCompleted||0}/${t.habitsTarget}`
    : `${t.habitsCompleted||0}`;
  const readingSub = (t.pagesRead || 0) > 0 ? `${t.pagesRead} ${tr('momentum.pages','pages')}` : null;
  return (
    <div>
      <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
        textTransform:'uppercase',color:C.textFaint,marginBottom:10}}>{tr('momentum.monthlyTotalsEyebrow','Monthly Totals')}</div>
      <div style={{display:'flex',gap:12,flexWrap:'wrap'}}>
        <Tile eyebrow={tr('momentum.kpiHabits','Habits')}  value={habitVal} sub={(t.extrasCompleted||0) > 0 ? `+${t.extrasCompleted} ${tr('momentum.extras','extras')}` : null}/>
        <Tile eyebrow={tr('momentum.kpiTasks','Tasks')}   value={t.tasksCompleted||0}/>
        <Tile eyebrow={tr('momentum.kpiIdeas','Ideas')}   value={t.ideasCaptured||0}/>
        <Tile eyebrow={tr('momentum.kpiReading','Reading')} value={t.readingSessions||0} sub={readingSub}/>
      </div>
    </div>
  );
};

// Two-up stat cards for Complete Days vs Active Days.
const CompleteActiveDaysPair = ({ completeDays, activeDays, daysElapsed }) => {
  const tr = useT();
  const Cell = ({ eyebrow, value, sub, accent }) => (
    <Card style={{padding:'18px 20px',flex:'1 1 200px',minWidth:0}}>
      <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',
        textTransform:'uppercase',color:accent,marginBottom:10}}>{eyebrow}</div>
      <div style={{display:'flex',alignItems:'baseline',gap:8,fontVariantNumeric:'tabular-nums'}}>
        <div style={{fontSize:32,fontWeight:300,color:C.textPrimary,letterSpacing:'-0.01em',lineHeight:1}}>{value}</div>
        {daysElapsed != null && (
          <div style={{fontSize:14,fontWeight:300,color:C.textFaint}}>/ {daysElapsed}</div>
        )}
      </div>
      {sub && <div style={{fontSize:11,color:C.textFaint,marginTop:8}}>{sub}</div>}
    </Card>
  );
  return (
    <div style={{display:'flex',gap:14,flexWrap:'wrap'}}>
      <Cell eyebrow={tr('momentum.completeDays','Complete Days')} value={completeDays || 0} accent={C.sageLight}
        sub={tr('momentum.completeDaysSub','All scheduled habits done.')}/>
      <Cell eyebrow={tr('momentum.activeDays','Active Days')}   value={activeDays   || 0} accent={C.sandLight}
        sub={tr('momentum.activeDaysSub','Any habit, task, idea, or reading.')}/>
    </div>
  );
};

// Strongest / Needs Support area pair — based on monthly area balance.
const StrongestNeedsSupportAreaPair = ({ strongest, needs }) => {
  if (!strongest && !needs) {
    return (
      <Card style={{padding:'18px 20px'}}>
        <div style={{fontSize:12,color:C.textFaint,fontStyle:'italic',textAlign:'center'}}>
          Not enough area data yet.
        </div>
      </Card>
    );
  }
  const sameOne = strongest && needs && strongest.id === needs.id;
  const Cell = ({ eyebrow, row, tone }) => {
    const isPos = tone === 'positive';
    const accent    = isPos ? C.sageLight : C.sandLight;
    const borderCol = isPos ? 'rgba(94,117,88,0.45)' : 'rgba(160,138,86,0.45)';
    const bgTint    = isPos ? 'rgba(94,117,88,0.06)' : 'rgba(160,138,86,0.06)';
    return (
      <Card style={{padding:'18px 20px',background:bgTint,border:`1px solid ${borderCol}`}}>
        <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',
          textTransform:'uppercase',color:accent,marginBottom:10}}>{eyebrow}</div>
        <div style={{fontSize:18,fontWeight:500,color:C.textPrimary,
          whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis',
          marginBottom:10}} title={row.area}>{row.area}</div>
        <div style={{display:'flex',alignItems:'baseline',gap:10,fontVariantNumeric:'tabular-nums'}}>
          <div style={{fontSize:24,fontWeight:300,color:C.textPrimary,letterSpacing:'-0.01em'}}>
            {row.completed}
          </div>
          <div style={{fontSize:14,fontWeight:300,color:C.textFaint}}>/ {row.scheduled}</div>
          <div style={{flex:1}}/>
          <div style={{fontSize:14,fontWeight:600,color:accent}}>{row.percent}%</div>
        </div>
      </Card>
    );
  };
  return (
    <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(240px,1fr))',gap:14}}>
      {strongest && <Cell eyebrow="Strongest Area" row={strongest} tone="positive"/>}
      {!sameOne && needs && <Cell eyebrow="Needs Support Area" row={needs} tone="warm"/>}
    </div>
  );
};

// Combined two-column insight card — used by Top Performers and Needs Support.
// Each side shows: small eyebrow → name → completed/target · percent.
// On mobile both halves stack via flex-wrap.
const MonthlyInsightDuoCard = ({ title, tone, leftLabel, leftRow, rightLabel, rightRow }) => {
  const tr = useT();
  const isPos = tone === 'positive';
  const accent    = isPos ? C.sageLight : C.sandLight;
  const borderCol = isPos ? 'rgba(94,117,88,0.45)' : 'rgba(160,138,86,0.45)';
  const bgTint    = isPos ? 'rgba(94,117,88,0.06)' : 'rgba(160,138,86,0.06)';

  const Side = ({ label, row, withDivider }) => (
    <div style={{
      flex:'1 1 140px',minWidth:0,padding:'4px 14px',
      borderLeft: withDivider ? `1px solid ${borderCol}` : 'none',
    }}>
      <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',
        textTransform:'uppercase',color:accent,marginBottom:8}}>{label}</div>
      {row ? (
        <>
          <div style={{fontSize:17,fontWeight:500,color:C.textPrimary,
            whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis',
            marginBottom:8}} title={row.name}>{row.name}</div>
          <div style={{display:'flex',alignItems:'baseline',gap:8,fontVariantNumeric:'tabular-nums'}}>
            <div style={{fontSize:22,fontWeight:300,color:C.textPrimary,letterSpacing:'-0.01em'}}>
              {row.completed}
            </div>
            <div style={{fontSize:13,fontWeight:300,color:C.textFaint}}>/ {row.target}</div>
            <div style={{flex:1}}/>
            <div style={{fontSize:13,fontWeight:600,color:accent}}>{row.percent}%</div>
          </div>
        </>
      ) : (
        <div style={{fontSize:12,color:C.textFaint,fontStyle:'italic'}}>
          {tr('momentum.notEnoughData','Not enough data yet.')}
        </div>
      )}
    </div>
  );

  // Responsive divider via CSS-only is hard inline; use flex-wrap and a thin
  // top border on the right side when wrapped. Simpler: always render left
  // border on the right side; on wrap (mobile) it sits at the left edge of
  // the new row, which still reads cleanly.
  return (
    <Card style={{padding:'16px 4px',background:bgTint,border:`1px solid ${borderCol}`,
      height:'100%',display:'flex',flexDirection:'column'}}>
      <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
        textTransform:'uppercase',color:accent,padding:'0 14px',marginBottom:12}}>
        {title}
      </div>
      <div style={{display:'flex',flexWrap:'wrap',gap:0}}>
        <Side label={leftLabel}  row={leftRow}  withDivider={false}/>
        <Side label={rightLabel} row={rightRow} withDivider={true}/>
      </div>
    </Card>
  );
};

// Reading This Month — only render if reliable data exists.
const ReadingThisMonth = ({ reading }) => {
  const tr = useT();
  if (!reading || !reading.hasReliablePages) return null;
  const Tile = ({ eyebrow, value }) => (
    <div style={{flex:'1 1 120px',minWidth:0,padding:'14px 16px',borderRadius:12,
      background:'rgba(250,250,248,0.02)',border:`1px solid ${C.border}`}}>
      <div style={{fontSize:9,fontWeight:700,letterSpacing:'0.22em',
        textTransform:'uppercase',color:C.textFaint,marginBottom:8}}>{eyebrow}</div>
      <div style={{fontSize:24,fontWeight:300,color:C.textPrimary,letterSpacing:'-0.01em',
        fontVariantNumeric:'tabular-nums',lineHeight:1}}>{value}</div>
    </div>
  );
  return (
    <div>
      <div style={{fontSize:10,fontWeight:700,letterSpacing:'0.22em',
        textTransform:'uppercase',color:C.textFaint,marginBottom:10}}>{tr('momentum.readingThisMonth','Reading This Month')}</div>
      <div style={{display:'flex',gap:12,flexWrap:'wrap'}}>
        <Tile eyebrow={tr('momentum.readingSessionsLabel','Sessions')}       value={reading.sessions || 0}/>
        <Tile eyebrow={tr('momentum.readingPagesLabel','Pages')}          value={reading.pagesRead || 0}/>
        <Tile eyebrow={tr('momentum.readingBooksFinished','Books Finished')} value={reading.booksFinished || 0}/>
      </div>
    </div>
  );
};

const MomentumView = ({ store, goTo }) => {
  const tr = useT();
  const [isMobile, setIsMobile] = React.useState(() =>
    typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(max-width: 768px)').matches);
  React.useEffect(() => {
    if (typeof window === 'undefined' || !window.matchMedia) return;
    const mq = window.matchMedia('(max-width: 768px)');
    const onChange = e => setIsMobile(e.matches);
    mq.addEventListener ? mq.addEventListener('change', onChange) : mq.addListener(onChange);
    return () => { mq.removeEventListener ? mq.removeEventListener('change', onChange) : mq.removeListener(onChange); };
  }, []);
  const _lang = (typeof getLang === 'function') ? getLang() : 'en';
  const _dateLocale = _lang === 'es' ? 'es-ES' : 'en-US';
  const _dateStr = new Date().toLocaleDateString(_dateLocale, { weekday:'long', month:'long', day:'numeric' });
  // "How it works" guide modal — manual trigger in the page header. Auto-opens
  // once for new users (vyb-momentum-guide-seen flag).
  const [guideOpen, setGuideOpen] = React.useState(false);
  React.useEffect(() => {
    try {
      if (localStorage.getItem('vyb-momentum-guide-seen') !== '1') {
        setGuideOpen(true);
      }
    } catch {}
  }, []);
  const closeGuide = React.useCallback(() => {
    setGuideOpen(false);
    try { localStorage.setItem('vyb-momentum-guide-seen','1'); } catch {}
  }, []);
  const calc = window.calculateMomentum || (() => null);
  // Force re-render when reading sessions change (localStorage-backed).
  const [_sessTick, _setSessTick] = React.useState(0);
  React.useEffect(() => {
    const h = () => _setSessTick(x => x + 1);
    window.addEventListener('vyb-reading-sessions-changed', h);
    window.addEventListener('storage', h);
    return () => {
      window.removeEventListener('vyb-reading-sessions-changed', h);
      window.removeEventListener('storage', h);
    };
  }, []);

  // Build readingEntries from confirmed sessions only (no slider-derived data).
  const readingEntriesFromSessions = React.useMemo(() => {
    const sessions = (window.readReadingSessions && window.readReadingSessions()) || [];
    return sessions.filter(s => s && s.finishedAt).map(s => {
      const d = new Date(s.finishedAt);
      const p = (n) => String(n).padStart(2,'0');
      return { date: `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`,
        pagesRead: Number(s.pagesRead) || 0 };
    });
  }, [_sessTick]);

  const m = React.useMemo(() => calc({
    habits: store.state.habits,
    tasks:  store.state.tasks,
    ideas:  store.state.ideas,
    books:  store.state.books,
    habitCategories: store.state.habitCategories,
    readingEntries: readingEntriesFromSessions,
  }), [store.state.habits, store.state.tasks, store.state.ideas, store.state.books, store.state.habitCategories, readingEntriesFromSessions]);

  if (!m) return null;

  const { today, weekly, streak, status } = m;
  const copy = (window.getMomentumCopy && window.getMomentumCopy(m)) || {};
  const isComplete = status.key === 'complete';

  const statusPill = {
    in_rhythm:  { color: C.sandLight, border: 'rgba(160,138,86,0.45)', bg: 'rgba(160,138,86,0.10)' },
    complete:   { color: C.sageLight, border: 'rgba(94,117,88,0.55)',  bg: 'rgba(94,117,88,0.14)' },
    building:   { color: C.sandLight, border: 'rgba(160,138,86,0.30)', bg: 'rgba(160,138,86,0.06)' },
    low_signal: { color: C.textMuted, border: C.border,                bg: 'transparent' },
  }[status.key];

  // Today's signal summary line — reuses the same composition as the widget.
  const segs = [];
  if (today.totalHabits > 0)    segs.push(`${today.completedHabits}/${today.totalHabits} habit${today.totalHabits===1?'':'s'}`);
  if (today.completedTasks > 0) segs.push(`${today.completedTasks} task${today.completedTasks===1?'':'s'}`);
  if (today.ideasCaptured > 0)  segs.push(`${today.ideasCaptured} idea${today.ideasCaptured===1?'':'s'}`);
  if (today.readingSessions > 0) segs.push(`${today.readingSessions} reading`);
  if (today.focusSessions > 0)  segs.push(`${today.focusSessions} focus`);
  const summary = segs.length ? segs.join(' · ') : (window.t ? window.t('overview.startWithSmall', 'Start with one small win.') : 'Start with one small win.');

  const accent = isComplete ? C.sage : C.sand;
  const accentLight = isComplete ? C.sageLight : C.sandLight;

  // Active book progress for the Reading breakdown row.
  const activeBook = store.state.books.find(b => b && b.status === 'reading');

  // Counts used to gate the empty-account starter state.
  const counts = {
    habits: (store.state.habits  || []).length,
    tasks:  (store.state.tasks   || []).length,
    ideas:  (store.state.ideas   || []).length,
    books:  (store.state.books   || []).length,
  };
  const isEmptyAccount = counts.habits === 0 && counts.tasks === 0
    && counts.ideas === 0 && counts.books === 0;

  // Momentum V2 — Update 1: Today section + Week/Month dividers + Aura.
  // Old chart components (MomentumTrendChart, AreaRadarChart, MomentumHeatmap,
  // HabitConsistencyCompact, ReadingSignalCard, MomentumScoreCard, TodaySignals,
  // NextMoveCard, TodayOverview, MomentumStarter, AIReflectionPlaceholder,
  // SystemRecommendation, MomentumGuideModal, MomentumHowItWorks, GuideVisual)
  // stay defined for potential reuse but are NOT rendered in this pass.

  // Extras = habits checked today that were NOT scheduled today.
  const todayKey = (() => {
    const d = new Date();
    const p = (n) => String(n).padStart(2,'0');
    return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`;
  })();
  const isSched = window.isHabitScheduledOn || (() => true);
  const _todayDate = new Date();
  const extrasCompletedToday = (store.state.habits || []).filter(h =>
    h && h.checkIns && h.checkIns[todayKey] && !isSched(h, _todayDate)
  ).length;

  // Weekly extras = checks beyond what was scheduled this week.
  const weeklyExtras = (weekly && weekly.consistency || []).reduce(
    (acc, r) => acc + Math.max(0, (r.completedAnyThisWeek||0) - (r.completedThisWeek||0)),
    0
  );

  return (
    <div style={{paddingBottom:40,display:'flex',flexDirection:'column',gap:28}}>
      {/* ── Momentum header ─────────────────────────────────── */}
      {isMobile ? (
        <div className="vyb-momentum-header" style={{marginTop:8,marginBottom:4}}>
          <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',gap:12,marginBottom:6}}>
            <div style={{fontSize:10,fontWeight:200,letterSpacing:'0.22em',color:C.textFaint,textTransform:'uppercase',whiteSpace:'nowrap'}}>{tr('momentum.label','Progress')}</div>
            <div style={{fontSize:10,fontWeight:300,letterSpacing:'0.12em',color:C.textFaint,textTransform:'uppercase',textAlign:'right',whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis',maxWidth:'58%'}}>{_dateStr}</div>
          </div>
          <div className="vyb-page-title" style={{fontSize:34,fontWeight:900,fontStyle:'italic',color:C.textPrimary,letterSpacing:'-0.025em',textTransform:'uppercase',lineHeight:1,marginBottom:12,wordBreak:'break-word'}}>
            {tr('momentum.title','Momentum')}
          </div>
          <div style={{display:'flex',justifyContent:'flex-end'}}>
            <button onClick={()=>setGuideOpen(true)}
              style={{background:'transparent',border:`1px solid ${C.border}`,borderRadius:8,
                padding:'6px 10px',color:C.textMuted,cursor:'pointer',
                fontFamily:'Montserrat,sans-serif',fontSize:9,fontWeight:600,
                letterSpacing:'0.16em',textTransform:'uppercase',
                display:'inline-flex',alignItems:'center',gap:6,transition:'all 0.15s'}}>
              <Icon name="info" size={10} color="currentColor"/>
              {tr('momentum.guide.button','How it works')}
            </button>
          </div>
        </div>
      ) : (
        <div className="vyb-momentum-header" style={{marginTop:32,marginBottom:4,display:'flex',justifyContent:'space-between',
          alignItems:'flex-start',gap:16,flexWrap:'wrap'}}>
          <div style={{minWidth:0,flex:'1 1 auto'}}>
            <div className="vyb-page-title" style={{fontSize:38,fontWeight:300,color:C.textPrimary,
              letterSpacing:'-0.02em',lineHeight:1.05,marginBottom:0}}>
              {tr('momentum.title','Momentum')}
            </div>
          </div>
          <button onClick={()=>setGuideOpen(true)}
            style={{background:'transparent',border:`1px solid ${C.border}`,borderRadius:8,
              padding:'8px 12px',color:C.textMuted,cursor:'pointer',
              fontFamily:'Montserrat,sans-serif',fontSize:10,fontWeight:600,
              letterSpacing:'0.16em',textTransform:'uppercase',
              display:'inline-flex',alignItems:'center',gap:7,transition:'all 0.15s',
              flexShrink:0}}
            onMouseEnter={e=>{e.currentTarget.style.color=C.sandLight;e.currentTarget.style.borderColor='rgba(160,138,86,0.45)';}}
            onMouseLeave={e=>{e.currentTarget.style.color=C.textMuted;e.currentTarget.style.borderColor=C.border;}}>
            <Icon name="info" size={11} color="currentColor"/>
            {tr('momentum.guide.button','How it works')}
          </button>
        </div>
      )}

      {guideOpen && <MomentumGuideModal onClose={closeGuide}/>}

      {/* ── MOMENTUM LOCKED (reuses Overview's DailyCompletionSeal) ─── */}
      <DailyCompletionSeal store={store}/>

      {/* ── TODAY ───────────────────────────────────────────── */}
      <MomentumPhaseHeader title={tr('momentum.todayTitle','Today')} subtitle={tr('momentum.todaySubtitle','Your progress today.')}/>
      <TodayHabitsCard
        planned={today.totalHabits}
        completed={today.completedHabits}
        extras={extrasCompletedToday}
        isComplete={today.isDailyHabitsComplete}
      />
      <TodaySnapshotCard
        tasks={today.completedTasks}
        ideas={today.ideasCaptured}
        reading={today.readingSessions}
        readingPages={today.pagesRead}
      />

      {/* ── THIS WEEK ───────────────────────────────────────── */}
      <MomentumPhaseHeader title={tr('momentum.weekTitle','This Week')} subtitle={tr('momentum.weekSubtitle','Your progress this week.')}/>
      <WeeklyProgressCard days={weekly && weekly.days}/>
      <HabitConsistencyWeek rows={weekly && weekly.consistency}/>
      <BestNeedsSupportPair
        rows={(weekly && weekly.consistency || []).map(r => {
          const target = r.weeklyTarget || r.scheduledThisWeek || 0;
          const completed = Math.min(r.completedAnyThisWeek || 0, target);
          return { id:r.id, name:r.name, completed, target,
            percent: target ? Math.round((completed/target)*100) : 0 };
        })}
        periodLabel="this week"/>
      <WeeklyAreaRadar areas={weekly && weekly.areaBalance}/>

      {/* ── THIS MONTH ───────────────────────────────────────── */}
      <MomentumPhaseHeader title={tr('momentum.monthTitle','This Month')} subtitle={tr('momentum.monthSubtitle','Your bigger pattern.')}/>
      <MonthlyMomentumMap
        period={m && m.month && m.month.period}
        days={m && m.month && m.month.days}
        monthlyPercent={(() => {
          const t = m && m.month && m.month.totals;
          if (!t || !(t.habitsTarget > 0)) return null;
          return Math.round((t.habitsCompleted / t.habitsTarget) * 100);
        })()}/>
      <MonthlyTotalsCard totals={m && m.month && m.month.totals}/>
      <CompleteActiveDaysPair
        completeDays={m && m.month && m.month.completeDays}
        activeDays={m && m.month && m.month.activeDays}
        daysElapsed={m && m.month && m.month.period && m.month.period.daysElapsed}/>
      {(() => {
        const mo = m && m.month || {};
        const bh = mo.bestHabit || null;
        const nsh = mo.needsSupportHabit || null;
        const sa = mo.strongestArea || null;
        const nsa = mo.needsSupportArea || null;
        const habitRow = (h) => h && (h.target || 0) > 0
          ? { name:h.name, completed:h.completed, target:h.target, percent:h.percent }
          : null;
        const areaRow = (a) => a && (a.scheduled || 0) > 0
          ? { name:a.area, completed:a.completed, target:a.scheduled, percent:a.percent }
          : null;
        // Only show needs side when truly behind (avoid surfacing 100%-done as "needs support").
        const needsHabitRow = nsh && nsh.percent < 100 ? habitRow(nsh) : null;
        const needsAreaRow  = nsa && nsa.percent  < 100 ? areaRow(nsa)  : null;
        return (
          <div style={{display:'grid',gap:14,
            gridTemplateColumns:'repeat(auto-fit, minmax(280px, 1fr))',
            alignItems:'stretch'}}>
            <MonthlyInsightDuoCard
              title={tr('momentum.topPerformers','Top Performers')} tone="positive"
              leftLabel={tr('momentum.bestHabit','Best Habit')}     leftRow={habitRow(bh)}
              rightLabel={tr('momentum.strongestArea','Strongest Area')} rightRow={areaRow(sa)}/>
            <MonthlyInsightDuoCard
              title={tr('momentum.needsSupport','Needs Support')} tone="warm"
              leftLabel={tr('momentum.duoHabit','Habit')}  leftRow={needsHabitRow}
              rightLabel={tr('momentum.duoArea','Area')}  rightRow={needsAreaRow}/>
          </div>
        );
      })()}
      <ReadingThisMonth reading={m && m.month && m.month.reading}/>

      {/* ── Aura — preserved. Do not delete. ────────────────── */}
      <PathsAndArtifactsSection momentum={m} store={store}/>
    </div>
  );
};

// Tiny ghost button placed above non-Overview sections so the user always
// has a one-click escape back to the dashboard. Subtle by design — it
// shouldn't compete with the page title or sidebar nav.
const BackToOverviewButton = ({ onClick, compact = false }) => {
  const tr = (typeof useT === 'function') ? useT() : ((k, f) => f);
  const [hover, setHover] = React.useState(false);
  return (
    <button onClick={onClick} className="vyb-back-overview"
      onMouseEnter={()=>setHover(true)} onMouseLeave={()=>setHover(false)}
      style={{
        background:'transparent',
        border:'none',
        borderRadius:6,
        padding:compact?'6px 8px':'4px 6px',
        color:hover?C.sandLight:C.textFaint,cursor:'pointer',
        fontFamily:'Montserrat,sans-serif',fontSize:9,fontWeight:600,
        letterSpacing:'0.18em',textTransform:'uppercase',
        display:'inline-flex',alignItems:'center',gap:6,
        transition:'color 0.15s',
      }}>
      <Icon name="arrow-left" size={10} color="currentColor"/>
      {compact
        ? tr('overview.kicker','Overview')
        : tr('nav.backToOverview','Back to Overview')}
    </button>
  );
};

Object.assign(window, { IdeasView, TasksView, MoodView, OverviewPage, MomentumView, AIReflectionPlaceholder,
  ARTIFACTS, RARITY_STYLE, ArtifactSigil, RarityPill, displayRarity, PATHS, computePathsState, readUnlockedArtifacts, readActiveArtifact,
  BackToOverviewButton,
  useDictation, appendDictated, DictationButton, RecordingComposer });

