// StreakGrid — collapsible group habit history with horizontal swipe.
// Now driven by `cells` from buildCells(today, cadence) — supports:
//   - daily: one cell per day, snap by day
//   - set-days: only configured weekdays (T/W/F), snap by day
//   - n-per-week: one cell per week, each shows N pips for the target count, snap by week
//   - weekly: one cell per week, snap by week
//
// Rows:
//   - Expanded: one row per member; left slot = avatar, cell = check circle / pip-row.
//   - Collapsed: single row; left slot = streak badge, cell = pie split by member.

const GREEN = '#2f6b52';
const GREEN_DEEP = '#285a45';
const GOLD = '#d4a536';
const GOLD_DEEP = '#b8882a';
const MUTED = '#e6e1d6';
const BORDER = '#d9d3c5';
const INK = '#1f2a24';
const INK_SOFT = '#6b7a72';

const PIE_SIZE = 50;

// ── Expanded ────────────────────────────────────────────────────
function ExpandedGrid({ members, today, cadence, cells, onMarkToday, onSelectDay, selectedDay, justMarked, allDoneByCell, onMemberTap, sinceDay }) {
  const rows = members.map((m, mi) => ({
    id: m.id,
    height: PIE_SIZE,
    leftSlot: <AvatarWithStreak member={m} onClick={onMemberTap ? () => onMemberTap(m) : undefined} />,
    renderCell: (cell, ctx) =>
    <ExpandedCell
      cell={cell}
      member={m}
      isYou={mi === 0}
      gold={allDoneByCell.get(cell.key)}
      prevGold={ctx.prevCell ? allDoneByCell.get(ctx.prevCell.key) : false}
      nextGold={ctx.nextCell ? allDoneByCell.get(ctx.nextCell.key) : false}
      prevDone={ctx.prevCell ? cellMemberDone(ctx.prevCell, m) : false}
      nextDone={ctx.nextCell ? cellMemberDone(ctx.nextCell, m) : false}
      onSelectDay={onSelectDay}
      selected={cell.meta && cell.meta.dateStr === selectedDay}
      justMarked={justMarked}
      sinceDay={sinceDay}
      cellW={ctx.cellW} />


  }));
  return (
    <TimelineStrip
      cells={cells}
      rows={rows}
      cellMaxWidth={PIE_SIZE}
      selectedDay={selectedDay}
      scrollKey={cadence.kind} />);


}

// Returns true if this member has a 'done' status on a given cell. (n-per-week
// weeks now also return the plain 'done' string when the weekly target is met.)
function cellMemberDone(cell, member) {
  if (!cell) return false;
  return cell.getStatus(member) === 'done';
}

function ExpandedCell({ cell, member, isYou, gold, prevGold, nextGold, prevDone, nextDone, onSelectDay, selected, justMarked, cellW, sinceDay }) {
  // Day, weekly, OR n-per-week cell: a single circle. A past day with no entry
  // (since sinceDay) renders as an explicit "missed" slot so the row looks like
  // a continuous timeline (as if the habit started earlier).
  let status = cell.getStatus(member);
  const dateStr = cell.meta && cell.meta.dateStr;
  const isPast = !cell.isToday && !cell.isFuture;
  if (status == null && isPast && sinceDay && dateStr && dateStr >= sinceDay) status = 'miss';
  // Match the folded pie's VISIBLE circle: DayPie draws an SVG (box = cellSize)
  // whose <circle r=18> lives in a 44-unit viewBox, so the drawn circle is only
  // 36/44 of the box. Our DayCircle is a full div, so scale it by 36/44 to match.
  const cellSize = Math.min(PIE_SIZE, cellW - 2);
  const size = Math.round(cellSize * 36 / 44);
  // Tapping any (non-future) cell selects that day — so you can switch days
  // straight from the unfolded view without folding back.
  const onClick = (!cell.isFuture && onSelectDay && dateStr) ? () => onSelectDay(dateStr) : null;
  const thisDone = status === 'done';
  // Connector bars to neighbors when both this and neighbor are done.
  // Color: gold if BOTH cells are gold (all-done day), else green.
  const linkLeft = thisDone && prevDone;
  const linkRight = thisDone && nextDone;
  const leftColor = gold && prevGold ? GOLD : GREEN;
  const rightColor = gold && nextGold ? GOLD : GREEN;
  // Bar height = circle diameter / 4, clamped — gives a visually substantial connector.
  const barH = Math.max(5, Math.round(size * 0.22));
  // Extend bars all the way to the cell center (and 1px past, for subpixel safety).
  // The portion under the circle is hidden by the circle (which sits at zIndex 1).
  const barW = Math.ceil(cellW / 2) + 1;
  return (
    <div onClick={onClick} style={{
      position: 'relative', width: cellW, height: PIE_SIZE,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      cursor: onClick ? 'pointer' : 'default',
      // circular highlight, ONLY on the logged-in user's (top) row
      background: (selected && isYou) ? 'radial-gradient(circle closest-side, rgba(47,107,82,0.15) 99%, transparent 100%)' : 'transparent',
      transition: 'background .18s',
    }}>
      {linkLeft &&
      <div style={{
        position: 'absolute', left: 0, top: '50%',
        width: barW, height: barH,
        background: leftColor, transform: 'translateY(-50%)', zIndex: 0
      }} />
      }
      {linkRight &&
      <div style={{
        position: 'absolute', right: 0, top: '50%',
        width: barW, height: barH,
        background: rightColor, transform: 'translateY(-50%)', zIndex: 0
      }} />
      }
      <div style={{ position: 'relative', zIndex: 1 }}>
        <DayCircle
          status={status}
          gold={gold}
          size={size}
          onClick={null}
          justMarked={justMarked && cell.isToday && isYou} />
        
      </div>
    </div>);

}

// The "did it" avatar (Stories Phase 2). The ring is now a completion cue, not a
// streak cue:
//   • member.didThisPeriod  → SOLID GREEN ring (they completed the current period)
//     — no ring at all when they haven't.
//   • member.pulse          → that green ring also PULSES (they attached media the
//     current user hasn't watched yet, still inside the media window).
// The bottom-right number badge still shows the PERSONAL streak count, coloured
// GREEN when the streak is 1 and GOLD once it reaches 2+. Handles 1-99 directly;
// 100+ collapses to "99+" so the badge stays within the avatar's footprint.
// `onClick(member)` is wired by the parent to open the story (if any) or history.
function AvatarWithStreak({ member, onClick, size = PIE_SIZE, showBadge = true }) {
  const streak = member.personalStreak || 0;
  const didIt = !!member.didThisPeriod;
  const pulse = !!member.pulse;
  const ring = didIt ? 'green' : null; // did-it → solid green; else no ring
  // Compact display for big numbers so the badge never balloons past the avatar.
  const label = streak > 99 ? '99+' : String(streak);
  // Streak 1 = green (just started); 2+ = gold (a real run).
  const badgeColor = streak >= 2 ? GOLD : GREEN;
  const small = size <= 34;
  const badgeH = small ? 13 : 16;
  const fontSize = label.length >= 3 ? (small ? 8 : 9) : (small ? 9 : 10);
  const Tag = onClick ? 'button' : 'div';
  const tapProps = onClick
    ? { onClick, 'aria-label': `${member.name} profile`, 'data-testid': 'member-' + member.id,
        style: { position: 'relative', width: size, height: size, padding: 0, border: 'none', background: 'transparent', cursor: 'pointer' } }
    : { style: { position: 'relative', width: size, height: size } };
  return (
    <Tag {...tapProps}>
      <Avatar variant={member.variant} size={size} ring={ring} pulse={pulse} />
      {showBadge && streak > 0 &&
      <div style={{
        position: 'absolute',
        right: -3, bottom: -2,
        minWidth: badgeH, height: badgeH,
        padding: '0 4px',
        borderRadius: badgeH / 2,
        background: badgeColor,
        color: '#fff',
        fontSize,
        fontWeight: 800,
        lineHeight: 1,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        border: '1.5px solid #fff',
        boxShadow: '0 1px 2px rgba(0,0,0,0.15)',
        letterSpacing: -0.2,
        fontVariantNumeric: 'tabular-nums'
      }}
      title={`${streak}-day personal streak`}>
        {label}</div>
      }
    </Tag>);

}

function DayCircle({ status, gold, size, onClick, justMarked }) {
  const base = {
    width: size, height: size,
    borderRadius: '50%',
    display: 'flex', alignItems: 'center', justifyContent: 'center',
    transition: 'background 0.3s, box-shadow 0.3s',
    flexShrink: 0
  };
  if (status === 'done') {
    const bg = gold ? GOLD : GREEN;
    return (
      <div style={{
        ...base,
        background: bg,
        boxShadow: justMarked ? `0 0 0 6px ${gold ? 'rgba(212,165,54,0.18)' : 'rgba(47,107,82,0.15)'}` : 'none',
        animation: justMarked ? 'pop 0.5s cubic-bezier(.2,.8,.2,1)' : 'none'
      }}>
        <svg width={size * 0.5} height={size * 0.5} viewBox="0 0 18 18" fill="none">
          <path d="M3.5 9.5 L7.5 13 L14.5 5.5" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
        </svg>
      </div>);

  }
  if (status === 'miss') {
    // Empty / not-done slot: SAME size as the done/today circles, light-gray
    // fill with a slightly darker border.
    return <div style={{ ...base, background: '#eeeae0', border: '1.5px solid #cfc8b8' }} />;
  }
  if (status === 'today') {
    return (
      <button onClick={onClick} aria-label="Mark today" data-testid="dc" style={{
        ...base, border: `1.5px solid ${GREEN}`, background: '#fff',
        cursor: onClick ? 'pointer' : 'default', padding: 0
      }} />);

  }
  if (status === 'future') {
    return (
      <div style={{ ...base, border: `1.5px dashed #c9c1ae`, background: 'transparent' }} />);

  }
  // null / rest day — render nothing (transparent)
  return <div style={{ ...base, opacity: 0 }} />;
}

// Row of N pips for n-per-week cells
function PipRow({ count, target, cellW, gold }) {
  // Distribute N pips horizontally inside cellW.
  const gap = 1.5;
  const maxSize = (cellW - 6 - gap * (target - 1)) / target;
  const size = Math.max(6, Math.min(14, maxSize));
  return (
    <div style={{ display: 'flex', gap, alignItems: 'center', justifyContent: 'center' }}>
      {Array.from({ length: target }).map((_, i) => {
        const filled = i < count;
        return (
          <div key={i} style={{
            width: size, height: size, borderRadius: '50%',
            background: filled ? gold ? GOLD : GREEN : 'transparent',
            border: filled ? 'none' : `1.2px solid ${MUTED}`
          }} />);

      })}
    </div>);

}

// ── Collapsed ───────────────────────────────────────────────────
function CollapsedGrid({ members, today, cadence, cells, onSelectDay, selectedDay, justMarked, allDoneByCell, streak, streakIcon, sinceDay }) {
  const rows = [{
    id: 'pie',
    height: PIE_SIZE,
    leftSlot: (
      <button onClick={() => onSelectDay && onSelectDay(ymd(today))} aria-label="Open timeline"
        style={{ border: 'none', background: 'transparent', padding: 0, cursor: 'pointer' }}>
        <StreakBadge streak={streak} icon={streakIcon} />
      </button>
    ),
    renderCell: (cell, ctx) =>
    <CollapsedCell
      cell={cell}
      members={members}
      gold={allDoneByCell.get(cell.key)}
      prevGold={ctx.prevCell ? allDoneByCell.get(ctx.prevCell.key) : false}
      nextGold={ctx.nextCell ? allDoneByCell.get(ctx.nextCell.key) : false}
      onSelectDay={onSelectDay}
      selected={cell.meta && cell.meta.dateStr === selectedDay}
      sinceDay={sinceDay}
      justMarked={justMarked}
      cellW={ctx.cellW} />


  }];
  return (
    <TimelineStrip
      cells={cells}
      rows={rows}
      cellMaxWidth={PIE_SIZE}
      selectedDay={selectedDay}
      scrollKey={cadence.kind} />);


}

function CollapsedCell({ cell, members, gold, prevGold, nextGold, onSelectDay, selected, sinceDay, justMarked, cellW }) {
  // Tapping ANY past/today day SELECTS it (unfolds the per-person rows + reveals
  // the mark controls) — it no longer marks done directly. The clickable area is
  // the WHOLE cell (not just the drawn pie) so that empty / older days with no
  // pie glyph are still tappable and open their section.
  const dateStr = cell.meta && cell.meta.dateStr;
  const onClick = (!cell.isFuture && onSelectDay && dateStr) ? () => onSelectDay(dateStr) : null;
  return (
    <div onClick={onClick} style={{
      width: cellW, height: PIE_SIZE,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      cursor: onClick ? 'pointer' : 'default',
    }}>
      <DayPie
        members={members}
        cell={cell}
        cellW={cellW}
        allDone={gold}
        selected={selected}
        sinceDay={sinceDay}
        linkLeft={gold && prevGold}
        linkRight={gold && nextGold}
        onClick={null}
        justMarked={justMarked && cell.isToday} />
    </div>);


}

// ── Streak badge (left slot in collapsed view) ──────────────────
// The badge now leads with a big legible number; the small symbol on top is
// configurable (was a tiny flame — Lupko found it ambiguous). Options:
//   'none'  — just the number (cleanest; reads as a milestone count)
//   'check' — checkmark ("days kept")
//   'leaf'  — small leaf (matches the forest palette / "growth")
//   'bolt'  — lightning ("momentum")
//   'flame' — classic Duolingo-style flame (original)
function StreakBadge({ streak, icon = 'none' }) {
  const active = streak > 0;
  const size = PIE_SIZE;
  // 1-99 fits comfortably; 100-999 needs smaller type; 1000+ folds to "999+".
  const display = streak >= 1000 ? '999+' : String(streak);
  const numFont = display.length >= 4 ? 10 : display.length === 3 ? 12 : 16;
  const showIcon = icon !== 'none' && display.length < 3; // hide icon when number is wide
  return (
    <div style={{
      width: size, height: size,
      borderRadius: '50%',
      background: active ?
      `radial-gradient(circle at 35% 30%, #ecc25c 0%, ${GOLD} 55%, ${GOLD_DEEP} 100%)` :
      '#fff',
      border: active ? `1px solid ${GOLD_DEEP}` : `1.5px dashed #c9c1ae`,
      display: 'flex', flexDirection: 'column',
      alignItems: 'center', justifyContent: 'center',
      boxShadow: active ? '0 1px 2px rgba(184,136,42,0.3), inset 0 1px 0 rgba(255,255,255,0.5)' : 'none',
      position: 'relative'
    }}
      title={active ? `${streak}-day group streak` : 'No streak yet'}
      data-comment-anchor="874b5f639a-path-284-13">
      {active ?
      <>
          {showIcon && <StreakSymbol kind={icon} />}
          <div style={{
          fontSize: numFont,
          fontWeight: 800,
          color: '#fff',
          lineHeight: 1,
          marginTop: showIcon ? 6 : 0,
          letterSpacing: -0.3,
          textShadow: '0 1px 0 rgba(0,0,0,0.12)',
          fontVariantNumeric: 'tabular-nums'
        }}>{display}</div>
        </> :

      <div style={{ fontSize: 14, color: INK_SOFT, fontWeight: 600 }}>—</div>
      }
    </div>);

}

// Small white glyph that sits above the streak number. Pure SVG so it stays crisp.
function StreakSymbol({ kind }) {
  const common = { width: 9, height: 9, viewBox: '0 0 12 12', style: { position: 'absolute', top: 4 } };
  if (kind === 'flame') return (
    <svg {...common}><path d="M6 1.5 C 7 4 9 4.5 9 7 A 3 3 0 0 1 3 7 C 3 5.5 4 5 4 4 C 5 4.5 5 3 6 1.5 Z" fill="#fff" opacity="0.9" /></svg>);
  if (kind === 'check') return (
    <svg {...common}><path d="M2.5 6.5 L5 9 L9.5 3.5" stroke="#fff" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" fill="none" opacity="0.95" /></svg>);
  if (kind === 'leaf') return (
    <svg {...common}><path d="M2 10 C 2 5 5 2 10 2 C 10 7 7 10 2 10 Z M3 9 L7 5" fill="#fff" stroke="#fff" strokeWidth="0.8" strokeLinecap="round" opacity="0.92" /></svg>);
  if (kind === 'bolt') return (
    <svg {...common}><path d="M7 1.5 L3 7 H6 L5 10.5 L9 5 H6 Z" fill="#fff" opacity="0.92" /></svg>);
  return null;
}

// ── Pie with photo wedges ──────────────────────────────────────
// Always a fixed N-way pie (member 0 = the logged-in user = RIGHT / top-right).
// Each member's slice: DID it → their photo + green half-border; else → grey
// (no photo). Gold ring when everyone did it. A grey "?" badge shows only when
// the LOGGED-IN user themselves still hasn't decided (done nor not-done).
function DayPie({ members, cell, cellW, allDone, linkLeft, linkRight, onClick, justMarked, selected, sinceDay }) {
  const statuses = members.map((m) => cell.getStatus(m));
  const anyDone = statuses.some((s) => s === 'done');
  const anyMiss = statuses.some((s) => s === 'miss');
  const n = members.length;
  const uid = React.useId().replace(/:/g, '');
  const size = Math.min(PIE_SIZE, cellW - 2);
  const GREY_FILL = '#faf7ef', GREY_STROKE = '#d8d2c4';

  const dateStr = cell.meta && cell.meta.dateStr;
  const isPast = !cell.isToday && !cell.isFuture;
  const withinWindow = dateStr && sinceDay && dateStr >= sinceDay;
  // Use the cell's own status for the logged-in user so this works for week
  // cells (n-per-week / weekly) too, where there is no single per-day entry.
  const youStatus = members[0] ? cell.getStatus(members[0]) : null;
  // "?" = the current user hasn't decided (no 'done'/'miss' record). The 'today'
  // sentinel (and an in-progress current week) also counts as undecided. Only
  // within the visible window.
  const youUndecided = (cell.isToday || (isPast && withinWindow)) && (youStatus == null || youStatus === 'today');

  const container = {
    width: cellW, height: PIE_SIZE,
    position: 'relative',
    cursor: onClick ? 'pointer' : 'default',
    animation: justMarked ? 'pop 0.5s cubic-bezier(.2,.8,.2,1)' : 'none',
    border: 'none', borderRadius: 12,
    // circular highlight (touches the sides, not the corners) when selected
    background: selected ? 'radial-gradient(circle closest-side, rgba(47,107,82,0.15) 99%, transparent 100%)' : 'transparent',
    transition: 'background .18s', padding: 0,
    display: 'flex', alignItems: 'center', justifyContent: 'center'
  };

  // Grey "?" badge (top-right) — only when the logged-in user still must decide.
  const QBadge = () => (
    <div style={{
      position: 'absolute', top: 1, right: Math.max(2, (cellW - size) / 2 - 3),
      width: 15, height: 15, borderRadius: 8, background: '#9aa39b',
      border: '1.5px solid #fff', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 3,
    }}><span style={{ fontSize: 10, fontWeight: 800, color: '#fff', lineHeight: 1 }}>?</span></div>
  );

  // Gold connector bars between consecutive all-done days — drawn centre-to-centre
  // (full cell width from the middle) so they always reach the circles.
  const ConnectorBars = () => (
    <>
      {linkRight && <div style={{ position: 'absolute', left: '50%', top: '50%', width: cellW, height: 5, background: GOLD, transform: 'translateY(-50%)', zIndex: 0 }} />}
      {linkLeft && <div style={{ position: 'absolute', right: '50%', top: '50%', width: cellW, height: 5, background: GOLD, transform: 'translateY(-50%)', zIndex: 0 }} />}
    </>
  );

  // Future day → dashed empty circle.
  if (cell.isFuture) {
    return (
      <div style={container}>
        <svg viewBox="0 0 44 44" width={size} height={size} style={{ position: 'relative', zIndex: 1 }}>
          <circle cx="22" cy="22" r="18" fill="none" stroke="#c9c1ae" strokeWidth="1.5" strokeDasharray="3 3" />
        </svg>
      </div>);
  }

  // Nothing to show (old day outside the window, no activity, nothing pending).
  if (!anyDone && !anyMiss && !youUndecided) return null;

  const cx = 22, cy = 22, r = 18;
  const Tag = onClick ? 'button' : 'div';

  // Single-member group → one full circle.
  if (n === 1) {
    const done = statuses[0] === 'done';
    return (
      <Tag onClick={onClick} style={container}>
        <ConnectorBars />
        <svg viewBox="0 0 44 44" width={size} height={size} style={{ position: 'relative', zIndex: 1 }}>
          <defs><clipPath id={`solo-${uid}`}><circle cx="22" cy="22" r="18" /></clipPath></defs>
          {done
            ? <>
                <g clipPath={`url(#solo-${uid})`}><image href={AVATAR_URLS[members[0].variant]} x="4" y="4" width="36" height="36" preserveAspectRatio="xMidYMid slice" /></g>
                <circle cx="22" cy="22" r="18" fill="none" stroke={GREEN} strokeWidth="2.4" />
              </>
            : <circle cx="22" cy="22" r="18" fill={GREY_FILL} stroke={GREY_STROKE} strokeWidth="1.5" />}
          {allDone && <circle cx="22" cy="22" r="18" fill="none" stroke={GOLD} strokeWidth="2.6" />}
        </svg>
        {youUndecided && <QBadge />}
      </Tag>
    );
  }

  // Fixed wedges for ALL members (member 0 = you = right side).
  const wedges = members.map((m, i) => {
    const startA = i / n * 2 * Math.PI - Math.PI / 2;
    const endA = (i + 1) / n * 2 * Math.PI - Math.PI / 2;
    const x1 = cx + r * Math.cos(startA), y1 = cy + r * Math.sin(startA);
    const x2 = cx + r * Math.cos(endA), y2 = cy + r * Math.sin(endA);
    const large = endA - startA > Math.PI ? 1 : 0;
    const path = `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} Z`;
    const arc = `M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2}`;
    const theta = endA - startA, midA = (startA + endA) / 2;
    const cd = 4 * r * Math.sin(theta / 2) / (3 * theta);
    return { path, arc, member: m, done: statuses[i] === 'done', wcx: cx + cd * Math.cos(midA), wcy: cy + cd * Math.sin(midA) };
  });

  return (
    <Tag onClick={onClick} style={container}>
      <ConnectorBars />
      <svg viewBox="0 0 44 44" width={size} height={size} style={{ position: 'relative', zIndex: 1 }}>
        <defs>
          {wedges.map((w, i) => w.done && <clipPath key={i} id={`wedge-${uid}-${i}`}><path d={w.path} /></clipPath>)}
        </defs>
        {/* each slice: photo (done) or grey fill (not done) */}
        {wedges.map((w, i) => w.done
          ? <g key={i} clipPath={`url(#wedge-${uid}-${i})`}>
              <image href={AVATAR_URLS[w.member.variant]} x={w.wcx - 20} y={w.wcy - 20} width="40" height="40" preserveAspectRatio="xMidYMid slice" />
            </g>
          : <path key={i} d={w.path} fill={GREY_FILL} />)}
        {/* per-slice outer rim: green if done, light-grey if not */}
        {wedges.map((w, i) =>
          <path key={'rim-' + i} d={w.arc} fill="none" stroke={w.done ? GREEN : GREY_STROKE} strokeWidth={w.done ? 2.4 : 1.5} strokeLinecap="butt" />)}
        {/* gold full ring when everyone did it */}
        {allDone && <circle cx={cx} cy={cy} r={r} fill="none" stroke={GOLD} strokeWidth="2.6" />}
      </svg>
      {youUndecided && <QBadge />}
    </Tag>);
}

// ── Unified grid ────────────────────────────────────────────────
// ONE TimelineStrip that stays mounted whether folded (single pie row) or
// unfolded (per-member rows). Toggling only swaps the `rows` — the date header
// and scroll position never remount, so nothing jumps.
function HabitGrid({ collapsed, members, today, cadence, cells, onSelectDay, selectedDay, sinceDay, justMarked, allDoneByCell, streak, streakIcon, onMemberTap }) {
  let rows;
  if (collapsed) {
    rows = [{
      id: 'pie',
      height: PIE_SIZE,
      leftSlot: (
        <button onClick={() => onSelectDay && onSelectDay(ymd(today))} aria-label="Open timeline"
          style={{ border: 'none', background: 'transparent', padding: 0, cursor: 'pointer' }}>
          <StreakBadge streak={streak} icon={streakIcon} />
        </button>
      ),
      renderCell: (cell, ctx) =>
        <CollapsedCell cell={cell} members={members} gold={allDoneByCell.get(cell.key)}
          prevGold={ctx.prevCell ? allDoneByCell.get(ctx.prevCell.key) : false}
          nextGold={ctx.nextCell ? allDoneByCell.get(ctx.nextCell.key) : false}
          onSelectDay={onSelectDay} selected={cell.meta && cell.meta.dateStr === selectedDay}
          sinceDay={sinceDay} justMarked={justMarked} cellW={ctx.cellW} />,
    }];
  } else {
    rows = members.map((m, mi) => ({
      id: m.id,
      height: PIE_SIZE,
      leftSlot: <AvatarWithStreak member={m} onClick={onMemberTap ? () => onMemberTap(m) : undefined} />,
      renderCell: (cell, ctx) =>
        <ExpandedCell cell={cell} member={m} isYou={mi === 0} gold={allDoneByCell.get(cell.key)}
          prevGold={ctx.prevCell ? allDoneByCell.get(ctx.prevCell.key) : false}
          nextGold={ctx.nextCell ? allDoneByCell.get(ctx.nextCell.key) : false}
          prevDone={ctx.prevCell ? cellMemberDone(ctx.prevCell, m) : false}
          nextDone={ctx.nextCell ? cellMemberDone(ctx.nextCell, m) : false}
          onSelectDay={onSelectDay} selected={cell.meta && cell.meta.dateStr === selectedDay}
          justMarked={justMarked} sinceDay={sinceDay} cellW={ctx.cellW} />,
    }));
  }
  return <TimelineStrip cells={cells} rows={rows} cellMaxWidth={PIE_SIZE} selectedDay={selectedDay} scrollKey={cadence.kind} />;
}

Object.assign(window, {
  ExpandedGrid, CollapsedGrid, HabitGrid, AvatarWithStreak,
  GREEN, GREEN_DEEP, GOLD, GOLD_DEEP, MUTED, BORDER, INK, INK_SOFT
});