// Now Snapshot · single top-of-dashboard card that fuses the old
// "What's happening now" (window-state) and "Ticket forensics · executive view"
// widgets into one 8-tile snapshot of current source-control activity.
// All data still comes from DATA.* selectors — no new state.
//
// When `devId` is passed (developer profile) the whole snapshot is scoped to
// that engineer's tickets and the team-only "Owner overloaded" tile is swapped
// for the engineer's "Blocked" count.

function NowSnapshot({ openTicketList, navigate, openSidePanel, devId }) {
  const isDev = !!devId;
  const [weeks, setWeeks] = useWindowPref(isDev ? 'profile.now' : 'nowSnapshot', DATA.DEFAULT_WEEKS);
  const win = DATA.windowFor(weeks);

  // Standup-feed collapse state (team view only) · persisted per browser.
  const STANDUP_COLS = ['attention', 'at-risk', 'progressing', 'going-well'];
  const [colCollapsed, setColCollapsed] = useWindowPref('nowSnapshot.colCollapsed.v2', { attention: true, 'at-risk': true, progressing: true, 'going-well': true });
  const toggleCol = (key) => setColCollapsed(prev => ({ ...prev, [key]: !prev[key] }));
  // The "Daily standup" bar batch-toggles ticket lists across all four questions;
  // headers stay visible. Mixed state counts as open, so one click always collapses all.
  const allColsCollapsed = STANDUP_COLS.every(k => colCollapsed[k]);
  const toggleAllCols = () => setColCollapsed(Object.fromEntries(STANDUP_COLS.map(k => [k, !allColsCollapsed])));

  const scopedInWindow = React.useMemo(() => {
    const all = DATA.ticketsInWindow(weeks);
    return isDev ? all.filter(t => t.assignee === devId) : all;
  }, [weeks, isDev, devId]);

  const s = React.useMemo(() => {
    if (!isDev) return DATA.windowState(weeks);
    const startOffset = -(weeks * 7 - 1);
    const cur = scopedInWindow;
    return {
      shipped: cur.filter(t => t.state === 'shipped').length,
      inProgress: cur.filter(t => t.state === 'in-progress' || t.state === 'review').length,
      atRisk: cur.filter(t =>
        (t.state === 'in-progress' || t.state === 'review' || t.state === 'blocked') &&
        (t.rootCause || (t.daysInStage || 0) >= 5)).length,
      blocked: cur.filter(t => t.state === 'blocked').length,
      aged: cur.filter(t => t.createdAtDays < startOffset && t.state !== 'shipped' && t.currentStage !== 'done').length,
    };
  }, [isDev, weeks, scopedInWindow]);

  const inflight = React.useMemo(
    () => scopedInWindow.filter(t => t.state !== 'shipped' && t.currentStage !== 'done'),
    [scopedInWindow]
  );

  const openDefectTickets = React.useMemo(() => {
    const rows = DATA.defectsInWindow(weeks).filter(
      r => r.ticket.state !== 'shipped' && r.ticket.currentStage !== 'done'
    );
    return (isDev ? rows.filter(r => r.ticket.assignee === devId) : rows).map(r => r.ticket);
  }, [weeks, isDev, devId]);
  const openDefects = openDefectTickets.length;

  const forensics = React.useMemo(() => {
    const stuckTickets = inflight.filter(t => {
      if (!t.pr) return false;
      const commentEv = (t.pr.events || []).find(e => e.kind === 'comment');
      return (commentEv && commentEv.latencyH >= 20)
          || (!commentEv && t.currentStage === 'review');
    });
    const reworkTickets = inflight.filter(t => (t.history || []).some(seg => seg.stage === 'rework'));

    const byAuthor = new Map();
    for (const t of inflight) byAuthor.set(t.assignee, (byAuthor.get(t.assignee) || 0) + 1);
    const overloadedAuthors = [...byAuthor.values()].filter(v => v >= 2).length;

    const tone = (n) => n >= 3 ? 'bad' : n >= 1 ? 'warn' : 'good';
    return {
      stuckTickets, stuckTone: tone(stuckTickets.length),
      reworkTickets, reworkTone: tone(reworkTickets.length),
      overloadedAuthors, overloadTone: tone(overloadedAuthors),
    };
  }, [inflight]);

  // Team standup feed · the 4-column "what the team would tell you today"
  // narrative that sits under the stat tiles. Team-scope only (skipped on the
  // per-engineer profile view). All derived from observed DATA.* selectors.
  const columns = React.useMemo(() => {
    if (isDev) return [];
    const inWin = DATA.ticketsInWindow(weeks);
    const dev = (id) => DATA.devById(id);
    // Age → tone for the "days in stage" chip (4d = watch, 7d+ = at risk).
    const ageTone = (d) => d >= 7 ? 'bad' : d >= 4 ? 'warn' : null;
    // Map a ticket to a StatusCircle kind so the row glyph reads its state.
    const statusKind = (t) =>
      t.state === 'shipped' || t.currentStage === 'done' ? 'done'
      : t.state === 'blocked' ? 'blocked'
      : t.currentStage === 'bug' ? 'bug'
      : (t.currentStage === 'review' || t.state === 'review') ? 'review'
      : 'progress';
    const BUCKET = {
      progressing:  { label: "What's progressing",      tone: 'accent' },
      'going-well': { label: "What's going well",        tone: 'good'   },
      'at-risk':    { label: "What's at risk",           tone: 'warn'   },
      attention:    { label: 'What needs attention now', tone: 'bad'    },
    };
    // Ticket items open a lightweight in-context "update" drawer; the full
    // forensics page stays one click away from inside the drawer.
    const goTicketUpdate = (id, bucket, reason) => () =>
      openSidePanel({
        kind: 'ticket-update', ticketId: id, title: DATA.ticketById(id)?.title || id,
        context: { bucket, bucketLabel: BUCKET[bucket].label, tone: BUCKET[bucket].tone, reason },
      });
    const goCommit = (c) => () => openSidePanel({ kind: 'commitment', commitment: c, title: c.label });

    // ── Natural-language helpers ──────────────────────────────────────────
    // These turn the same observed DATA into plain-English sentences: one
    // summary per question, and several "statements" per deliverable (shown
    // when a row is expanded). No new data — just phrasing of what we have.
    const plural = (n, one, many) => `${n} ${n === 1 ? one : (many || one + 's')}`;
    // Oxford-join clause fragments into one readable phrase.
    const joinClauses = (parts) => {
      parts = parts.filter(Boolean);
      if (parts.length === 0) return '';
      if (parts.length === 1) return parts[0];
      if (parts.length === 2) return `${parts[0]} and ${parts[1]}`;
      return `${parts.slice(0, -1).join(', ')}, and ${parts[parts.length - 1]}`;
    };
    const sevTone = (sev) => sev === 'bad' ? 'bad' : sev === 'warn' ? 'warn' : null;
    const endPeriod = (s) => /[.!?·]$/.test(s.trim()) ? s : `${s}.`;
    // PR-event verbs, mirrored from the side-panel activity stream.
    const evtLabel = { open: 'opened the PR', comment: 'commented', change: 'requested changes', approve: 'approved', merge: 'merged' };
    // Newest-first merge of commits + PR events (same shape as TicketUpdateDrill).
    const recentActivity = (t, n = 3) => [
      ...(t.commits || []).map(c => ({ dayOffset: c.dayOffset, who: c.author, label: `committed "${c.message}"`, ai: c.aiPct })),
      ...((t.pr && t.pr.events) || []).map(e => ({ dayOffset: e.dayOffset, who: e.by, label: evtLabel[e.kind] || e.kind })),
    ].sort((a, b) => b.dayOffset - a.dayOffset).slice(0, n);
    // One activity event → a natural sentence (first name + verb + relative time).
    const activityStatement = (a) => {
      const who = dev(a.who);
      const first = who ? who.name.split(' ')[0] : a.who;
      const ai = (a.ai != null && a.ai >= 60) ? ` (${a.ai}% AI)` : '';
      return { text: `${first} ${a.label}${ai} ${fmtRelative(-a.dayOffset)}.`, tone: null };
    };
    // Status line for a ticket-backed item.
    const statusStatement = (t) => {
      const name = dev(t.assignee)?.name || t.assignee;
      return { text: `${name} has this in ${DATA.STAGE_LABEL[t.currentStage]} for ${fmtDays(t.daysInStage)}.`, tone: null };
    };
    // Shared statement set for commitment items (drift + missed-commit).
    const commitStatements = (c, whyText, whyTone) => {
      const sig = DATA.commitmentSignals(c);
      return [
        { text: whyText, tone: whyTone },
        { text: `Committed to: ${c.audience}.`, tone: null },
        ...(sig.headline ? [{ text: endPeriod(sig.headline), tone: whyTone }] : []),
        ...(c.drivers || []).slice(0, 2).map(d => ({ text: endPeriod(d), tone: 'warn' })),
        { text: `Committed ${c.committedDate}, now ETA ${c.currentEta}.`, tone: null },
      ];
    };

    // 1 · Progressing — healthy in-flight work, freshest first.
    const progressing = inWin
      .filter(t => (t.state === 'in-progress' || t.state === 'review') && !t.rootCause)
      .sort((a, b) => (a.daysInStage || 0) - (b.daysInStage || 0))
      .slice(0, 4)
      .map(t => {
        const meta = `${DATA.STAGE_LABEL[t.currentStage]} · ${t.daysInStage}d in stage`;
        return {
          key: t.id, id: t.id, text: t.title, meta,
          status: statusKind(t), initials: dev(t.assignee)?.initials, name: dev(t.assignee)?.name,
          chips: [{ text: `${DATA.STAGE_LABEL[t.currentStage]} · ${t.daysInStage}d`, tone: ageTone(t.daysInStage) }],
          kind: 'progressing',
          stageWord: DATA.STAGE_LABEL[t.currentStage].toLowerCase(),
          statements: [
            { text: 'Moving cleanly, no blockers.', tone: 'good' },
            statusStatement(t),
            ...recentActivity(t, 2).map(activityStatement),
          ],
          onClick: goTicketUpdate(t.id, 'progressing', meta),
        };
      });

    // 2 · Going well — recently shipped + on-track / ahead commitments.
    const shipped = inWin
      .filter(t => t.state === 'shipped')
      .slice(0, 3)
      .map(t => ({
        key: t.id, id: t.id, text: t.title, meta: 'shipped',
        status: 'done', initials: dev(t.assignee)?.initials, name: dev(t.assignee)?.name,
        chips: [{ text: 'Shipped', tone: 'good' }],
        kind: 'shipped',
        statements: [
          { text: `Shipped by ${dev(t.assignee)?.name || t.assignee}, done.`, tone: 'good' },
          ...recentActivity(t, 2).map(activityStatement),
        ],
        onClick: goTicketUpdate(t.id, 'going-well', 'Shipped in this window'),
      }));
    const onTrack = DATA.commitments
      .filter(c => c.risk === 'good')
      .sort((a, b) => (a.drift || 0) - (b.drift || 0))
      .slice(0, 2)
      .map(c => ({
        key: c.id, id: c.ticketId, text: c.label,
        meta: c.drift < 0 ? `${-c.drift}d ahead of commit` : 'on track for commit',
        chips: [{ text: c.drift < 0 ? `${-c.drift}d ahead` : 'on track', tone: 'good' }],
        note: c.audience,
        kind: 'on-track',
        statements: [
          { text: c.drift < 0 ? `${-c.drift}d ahead of the committed date.` : 'On track for the committed date.', tone: 'good' },
          { text: `Committed to: ${c.audience}.`, tone: null },
          ...(c.drivers || []).slice(0, 1).map(d => ({ text: endPeriod(d), tone: 'good' })),
        ],
        onClick: goCommit(c),
      }));
    const goingWell = [...shipped, ...onTrack].slice(0, 4);

    // 3 · At risk — aging tickets (not yet hard-blocked) + drifting commitments.
    const riskTickets = DATA.atRiskInWindow(weeks)
      .filter(t => t.state !== 'blocked')
      .slice(0, 3)
      .map(t => {
        const meta = `${t.rootCause} · ${t.daysInStage}d in stage`;
        return {
          key: t.id, id: t.id, text: t.title, meta,
          status: statusKind(t), initials: dev(t.assignee)?.initials, name: dev(t.assignee)?.name,
          chips: [
            { text: t.rootCause, tone: 'warn' },
            { text: `${t.daysInStage}d`, tone: ageTone(t.daysInStage) || 'warn' },
          ],
          kind: 'aging',
          statements: [
            { text: `Aging in stage (${fmtDays(t.daysInStage)}), at risk of slipping.`, tone: 'warn' },
            statusStatement(t),
            ...(t.rootCause ? [{ text: `Root cause: ${t.rootCause}.`, tone: 'warn' }] : []),
            ...(t.evidence ? [{ text: endPeriod(t.evidence), tone: 'warn' }] : []),
            ...recentActivity(t, 3).map(activityStatement),
          ],
          onClick: goTicketUpdate(t.id, 'at-risk', meta),
        };
      });
    const driftCommits = DATA.commitments
      .filter(c => c.risk === 'warn')
      .sort((a, b) => (b.drift || 0) - (a.drift || 0))
      .slice(0, 2)
      .map(c => ({
        key: c.id, id: c.ticketId, text: c.label,
        meta: `${c.audience} · drifting +${c.drift}d`,
        chips: [{ text: `drifting +${c.drift}d`, tone: 'warn' }],
        note: c.audience,
        kind: 'drift',
        statements: commitStatements(c, `Drifting +${c.drift}d past the committed date.`, 'warn'),
        onClick: goCommit(c),
      }));
    const atRisk = [...riskTickets, ...driftCommits].slice(0, 4);

    // 4 · Needs attention now — blocked work, missed commitments, critical
    //     signals, and stale cross-team dependencies. Most urgent first.
    const blocked = inWin
      .filter(t => t.state === 'blocked')
      .map(t => {
        const meta = `blocked ${t.daysInStage}d · ${t.rootCause || 'no owner movement'}`;
        return {
          key: t.id, id: t.id, text: t.title, meta,
          status: 'blocked', initials: dev(t.assignee)?.initials, name: dev(t.assignee)?.name,
          chips: [
            { text: t.rootCause || 'no owner movement', tone: 'bad' },
            { text: `${t.daysInStage}d`, tone: 'bad' },
          ],
          kind: 'blocked',
          statements: [
            { text: `Blocked for ${fmtDays(t.daysInStage)}, needs your attention.`, tone: 'bad' },
            statusStatement(t),
            ...(t.rootCause ? [{ text: `Root cause: ${t.rootCause}.`, tone: 'bad' }] : []),
            ...(t.evidence ? [{ text: endPeriod(t.evidence), tone: 'warn' }] : []),
            ...recentActivity(t, 3).map(activityStatement),
          ],
          onClick: goTicketUpdate(t.id, 'attention', meta),
        };
      });
    const missed = DATA.commitments
      .filter(c => c.risk === 'bad')
      .sort((a, b) => (b.drift || 0) - (a.drift || 0))
      .map(c => ({
        key: c.id, id: c.ticketId, text: c.label,
        meta: DATA.commitmentSignals(c).headline || c.audience,
        chips: [{ text: c.drift > 0 ? `missed +${c.drift}d` : 'missed', tone: 'bad' }],
        note: DATA.commitmentSignals(c).headline || c.audience,
        kind: 'missed-commit',
        statements: commitStatements(c, c.drift > 0 ? `Missed commit by ${c.drift}d.` : 'Committed date missed.', 'bad'),
        onClick: goCommit(c),
      }));
    const critical = DATA.signals
      .filter(s => s.severity === 'bad')
      .map(s => ({
        key: s.id, id: dev(s.devId)?.name || s.devId, text: s.name, meta: s.callout,
        chips: [{ text: 'signal', tone: 'bad' }], note: s.callout,
        initials: dev(s.devId)?.initials, name: dev(s.devId)?.name,
        kind: 'signal',
        statements: [
          { text: `${dev(s.devId)?.name || s.devId}: ${endPeriod(s.name)}`, tone: sevTone(s.severity) },
          ...(s.callout ? [{ text: `Suggested: ${endPeriod(s.callout)}`, tone: null }] : []),
        ],
        onClick: () => navigate('#/signals'),
      }));
    const staleDeps = DATA.dependencies
      .filter(d => d.freshness === 'bad')
      .map(d => ({
        key: d.id, id: d.blockedTicket, text: `Blocked on ${d.counterTeam} · ${d.counterTicket}`,
        meta: d.note, chips: [{ text: 'dep blocked', tone: 'bad' }], note: d.note,
        kind: 'stale-dep',
        statements: [
          { text: `Blocked on ${d.counterTeam} (${d.counterTicket}), silent ${fmtDays(d.lastActivityDaysAgo)}.`, tone: 'bad' },
          ...(d.note ? [{ text: endPeriod(d.note), tone: 'warn' }] : []),
        ],
        onClick: goTicketUpdate(d.blockedTicket, 'attention', d.note),
      }));
    const attention = [...blocked, ...missed, ...critical, ...staleDeps].slice(0, 4);

    // Roll each bucket's items up into one plain-English sentence (the
    // "question" a manager would ask, answered). Counts drive the phrasing.
    const summarize = (key, items) => {
      const n = (k) => items.filter(it => it.kind === k).length;
      if (key === 'attention') {
        if (items.length === 0) return 'Nothing needs attention right now, all clear.';
        const clauses = joinClauses([
          n('blocked') && `${plural(n('blocked'), 'ticket')} ${n('blocked') === 1 ? 'is' : 'are'} blocked`,
          n('missed-commit') && `${plural(n('missed-commit'), 'commitment')} slipped ${n('missed-commit') === 1 ? 'its' : 'their'} date`,
          n('signal') && `${plural(n('signal'), 'critical signal')} flagged`,
          n('stale-dep') && `${plural(n('stale-dep'), 'cross-team dependency', 'cross-team dependencies')} ${n('stale-dep') === 1 ? 'is' : 'are'} stale`,
        ]);
        return `${plural(items.length, 'thing')} need${items.length === 1 ? 's' : ''} your attention, ${clauses}.`;
      }
      if (key === 'at-risk') {
        if (items.length === 0) return 'Nothing at risk, everything in flight is on pace.';
        const clauses = joinClauses([
          n('aging') && `${plural(n('aging'), 'ticket')} ${n('aging') === 1 ? 'is' : 'are'} aging in stage`,
          n('drift') && `${plural(n('drift'), 'commitment')} ${n('drift') === 1 ? 'is' : 'are'} drifting past commit`,
        ]);
        return `${plural(items.length, 'item')} to watch, ${clauses}.`;
      }
      if (key === 'progressing') {
        if (items.length === 0) return 'Nothing in flight right now.';
        const stages = joinClauses([...new Set(items.map(it => it.stageWord).filter(Boolean))]);
        return `${plural(items.length, 'ticket')} moving cleanly${stages ? ` through ${stages}` : ''}, no blockers.`;
      }
      // going-well
      if (items.length === 0) return 'No closed wins in this window yet.';
      const clauses = joinClauses([
        n('shipped') && `${plural(n('shipped'), 'ticket')} shipped`,
        n('on-track') && `${plural(n('on-track'), 'commitment')} ${n('on-track') === 1 ? 'is' : 'are'} on track`,
      ]);
      return `${plural(items.length, 'win')}, ${clauses}.`;
    };

    return [
      { key: 'attention',   tone: 'bad',    icon: 'flame',          label: 'What needs attention now', items: attention,   summary: summarize('attention', attention) },
      { key: 'at-risk',     tone: 'warn',   icon: 'alert-triangle', label: "What's at risk",           items: atRisk,      summary: summarize('at-risk', atRisk) },
      { key: 'progressing', tone: 'accent', icon: 'activity',      label: "What's progressing",       items: progressing, summary: summarize('progressing', progressing) },
      { key: 'going-well',  tone: 'good',   icon: 'check-circle',   label: "What's going well",        items: goingWell,   summary: summarize('going-well', goingWell) },
    ];
  }, [weeks, isDev, navigate, openSidePanel]);
  const feedTotal = columns.reduce((n, c) => n + c.items.length, 0);

  const idsList = (tickets) => tickets.map(t => t.id);
  const scopeTitle = isDev ? (DATA.devById(devId)?.name || 'engineer') : null;
  const titleFor = (label) => isDev
    ? `${label} · ${scopeTitle} · last ${weeks * 7}d`
    : `${label} · last ${weeks * 7}d`;

  const stateTiles = [
    { key: 'shipped',     label: 'Shipped',     value: s.shipped,    icon: 'check-circle',   tone: 'good',
      onClick: () => openTicketList({ state: 'shipped',     assignee: devId, weeks, title: titleFor('Shipped') }) },
    { key: 'in-progress', label: 'In progress', value: s.inProgress, icon: 'circle-dot',     tone: 'accent',
      onClick: () => openTicketList({ state: 'in-progress', assignee: devId, weeks, title: titleFor('In progress') }) },
    { key: 'at-risk',     label: 'At risk',     value: s.atRisk,     icon: 'alert-triangle', tone: 'warn',
      onClick: () => openTicketList({ atRisk: true,         assignee: devId, weeks, title: titleFor('At risk') }) },
  ].map(t => ({ ...t, sub: 'tickets' }));

  const forensicsTiles = isDev
    ? [
        { key: 'rework',  label: 'Rework loops',   value: forensics.reworkTickets.length, sub: 'tickets in rework',        tone: forensics.reworkTone,
          onClick: () => openTicketList({ ids: idsList(forensics.reworkTickets), assignee: devId, weeks, title: titleFor('Rework loops') }) },
        { key: 'blocked', label: 'Blocked',        value: s.blocked,                      sub: 'tickets blocked',          tone: s.blocked >= 2 ? 'bad' : s.blocked >= 1 ? 'warn' : 'good',
          onClick: () => openTicketList({ state: 'blocked', assignee: devId, weeks, title: titleFor('Blocked') }) },
        { key: 'defects', label: 'Open defects',   value: openDefects,                    sub: 'unresolved in window',     tone: openDefects >= 3 ? 'bad' : openDefects >= 1 ? 'warn' : 'good',
          onClick: () => openTicketList({ ids: idsList(openDefectTickets), assignee: devId, weeks, title: titleFor('Open defects') }) },
      ]
    : [
        { key: 'rework',   label: 'Rework loops',     value: forensics.reworkTickets.length, sub: 'tickets in rework',        tone: forensics.reworkTone,   icon: 'shuffle' },
        { key: 'overload', label: 'Owner overloaded', value: forensics.overloadedAuthors,    sub: 'authors w/ 2+ open',       tone: forensics.overloadTone, icon: 'user' },
        { key: 'defects',  label: 'Open defects',     value: openDefects,                    sub: 'unresolved in window',     tone: openDefects >= 3 ? 'bad' : openDefects >= 1 ? 'warn' : 'good', icon: 'shield' },
      ].map(t => ({ ...t, onClick: () => navigate('#/ticket-forensics') }));

  if (isDev) {
    const devIcons = { rework: 'shuffle', blocked: 'alert-triangle', defects: 'shield' };
    for (const t of forensicsTiles) t.icon = devIcons[t.key];
  }

  const tiles = [...stateTiles, ...forensicsTiles];

  return (
    <div className="card">
      <div className="card-head">
        <div>
          <div className="ch-eyebrow" style={!isDev ? { display: 'flex', alignItems: 'center', gap: 7 } : undefined}>
            {!isDev && <span className="live-dot" />}
            <span>{isDev ? `This engineer · last ${weeks * 7} days` : `Source control + standup · last ${weeks * 7} days`}</span>
          </div>
          <div className="ch-title" style={{ marginTop: 4, display: 'flex', alignItems: 'center', gap: 6 }}>
            <Icon name="git-branch" size={13} />
            <span>What's happening now</span>
          </div>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
          <span className="mono" style={{ color: 'var(--text-3)', fontSize: 11 }}>{win.dateLabel}</span>
          <WindowSizeChip value={weeks} onChange={setWeeks} />
          <span style={{ color: 'var(--text-4)' }}>·</span>
          <span className="mono" style={{ color: 'var(--text-3)', fontSize: 11 }}>
            {inflight.length} in-flight
          </span>
          {!isDev && (
            <a
              href="#/ticket-forensics"
              style={{ fontSize: 11, color: 'var(--accent)', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 4 }}
            >View full forensics <Icon name="chevron-right" size={11} /></a>
          )}
        </div>
      </div>

      <div className="window-state-grid">
        {tiles.map((t, i) => {
          const isForensics = i >= stateTiles.length;
          return (
            <div
              key={t.key}
              className={`window-stat ${t.tone ? 'tone-' + t.tone : ''}`}
              onClick={t.onClick}
              style={{ cursor: 'pointer' }}
              title={!isDev && isForensics ? 'Open full forensics' : undefined}
            >
              <div className="ss-label"><Icon name={t.icon} size={11} />{t.label}</div>
              <div className="ss-value">{t.value}</div>
              <div className="ss-sub">{t.sub}</div>
            </div>
          );
        })}
      </div>

      {!isDev && (
        <button className="pulse-bar" onClick={toggleAllCols} aria-expanded={!allColsCollapsed}>
          <Icon name={allColsCollapsed ? 'chevron-right' : 'chevron-down'} size={13} className="pb-caret" />
          <span className="pb-label">Daily standup</span>
          <span className="tag mono">{feedTotal} updates</span>
          <span className="pb-hint">{allColsCollapsed ? 'Show' : 'Hide'}</span>
        </button>
      )}

      {!isDev && (
        <div className="pulse-stack">
          {columns.map(col => {
            const colOpen = !colCollapsed[col.key];
            return (
              <section key={col.key} className={`pulse-section tone-${col.tone} ${colOpen ? '' : 'collapsed'}`}>
                <button
                  className="pulse-section-head"
                  onClick={() => toggleCol(col.key)}
                  aria-expanded={colOpen}
                >
                  <Icon name={colOpen ? 'chevron-down' : 'chevron-right'} size={14} className="psh-caret" />
                  <Icon name={col.icon} size={15} className="psh-icon" />
                  <span className="psh-label">{col.label}</span>
                  <span className={`tag mono tone-${col.tone} psh-count`}>{col.items.length}</span>
                </button>
                {col.summary && <p className="pulse-summary-text">{col.summary}</p>}
                {colOpen && (
                  <div className="pulse-section-body">
                    <div className="pulse-list">
                      {col.items.map((it, i) => (
                        <StandupRow key={it.key} item={it} index={i} />
                      ))}
                      {col.items.length === 0 && <div className="pulse-empty">Nothing here · clean</div>}
                    </div>
                  </div>
                )}
              </section>
            );
          })}
        </div>
      )}

    </div>
  );
}

// StandupRow · one deliverable in the standup feed. Collapsed it reads as a
// single line (status glyph · id · title · key chips); expanded it reveals the
// item's natural-language statements plus a link into the full side panel.
// Expansion is a transient reading gesture, so it lives in local state (the
// structural section/feed collapses persist via useWindowPref — this doesn't).
function StandupRow({ item, index }) {
  const [expanded, setExpanded] = React.useState(false);
  const statements = item.statements || [];
  const hasDetail = statements.length > 0;
  // The first statement is the salient "why" line — surface it onto the row so
  // it reads at a glance even while collapsed (the full list still shows on expand).
  const lead = statements[0] || null;
  const rowId = `pi-detail-${item.key}`;

  return (
    <div className={`pulse-item ${expanded ? 'expanded' : ''}`} style={{ animationDelay: (index * 60) + 'ms' }}>
      <button
        className="pi-row"
        onClick={() => hasDetail ? setExpanded(e => !e) : item.onClick()}
        aria-expanded={hasDetail ? expanded : undefined}
        aria-controls={hasDetail ? rowId : undefined}
        title={item.meta}
      >
        <div className="pi-body">
          <div className="pi-line">
            <span className="pi-id">{item.id}</span>
            <span className="pi-text" title={item.text}>{item.text}</span>
            {lead && (
              <span className={`pi-lead${lead.tone ? ' tone-' + lead.tone : ''}`}>
                <span className="pi-stmt-dot" />
                <span className="pi-stmt-text">{lead.text}</span>
              </span>
            )}
          </div>
          <div className="pi-chips">
            {(item.name || item.initials) && (
              <span className="pi-dev">
                {item.initials && <Avatar initials={item.initials} size={15} />}
                {item.name && <span className="pi-dev-name">{item.name}</span>}
              </span>
            )}
            {(item.chips || []).map((c, j) => (
              <span key={j} className={`pi-tag${c.tone ? ' tone-' + c.tone : ''}`}>{c.text}</span>
            ))}
            {item.note && <span className="pi-cause" title={item.note}>{item.note}</span>}
          </div>
        </div>
        {hasDetail && <Icon name="chevron-down" size={13} className="pi-caret" />}
      </button>

      {expanded && hasDetail && (
        <div className="pi-detail" id={rowId}>
          <ul className="pi-statements">
            {statements.map((st, j) => (
              <li key={j} className={`pi-statement${st.tone ? ' tone-' + st.tone : ''}`}>
                <span className="pi-stmt-dot" />
                <span className="pi-stmt-text">{st.text}</span>
              </li>
            ))}
          </ul>
          <button className="pi-detail-link" onClick={item.onClick}>
            <Icon name="external" size={11} />
            Open full forensics
          </button>
        </div>
      )}
    </div>
  );
}

window.NowSnapshot = NowSnapshot;
