/* Calendar tab — month nav rail, monthly grid, day timeline, idea pool */

const MONTH_NAMES = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const WEEKDAYS = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
const STATUS_COLOR = {
  live:      { dot: 'var(--good)',  label: 'Live'        },
  scheduled: { dot: 'var(--accent)',label: 'Scheduled'   },
  draft:     { dot: 'var(--fg-3)',  label: 'Draft'       },
  idea:      { dot: 'var(--warn)',  label: 'Idea'        },
};
const STATUS_LIST = ['live','scheduled','draft','idea'];           // shown in filters + grid chips
// Idea is selectable too — picking it swaps the "From Mediathek" picker for
// "From Ideas" (saved external references). Live is still read-only since it
// only exists for posts that have already shipped.
const EDITABLE_STATUS_LIST = ['scheduled','draft','idea'];
// v3: every non-idea sample post now ships with a campaign + media so the
// scheduled queue matches what real production data looks like. Bumping the
// key drops any stale v2 entries that lacked those fields.
const STORAGE_KEY = 'hookline:calendar-posts:v4';
const LEGACY_KEY  = 'hookline:calendar-posts:v3';
const COMPOSE_INCOMING_KEY = 'hookline:compose-incoming';

const HOURS   = Array.from({ length: 24 }, (_, i) => i);
const MINUTES = Array.from({ length: 12 }, (_, i) => i * 5); // 0,5,…,55

function daysInMonth(year, monthIdx) {
  return new Date(year, monthIdx + 1, 0).getDate();
}
function fmt2(n) { return String(n).padStart(2, '0'); }
function uid() {
  return 'p_' + Math.random().toString(36).slice(2, 9) + Date.now().toString(36).slice(-3);
}

function normalizePost(p) {
  // A post is either dated (d set → time/channel/kind filled) or pooled
  // (no d → time/ch/mk/kind all stay null until the user schedules it via
  // the editor). Skipping month/year defaults for pooled keeps them out of
  // month-bucket stats and the calendar grid.
  const hasDate = p.d != null;
  // Migrate kinds the platform no longer supports for scheduling (e.g. an
  // old saved fb 'live' post becomes 'video'). Skipped for pooled posts
  // since they don't have a channel yet.
  let safeKind = p.kind ?? null;
  if (p.ch) {
    const allowed = new Set(kindsForChannel(p.ch).map(k => k.id));
    safeKind = p.kind && allowed.has(p.kind) ? p.kind : defaultKindForChannel(p.ch);
  }
  return {
    id: p.id || uid(),
    d: p.d ?? null,
    h: p.h ?? (hasDate ? 12 : null),
    min: p.min ?? (hasDate ? 0 : null),
    m: p.m ?? (hasDate ? TODAY.getMonth() : null),
    y: p.y ?? (hasDate ? TODAY.getFullYear() : null),
    ch: p.ch ?? null,
    mk: p.mk ?? null,
    title: p.title, kind: safeKind, status: p.status,
    campaign: p.campaign ?? null,
    media: p.media ?? null,
    // For status='idea' posts: id into IDEA_LIBRARY pointing at the saved
    // external reference inspiring this slot. Kept on the post even after the
    // status flips so we don't lose the reference when the user toggles.
    idea: p.idea ?? null,
  };
}

// Compare two YMD-coords to TODAY (date-only, ignoring hours/minutes)
function startOfDay(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate()); }
const TODAY_START = startOfDay(TODAY);
function isFutureOrToday({ y, m, d }) {
  return new Date(y, m, d) >= TODAY_START;
}

// Media is platform-agnostic now — the same asset can be reused on any channel.
// These helpers kept their names for backward compatibility with existing call
// sites; the `ch` argument is ignored.
function mediaForChannel(_ch) {
  return MEDIA_ASSETS;
}
function campaignsForChannel(_ch) {
  const ids = new Set(MEDIA_ASSETS.map(a => a.campaign));
  return CAMPAIGNS.filter(c => ids.has(c.id));
}

function CalendarTab() {
  const [view, setView] = useState('month');
  const [monthIdx, setMonthIdx] = useState(TODAY.getMonth());
  const [year, setYear] = useState(TODAY.getFullYear());
  const [selectedDay, setSelectedDay] = useState(TODAY.getDate());
  const [filterCh, setFilterCh] = useState('all');
  const [filterStatus, setFilterStatus] = useState('all');
  const [filterMk, setFilterMk] = useState('all');
  const [filtersOpen, setFiltersOpen] = useState(false);
  const [toast, setToast] = useState(null);

  const [posts, setPosts] = useState(() => {
    // Drop any posts pointing at retired channels (li/x) that might be sitting
    // in localStorage from earlier versions of the app.
    // Drop posts on retired channels (e.g. an old li/x post lingering in
    // localStorage from earlier app versions). Pooled ideas have no channel
    // yet, so a null ch is allowed through.
    const keepActive = (list) => list.filter(p => p.ch == null || ALLOWED_CH.has(p.ch));
    try {
      const v2 = localStorage.getItem(STORAGE_KEY);
      if (v2) return keepActive(JSON.parse(v2).map(normalizePost));
      const v1 = localStorage.getItem(LEGACY_KEY);
      if (v1) return keepActive(JSON.parse(v1).map(normalizePost));
    } catch (e) {}
    return keepActive(POSTS.map(normalizePost));
  });
  useEffect(() => {
    try { localStorage.setItem(STORAGE_KEY, JSON.stringify(posts)); } catch (e) {}
  }, [posts]);

  const [editor, setEditor] = useState(null);

  // ---- Drag-and-drop state (shared across views) ----
  const [dragId, setDragId] = useState(null);
  const [dropHover, setDropHover] = useState(null);
  const setDropHoverIfChanged = (key) => setDropHover(prev => (prev === key ? prev : key));

  // Auto-scroll the main route container while dragging: when the cursor sits
  // within EDGE px of the top or bottom of the viewport, we tick the
  // scrollTop on a rAF loop. Velocity grows the closer the cursor is to the
  // edge so users get fine control near the boundary and a fast ride when
  // they shove all the way to the edge. Without this you can only drop on
  // dates already visible on screen.
  useEffect(() => {
    if (!dragId) return;
    const scroller = document.querySelector('.route-anim');
    if (!scroller) return;

    const EDGE = 90;          // px — autoscroll zone at top & bottom
    const MAX_SPEED = 22;     // px per frame at the very edge
    let velocity = 0;
    let raf = null;

    const tick = () => {
      if (velocity === 0) { raf = null; return; }
      scroller.scrollTop += velocity;
      raf = requestAnimationFrame(tick);
    };

    const onDragOver = (e) => {
      const rect = scroller.getBoundingClientRect();
      if (e.clientY < rect.top + EDGE) {
        const dist = Math.max(0, e.clientY - rect.top);
        velocity = -Math.max(2, Math.round(MAX_SPEED * (1 - dist / EDGE)));
      } else if (e.clientY > rect.bottom - EDGE) {
        const dist = Math.max(0, rect.bottom - e.clientY);
        velocity = Math.max(2, Math.round(MAX_SPEED * (1 - dist / EDGE)));
      } else {
        velocity = 0;
      }
      if (velocity !== 0 && raf == null) raf = requestAnimationFrame(tick);
    };

    document.addEventListener('dragover', onDragOver);
    return () => {
      document.removeEventListener('dragover', onDragOver);
      if (raf) cancelAnimationFrame(raf);
    };
  }, [dragId]);

  const filtered = posts.filter(p =>
    (filterCh     === 'all' || p.ch     === filterCh) &&
    (filterStatus === 'all' || p.status === filterStatus) &&
    (filterMk     === 'all' || p.mk     === filterMk)
  );
  const monthPosts = filtered.filter(p => p.m === monthIdx && p.y === year);
  const activeFilterCount = (filterStatus !== 'all' ? 1 : 0) + (filterMk !== 'all' ? 1 : 0);

  const goToday = () => {
    setMonthIdx(TODAY.getMonth());
    setYear(TODAY.getFullYear());
    setSelectedDay(TODAY.getDate());
  };
  const goPrev = () => {
    if (monthIdx === 0) { setMonthIdx(11); setYear(y => y - 1); }
    else setMonthIdx(m => m - 1);
  };
  const goNext = () => {
    if (monthIdx === 11) { setMonthIdx(0); setYear(y => y + 1); }
    else setMonthIdx(m => m + 1);
  };

  const openNew = (init = {}) => setEditor({
    mode: 'new',
    post: normalizePost({
      d: selectedDay, h: 12, min: 0,
      m: monthIdx, y: year,
      ch: 'ig', mk: 'de',
      title: '', kind: defaultKindForChannel('ig'), status: 'scheduled',
      campaign: null, media: null, idea: null,
      ...init,
    }),
  });
  const openEdit = (post) => {
    const normalized = normalizePost(post);
    setEditor({
      mode: 'edit', originalId: post.id,
      post: { ...normalized },
      // Baseline snapshot for dirty detection. Save is only allowed when the
      // user changed nothing beyond date/time — anything else has to go through
      // Compose so the caption stays in sync with the planning fields.
      original: { ...normalized },
    });
  };
  const savePost = () => {
    if (!editor || !editor.post.title.trim()) return;
    setPosts(prev => editor.mode === 'new'
      ? [...prev, editor.post]
      : prev.map(p => p.id === editor.originalId ? editor.post : p));
    flash(editor.mode === 'new' ? 'Post scheduled' : 'Post updated');
    setEditor(null);
  };

  // Hand the planning over to the Compose tab — caption + creative preview live there.
  // No save here; Compose owns the final schedule action.
  const continueInCompose = () => {
    if (!editor) return;
    const p = editor.post;
    const dt = new Date(p.y, p.m, p.d, p.h, p.min);
    const pad = (n) => String(n).padStart(2, '0');
    const scheduleAt =
      `${dt.getFullYear()}-${pad(dt.getMonth()+1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
    const incoming = {
      title: p.title,
      channels: [p.ch],
      markets: [p.mk],
      scheduleAt,
      media: p.media || null,
      campaign: p.campaign || null,
      // Pass the inspiration reference along so Compose can show "you're
      // creating this in response to <idea>" even though it won't end up in
      // the final post payload.
      idea: p.idea || null,
      kind: p.kind,
      status: p.status,
      origin: editor.mode === 'edit' ? { id: editor.originalId } : null,
    };
    try { sessionStorage.setItem(COMPOSE_INCOMING_KEY, JSON.stringify(incoming)); } catch (e) {}
    // If this editor opened from a drag-drop reschedule, revert the optimistic
    // calendar update. The new date now rides along in the Compose handoff and
    // only commits when Compose actually publishes. That way returning to the
    // calendar after abandoning Compose doesn't leave the post on a date the
    // user never confirmed.
    if (editor.revertTo) {
      const revert = editor.revertTo;
      setPosts(prev => prev.map(p => p.id === revert.id ? revert : p));
      flash('Reschedule held — commits when you publish from Compose');
    }
    setEditor(null);
    window.postMessage({ type: 'navigate', to: 'compose' }, '*');
  };
  const deletePostById = (id) => {
    setPosts(prev => prev.filter(p => p.id !== id));
    flash('Removed');
    if (editor && editor.originalId === id) setEditor(null);
  };

  // Drag-drop: update the post immediately, then open the editor preloaded so the
  // user can confirm (save) or revert (cancel). The revertTo snapshot is restored
  // on cancel via closeEditor below.
  const dropPostOnto = (id, target) => {
    setDragId(null); setDropHover(null);
    const original = posts.find(p => p.id === id);
    if (!original) return;
    const updated = { ...original, ...target };
    const sameSpot = original.d === updated.d && original.m === updated.m && original.y === updated.y
                  && original.h === updated.h && original.min === updated.min;
    if (sameSpot) return;
    setPosts(prev => prev.map(p => p.id === id ? updated : p));
    setEditor({
      mode: 'edit',
      originalId: id,
      post: normalizePost(updated),
      revertTo: original,
      // Baseline is the pre-drop post — so date/time appear dirty and Save is
      // enabled, while caption/media/etc. read as untouched.
      original: normalizePost(original),
    });
  };

  const closeEditor = () => {
    if (editor?.revertTo) {
      const revert = editor.revertTo;
      setPosts(prev => prev.map(p => p.id === revert.id ? revert : p));
      flash('Reschedule reverted');
    }
    setEditor(null);
  };
  // Pool ideas — pull every saved external reference (IDEA_LIBRARY) into the
  // idea pool as a date-less idea post. The pool is the staging area for
  // "things I saw on IG/TT/FB/YT and want to do some day"; scheduling slots
  // gets decided later. Re-clicking is idempotent: only refs not already in
  // the pool get added.
  const poolIdeas = () => {
    const alreadyPooled = new Set(
      posts.filter(p => p.status === 'idea' && p.d == null && p.idea).map(p => p.idea)
    );
    const fresh = IDEA_LIBRARY
      .filter(ref => !alreadyPooled.has(ref.id))
      .map(ref => normalizePost({
        id: 'pool_' + ref.id,
        status: 'idea',
        idea: ref.id,
        title: ref.title,
        // Intentionally no d/h/min/m/y/ch/mk — these get chosen when the user
        // clicks Schedule on the card.
      }));
    if (fresh.length === 0) {
      flash('All saved ideas are already in the pool');
      return;
    }
    setPosts(prev => [...prev, ...fresh]);
    flash(`Pulled ${fresh.length} saved idea${fresh.length === 1 ? '' : 's'} into the pool`);
  };

  // Schedule a pooled idea — open the editor pre-loaded with today's date,
  // the user's current wall-clock time, and the idea reference already
  // selected. The user fills in channel / market / kind / campaign themselves.
  // (We use the user's real Date for the time component because the app's
  // TODAY mock has no time, which would otherwise pre-fill 00:00.)
  const scheduleFromPool = (idea) => {
    const wall = new Date();
    const min5raw = Math.round(wall.getMinutes() / 5) * 5;
    const carryHour = min5raw === 60;
    const prefilled = normalizePost({
      ...idea,
      d: TODAY.getDate(),
      m: TODAY.getMonth(),
      y: TODAY.getFullYear(),
      h: carryHour ? (wall.getHours() + 1) % 24 : wall.getHours(),
      min: carryHour ? 0 : min5raw,
      ch: idea.ch || 'ig',
      mk: idea.mk || 'de',
      kind: idea.kind || defaultKindForChannel(idea.ch || 'ig'),
      status: 'idea',
    });
    setEditor({
      mode: 'edit',
      originalId: idea.id,
      post: prefilled,
      // Pre-fill IS the baseline — saving immediately without changes works.
      // The user touched nothing yet beyond accepting the defaults.
      original: prefilled,
    });
  };
  const resetData = () => {
    setPosts(POSTS.map(normalizePost));
    try { localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(LEGACY_KEY); } catch (e) {}
    setFiltersOpen(false);
    flash('Sample data restored');
  };

  const toastTimer = useRef(null);
  const flash = (msg) => {
    setToast(msg);
    if (toastTimer.current) clearTimeout(toastTimer.current);
    toastTimer.current = setTimeout(() => setToast(null), 1800);
  };

  return (
    <div style={{ padding: '24px 36px 60px', display: 'flex', flexDirection: 'column', gap: 22 }}>

      {/* Toolbar */}
      <div className="anim-fade-up" style={{
        display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap',
        position: 'relative', zIndex: filtersOpen ? 1000 : 'auto',
      }}>
        <MonthStepper monthIdx={monthIdx} year={year} onPrev={goPrev} onNext={goNext} onToday={goToday} />

        <div className="seg">
          <button className={view==='month'?'on':''}    onClick={() => setView('month')}>Month</button>
          <button className={view==='week'?'on':''}     onClick={() => setView('week')}>Week</button>
          <button className={view==='timeline'?'on':''} onClick={() => setView('timeline')}>Timeline</button>
        </div>

        <div className="seg">
          <button className={filterCh==='all'?'on':''} onClick={()=>setFilterCh('all')}>All</button>
          {CHANNELS.map(c => (
            <button key={c.code} className={filterCh===c.code?'on':''} onClick={()=>setFilterCh(c.code)} style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
              <ChannelGlyph ch={c.code} size={10} /> {c.name}
            </button>
          ))}
        </div>

        <div style={{ flex: 1 }} />

        <div style={{ display: 'flex', gap: 8, position: 'relative', zIndex: filtersOpen ? 1000 : 'auto' }}>
          <button className="btn press" onClick={()=>setFiltersOpen(o=>!o)}>
            <Icon name="filter_list" size={16} /> Filters
            {activeFilterCount > 0 && (
              <span className="chip accent" style={{ padding: '0 6px', height: 16, fontSize: 10 }}>{activeFilterCount}</span>
            )}
          </button>
          {filtersOpen && (
            <FiltersDropdown
              filterStatus={filterStatus} setFilterStatus={setFilterStatus}
              filterMk={filterMk}         setFilterMk={setFilterMk}
              onReset={resetData}
              onClose={()=>setFiltersOpen(false)}
            />
          )}
          <button className="btn primary press" onClick={()=>openNew()}>
            <Icon name="add" size={16} /> Schedule post
          </button>
        </div>
      </div>

      <YearRail monthIdx={monthIdx} setMonthIdx={setMonthIdx} year={year} posts={filtered} />

      <PlanningLegend monthPosts={monthPosts} />

      <div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 14 }}>
        <Stat label="Scheduled this month" value={monthPosts.filter(p=>p.status==='scheduled').length} sub={`across ${new Set(monthPosts.map(p=>p.mk)).size || 0} markets`} icon="event_available" tone="accent" />
        <Stat label="Live so far"          value={monthPosts.filter(p=>p.status==='live').length}      sub={`${MONTH_NAMES[monthIdx].slice(0,3)} · current month`} icon="visibility" tone="good" />
        <Stat
          label="Needs composing"
          value={monthPosts.filter(p => {
            const lvl = planningSign(p).level;
            return lvl === 'planned' || lvl === 'urgent';
          }).length}
          sub="planned + ideas combined"
          icon="edit_note"
          tone="warn"
        />
        <Stat label="Ideas in pipeline"    value={monthPosts.filter(p=>p.status==='idea').length}      sub="unscheduled" icon="lightbulb" tone="warn" />
      </div>

      {(() => {
        const drag = {
          dragId,
          dropHover,
          onStart: (id) => setDragId(id),
          onEnd:   () => { setDragId(null); setDropHover(null); },
          onHover: setDropHoverIfChanged,
          onDrop:  dropPostOnto,
        };
        return (
          <React.Fragment>
            {view === 'month' && (
              <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 380px', gap: 20 }}>
                <MonthGrid monthIdx={monthIdx} year={year} posts={monthPosts} selectedDay={selectedDay} setSelectedDay={setSelectedDay} onPostClick={openEdit} drag={drag} />
                <DayPanel  monthIdx={monthIdx} year={year} day={selectedDay} posts={monthPosts} onPostClick={openEdit} onSchedule={()=>openNew()} drag={drag} />
              </div>
            )}

            {view === 'week' &&
              <WeekStrip monthIdx={monthIdx} year={year} selectedDay={selectedDay} setSelectedDay={setSelectedDay} posts={filtered} onPostClick={openEdit} drag={drag} />
            }
            {view === 'timeline' &&
              <TimelineView monthIdx={monthIdx} year={year} posts={monthPosts} onPostClick={openEdit} />
            }
          </React.Fragment>
        );
      })()}

      <IdeaPool
        // Pool = idea-status posts that haven't been scheduled yet (d == null).
        // Once an idea picks up a date via scheduleFromPool, it leaves the pool
        // and joins the calendar grid. Channel/market filters intentionally do
        // not apply here because pooled ideas don't have those fields yet.
        posts={posts.filter(p => p.status === 'idea' && p.d == null)}
        onPool={poolIdeas}
        onSchedule={scheduleFromPool}
        onDismiss={(p)=>deletePostById(p.id)}
      />

      {editor && (
        <EditorModal
          editor={editor}
          onChange={(patch)=>setEditor(e => ({ ...e, post: { ...e.post, ...patch } }))}
          onSave={savePost}
          onContinue={continueInCompose}
          onDelete={()=>deletePostById(editor.originalId)}
          onClose={closeEditor}
        />
      )}

      {toast && (
        <div className="anim-fade-up" style={{
          position: 'fixed', left: '50%', bottom: 28, transform: 'translateX(-50%)',
          background: 'var(--bg-3)', border: '1px solid var(--line-2)', color: 'var(--fg)',
          padding: '10px 16px', borderRadius: 10, fontSize: 13, zIndex: 100,
          boxShadow: '0 12px 32px -8px rgba(0,0,0,.5)',
        }}>{toast}</div>
      )}
    </div>
  );
}

function MonthStepper({ monthIdx, year, onPrev, onNext, onToday }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
      <button onClick={onPrev} className="btn ghost press" style={{ width: 36, padding: 0, justifyContent: 'center' }}>
        <Icon name="chevron_left" size={18} />
      </button>
      <div className="display" style={{ fontSize: 22, minWidth: 200 }}>
        {MONTH_NAMES[monthIdx]} <span style={{ color: 'var(--fg-3)', fontWeight: 500 }}>{year}</span>
      </div>
      <button onClick={onNext} className="btn ghost press" style={{ width: 36, padding: 0, justifyContent: 'center' }}>
        <Icon name="chevron_right" size={18} />
      </button>
      <button onClick={onToday} className="btn sm press">Today</button>
    </div>
  );
}

function YearRail({ monthIdx, setMonthIdx, year, posts }) {
  const counts = Array.from({ length: 12 }, (_, i) =>
    posts.filter(p => p.m === i && p.y === year).length
  );
  const max = Math.max(1, ...counts);
  return (
    <div className="panel anim-fade-up" style={{ padding: 12 }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(12, 1fr)', gap: 6 }}>
        {MONTH_NAMES.map((m, i) => {
          const isActive = i === monthIdx;
          const count = counts[i];
          const pct = count / max;
          return (
            <button key={m} onClick={() => setMonthIdx(i)} className="press" style={{
              background: isActive ? 'var(--bg-3)' : 'transparent',
              border: '1px solid ' + (isActive ? 'var(--line-3)' : 'transparent'),
              borderRadius: 10, padding: '10px 8px',
              display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 6,
              cursor: 'pointer', color: 'var(--fg)',
              transition: 'all .25s var(--ease)',
            }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', width: '100%', alignItems: 'baseline' }}>
                <span className="mono" style={{ fontSize: 10.5, color: isActive ? 'var(--fg)' : 'var(--fg-3)', letterSpacing: 0.06 }}>{m.slice(0,3).toUpperCase()}</span>
                <span className="num" style={{ fontSize: 11, color: isActive ? 'var(--fg)' : 'var(--fg-4)' }}>{count}</span>
              </div>
              <div style={{ width: '100%', height: 4, background: 'var(--bg-3)', borderRadius: 99, overflow: 'hidden' }}>
                <div style={{
                  width: `${pct * 100}%`, height: '100%',
                  background: isActive ? 'var(--fg)' : 'var(--line-3)',
                  borderRadius: 99,
                  transition: 'width .6s var(--ease)',
                }} />
              </div>
            </button>
          );
        })}
      </div>
    </div>
  );
}

function PlanningLegend({ monthPosts }) {
  const counts = monthPosts.reduce((acc, p) => {
    const lvl = planningSign(p).level;
    acc[lvl] = (acc[lvl] || 0) + 1;
    return acc;
  }, {});
  const ready   = (counts.live || 0) + (counts.ready || 0);
  const planned = counts.planned || 0;
  const urgent  = counts.urgent || 0;
  const ideaCount = monthPosts.filter(p => p.status === 'idea').length;

  const Item = ({ swatch, label, sub, count }) => (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
      {swatch}
      <div style={{ display: 'flex', flexDirection: 'column', minWidth: 0 }}>
        <span style={{ fontSize: 12, fontWeight: 500, color: 'var(--fg)' }}>
          {label}{count != null && <span className="num" style={{ marginLeft: 6, color: 'var(--fg-3)' }}>{count}</span>}
        </span>
        <span className="mono" style={{ fontSize: 10, color: 'var(--fg-3)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{sub}</span>
      </div>
    </div>
  );

  const Dot = ({ color, size = 9, glow }) => (
    <span style={{
      width: size, height: size, borderRadius: 99,
      background: color, flexShrink: 0,
      boxShadow: glow ? '0 0 0 3px rgba(255,138,126,.15)' : undefined,
    }} />
  );

  return (
    <div className="panel anim-fade-up" style={{
      padding: '10px 14px',
      display: 'flex', alignItems: 'center', gap: 22, flexWrap: 'wrap',
    }}>
      <div className="caps" style={{ color: 'var(--fg-3)', flexShrink: 0 }}>Planning legend</div>
      <Item
        swatch={<Dot color="var(--good)" />}
        label="Ready"
        sub="Composed · scheduled or live"
        count={ready}
      />
      <Item
        swatch={<Dot color="var(--warn)" />}
        label="Planned"
        sub="Needs composing"
        count={planned}
      />
      <Item
        swatch={<Dot color="var(--bad)" glow />}
        label="Critical"
        sub="≤1 d for posts · ≤3 d for ideas"
        count={urgent}
      />
      <Item
        swatch={<Icon name="lightbulb" size={14} fill style={{ color: 'var(--warn)' }} />}
        label="Idea"
        sub="Lightbulb badge replaces dot"
        count={ideaCount}
      />
    </div>
  );
}

function Stat({ label, value, sub, icon, tone }) {
  const toneColor = tone === 'good' ? 'var(--good)' : tone === 'accent' ? 'var(--accent)' : tone === 'warn' ? 'var(--warn)' : 'var(--fg-2)';
  return (
    <div className="panel lift" style={{ padding: 18, display: 'flex', flexDirection: 'column', gap: 6 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <span className="caps" style={{ color: 'var(--fg-3)' }}>{label}</span>
        <Icon name={icon} size={18} style={{ color: toneColor }} />
      </div>
      <div className="display" style={{ fontSize: 36, lineHeight: 1, color: toneColor }}>
        <Counter to={value} />
      </div>
      <div style={{ fontSize: 12, color: 'var(--fg-3)' }}>{sub}</div>
    </div>
  );
}

function MonthGrid({ monthIdx, year, posts, selectedDay, setSelectedDay, onPostClick, drag }) {
  const firstDay = new Date(year, monthIdx, 1);
  const startCol = (firstDay.getDay() + 6) % 7;
  const dim = daysInMonth(year, monthIdx);
  const cells = [];
  for (let i = 0; i < startCol; i++) cells.push(null);
  for (let d = 1; d <= dim; d++) cells.push(d);
  while (cells.length % 7 !== 0) cells.push(null);

  const isToday = (d) =>
    d === TODAY.getDate() && monthIdx === TODAY.getMonth() && year === TODAY.getFullYear();

  return (
    <div className="panel anim-fade-up" style={{ padding: 14, minWidth: 0 }}>
      <div style={{ borderRadius: 10, overflow: 'hidden' }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 1, background: 'var(--line)' }}>
        {WEEKDAYS.map(w => (
          <div key={w} style={{
            background: 'var(--bg-1)', padding: '10px 12px',
            fontFamily: 'JetBrains Mono', fontSize: 10.5, letterSpacing: 0.08, textTransform: 'uppercase', color: 'var(--fg-3)',
          }}>{w}</div>
        ))}
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gridAutoRows: 110, gap: 1, background: 'var(--line)' }}>
        {cells.map((d, i) => {
          if (d === null) return <div key={i} style={{ background: 'var(--bg)' }} />;
          const dayPosts = posts.filter(p => p.d === d);
          const selected = d === selectedDay;
          const cellKey = `m:${year}-${monthIdx}-${d}`;
          const isDropping = drag && drag.dragId && drag.dropHover === cellKey;
          return (
            <button
              key={i}
              onClick={() => setSelectedDay(d)}
              onDragOver={(e) => {
                if (!drag || !drag.dragId) return;
                e.preventDefault();
                e.dataTransfer.dropEffect = 'move';
                drag.onHover(cellKey);
              }}
              onDragLeave={(e) => {
                if (!drag) return;
                if (e.currentTarget.contains(e.relatedTarget)) return;
                drag.onHover(null);
              }}
              onDrop={(e) => {
                if (!drag) return;
                e.preventDefault();
                const id = e.dataTransfer.getData('text/plain') || drag.dragId;
                if (id) drag.onDrop(id, { d, m: monthIdx, y: year });
              }}
              className="press"
              style={{
                background: isDropping ? 'var(--bg-3)' : (selected ? 'var(--bg-3)' : 'var(--bg-1)'),
                border: 'none', padding: '8px 8px 6px',
                display: 'flex', flexDirection: 'column', gap: 6,
                cursor: 'pointer', textAlign: 'left', position: 'relative', color: 'var(--fg)',
                overflow: 'hidden',
                outline: isDropping ? '2px solid var(--accent)' : 'none',
                outlineOffset: -2,
                transition: 'background .2s var(--ease), outline-color .15s var(--ease)',
              }}>
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                <span style={{
                  fontFamily: 'Hanken Grotesk', fontSize: 14, fontWeight: 600,
                  width: 24, height: 24, borderRadius: 99,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  background: isToday(d) ? 'var(--fg)' : 'transparent',
                  color: isToday(d) ? '#0e0e10' : 'var(--fg)',
                }}>{d}</span>
                {dayPosts.length > 0 && <span className="mono" style={{ fontSize: 10, color: 'var(--fg-3)' }}>{dayPosts.length}</span>}
              </div>

              <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
                {dayPosts.slice(0, 3).map((p) => {
                  const beingDragged = drag && drag.dragId === p.id;
                  return (
                    <div key={p.id} title={p.title}
                      draggable={!!drag}
                      onDragStart={(e) => {
                        if (!drag) return;
                        e.stopPropagation();
                        e.dataTransfer.setData('text/plain', p.id);
                        e.dataTransfer.effectAllowed = 'move';
                        drag.onStart(p.id);
                      }}
                      onDragEnd={(e) => { e.stopPropagation(); drag && drag.onEnd(); }}
                      onClick={(e) => { e.stopPropagation(); onPostClick(p); }}
                      style={{
                        display: 'flex', alignItems: 'center', gap: 5,
                        fontSize: 10.5, color: 'var(--fg-2)',
                        background: 'var(--bg-2)', padding: '3px 6px', borderRadius: 5,
                        border: '1px solid var(--line)',
                        overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis',
                        cursor: drag ? 'grab' : 'pointer',
                        opacity: beingDragged ? 0.4 : 1,
                        transition: 'opacity .15s var(--ease)',
                      }}>
                      <PlanningSignDot post={p} size={5} />
                      <ChannelGlyph ch={p.ch} size={9} />
                      <span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.title}</span>
                    </div>
                  );
                })}
                {dayPosts.length > 3 && (
                  <span style={{ fontSize: 10, color: 'var(--fg-3)' }}>+{dayPosts.length - 3} more</span>
                )}
              </div>
            </button>
          );
        })}
      </div>
      </div>
    </div>
  );
}

function DayPanel({ monthIdx, year, day, posts, onPostClick, onSchedule, drag }) {
  // Each hour is HOUR_H pixels tall, the whole day (24 h) lives inside a
  // VIEWPORT_H tall scroll container. Posts are positioned absolutely so the
  // visual offset between, say, 11:00 and 13:45 reflects the actual 2h 45m
  // gap instead of collapsing into a fixed-height band.
  const HOUR_H = 52;
  const VIEWPORT_H = 460;
  const dayPosts = posts.filter(p => p.d === day).sort((a,b) => (a.h - b.h) || (a.min - b.min));
  const date = new Date(year, monthIdx, day);
  const wd = WEEKDAYS[(date.getDay()+6)%7];
  const isToday = day === TODAY.getDate() && monthIdx === TODAY.getMonth() && year === TODAY.getFullYear();
  const nowTop = isToday ? (TODAY.getHours() + TODAY.getMinutes()/60) * HOUR_H : null;

  const scrollRef = useRef(null);
  // When the day changes, jump the scroll viewport so the first post (or
  // today's "now" marker, or a sensible 08:00 default) lands near the top.
  // Without this the viewport always re-opens at 00:00 — fine if you only
  // ever post in the morning, otherwise an extra scroll for every day click.
  useEffect(() => {
    if (!scrollRef.current) return;
    let target;
    if (dayPosts.length > 0) target = Math.max(0, (dayPosts[0].h - 1) * HOUR_H);
    else if (isToday)        target = Math.max(0, (TODAY.getHours() - 1) * HOUR_H);
    else                     target = 7 * HOUR_H;
    scrollRef.current.scrollTop = target;
  }, [day, monthIdx, year]);

  return (
    <aside className="panel anim-fade-up" style={{ padding: 18, display: 'flex', flexDirection: 'column', gap: 14, height: 'fit-content' }}>
      <div>
        <div className="caps" style={{ color: 'var(--fg-3)' }}>{wd} · {MONTH_NAMES[monthIdx].slice(0,3)} {year}</div>
        <div className="display" style={{ fontSize: 44, lineHeight: 1 }}>{fmt2(day)}</div>
      </div>

      <div
        ref={scrollRef}
        style={{
          position: 'relative', borderRadius: 12,
          background: 'var(--bg-2)', border: '1px solid var(--line)',
          height: VIEWPORT_H, overflowY: 'auto',
        }}
      >
        <div style={{ position: 'relative', height: 24 * HOUR_H }}>
          {/* Hour gridlines + drop targets — one per hour, full 24 h */}
          {Array.from({ length: 24 }, (_, h) => {
            const slotKey = `t:${year}-${monthIdx}-${day}-${h}`;
            const isDropping = drag && drag.dragId && drag.dropHover === slotKey;
            return (
              <div
                key={h}
                onDragOver={(e) => {
                  if (!drag || !drag.dragId) return;
                  e.preventDefault();
                  e.dataTransfer.dropEffect = 'move';
                  drag.onHover(slotKey);
                }}
                onDragLeave={(e) => {
                  if (!drag) return;
                  if (e.currentTarget.contains(e.relatedTarget)) return;
                  drag.onHover(null);
                }}
                onDrop={(e) => {
                  if (!drag) return;
                  e.preventDefault();
                  const id = e.dataTransfer.getData('text/plain') || drag.dragId;
                  if (id) drag.onDrop(id, { d: day, m: monthIdx, y: year, h, min: 0 });
                }}
                style={{
                  position: 'absolute', top: h * HOUR_H, left: 0, right: 0, height: HOUR_H,
                  display: 'flex', alignItems: 'flex-start',
                  borderTop: h === 0 ? 'none' : '1px solid var(--line)',
                  background: isDropping ? 'var(--bg-3)' : 'transparent',
                  outline: isDropping ? '2px solid var(--accent)' : 'none',
                  outlineOffset: -2,
                  transition: 'background .15s var(--ease)',
                }}>
                <span className="mono" style={{
                  fontSize: 10.5, color: 'var(--fg-3)',
                  width: 44, paddingLeft: 10, paddingTop: 4,
                  flexShrink: 0,
                }}>{fmt2(h)}:00</span>
              </div>
            );
          })}

          {/* "Now" indicator — only rendered when viewing today */}
          {nowTop != null && (
            <div style={{
              position: 'absolute', top: nowTop, left: 44, right: 12, height: 0,
              borderTop: '1.5px solid var(--accent)',
              boxShadow: '0 0 0 1px rgba(122,167,255,.12)',
              pointerEvents: 'none', zIndex: 3,
            }}>
              <span style={{
                position: 'absolute', top: -4, left: -6, width: 8, height: 8,
                borderRadius: 99, background: 'var(--accent)',
              }} />
            </div>
          )}

          {/* Posts positioned at their precise time — gap between two posts
              equals the actual minute gap × pixels-per-minute. */}
          {dayPosts.map((p) => {
            const top = (p.h + p.min / 60) * HOUR_H + 2;
            const beingDragged = drag && drag.dragId === p.id;
            return (
              <div key={p.id} className="lift"
                draggable={!!drag}
                onDragStart={(e) => {
                  if (!drag) return;
                  e.stopPropagation();
                  e.dataTransfer.setData('text/plain', p.id);
                  e.dataTransfer.effectAllowed = 'move';
                  drag.onStart(p.id);
                }}
                onDragEnd={(e) => { e.stopPropagation(); drag && drag.onEnd(); }}
                onClick={()=>onPostClick(p)}
                style={{
                  position: 'absolute',
                  top, left: 56, right: 10,
                  display: 'flex', alignItems: 'flex-start', gap: 8,
                  padding: '6px 8px', borderRadius: 8,
                  background: 'var(--bg-3)', border: '1px solid var(--line-2)',
                  cursor: drag ? 'grab' : 'pointer',
                  opacity: beingDragged ? 0.4 : 1,
                  zIndex: 2,
                  transition: 'opacity .15s var(--ease)',
                }}>
                <span style={{ flexShrink: 0, marginTop: 2 }}><PlanningSignDot post={p} size={6} /></span>
                <ChannelGlyph ch={p.ch} size={10} style={{ flexShrink: 0, marginTop: 1 }} />
                <Flag code={p.mk} size={11} style={{ flexShrink: 0, marginTop: 1 }} />
                <span style={{ fontSize: 12, flex: 1, lineHeight: 1.4, wordBreak: 'break-word' }}>{p.title}</span>
                <span className="mono" style={{ fontSize: 10, color: 'var(--fg-3)', flexShrink: 0, marginTop: 2 }}>{fmt2(p.h)}:{fmt2(p.min)}</span>
              </div>
            );
          })}
        </div>
      </div>

      <button className="btn press" onClick={onSchedule} style={{ justifyContent: 'center' }}>
        <Icon name="add" size={16} /> Schedule for this day
      </button>
    </aside>
  );
}

function WeekStrip({ monthIdx, year, selectedDay, setSelectedDay, posts, onPostClick, drag }) {
  const ref = new Date(year, monthIdx, selectedDay);
  const dow = (ref.getDay() + 6) % 7;
  ref.setDate(ref.getDate() - dow);
  const days = Array.from({ length: 7 }, (_, i) => {
    const d = new Date(ref);
    d.setDate(d.getDate() + i);
    return d;
  });

  const isSameMonth = (d) => d.getMonth() === monthIdx && d.getFullYear() === year;
  const isTodayDate = (d) =>
    d.getDate() === TODAY.getDate() && d.getMonth() === TODAY.getMonth() && d.getFullYear() === TODAY.getFullYear();

  return (
    <div className="panel anim-fade-up" style={{ padding: 16 }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 1, background: 'var(--line)', borderRadius: 12, overflow: 'hidden' }}>
        {days.map((d, i) => {
          const dayNum = d.getDate();
          const dayM = d.getMonth();
          const dayY = d.getFullYear();
          const dayPosts = posts
            .filter(p => p.m === dayM && p.y === dayY && p.d === dayNum)
            .sort((a,b) => (a.h - b.h) || (a.min - b.min));
          const today = isTodayDate(d);
          const muted = !isSameMonth(d);
          const colKey = `w:${dayY}-${dayM}-${dayNum}`;
          const isDropping = drag && drag.dragId && drag.dropHover === colKey;
          return (
            <div key={i}
              onClick={() => isSameMonth(d) && setSelectedDay(dayNum)}
              onDragOver={(e) => {
                if (!drag || !drag.dragId) return;
                e.preventDefault();
                e.dataTransfer.dropEffect = 'move';
                drag.onHover(colKey);
              }}
              onDragLeave={(e) => {
                if (!drag) return;
                if (e.currentTarget.contains(e.relatedTarget)) return;
                drag.onHover(null);
              }}
              onDrop={(e) => {
                if (!drag) return;
                e.preventDefault();
                const id = e.dataTransfer.getData('text/plain') || drag.dragId;
                if (id) drag.onDrop(id, { d: dayNum, m: dayM, y: dayY });
              }}
              style={{
                background: isDropping ? 'var(--bg-3)' : 'var(--bg-1)',
                padding: 14, minHeight: 360,
                display: 'flex', flexDirection: 'column', gap: 8,
                cursor: isSameMonth(d) ? 'pointer' : 'default',
                opacity: muted ? 0.45 : 1,
                outline: isDropping ? '2px solid var(--accent)' : 'none',
                outlineOffset: -2,
                transition: 'background .15s var(--ease)',
              }}>
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                <div>
                  <div className="caps" style={{ color: 'var(--fg-3)' }}>{WEEKDAYS[i]}</div>
                  <div style={{ fontFamily: 'Hanken Grotesk', fontSize: 22, fontWeight: 600, color: today ? 'var(--accent)' : 'var(--fg)' }}>{dayNum}</div>
                </div>
                {today && <Status tone="live" label="Today" />}
              </div>
              {dayPosts.map((p) => {
                const beingDragged = drag && drag.dragId === p.id;
                return (
                  <div key={p.id} className="lift"
                    draggable={!!drag}
                    onDragStart={(e) => {
                      if (!drag) return;
                      e.stopPropagation();
                      e.dataTransfer.setData('text/plain', p.id);
                      e.dataTransfer.effectAllowed = 'move';
                      drag.onStart(p.id);
                    }}
                    onDragEnd={(e) => { e.stopPropagation(); drag && drag.onEnd(); }}
                    onClick={(e)=>{ e.stopPropagation(); onPostClick(p); }}
                    style={{
                      background: 'var(--bg-2)', border: '1px solid var(--line)',
                      borderRadius: 10, padding: 10, display: 'flex', flexDirection: 'column', gap: 6,
                      cursor: drag ? 'grab' : 'pointer',
                      opacity: beingDragged ? 0.4 : 1,
                      transition: 'opacity .15s var(--ease)',
                    }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                      <PlanningSignDot post={p} size={6} />
                      <ChannelGlyph ch={p.ch} size={10} />
                      <Flag code={p.mk} size={11} />
                      <span className="mono" style={{ fontSize: 10, color: 'var(--fg-3)', marginLeft: 'auto' }}>{fmt2(p.h)}:{fmt2(p.min)}</span>
                    </div>
                    <div style={{ fontSize: 12.5, lineHeight: 1.35 }}>{p.title}</div>
                  </div>
                );
              })}
            </div>
          );
        })}
      </div>
    </div>
  );
}

function TimelineView({ monthIdx, year, posts, onPostClick }) {
  const sorted = [...posts].sort((a,b) => (a.d - b.d) || (a.h - b.h) || (a.min - b.min));
  return (
    <div className="panel anim-fade-up" style={{ padding: 22 }}>
      {sorted.length === 0 && (
        <div style={{ textAlign: 'center', color: 'var(--fg-3)', padding: 28, fontSize: 13 }}>
          No posts match the current filters.
        </div>
      )}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 0, position: 'relative' }}>
        {sorted.length > 0 && <div style={{ position: 'absolute', left: 78, top: 0, bottom: 0, width: 1, background: 'var(--line)' }} />}
        {sorted.map((p) => (
          <div key={p.id} style={{ display: 'flex', alignItems: 'center', gap: 16, padding: '8px 0', position: 'relative' }}>
            <div className="mono" style={{ width: 64, fontSize: 11, color: 'var(--fg-3)', textAlign: 'right' }}>{MONTH_NAMES[p.m].slice(0,3)} {fmt2(p.d)}</div>
            <div title={planningSign(p).label} style={{
              width: 14, height: 14, borderRadius: 99,
              background: 'var(--bg)',
              border: '2px solid var(--bg)', zIndex: 1, flexShrink: 0,
              display: 'flex', alignItems: 'center', justifyContent: 'center',
            }}>
              <PlanningSignDot post={p} size={10} />
            </div>
            <div className="lift" onClick={()=>onPostClick(p)} style={{ flex: 1, padding: '10px 14px', background: 'var(--bg-1)', border: '1px solid var(--line)', borderRadius: 10, display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer' }}>
              <ChannelGlyph ch={p.ch} size={11} />
              <Flag code={p.mk} size={13} />
              <span style={{ fontSize: 13, flex: 1 }}>{p.title}</span>
              <span className="chip" style={{ background: 'transparent' }} title={planningSign(p).label}>
                <span style={{ width: 5, height: 5, borderRadius: 99, background: STATUS_COLOR[p.status].dot }} />
                {STATUS_COLOR[p.status].label}
              </span>
              <span className="mono" style={{ fontSize: 11, color: 'var(--fg-3)' }}>{fmt2(p.h)}:{fmt2(p.min)}</span>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

function IdeaPool({ posts, onPool, onSchedule, onDismiss }) {
  // Each pooled idea points at an IDEA_LIBRARY entry via post.idea — fall back
  // to the post's own fields when the ref isn't found (e.g., the library
  // entry was deleted) so we don't render a blank card.
  const refFor = (p) => IDEA_LIBRARY.find(r => r.id === p.idea) || null;
  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
        <Icon name="lightbulb" size={18} style={{ color: 'var(--warn)' }} />
        <div className="headline" style={{ fontSize: 16 }}>Idea pool</div>
        <span className="chip">{posts.length} unscheduled</span>
        <div style={{ flex: 1 }} />
        <button className="btn sm press" onClick={onPool} title="Pull every saved reference into the pool">
          <Icon name="download" size={14} /> Pool ideas
        </button>
      </div>
      {posts.length === 0 ? (
        <div className="panel" style={{ padding: 24, textAlign: 'center', color: 'var(--fg-3)', fontSize: 13 }}>
          The pool is empty — hit <strong style={{ color: 'var(--fg-2)' }}>Pool ideas</strong> to pull your saved Instagram, TikTok, Facebook and YouTube references in.
        </div>
      ) : (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 12 }}>
          {posts.map((p) => {
            const ref = refFor(p);
            const ph = ref?.ph || 'ph-photo';
            const isVideo = (ref?.kind || p.kind) === 'video';
            const dur = ref?.dur;
            const author = ref?.author;
            const source = ref?.source;
            const title = ref?.title || p.title;
            return (
              <div key={p.id} className="panel lift" style={{
                padding: 14, display: 'flex', flexDirection: 'column', gap: 10,
              }}>
                <div style={{ position: 'relative' }}>
                  <PhotoTile ph={ph} style={{ height: 110, borderRadius: 8 }} video={isVideo} dur={dur} />
                  {source && (
                    <div style={{
                      position: 'absolute', top: 6, left: 6,
                      display: 'inline-flex', alignItems: 'center', gap: 4,
                      background: 'rgba(0,0,0,.55)', padding: '3px 5px', borderRadius: 5,
                    }}>
                      <ChannelGlyph ch={source} size={9} />
                    </div>
                  )}
                  <div style={{
                    position: 'absolute', top: 6, right: 6,
                    display: 'inline-flex', alignItems: 'center', gap: 4,
                    background: 'rgba(0,0,0,.55)', padding: '2px 6px', borderRadius: 5,
                  }}>
                    <Icon name="lightbulb" size={10} fill style={{ color: 'var(--warn)' }} />
                    <span className="mono" style={{ fontSize: 9, color: '#fff', letterSpacing: 0.06, textTransform: 'uppercase' }}>Idea</span>
                  </div>
                </div>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
                  <div style={{ fontSize: 13, lineHeight: 1.4 }}>{title}</div>
                  {author && (
                    <div className="mono" style={{ fontSize: 10.5, color: 'var(--fg-3)', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>{author}</div>
                  )}
                </div>
                <div style={{ display: 'flex', gap: 6 }}>
                  <button className="btn sm primary press" onClick={()=>onSchedule(p)} style={{ flex: 1, justifyContent: 'center' }} title="Open the editor with today's date pre-filled — pick channel / market / kind">
                    <Icon name="event_available" size={13} /> Schedule
                  </button>
                  <button className="btn sm press" onClick={()=>onDismiss(p)} style={{ width: 32, padding: 0, justifyContent: 'center' }} title="Remove from pool">
                    <Icon name="close" size={14} />
                  </button>
                </div>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

function FiltersDropdown({ filterStatus, setFilterStatus, filterMk, setFilterMk, onReset, onClose }) {
  const ref = useRef(null);
  useEffect(() => {
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [onClose]);

  return (
    <div ref={ref} className="panel anim-scale-in" style={{
      position: 'absolute', top: 'calc(100% + 6px)', right: 0,
      width: 280, padding: 14, zIndex: 50,
      boxShadow: '0 16px 40px -12px rgba(0,0,0,.55)',
      display: 'flex', flexDirection: 'column', gap: 14,
    }}>
      <div>
        <div className="caps" style={{ color: 'var(--fg-3)', marginBottom: 8 }}>Status</div>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
          <button className={'btn sm press' + (filterStatus === 'all' ? ' primary' : '')} onClick={()=>setFilterStatus('all')}>All</button>
          {STATUS_LIST.map(s => (
            <button key={s} className={'btn sm press' + (filterStatus === s ? ' primary' : '')} onClick={()=>setFilterStatus(s)} style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
              <span style={{ width: 6, height: 6, borderRadius: 99, background: STATUS_COLOR[s].dot }} />
              {STATUS_COLOR[s].label}
            </button>
          ))}
        </div>
      </div>

      <div>
        <div className="caps" style={{ color: 'var(--fg-3)', marginBottom: 8 }}>Market</div>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
          <button className={'btn sm press' + (filterMk === 'all' ? ' primary' : '')} onClick={()=>setFilterMk('all')}>All</button>
          {MARKETS.map(m => (
            <button key={m.code} className={'btn sm press' + (filterMk === m.code ? ' primary' : '')} onClick={()=>setFilterMk(m.code)} style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
              <Flag code={m.code} size={12} /> {m.code.toUpperCase()}
            </button>
          ))}
        </div>
      </div>

      <div style={{ display: 'flex', gap: 6, paddingTop: 4, borderTop: '1px solid var(--line)' }}>
        <button className="btn sm ghost press" onClick={onReset} title="Restore sample data">
          <Icon name="restart_alt" size={13} /> Reset data
        </button>
        <div style={{ flex: 1 }} />
        <button className="btn sm press" onClick={()=>{ setFilterStatus('all'); setFilterMk('all'); }}>Clear</button>
        <button className="btn sm primary press" onClick={onClose}>Done</button>
      </div>
    </div>
  );
}

/* ---------------- Editor ---------------- */

function EditorModal({ editor, onChange, onSave, onContinue, onDelete, onClose }) {
  const { post, mode, original } = editor;

  // Edit mode contract:
  //   Save             → only date / time differ from the original
  //   Continue in Compose → required for any caption / channel / market /
  //                         kind / status / campaign / media change
  // The reasoning: the caption was written against a specific creative + channel
  // in Compose, so changing any of those without revisiting Compose would
  // produce a desync between what's planned and what ships.
  const NON_TIME_FIELDS = ['title', 'ch', 'mk', 'kind', 'status', 'campaign', 'media', 'idea'];
  const contentDirty = mode === 'edit' && original
    ? NON_TIME_FIELDS.some(k => (original[k] ?? null) !== (post[k] ?? null))
    : false;

  // "Continue in Compose" requires every planning attribute to be filled.
  // Idea posts have a different shape: no campaign/creative yet, but they must
  // point at a saved reference (post.idea). Picking the media happens later in
  // Compose — that's how the user promotes the idea to a real scheduled post.
  const isIdeaPost = post.status === 'idea';
  const missingAttrs = [];
  if (!post.title.trim()) missingAttrs.push('title');
  if (isIdeaPost) {
    if (!post.idea)       missingAttrs.push('idea reference');
  } else {
    if (!post.campaign)   missingAttrs.push('campaign');
    if (!post.media)      missingAttrs.push('creative');
  }
  const allAttrsSet = missingAttrs.length === 0;
  const composeHint = allAttrsSet
    ? (isIdeaPost
        ? 'Hand off to Compose — pick a creative that makes this idea real'
        : 'Hand off to Compose — write the caption with the creative in view')
    : `Pick a ${missingAttrs.join(' + ')} first`;

  const saveAllowed = mode === 'new'
    ? post.title.trim().length > 0
    : !contentDirty && post.title.trim().length > 0;
  const saveHint = mode === 'new'
    ? 'Save the placeholder without writing a caption'
    : contentDirty
      ? 'Caption / channel / market / creative changed — finish in Compose'
      : 'Save the new date or time';

  const primaryAction = saveAllowed && !contentDirty
    ? onSave
    : (allAttrsSet ? onContinue : onSave);
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') onClose();
      if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) primaryAction();
    };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [onClose, primaryAction]);

  return ReactDOM.createPortal((
    <div onClick={onClose} className="anim-fade-in" style={{
      // Rendered into document.body via a portal so the overlay escapes the
      // route-anim wrapper (which uses transform-based animation, creating a
      // new containing block for position: fixed). Without the portal, the
      // blur would only cover the calendar content — not the sidebar or
      // topbar — which is exactly the bug we're fixing.
      position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
      width: '100vw', height: '100vh',
      background: 'rgba(6,7,10,.62)',
      WebkitBackdropFilter: 'blur(22px) saturate(140%)',
      backdropFilter: 'blur(22px) saturate(140%)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 80,
    }}>
      <style>{`
        .hookline-wheel::-webkit-scrollbar { display: none; }
        .hookline-media::-webkit-scrollbar { height: 6px; }
        .hookline-media::-webkit-scrollbar-track { background: transparent; }
        .hookline-media::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 99px; }
        @keyframes mediaTilePulse {
          0%   { transform: scale(1);    box-shadow: 0 0 0 0 rgba(122,167,255,.55); }
          45%  { transform: scale(1.06); box-shadow: 0 0 0 6px rgba(122,167,255,.22); }
          100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(122,167,255,0); }
        }
        .media-tile-pulse { animation: mediaTilePulse .35s var(--ease); }
      `}</style>
      <div onClick={(e)=>e.stopPropagation()} className="anim-scale-in" style={{
        width: 660, maxWidth: 'calc(100vw - 32px)', maxHeight: 'calc(100vh - 48px)',
        overflowY: 'auto',
        padding: 22, display: 'flex', flexDirection: 'column', gap: 14,
        // Glassmorphism: translucent panel sitting on the blurred backdrop.
        background: 'rgba(20,21,25,.78)',
        border: '1px solid rgba(255,255,255,.12)',
        borderRadius: 'var(--radius-lg)',
        WebkitBackdropFilter: 'blur(18px) saturate(160%)',
        backdropFilter: 'blur(18px) saturate(160%)',
        boxShadow: '0 24px 64px -12px rgba(0,0,0,.65), inset 0 1px 0 rgba(255,255,255,.04)',
      }}>
        {/* Header */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          <Icon name={mode === 'new' ? 'add_circle' : 'edit'} size={18} style={{ color: 'var(--accent)' }} />
          <div className="headline" style={{ fontSize: 16, flex: 1 }}>
            {mode === 'new' ? 'Schedule new post' : 'Edit post'}
          </div>
          <button className="btn icon sm ghost press" onClick={onClose}><Icon name="close" size={14} /></button>
        </div>

        {/* Title */}
        <Field label="Title">
          <input
            className="input"
            value={post.title}
            placeholder="What's the post about?"
            autoFocus
            onChange={(e)=>onChange({ title: e.target.value })}
          />
        </Field>

        {/* Channel — switching it auto-clears campaign/media/kind that don't exist on the new channel */}
        <Field label="Channel">
          <ChipRow>
            {CHANNELS.map(c => (
              <ChipButton key={c.code} active={post.ch === c.code} onClick={()=>{
                if (c.code === post.ch) return;
                const validMediaNames   = new Set(mediaForChannel(c.code).map(a => a.name));
                const validCampaignIds  = new Set(campaignsForChannel(c.code).map(cp => cp.id));
                const validKindIds      = new Set(kindsForChannel(c.code).map(k => k.id));
                onChange({
                  ch: c.code,
                  kind:     validKindIds.has(post.kind) ? post.kind : defaultKindForChannel(c.code),
                  media:    post.media    && validMediaNames.has(post.media)       ? post.media    : null,
                  campaign: post.campaign && validCampaignIds.has(post.campaign)   ? post.campaign : null,
                });
              }}>
                <ChannelGlyph ch={c.code} size={10} /> {c.name}
              </ChipButton>
            ))}
          </ChipRow>
        </Field>

        {/* Market */}
        <Field label="Market">
          <ChipRow>
            {MARKETS.map(m => (
              <ChipButton key={m.code} active={post.mk === m.code} onClick={()=>onChange({ mk: m.code })}>
                <Flag code={m.code} size={12} /> {m.name}
              </ChipButton>
            ))}
          </ChipRow>
        </Field>

        {/* Date + Time */}
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 220px', gap: 12, alignItems: 'stretch' }}>
          <Field label="Date">
            <DatePicker
              value={{ y: post.y, m: post.m, d: post.d }}
              onChange={(v) => onChange(v)}
            />
          </Field>
          <Field label="Time">
            <TimeWheel
              h={post.h} min={post.min}
              onChange={(v) => onChange(v)}
            />
          </Field>
        </div>

        {/* Status — Scheduled / Draft / Idea are pickable. Live is read-only
            since posts only flip to live after publishing. */}
        <Field label="Status">
          <ChipRow>
            {EDITABLE_STATUS_LIST.map(s => (
              <ChipButton key={s} active={post.status === s} onClick={()=>onChange({ status: s })}>
                <span style={{ width: 6, height: 6, borderRadius: 99, background: STATUS_COLOR[s].dot }} />
                {STATUS_COLOR[s].label}
              </ChipButton>
            ))}
            {post.status === 'live' && (
              <span className="chip success" title="Set automatically once the post ships">
                <span style={{ width: 6, height: 6, borderRadius: 99, background: STATUS_COLOR.live.dot }} />
                {STATUS_COLOR.live.label}
              </span>
            )}
          </ChipRow>
        </Field>

        {/* Kind — varies by channel (YouTube has Long-form / Short / Live / Premiere) */}
        <Field label={`Kind · ${CHANNELS.find(c=>c.code===post.ch)?.name || post.ch}`}>
          <ChipRow>
            {kindsForChannel(post.ch).map(k => (
              <ChipButton key={k.id} active={post.kind === k.id} onClick={()=>onChange({ kind: k.id })}>
                <Icon name={k.icon} size={12} />
                <span>{k.label}</span>
              </ChipButton>
            ))}
          </ChipRow>
        </Field>

        {/* Campaign — hidden for ideas: a saved external reference doesn't
            belong to one of OUR campaigns yet. Campaign gets attached when the
            user promotes the idea inside Compose. */}
        {!isIdeaPost && (
          <Field label="Campaign">
            {(() => {
              const validCampaigns = CAMPAIGNS;
              if (validCampaigns.length === 0) {
                return <EmptyHint icon="campaign">No campaigns yet.</EmptyHint>;
              }
              return (
                <ChipRow>
                  <ChipButton active={post.campaign == null} onClick={()=>onChange({ campaign: null })}>
                    <Icon name="block" size={12} /> None
                  </ChipButton>
                  {validCampaigns.map(c => (
                    <ChipButton key={c.id} active={post.campaign === c.id} onClick={()=>{
                      // If a media is already chosen, drop it when it doesn't belong to the new campaign
                      let media = post.media;
                      if (media && !MEDIA_ASSETS.some(a => a.campaign === c.id && a.name === media)) {
                        media = null;
                      }
                      onChange({ campaign: c.id, media });
                    }}>
                      <span style={{ width: 8, height: 8, borderRadius: 99, background: c.color, flexShrink: 0 }} />
                      {c.name}
                    </ChipButton>
                  ))}
                </ChipRow>
              );
            })()}
          </Field>
        )}

        {/* Bottom picker — ideas pull from the saved-references library;
            everything else pulls from our own Mediathek. They never appear
            together: an idea is a reference, a creative is the real asset.
            Switching status flips the picker but keeps both fields on the post
            so the user doesn't lose work when they toggle. */}
        {isIdeaPost ? (
          <Field label="From Ideas">
            {IDEA_LIBRARY.length === 0 ? (
              <EmptyHint icon="lightbulb">No saved ideas yet — save posts from Instagram, TikTok, Facebook or YouTube to build the library.</EmptyHint>
            ) : (
              <IdeasPicker
                items={IDEA_LIBRARY}
                value={post.idea}
                onChange={(id) => onChange({ idea: id })}
              />
            )}
            <div style={{ fontSize: 11.5, color: 'var(--fg-3)', marginTop: 8, lineHeight: 1.5 }}>
              Pick a saved reference. To attach a real creative and publish, use <span style={{ color: 'var(--fg-2)' }}>Continue in Compose</span>.
            </div>
          </Field>
        ) : (
          <Field label={`From Mediathek${post.campaign ? ' · ' + (CAMPAIGNS.find(c=>c.id===post.campaign)?.name || '') : ''}`}>
            {(() => {
              let avail = MEDIA_ASSETS;
              if (post.campaign) avail = avail.filter(a => a.campaign === post.campaign);
              if (avail.length === 0) {
                return <EmptyHint icon="image">No creatives in this campaign yet.</EmptyHint>;
              }
              return <MediaPicker assets={avail} value={post.media} onChange={(name) => onChange({ media: name })} />;
            })()}
          </Field>
        )}

        {/* Footer */}
        <div style={{
          display: 'flex', gap: 8, paddingTop: 10, borderTop: '1px solid var(--line)',
          position: 'sticky', bottom: -22, marginBottom: -22, paddingBottom: 22,
          // Match the glass panel so the sticky footer doesn't show a solid stripe.
          background: 'rgba(20,21,25,.78)',
          WebkitBackdropFilter: 'blur(18px) saturate(160%)',
          backdropFilter: 'blur(18px) saturate(160%)',
        }}>
          {mode === 'edit' && (
            <button className="btn sm destructive press" onClick={onDelete}>
              <Icon name="delete" size={14} /> Delete
            </button>
          )}
          <div style={{ flex: 1 }} />
          <button className="btn press" onClick={onClose}>Cancel</button>
          <button
            className={'btn press' + (saveAllowed && !contentDirty && mode === 'edit' ? ' primary' : '')}
            onClick={onSave}
            disabled={!saveAllowed}
            title={saveHint}
          >
            <Icon name={mode === 'new' ? 'bookmark' : 'check'} size={14} />
            {mode === 'new' ? 'Save here' : 'Save'}
          </button>
          <button
            className={'btn press' + (contentDirty || mode === 'new' ? ' primary' : '')}
            onClick={onContinue}
            disabled={!allAttrsSet}
            title={composeHint}
          >
            <Icon name="arrow_forward" size={14} /> Continue in Compose
          </button>
        </div>
      </div>
    </div>
  ), document.body);
}

function Field({ label, children }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
      <div className="caps" style={{ color: 'var(--fg-3)' }}>{label}</div>
      {children}
    </div>
  );
}
function ChipRow({ children }) {
  return <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>{children}</div>;
}
function ChipButton({ active, onClick, children }) {
  return (
    <button onClick={onClick} className={'btn sm press' + (active ? ' primary' : '')} style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
      {children}
    </button>
  );
}
function EmptyHint({ icon = 'info', children }) {
  return (
    <div className="panel-2" style={{ padding: 14, display: 'flex', alignItems: 'center', gap: 10, color: 'var(--fg-3)', fontSize: 12.5 }}>
      <Icon name={icon} size={16} style={{ color: 'var(--fg-3)' }} />
      <span>{children}</span>
    </div>
  );
}

/* ---------------- Date Picker ---------------- */

function DatePicker({ value, onChange }) {
  const [viewY, setViewY] = useState(value.y);
  const [viewM, setViewM] = useState(value.m);

  useEffect(() => { setViewY(value.y); setViewM(value.m); }, [value.y, value.m]);

  const firstDay = new Date(viewY, viewM, 1);
  const startCol = (firstDay.getDay() + 6) % 7;
  const dim = daysInMonth(viewY, viewM);
  const cells = [];
  for (let i = 0; i < startCol; i++) cells.push(null);
  for (let d = 1; d <= dim; d++) cells.push(d);
  while (cells.length % 7 !== 0) cells.push(null);

  const prev = () => {
    if (viewM === 0) { setViewM(11); setViewY(viewY - 1); }
    else setViewM(viewM - 1);
  };
  const next = () => {
    if (viewM === 11) { setViewM(0); setViewY(viewY + 1); }
    else setViewM(viewM + 1);
  };

  const isToday = (d) =>
    d === TODAY.getDate() && viewM === TODAY.getMonth() && viewY === TODAY.getFullYear();
  const isSelected = (d) =>
    d === value.d && viewM === value.m && viewY === value.y;

  return (
    <div className="panel-2" style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 8 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <button className="btn icon sm ghost press" onClick={prev}><Icon name="chevron_left" size={14} /></button>
        <span style={{ fontFamily: 'Hanken Grotesk', fontWeight: 600, fontSize: 13 }}>
          {MONTH_NAMES[viewM]} <span style={{ color: 'var(--fg-3)', fontWeight: 500 }}>{viewY}</span>
        </span>
        <button className="btn icon sm ghost press" onClick={next}><Icon name="chevron_right" size={14} /></button>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 2 }}>
        {WEEKDAYS.map(w => (
          <div key={w} style={{
            textAlign: 'center', fontSize: 9.5,
            color: 'var(--fg-3)', fontFamily: 'JetBrains Mono',
            letterSpacing: 0.06, padding: '2px 0',
          }}>{w[0]}</div>
        ))}
        {cells.map((d, i) => {
          if (d === null) return <div key={i} style={{ height: 30 }} />;
          const sel = isSelected(d);
          const today = isToday(d);
          return (
            <button key={i} className="press" onClick={() => onChange({ d, m: viewM, y: viewY })} style={{
              height: 30, borderRadius: 99,
              background: sel ? 'var(--fg)' : 'transparent',
              color: sel ? '#0e0e10' : 'var(--fg)',
              border: '1px solid ' + (today && !sel ? 'var(--accent)' : 'transparent'),
              fontFamily: 'Hanken Grotesk', fontSize: 12, fontWeight: sel ? 600 : 500,
              cursor: 'pointer',
              transition: 'background .15s var(--ease), border-color .15s var(--ease)',
            }}>{d}</button>
          );
        })}
      </div>
    </div>
  );
}

/* ---------------- Time Wheel (Apple-style) ---------------- */

function TimeWheel({ h, min, onChange }) {
  return (
    <div className="panel-2" style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 6 }}>
      <div style={{ position: 'relative', display: 'grid', gridTemplateColumns: '1fr auto 1fr', alignItems: 'center', gap: 4 }}>
        <Wheel values={HOURS}   value={h}   onChange={(v)=>onChange({ h: v })} label="HOUR" />
        <div style={{ fontFamily: 'JetBrains Mono', fontSize: 18, color: 'var(--fg-3)', paddingBottom: 4 }}>:</div>
        <Wheel values={MINUTES} value={min} onChange={(v)=>onChange({ min: v })} label="MIN" />
      </div>
    </div>
  );
}

function Wheel({ values, value, onChange, label }) {
  const ITEM_H = 30;
  const VISIBLE = 5;
  const PAD = ((VISIBLE - 1) / 2) * ITEM_H;
  const CONT_H = VISIBLE * ITEM_H;

  const ref = useRef(null);
  const debounceRef = useRef(null);
  const settingFromProp = useRef(false);

  useLayoutEffect(() => {
    if (!ref.current) return;
    const idx = values.indexOf(value);
    if (idx < 0) return;
    const target = idx * ITEM_H;
    if (Math.abs(ref.current.scrollTop - target) < 1) return;
    settingFromProp.current = true;
    ref.current.scrollTop = target;
    setTimeout(() => { settingFromProp.current = false; }, 100);
  }, [value, values]);

  const onScroll = () => {
    if (settingFromProp.current) return;
    if (debounceRef.current) clearTimeout(debounceRef.current);
    debounceRef.current = setTimeout(() => {
      if (!ref.current) return;
      const idx = Math.round(ref.current.scrollTop / ITEM_H);
      const clamped = Math.max(0, Math.min(values.length - 1, idx));
      const v = values[clamped];
      // snap to exact position
      const target = clamped * ITEM_H;
      if (Math.abs(ref.current.scrollTop - target) > 0.5) {
        ref.current.scrollTo({ top: target, behavior: 'smooth' });
      }
      if (v !== value) onChange(v);
    }, 110);
  };

  const activeIdx = values.indexOf(value);

  return (
    <div style={{ position: 'relative', height: CONT_H, overflow: 'hidden' }}>
      <div
        ref={ref}
        onScroll={onScroll}
        className="hookline-wheel"
        style={{
          height: CONT_H, overflowY: 'auto',
          scrollSnapType: 'y mandatory',
          scrollbarWidth: 'none',
          WebkitOverflowScrolling: 'touch',
        }}
      >
        <div style={{ paddingTop: PAD, paddingBottom: PAD }}>
          {values.map((v, i) => {
            const dist = activeIdx < 0 ? 99 : Math.abs(i - activeIdx);
            const active = dist === 0;
            return (
              <div key={v} onClick={() => onChange(v)} style={{
                height: ITEM_H, scrollSnapAlign: 'center',
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                fontFamily: 'JetBrains Mono',
                fontSize: active ? 18 : 14,
                fontWeight: active ? 600 : 500,
                color: 'var(--fg)',
                opacity: active ? 1 : Math.max(0.2, 1 - dist * 0.28),
                cursor: 'pointer',
                transition: 'opacity .12s var(--ease), font-size .12s var(--ease)',
              }}>{fmt2(v)}</div>
            );
          })}
        </div>
      </div>
      {/* selection band */}
      <div style={{
        position: 'absolute', left: 4, right: 4, top: '50%',
        transform: 'translateY(-50%)', height: ITEM_H,
        borderTop: '1px solid var(--line-2)',
        borderBottom: '1px solid var(--line-2)',
        background: 'rgba(255,255,255,.02)',
        borderRadius: 6,
        pointerEvents: 'none',
      }} />
      {/* fade masks (match panel-2 bg) */}
      <div style={{
        position: 'absolute', inset: 0, pointerEvents: 'none',
        background: 'linear-gradient(180deg, var(--bg-2) 0%, transparent 28%, transparent 72%, var(--bg-2) 100%)',
      }} />
    </div>
  );
}

/* ---------------- Mediathek picker ---------------- */

function MediaPicker({ value, onChange, assets = MEDIA_ASSETS }) {
  // React-controlled pulse: bumping `pulseTick` re-keys the just-clicked tile
  // so it remounts and replays the CSS animation from scratch — even if the
  // user clicks the same tile twice.
  const [pulse, setPulse] = useState({ idx: -1, tick: 0 });

  const handleClick = (idx, name) => {
    setPulse((p) => ({ idx, tick: p.tick + 1 }));
    onChange(name);
  };

  const items = [
    { name: null, label: 'None', kind: 'none' },
    ...assets.map(a => ({ name: a.name, label: a.name, ph: a.ph, kind: a.kind, dur: a.dur, mk: a.mk })),
  ];
  return (
    <div className="hookline-media" style={{
      display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 6,
      scrollSnapType: 'x proximity',
    }}>
      {items.map((it, i) => {
        const active = value === it.name;
        const isPulsing = pulse.idx === i;
        return (
          <button
            // Re-key the just-clicked tile so it remounts and the CSS animation
            // restarts from scratch. Other tiles keep their stable key — no
            // remount, no flash on the previously selected tile.
            key={isPulsing ? `${i}:${pulse.tick}` : `${i}:idle`}
            onClick={() => handleClick(i, it.name)}
            className="press"
            style={{
              flexShrink: 0, width: 108,
              background: active ? 'var(--bg-3)' : 'var(--bg-2)',
              // Border colour change is instant — no fade out on the previous
              // selection (that previously read as a "blink" on the wrong tile).
              border: '1px solid ' + (active ? 'var(--accent)' : 'var(--line)'),
              borderRadius: 10, padding: 6,
              scrollSnapAlign: 'start',
              display: 'flex', flexDirection: 'column', gap: 5, cursor: 'pointer',
              color: 'var(--fg)', textAlign: 'left',
              transition: 'background .15s var(--ease)',
              animation: isPulsing ? 'mediaTilePulse .35s var(--ease)' : undefined,
            }}>
            {it.kind === 'none' ? (
              <div style={{
                height: 64, background: 'var(--bg-3)', borderRadius: 6,
                display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--fg-3)',
              }}>
                <Icon name="block" size={18} />
              </div>
            ) : (
              <div style={{ position: 'relative' }}>
                <PhotoTile ph={it.ph} style={{ height: 64, borderRadius: 6 }} video={it.kind === 'video'} dur={it.dur} />
                <div style={{
                  position: 'absolute', top: 4, left: 4,
                  display: 'inline-flex', alignItems: 'center', gap: 3,
                  background: 'rgba(0,0,0,.5)', padding: '1px 5px',
                  borderRadius: 5, fontSize: 9, color: '#fff',
                  fontFamily: 'JetBrains Mono', letterSpacing: 0.04, textTransform: 'uppercase',
                }}>
                  <Flag code={it.mk} size={8} /> {it.mk}
                </div>
                {active && (
                  <div style={{ position: 'absolute', top: 4, right: 4, background: 'var(--accent)', color: '#0a1424', borderRadius: 99, width: 18, height: 18, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                    <Icon name="check" size={12} />
                  </div>
                )}
              </div>
            )}
            <div style={{ fontSize: 10, color: active ? 'var(--fg)' : 'var(--fg-3)', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', fontFamily: 'JetBrains Mono' }}>
              {it.label}
            </div>
          </button>
        );
      })}
    </div>
  );
}

/* ---------------- Ideas picker ----------------
   Visually similar to MediaPicker but each tile shows a saved external
   reference: source platform glyph in the corner + author handle. No campaign
   bucket — ideas live outside our own campaign structure. */

function IdeasPicker({ items, value, onChange }) {
  const [pulse, setPulse] = useState({ idx: -1, tick: 0 });

  const handleClick = (idx, id) => {
    setPulse((p) => ({ idx, tick: p.tick + 1 }));
    onChange(id);
  };

  const tiles = [
    { id: null, kind: 'none' },
    ...items.map(it => ({ ...it })),
  ];

  return (
    <div className="hookline-media" style={{
      display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 6,
      scrollSnapType: 'x proximity',
    }}>
      {tiles.map((it, i) => {
        const active = value === it.id;
        const isPulsing = pulse.idx === i;
        return (
          <button
            key={isPulsing ? `${i}:${pulse.tick}` : `${i}:idle`}
            onClick={() => handleClick(i, it.id)}
            className="press"
            style={{
              flexShrink: 0, width: 130,
              background: active ? 'var(--bg-3)' : 'var(--bg-2)',
              border: '1px solid ' + (active ? 'var(--accent)' : 'var(--line)'),
              borderRadius: 10, padding: 6,
              scrollSnapAlign: 'start',
              display: 'flex', flexDirection: 'column', gap: 5, cursor: 'pointer',
              color: 'var(--fg)', textAlign: 'left',
              transition: 'background .15s var(--ease)',
              animation: isPulsing ? 'mediaTilePulse .35s var(--ease)' : undefined,
            }}>
            {it.kind === 'none' ? (
              <div style={{
                height: 78, background: 'var(--bg-3)', borderRadius: 6,
                display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--fg-3)',
              }}>
                <Icon name="block" size={18} />
              </div>
            ) : (
              <div style={{ position: 'relative' }}>
                <PhotoTile ph={it.ph} style={{ height: 78, borderRadius: 6 }} video={it.kind === 'video'} dur={it.dur} />
                <div style={{
                  position: 'absolute', top: 4, left: 4,
                  display: 'inline-flex', alignItems: 'center', gap: 3,
                  background: 'rgba(0,0,0,.55)', padding: '2px 4px',
                  borderRadius: 5,
                }}>
                  <ChannelGlyph ch={it.source} size={8} />
                </div>
                <div style={{
                  position: 'absolute', top: 4, right: 4,
                  display: 'inline-flex', alignItems: 'center', gap: 3,
                  background: 'rgba(0,0,0,.5)', padding: '1px 5px',
                  borderRadius: 5, fontSize: 9, color: '#fff',
                  fontFamily: 'JetBrains Mono', letterSpacing: 0.04, textTransform: 'uppercase',
                }}>
                  <Flag code={it.mk} size={8} /> {it.mk}
                </div>
                {active && (
                  <div style={{
                    position: 'absolute', bottom: 4, right: 4,
                    background: 'var(--accent)', color: '#0a1424', borderRadius: 99,
                    width: 18, height: 18, display: 'flex', alignItems: 'center', justifyContent: 'center',
                  }}>
                    <Icon name="check" size={12} />
                  </div>
                )}
              </div>
            )}
            {it.kind === 'none' ? (
              <div style={{ fontSize: 10, color: active ? 'var(--fg)' : 'var(--fg-3)', fontFamily: 'JetBrains Mono' }}>
                None
              </div>
            ) : (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0 }}>
                <div style={{
                  fontSize: 10.5, color: 'var(--fg)', lineHeight: 1.25,
                  overflow: 'hidden', display: '-webkit-box',
                  WebkitBoxOrient: 'vertical', WebkitLineClamp: 2,
                }}>
                  {it.title}
                </div>
                <div style={{
                  fontSize: 9.5, color: 'var(--fg-3)', fontFamily: 'JetBrains Mono',
                  overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis',
                }}>
                  {it.author}
                </div>
              </div>
            )}
          </button>
        );
      })}
    </div>
  );
}

Object.assign(window, { CalendarTab });
