/* trendchaser.jsx — Trendchaser · AI/Dev signal instrument

   The detailed web companion to the Telegram brief. Renders briefs.json,
   committed four times a day by the Trendchaser curation agent.

   Layout: an instrument-readout masthead, then a two-pane console — a
   month calendar (each day split into the 4 daily scan slots) on the
   left, the selected brief's signal register on the right. Picking a
   slot swaps the right pane; topics open one at a time, accordion-style.
   Axes follow the Trendchaser scoring model (README §점수화 공식). */

const { useState: useStateTC, useEffect: useEffectTC, useMemo: useMemoTC, useRef: useRefTC, useDeferredValue: useDeferredValueTC } = React;

const TC_BRIEFS_FALLBACK = [];
const TC_SLOTS = ["morning", "afternoon", "evening", "night"];
const TC_SLOT_HOUR = { morning: 10, afternoon: 14, evening: 18, night: 22 };
// Filed-slot fill shades — light (morning) → dark (night), so a day's four
// slots read as four distinct tones instead of four identical black blocks.
const TC_SLOT_SHADE = {
  morning: "#b2b2b2", afternoon: "#828282", evening: "#4c4c4c", night: "var(--fg)",
};
const TC_MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
const TC_WEEKDAYS = ["S", "M", "T", "W", "T", "F", "S"];

// Six scoring axes, in hexagon order (clockwise from top). README weighting:
// total = .25 signal + .25 affinity + .20 recency + .20 novelty + .05 velocity + .05 freshness
const TC_AXES = [
  { key: "signal",    name: "signal",    weight: 0.25 },
  { key: "affinity",  name: "affinity",  weight: 0.25 },
  { key: "recency",   name: "recency",   weight: 0.20 },
  { key: "novelty",   name: "novelty",   weight: 0.20 },
  { key: "velocity",  name: "velocity",  weight: 0.05 },
  { key: "freshness", name: "freshness", weight: 0.05 },
];

// ─────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────
function tcRenderMd(src) {
  if (!src) return "";
  const m = window.marked;
  if (!m) return src;
  m.setOptions({ breaks: true, gfm: true });
  const pre = src.replace(/\*\*([^*\n]+?)\*\*/g, "<strong>$1</strong>");
  return m.parse(pre);
}

function tcCap(s) {
  return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
}

// Search highlight — split `text` on a (lowercased) query and wrap the
// matched runs in <mark>. Returns a React-renderable array; matched runs
// preserve the original casing.
function tcHighlight(text, q) {
  if (!q || !text) return text;
  const lower = text.toLowerCase();
  const out = [];
  let from = 0, i;
  while ((i = lower.indexOf(q, from)) !== -1) {
    if (i > from) out.push(text.slice(from, i));
    out.push(<mark className="tc-hl" key={out.length}>{text.slice(i, i + q.length)}</mark>);
    from = i + q.length;
  }
  if (from < text.length) out.push(text.slice(from));
  return out;
}

// A short body excerpt centred on the first match of `q`, with light markdown
// markers stripped so the snippet reads as plain prose. null when no match.
function tcSnippet(body, q, ctx = 52) {
  if (!body || !q) return null;
  const flat = body.replace(/\*\*/g, "").replace(/^[\s>#-]+/gm, "").replace(/\s+/g, " ").trim();
  const i = flat.toLowerCase().indexOf(q);
  if (i === -1) return null;
  const start = Math.max(0, i - ctx);
  const end = Math.min(flat.length, i + q.length + ctx);
  return { text: flat.slice(start, end), lead: start > 0, trail: end < flat.length };
}

// Relevance score for a search hit. Field weighting mirrors how a reader scans:
// a hit in the headline counts most, then the source id, then the body. Repeat
// occurrences add up, and a headline that *starts* with the query gets a nudge.
function tcRelevance(topic, q) {
  const count = (s) => {
    if (!s) return 0;
    const low = s.toLowerCase();
    let n = 0, i = 0;
    while ((i = low.indexOf(q, i)) !== -1) { n += 1; i += q.length; }
    return n;
  };
  let rel = count(topic.headline) * 3 + count(topic.source) * 2 + count(topic.body);
  if ((topic.headline || "").toLowerCase().startsWith(q)) rel += 2;
  return rel;
}

function tcClamp(n) {
  return Math.max(0, Math.min(100, n || 0));
}

// Composite score from the 6 axes, per the README weighting.
function tcComposite(scores) {
  if (!scores) return null;
  let total = 0;
  for (const a of TC_AXES) total += tcClamp(scores[a.key]) * a.weight;
  return Math.round(total);
}

function tcPad(n) {
  return String(n).padStart(2, "0");
}

// Animate 0 → target once `run` becomes true.
function useTcCountUp(target, run, dur = 780) {
  const [val, setVal] = useStateTC(0);
  useEffectTC(() => {
    if (!run) return;
    let raf = 0;
    let t0 = 0;
    const tick = (ts) => {
      if (!t0) t0 = ts;
      const p = Math.min(1, (ts - t0) / dur);
      const eased = 1 - Math.pow(1 - p, 3);
      setVal(Math.round(target * eased));
      if (p < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [target, run, dur]);
  return val;
}

// ─────────────────────────────────────────────────────────────
// SlotDial — 24h clock face; the hand marks the slot's scan hour
// ─────────────────────────────────────────────────────────────
function SlotDial({ slot, size = 24 }) {
  const hour = TC_SLOT_HOUR[slot] != null ? TC_SLOT_HOUR[slot] : 12;
  const ang = (hour / 24) * 2 * Math.PI;
  const r = size / 2 - 3.5;
  const c = size / 2;
  const dx = c + r * Math.sin(ang);
  const dy = c - r * Math.cos(ang);
  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}
      aria-hidden="true" style={{ display: "block", overflow: "visible" }}>
      <circle cx={c} cy={c} r={r} fill="none" stroke="currentColor" strokeWidth="1" opacity="0.4" />
      <line x1={c} y1={c} x2={dx} y2={dy} stroke="currentColor" strokeWidth="1" opacity="0.55" />
      <circle cx={dx} cy={dy} r="2.6" fill="currentColor" />
    </svg>
  );
}

// ─────────────────────────────────────────────────────────────
// SignalFingerprint — 6-axis hexagon radar of a topic's score
// ─────────────────────────────────────────────────────────────
function SignalFingerprint({ scores, size = 28, detail = false }) {
  if (!scores) return null;
  const c = size / 2;
  const R = size / 2 - (detail ? 14 : 3);
  const pt = (i, frac) => {
    const a = (-90 + i * 60) * Math.PI / 180;
    return [
      +(c + R * frac * Math.cos(a)).toFixed(2),
      +(c + R * frac * Math.sin(a)).toFixed(2),
    ];
  };
  const ring = (frac) => TC_AXES.map((_, i) => pt(i, frac).join(",")).join(" ");
  const shape = TC_AXES.map((a, i) => pt(i, tcClamp(scores[a.key]) / 100).join(",")).join(" ");
  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}
      aria-hidden="true" style={{ display: "block", overflow: "visible" }}>
      <polygon points={ring(1)} fill="none" stroke="currentColor" strokeWidth="1" opacity="0.3" />
      {detail && <polygon points={ring(0.66)} fill="none" stroke="currentColor" strokeWidth="1" opacity="0.15" />}
      {detail && <polygon points={ring(0.33)} fill="none" stroke="currentColor" strokeWidth="1" opacity="0.15" />}
      {detail && TC_AXES.map((_, i) => {
        const [x, y] = pt(i, 1);
        return <line key={i} x1={c} y1={c} x2={x} y2={y} stroke="currentColor" strokeWidth="1" opacity="0.15" />;
      })}
      <polygon points={shape} fill="currentColor" fillOpacity={detail ? 0.11 : 0.2}
        stroke="currentColor" strokeWidth={detail ? 1.5 : 1} strokeLinejoin="round" />
      {TC_AXES.map((a, i) => {
        const [x, y] = pt(i, tcClamp(scores[a.key]) / 100);
        return <circle key={i} cx={x} cy={y} r={detail ? 2.4 : 1.5} fill="currentColor" />;
      })}
    </svg>
  );
}

// ─────────────────────────────────────────────────────────────
// ScoreMeter — segmented composite bar (LED-meter style)
// ─────────────────────────────────────────────────────────────
function ScoreMeter({ value, cells = 10 }) {
  const filled = Math.round((tcClamp(value) / 100) * cells);
  return (
    <span style={{ display: "inline-flex", gap: 2, alignItems: "center" }} aria-hidden="true">
      {Array.from({ length: cells }).map((_, i) => (
        <span key={i} style={{
          width: 5, height: 12, boxSizing: "border-box",
          background: i < filled ? "var(--fg)" : "transparent",
          border: `1px solid ${i < filled ? "var(--fg)" : "var(--line)"}`,
        }} />
      ))}
    </span>
  );
}

// ─────────────────────────────────────────────────────────────
// Funnel — raw ▸ dedup ▸ filed, counting up when `run` flips true
// ─────────────────────────────────────────────────────────────
function Funnel({ raw, dedup, picked, run }) {
  const r = useTcCountUp(raw || 0, run);
  const d = useTcCountUp(dedup || 0, run);
  const p = useTcCountUp(picked || 0, run);
  const numStyle = { fontFamily: "var(--font-mono)", fontSize: 15, fontWeight: 500, fontVariantNumeric: "tabular-nums" };
  const arrow = { color: "var(--fg-faint)", fontFamily: "var(--font-mono)", fontSize: 12 };
  return (
    <div style={{ display: "inline-flex", alignItems: "baseline", gap: 9, flexWrap: "wrap" }}>
      <span className="micro" style={{ color: "var(--fg-faint)" }}>raw</span>
      <span style={numStyle}>{r}</span>
      <span style={arrow}>▸▸</span>
      <span className="micro" style={{ color: "var(--fg-faint)" }}>dedup</span>
      <span style={numStyle}>{d}</span>
      <span style={arrow}>▸▸</span>
      <span className="micro" style={{ color: "var(--fg-faint)" }}>filed</span>
      <span style={{ ...numStyle, fontSize: 17, fontWeight: 600 }}>{p}</span>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// ScanStrip — masthead channel bank with a sweeping scan line
// ─────────────────────────────────────────────────────────────
function ScanStrip() {
  const ticks = useMemoTC(() => {
    const out = [];
    let s = 1373;
    for (let i = 0; i < 116; i++) {
      s = (s * 1103515245 + 12345) & 0x7fffffff;
      out.push(0.2 + (s % 1000) / 1000 * 0.8);
    }
    return out;
  }, []);
  return (
    <div style={{ position: "relative", height: 48, overflow: "hidden" }}>
      <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "flex-end", gap: 2 }}>
        {ticks.map((h, i) => (
          <span key={i} className="tc-tick" style={{
            flex: 1, height: `${Math.round(h * 100)}%`,
            background: "var(--fg-faint)", animationDelay: `${i * 6}ms`,
          }} />
        ))}
      </div>
      <div className="tc-sweep" aria-hidden="true" />
      <div style={{ position: "absolute", left: 0, right: 0, bottom: 0, height: 1, background: "var(--line)" }} />
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// TopicDetail — expanded panel: prose + signal readout
// ─────────────────────────────────────────────────────────────
function TopicDetail({ topic }) {
  const composite = tcComposite(topic.scores);
  return (
    <div style={{ padding: "8px 18px 36px" }}>
      <div className="tc-detail-grid">
        <div>
          <h3 className="display-md" style={{
            fontSize: "clamp(18px, 1.7vw, 23px)", fontWeight: 600,
            lineHeight: 1.32, marginBottom: 14, maxWidth: 640,
          }}>
            {topic.headline}
          </h3>
          <div
            className="post-body"
            style={{ color: "var(--fg)", maxWidth: 640 }}
            dangerouslySetInnerHTML={{ __html: tcRenderMd(topic.body) }}
          />
          {(topic.source || topic.url) && (
            <div style={{ display: "flex", alignItems: "center", gap: 14, marginTop: 20, flexWrap: "wrap" }}>
              {topic.source && (
                <span className="micro" style={{ textTransform: "none", letterSpacing: "0.02em", color: "var(--fg-faint)" }}>
                  {topic.source}
                </span>
              )}
              {topic.url && (
                <a href={topic.url} target="_blank" rel="noopener noreferrer"
                  data-cursor="link" data-cursor-label={t("Source")}
                  style={{
                    fontFamily: "var(--font-mono)", fontSize: 11, letterSpacing: "0.04em",
                    color: "var(--fg)", borderBottom: "1px solid var(--fg)", paddingBottom: 2,
                  }}>
                  원문 ↗
                </a>
              )}
            </div>
          )}
        </div>

        {topic.scores && (
          <aside className="tc-readout">
            <div className="micro" style={{ marginBottom: 14 }}>{t("Signal readout")}</div>
            <div style={{ display: "flex", justifyContent: "center", color: "var(--fg)" }}>
              <SignalFingerprint scores={topic.scores} size={150} detail />
            </div>
            <div style={{
              display: "flex", justifyContent: "space-between", alignItems: "baseline",
              margin: "16px 0 4px", paddingBottom: 10, borderBottom: "1px solid var(--line-soft)",
            }}>
              <span className="micro">{t("Composite")}</span>
              <span style={{ fontFamily: "var(--font-mono)", fontSize: 24, fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>
                {composite}
              </span>
            </div>
            {TC_AXES.map(a => {
              const v = tcClamp(topic.scores[a.key]);
              return (
                <div key={a.key} style={{ display: "grid", gridTemplateColumns: "62px 1fr 26px", gap: 8, alignItems: "center", marginTop: 8 }}>
                  <span className="micro" style={{ color: "var(--fg-muted)" }}>{a.name}</span>
                  <span style={{ position: "relative", height: 3, background: "var(--line-soft)" }}>
                    <span style={{ position: "absolute", left: 0, top: 0, height: "100%", width: `${v}%`, background: "var(--fg)" }} />
                  </span>
                  <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, textAlign: "right", fontVariantNumeric: "tabular-nums" }}>
                    {v}
                  </span>
                </div>
              );
            })}
          </aside>
        )}
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// TopicRow — one register line; one opens at a time (accordion).
// Height animates via the grid-template-rows 0fr↔1fr technique.
// ─────────────────────────────────────────────────────────────
function TopicRow({ topic, n, anchorId, expanded, onToggle }) {
  const composite = tcComposite(topic.scores);
  const tag = topic.tag || "AI";
  const chipBase = {
    fontFamily: "var(--font-mono)", fontSize: 9.5, fontWeight: 500,
    letterSpacing: "0.1em", textTransform: "uppercase", padding: "2px 6px",
    border: "1px solid var(--fg)", whiteSpace: "nowrap", justifySelf: "start",
  };
  const chip = tag === "AI"
    ? { ...chipBase, background: "var(--fg)", color: "var(--bg)" }
    : tag === "Watch"
      ? { ...chipBase, border: "1px solid var(--fg-faint)", color: "var(--fg-muted)" }
      : chipBase;
  return (
    <div id={anchorId} style={{ borderTop: "1px solid var(--line-soft)", scrollMarginTop: 128 }}>
      <button
        type="button"
        onClick={onToggle}
        className={"tc-row" + (expanded ? " tc-row-open" : "")}
        data-cursor="link"
        data-cursor-label={t(expanded ? "Close" : "Open")}>
        <span className="tc-c-num micro" style={{ color: "var(--fg-faint)" }}>
          {tcPad(n)}
        </span>
        <span className="tc-c-head" style={{ fontSize: 15.5, fontWeight: 500, lineHeight: 1.4 }}>
          {topic.headline}
        </span>
        {/* meter + fingerprint + tag — `display:contents` on desktop so each
            still lands in its own grid column; a flex row on mobile. */}
        <span className="tc-c-meta">
          <span className="tc-c-meter">
            {composite != null && <ScoreMeter value={composite} />}
          </span>
          <span className="tc-c-glyph" style={{ color: "var(--fg)" }}>
            <SignalFingerprint scores={topic.scores} size={28} />
          </span>
          <span className="tc-c-tag" style={chip}>{tag}</span>
        </span>
        <span className="tc-c-arrow" style={{
          fontFamily: "var(--font-mono)",
          transform: expanded ? "rotate(90deg)" : "none",
          transition: "transform 280ms var(--easing-default)",
        }}>→</span>
      </button>
      <div className="tc-acc" style={{ gridTemplateRows: expanded ? "1fr" : "0fr" }}>
        <div style={{
          minHeight: 0, overflow: "hidden",
          opacity: expanded ? 1 : 0,
          transition: "opacity 300ms var(--easing-default)",
        }}>
          {expanded && <TopicDetail topic={topic} />}
        </div>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// SlotSquare — one of a day's four scan slots in the calendar.
// Filled = a brief was filed; the selected slot gets an outer ring.
// ─────────────────────────────────────────────────────────────
function SlotSquare({ slot, brief, selected, onSelect, searchActive, matched }) {
  const has = !!brief;
  const shade = TC_SLOT_SHADE[slot] || "var(--fg)";
  const dim = searchActive && has && !matched;
  const style = {
    width: "100%", aspectRatio: "1 / 1", boxSizing: "border-box", padding: 0,
    border: `1px solid ${has ? shade : "var(--line-soft)"}`,
    background: has ? shade : "transparent",
    cursor: has ? "none" : "default",
    opacity: dim ? 0.14 : 1,
    boxShadow: selected
      ? "0 0 0 1.5px var(--bg), 0 0 0 3px var(--fg)"
      : (searchActive && matched ? "0 0 0 1.5px var(--bg), 0 0 0 2px var(--fg)" : "none"),
    transition: "box-shadow 220ms var(--easing-default), background 200ms var(--easing-default), opacity 200ms var(--easing-default)",
  };
  if (!has) return <span aria-hidden="true" style={style} />;
  return (
    <button
      type="button"
      style={style}
      onClick={() => onSelect(brief.id)}
      data-cursor="link"
      data-cursor-label={tcCap(slot)}
      aria-label={`${slot} brief — ${brief.title}`}
      title={tcCap(slot)}
    />
  );
}

// ─────────────────────────────────────────────────────────────
// DayCell — one date in the calendar: date label + 2×2 slot grid
// ─────────────────────────────────────────────────────────────
function DayCell({ dayNum, slotsForDay, selectedBriefId, onSelect, isToday, searchActive, matchedBriefIds }) {
  const hasAny = TC_SLOTS.some(s => slotsForDay[s]);
  return (
    <div className={hasAny ? "tc-cal-cell tc-cal-cell--has" : "tc-cal-cell"}>
      <div className="tc-cal-date" style={{ color: hasAny ? "var(--fg)" : "var(--fg-faint)" }}>
        <span>{tcPad(dayNum)}</span>
        {isToday && <span className="tc-cal-today" />}
      </div>
      <div className="tc-cal-quad">
        {TC_SLOTS.map(slot => {
          const brief = slotsForDay[slot];
          return (
            <SlotSquare
              key={slot}
              slot={slot}
              brief={brief}
              selected={!!brief && brief.id === selectedBriefId}
              onSelect={onSelect}
              searchActive={searchActive}
              matched={!!brief && matchedBriefIds.has(brief.id)}
            />
          );
        })}
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Calendar — month grid; each day carries the 4 scan slots
// ─────────────────────────────────────────────────────────────
function Calendar({ calMonth, setCalMonth, briefsByDateSlot, selectedBriefId, onSelect, todayStr, searchActive, matchedBriefIds }) {
  useLang();
  const { y, m } = calMonth;
  const startWd = new Date(y, m, 1).getDay();
  const daysIn = new Date(y, m + 1, 0).getDate();

  const cells = [];
  for (let i = 0; i < startWd; i++) cells.push(0);
  for (let d = 1; d <= daysIn; d++) cells.push(d);
  while (cells.length % 7) cells.push(0);

  const shift = (delta) => {
    let nm = m + delta;
    let ny = y;
    if (nm < 0) { nm = 11; ny -= 1; }
    if (nm > 11) { nm = 0; ny += 1; }
    setCalMonth({ y: ny, m: nm });
  };

  return (
    <div className="tc-cal">
      <div className="tc-cal-head">
        <button type="button" onClick={() => shift(-1)} className="tc-cal-nav"
          data-cursor="link" data-cursor-label={t("Previous")} aria-label={t("Previous month")}>‹</button>
        <span className="micro micro-fg">{TC_MONTHS[m]} {y}</span>
        <button type="button" onClick={() => shift(1)} className="tc-cal-nav"
          data-cursor="link" data-cursor-label={t("Next")} aria-label={t("Next month")}>›</button>
      </div>
      <div className="tc-cal-wd">
        {TC_WEEKDAYS.map((w, i) => <span key={i} className="micro">{w}</span>)}
      </div>
      <div className="tc-cal-grid" key={`${y}-${m}`}>
        {cells.map((d, i) => {
          if (!d) return <div key={i} className="tc-cal-cell tc-cal-blank" aria-hidden="true" />;
          const ds = `${y}-${tcPad(m + 1)}-${tcPad(d)}`;
          return (
            <DayCell
              key={i}
              dayNum={d}
              slotsForDay={briefsByDateSlot[ds] || {}}
              selectedBriefId={selectedBriefId}
              onSelect={onSelect}
              isToday={ds === todayStr}
              searchActive={searchActive}
              matchedBriefIds={matchedBriefIds}
            />
          );
        })}
      </div>
      <div className="tc-cal-legend">
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "6px 14px" }}>
          {[["↖", "morning"], ["↗", "afternoon"], ["↙", "evening"], ["↘", "night"]].map(([arrow, slot]) => (
            <span key={slot} style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
              <span style={{
                width: 9, height: 9, flexShrink: 0,
                background: TC_SLOT_SHADE[slot], border: `1px solid ${TC_SLOT_SHADE[slot]}`,
              }} />
              <span className="micro" style={{ color: "var(--fg-muted)" }}>{arrow} {t(slot)}</span>
            </span>
          ))}
        </div>
        <div style={{ display: "inline-flex", alignItems: "center", gap: 6, marginTop: 10 }}>
          <span style={{ width: 9, height: 9, flexShrink: 0, border: "1px solid var(--line-soft)" }} />
          <span className="micro" style={{ textTransform: "none", color: "var(--fg-muted)" }}>{t("no scan")}</span>
        </div>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// BriefPane — the selected brief: header + accordion topic register
// ─────────────────────────────────────────────────────────────
function BriefPane({ brief, activeTag, setActiveTag, expandedTopicId, onToggleTopic }) {
  useLang();
  const [ready, setReady] = useStateTC(false);
  useEffectTC(() => {
    const id = setTimeout(() => setReady(true), 40);
    return () => clearTimeout(id);
  }, []);

  const counts = useMemoTC(() => {
    const c = { AI: 0, General: 0, Watch: 0 };
    brief.topics.forEach(tp => { if (c[tp.tag] != null) c[tp.tag] += 1; });
    return c;
  }, [brief]);
  const filterTags = ["All", "AI", "General", "Watch"].filter(
    tg => tg === "All" || counts[tg] > 0);
  const topics = activeTag === "All"
    ? brief.topics
    : brief.topics.filter(tp => tp.tag === activeTag);

  return (
    <div className="tc-pane">
      <header className="tc-brief-head">
        <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
          <span style={{ color: "var(--fg)" }}><SlotDial slot={brief.slot} size={24} /></span>
          <span className="display-md" style={{ fontSize: "clamp(20px, 1.9vw, 26px)", fontWeight: 600 }}>
            {tcCap(brief.slot)}
          </span>
        </div>
        <Funnel raw={brief.raw} dedup={brief.dedup} picked={brief.picked} run={ready} />
      </header>
      <div className="micro tc-brief-meta">
        {[brief.date.replace(/-/g, "."), brief.time, brief.lookback].filter(Boolean).join(" · ")}
      </div>

      <div className="tc-pane-filter">
        {filterTags.map(tg => {
          const active = tg === activeTag;
          const count = tg === "All" ? brief.topics.length : counts[tg];
          return (
            <button
              key={tg}
              type="button"
              onClick={() => setActiveTag(tg)}
              data-cursor="link"
              data-cursor-label={t("Filter")}
              style={{
                padding: "5px 11px", border: "1px solid var(--line)",
                fontFamily: "var(--font-mono)", fontSize: 10, letterSpacing: "0.07em",
                textTransform: "uppercase",
                background: active ? "var(--fg)" : "transparent",
                color: active ? "var(--bg)" : "var(--fg)", cursor: "none",
                transition: "background 200ms var(--easing-default), color 200ms var(--easing-default)",
              }}>
              {tg} <span style={{ opacity: 0.5, marginLeft: 5 }}>{count}</span>
            </button>
          );
        })}
      </div>

      <div>
        {topics.map(tp => {
          const realIdx = brief.topics.indexOf(tp);
          const aId = `${brief.id}--${realIdx + 1}`;
          return (
            <TopicRow
              key={aId}
              topic={tp}
              n={realIdx + 1}
              anchorId={aId}
              expanded={expandedTopicId === aId}
              onToggle={() => onToggleTopic(aId)}
            />
          );
        })}
        {topics.length === 0 && (
          <div className="body-sm" style={{ padding: "28px 18px", color: "var(--fg-muted)", borderTop: "1px solid var(--line-soft)" }}>
            {t("No topics under this filter.")}
          </div>
        )}
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// SearchResultRow — one matching card: date·slot context + highlighted
// headline (+ body snippet when the match is in the body), accordion detail.
// ─────────────────────────────────────────────────────────────
function SearchResultRow({ brief, topic, anchorId, q, expanded, onToggle }) {
  const tag = topic.tag || "AI";
  const composite = tcComposite(topic.scores);
  const chipBase = {
    fontFamily: "var(--font-mono)", fontSize: 9.5, fontWeight: 500,
    letterSpacing: "0.1em", textTransform: "uppercase", padding: "2px 6px",
    border: "1px solid var(--fg)", whiteSpace: "nowrap", justifySelf: "start",
  };
  const chip = tag === "AI"
    ? { ...chipBase, background: "var(--fg)", color: "var(--bg)" }
    : tag === "Watch"
      ? { ...chipBase, border: "1px solid var(--fg-faint)", color: "var(--fg-muted)" }
      : chipBase;
  const headlineHasMatch = topic.headline.toLowerCase().includes(q);
  const snip = headlineHasMatch ? null : tcSnippet(topic.body, q);
  return (
    <div id={anchorId} style={{ borderTop: "1px solid var(--line-soft)", scrollMarginTop: 128 }}>
      <button
        type="button"
        onClick={onToggle}
        className={"tc-row" + (expanded ? " tc-row-open" : "")}
        data-cursor="link"
        data-cursor-label={t(expanded ? "Close" : "Open")}>
        <span className="tc-c-num micro tc-search-ctx" style={{ color: "var(--fg-faint)" }}>
          {brief.date.slice(5).replace("-", ".")}<br />{t(brief.slot)}
        </span>
        <span className="tc-c-head" style={{ fontSize: 15.5, fontWeight: 500, lineHeight: 1.4 }}>
          {tcHighlight(topic.headline, q)}
          {snip && (
            <span className="tc-search-snip">
              {snip.lead ? "… " : ""}{tcHighlight(snip.text, q)}{snip.trail ? " …" : ""}
            </span>
          )}
        </span>
        <span className="tc-c-meta">
          <span className="tc-c-meter">
            {composite != null && <ScoreMeter value={composite} />}
          </span>
          <span className="tc-c-glyph" style={{ color: "var(--fg)" }}>
            <SignalFingerprint scores={topic.scores} size={28} />
          </span>
          <span className="tc-c-tag" style={chip}>{tag}</span>
        </span>
        <span className="tc-c-arrow" style={{
          fontFamily: "var(--font-mono)",
          transform: expanded ? "rotate(90deg)" : "none",
          transition: "transform 280ms var(--easing-default)",
        }}>→</span>
      </button>
      <div className="tc-acc" style={{ gridTemplateRows: expanded ? "1fr" : "0fr" }}>
        <div style={{
          minHeight: 0, overflow: "hidden",
          opacity: expanded ? 1 : 0,
          transition: "opacity 300ms var(--easing-default)",
        }}>
          {expanded && <TopicDetail topic={topic} />}
        </div>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// SearchResults — flat newest-first list of matching cards across all briefs.
// ─────────────────────────────────────────────────────────────
function SearchResults({ results, q, sortMode, setSortMode, expandedTopicId, onToggleTopic }) {
  useLang();
  const sorts = [["relevance", "Relevance"], ["newest", "Newest"], ["oldest", "Oldest"]];
  return (
    <div className="tc-pane">
      <div className="tc-search-resulthead">
        <span className="micro">
          {results.length
            ? `${results.length} ${t("matching cards")}`
            : t("No cards match.")}
        </span>
        {results.length > 0 && (
          <span className="tc-sort" role="group" aria-label={t("Sort")}>
            {sorts.map(([mode, label]) => (
              <button
                key={mode}
                type="button"
                onClick={() => setSortMode(mode)}
                className={"tc-sort-btn micro" + (sortMode === mode ? " tc-sort-btn--on" : "")}
                data-cursor="link"
                data-cursor-label={t("Sort")}
                aria-pressed={sortMode === mode}>
                {t(label)}
              </button>
            ))}
          </span>
        )}
      </div>
      <div>
        {results.map(({ brief, topic, idx }) => {
          const aId = `${brief.id}--${idx + 1}`;
          return (
            <SearchResultRow
              key={aId}
              brief={brief}
              topic={topic}
              anchorId={aId}
              q={q}
              expanded={expandedTopicId === aId}
              onToggle={() => onToggleTopic(aId)}
            />
          );
        })}
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Trendchaser — the page
// ─────────────────────────────────────────────────────────────
function Trendchaser() {
  useLang();
  const [progress, setProgress] = useStateTC(0);
  const [briefs, setBriefs] = useStateTC(TC_BRIEFS_FALLBACK);
  const [syncState, setSyncState] = useStateTC("idle"); // idle | loaded | failed
  const [selectedBriefId, setSelectedBriefId] = useStateTC(null);
  const [expandedTopicId, setExpandedTopicId] = useStateTC(null);
  const [activeTag, setActiveTag] = useStateTC("All");
  const [calMonth, setCalMonth] = useStateTC(null); // { y, m }
  const [searchQuery, setSearchQuery] = useStateTC("");
  const [sortMode, setSortMode] = useStateTC("relevance"); // relevance | newest | oldest
  const paneRef = useRefTC(null);

  // Reading progress
  useEffectTC(() => {
    const onScroll = () => {
      const h = document.documentElement.scrollHeight - window.innerHeight;
      setProgress(h > 0 ? (window.scrollY / h) * 100 : 0);
    };
    window.addEventListener("scroll", onScroll);
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  // Live brief feed via /api/briefs (parses PRISMA brief-stream on demand,
  // edge-cached). The newest brief is shown by default; a `#<brief-id>` /
  // `#<brief-id>--<n>` hash deep-links a brief / topic. A deep-link can arrive
  // (via Telegram) before the API has the brief — poll briefly, then fall back
  // to the newest brief. On API failure the bundled fallback feed renders.
  useEffectTC(() => {
    let cancelled = false;
    const hash = (window.location.hash || "").replace(/^#/, "");
    const wantId = hash.split("--")[0];
    let tries = 0;
    const apply = (data, match) => {
      setBriefs(data);
      setSyncState("loaded");
      const initial = match || data[0];
      setSelectedBriefId(initial.id);
      const parts = initial.date.split("-");
      setCalMonth({ y: Number(parts[0]), m: Number(parts[1]) - 1 });
      if (match && hash.includes("--")) {
        setExpandedTopicId(hash);
        // The deep-linked topic should land at the top. Page layout keeps
        // shifting for a couple seconds after load (web fonts, accordion,
        // funnel), so watch the topic's absolute position and re-scroll on
        // every shift for ~4s, then snap it exactly into place.
        let lastY = -1, ticks = 0;
        const settle = () => {
          const el = document.getElementById(hash);
          if (++ticks > 20) {
            if (el) el.scrollIntoView({ block: "start" });
            return;
          }
          if (el) {
            const y = Math.round(el.getBoundingClientRect().top + window.scrollY);
            if (Math.abs(y - lastY) >= 2) {
              el.scrollIntoView({ behavior: "smooth", block: "start" });
              lastY = y;
            }
          }
          setTimeout(settle, 200);
        };
        setTimeout(settle, 280);
      }
    };
    const load = () => {
      fetch("/api/briefs", { cache: "no-cache" })
        .then(r => (r.ok ? r.json() : Promise.reject(r.status)))
        .then(data => {
          if (cancelled) return;
          if (!Array.isArray(data) || !data.length) return;
          const match = wantId ? data.find(b => b.id === wantId) : null;
          if (wantId && !match && tries < 16) {
            tries += 1;          // deep-linked brief not published yet — retry
            setTimeout(load, 18000);
            return;
          }
          apply(data, match);
        })
        .catch(() => { if (!cancelled) setSyncState("failed"); });
    };
    load();
    return () => { cancelled = true; };
  }, []);

  const briefsByDateSlot = useMemoTC(() => {
    const map = {};
    briefs.forEach(b => {
      if (!map[b.date]) map[b.date] = {};
      map[b.date][b.slot] = b;
    });
    return map;
  }, [briefs]);

  const totalTopics = useMemoTC(
    () => briefs.reduce((n, b) => n + b.topics.length, 0), [briefs]);

  // Search — pure in-memory filter over every topic's headline + body +
  // source. `searchResults` is a flat, newest-first list of matching cards;
  // `matchedBriefIds` drives the calendar slot highlighting.
  // The query is deferred so typing stays responsive: the input updates on
  // every keystroke, but the heavier filter + result render runs on a
  // low-priority pass React can interrupt. The lowercase haystack per topic is
  // built once per briefs load (searchIndex), not re-lowercased each keystroke.
  const deferredQuery = useDeferredValueTC(searchQuery);
  const searchQ = deferredQuery.trim().toLowerCase();
  const searchActive = searchQ.length > 0;
  const searchIndex = useMemoTC(() => {
    const idx = [];
    briefs.forEach(b => {
      b.topics.forEach((tp, i) => {
        idx.push({ brief: b, topic: tp, idx: i,
          hay: `${tp.headline}\n${tp.body}\n${tp.source}`.toLowerCase() });
      });
    });
    return idx;
  }, [briefs]);
  const searchResults = useMemoTC(() => {
    if (!searchQ) return [];
    const out = [];
    for (const e of searchIndex) {
      if (e.hay.includes(searchQ)) {
        out.push({ brief: e.brief, topic: e.topic, idx: e.idx, rel: tcRelevance(e.topic, searchQ) });
      }
    }
    return out; // briefs already sorted newest-first
  }, [searchIndex, searchQ]);
  // Apply the chosen sort. `searchResults` is newest-first as built, so that is
  // the identity order; oldest is its reverse; relevance sorts by score with a
  // stable newest-first tiebreak.
  const sortedResults = useMemoTC(() => {
    if (sortMode === "oldest") return searchResults.slice().reverse();
    if (sortMode === "relevance") {
      return searchResults
        .map((r, i) => [r, i])
        .sort((a, b) => b[0].rel - a[0].rel || a[1] - b[1])
        .map(([r]) => r);
    }
    return searchResults; // newest
  }, [searchResults, sortMode]);
  const matchedBriefIds = useMemoTC(
    () => new Set(searchResults.map(r => r.brief.id)), [searchResults]);

  const todayStr = useMemoTC(() => {
    const d = new Date();
    return `${d.getFullYear()}-${tcPad(d.getMonth() + 1)}-${tcPad(d.getDate())}`;
  }, []);

  const selectedBrief = briefs.find(b => b.id === selectedBriefId) || null;

  const selectBrief = (id) => {
    if (id === selectedBriefId) return;
    setSelectedBriefId(id);
    setExpandedTopicId(null);
    setActiveTag("All");
    if (window.innerWidth < 1000 && paneRef.current) {
      requestAnimationFrame(() => {
        paneRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
      });
    }
  };

  const toggleTopic = (id) =>
    setExpandedTopicId(prev => (prev === id ? null : id));

  const loaded = syncState === "loaded";
  const channels = useTcCountUp(153, loaded);
  const scans = useTcCountUp(4, loaded);
  const filed = useTcCountUp(totalTopics, loaded);
  const latest = briefs.length
    ? `${briefs[0].date.slice(5).replace("-", ".")} · ${briefs[0].time.replace(" KST", "")}`
    : "—";

  const readout = [
    { label: t("Channels"), value: loaded ? channels : 0 },
    { label: t("Scans / day"), value: loaded ? scans : 0 },
    { label: t("Signals filed"), value: loaded ? filed : 0 },
    { label: t("Latest scan"), value: latest, text: true },
  ];

  return (
    <div className="page" style={{ paddingTop: 140 }}>
      {/* Reading progress */}
      <div style={{
        position: "fixed", top: 88, left: 0, right: 0, height: 1, zIndex: 50,
        background: "var(--line-soft)",
      }}>
        <div style={{ width: `${progress}%`, height: "100%", background: "var(--fg)", transition: "width 100ms" }} />
      </div>

      <div className="page-eyebrow">
        <span className="micro">{t("Index")} 07</span>
        <span style={{ width: 24, height: 1, background: "var(--fg)" }} />
        <span className="micro micro-fg">{t("Trendchaser · AI/Dev signal instrument")}</span>
      </div>

      <div className="tc-frame">

        {/* Masthead */}
        <h1 className="display-lg" style={{ marginBottom: 20, maxWidth: 920 }}>
          {t("What's moving in AI, measured four times a day.")}
        </h1>
        <p className="body-lg" style={{ color: "var(--fg-muted)", maxWidth: 680, marginBottom: 24 }}>
          {t("A six-axis scoring pass over ~153 source channels — labs, repositories, papers, forums. Every brief below is what cleared the cut.")}
        </p>

        {/* KakaoTalk open-chat invite */}
        <a
          href="https://open.kakao.com/o/pfQMgHsi"
          target="_blank"
          rel="noopener noreferrer"
          className="tc-kakao"
          data-cursor="link"
          data-cursor-label={t("Join")}
        >
          <span>
            <span className="micro" style={{ color: "var(--fg-muted)" }}>KakaoTalk 오픈채팅</span>
            <span style={{ display: "block", marginTop: 6, fontSize: 15, fontWeight: 500, lineHeight: 1.5 }}>
              이 소식을 매일 네 번, 무료로 익명으로 받아보고 싶다면 들어오세요.
            </span>
          </span>
          <span className="tc-kakao-cta">입장하기 <span style={{ fontFamily: "var(--font-mono)" }}>→</span></span>
        </a>

        {/* Instrument readout panel */}
        <div className="tc-readout-panel">
          <div className="tc-readout-grid">
            {readout.map((cell, i) => (
              <div key={i} className="tc-readout-cell">
                <div className="micro" style={{ color: "var(--fg-muted)", marginBottom: 8 }}>{cell.label}</div>
                <div style={{
                  fontFamily: "var(--font-mono)",
                  fontSize: cell.text ? 17 : 30,
                  fontWeight: 600, lineHeight: 1, fontVariantNumeric: "tabular-nums",
                }}>
                  {cell.value}
                </div>
              </div>
            ))}
          </div>
          <div style={{ borderTop: "1px solid var(--line-soft)", padding: "14px 16px 12px" }}>
            <ScanStrip />
            <div className="micro" style={{ color: "var(--fg-faint)", marginTop: 10, textTransform: "none", letterSpacing: "0.04em" }}>
              {t("Channel bank · ~70 feeds · 153 emitting paths · scanned 10:00 / 14:00 / 18:00 / 22:00 KST")}
            </div>
          </div>
        </div>

        {/* Search bar */}
        {loaded && calMonth && selectedBrief && (
          <div className="tc-search">
            <span className="tc-search-icon micro" aria-hidden="true">⌕</span>
            <input
              type="text"
              className="tc-search-input"
              value={searchQuery}
              onChange={e => { setSearchQuery(e.target.value); setExpandedTopicId(null); }}
              placeholder={t("Search briefs — headline, body, source")}
              aria-label={t("Search briefs")}
              data-cursor="text"
            />
            {searchActive && (
              <button
                type="button"
                className="tc-search-clear micro"
                onClick={() => { setSearchQuery(""); setExpandedTopicId(null); }}
                data-cursor="link"
                data-cursor-label={t("Clear")}
                aria-label={t("Clear search")}>
                {searchResults.length} · ✕
              </button>
            )}
          </div>
        )}

        {/* Calendar + brief pane */}
        {loaded && calMonth && selectedBrief ? (
          <div className="tc-split">
            <div className="tc-cal-col">
              <Calendar
                calMonth={calMonth}
                setCalMonth={setCalMonth}
                briefsByDateSlot={briefsByDateSlot}
                selectedBriefId={selectedBriefId}
                onSelect={selectBrief}
                todayStr={todayStr}
                searchActive={searchActive}
                matchedBriefIds={matchedBriefIds}
              />
            </div>
            <div className="tc-pane-col" ref={paneRef}>
              {searchActive ? (
                <SearchResults
                  results={sortedResults}
                  q={searchQ}
                  sortMode={sortMode}
                  setSortMode={setSortMode}
                  expandedTopicId={expandedTopicId}
                  onToggleTopic={toggleTopic}
                />
              ) : (
                <BriefPane
                  key={selectedBrief.id}
                  brief={selectedBrief}
                  activeTag={activeTag}
                  setActiveTag={setActiveTag}
                  expandedTopicId={expandedTopicId}
                  onToggleTopic={toggleTopic}
                />
              )}
            </div>
          </div>
        ) : (
          <div style={{
            border: "1px solid var(--line-soft)", margin: "44px 0 0",
            padding: "120px 0", textAlign: "center",
          }}>
            <div className="micro" style={{ marginBottom: 16, color: "var(--fg-muted)" }}>{t("Status")}</div>
            <div className="display-md" style={{ marginBottom: 12 }}>
              {syncState === "failed" ? t("Instrument offline.") : t("Calibrating…")}
            </div>
            <div className="body-sm" style={{ color: "var(--fg-muted)", maxWidth: 440, margin: "0 auto" }}>
              {t("The Trendchaser agent files a brief here four times a day. The next scan will appear shortly.")}
            </div>
          </div>
        )}

        {/* Method note */}
        <div className="responsive-stack" style={{
          display: "grid", gridTemplateColumns: "1fr 260px", gap: 48,
          padding: "56px 0", marginTop: 64, borderTop: "1px solid var(--line)",
        }}>
          <div>
            <div className="micro" style={{ marginBottom: 16 }}>{t("Method · How a signal is scored")}</div>
            <div className="display-md" style={{ maxWidth: 680 }}>
              {t("Six axes, one number — fetch, dedupe, score, verify.")}
            </div>
          </div>
          <aside style={{ borderLeft: "1px solid var(--line-soft)", paddingLeft: 16 }}>
            <div className="micro" style={{ marginBottom: 6 }}>{t("Note · The six axes")}</div>
            <div className="body-sm" style={{ color: "var(--fg-muted)" }}>
              {t("signal (source authority), affinity (profile match), recency, novelty (first emission vs. trending echo), velocity, freshness. Weighted 25/25/20/20/5/5 into the composite, then cross-checked against the raw source before it reaches this page.")}
            </div>
          </aside>
        </div>

      </div>

      <style>{`
        .tc-frame { width: 100%; max-width: 1120px; margin: 0 auto; }

        /* KakaoTalk open-chat invite — masthead call to action */
        .tc-kakao {
          display: flex; align-items: center; justify-content: space-between;
          gap: 14px 24px; flex-wrap: wrap;
          border: 1px solid var(--fg); padding: 15px 18px; margin-bottom: 32px;
          color: var(--fg); text-decoration: none; cursor: none;
          transition: background 200ms var(--easing-default);
        }
        .tc-kakao:hover { background: var(--line-soft); }
        .tc-kakao-cta {
          font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em;
          text-transform: uppercase; white-space: nowrap;
          border-bottom: 1px solid var(--fg); padding-bottom: 3px;
        }

        /* Instrument readout panel */
        .tc-readout-panel { border: 1px solid var(--line); }
        .tc-readout-grid { display: grid; grid-template-columns: repeat(4, 1fr); }
        .tc-readout-cell { padding: 18px 16px; border-right: 1px solid var(--line-soft); }
        .tc-readout-cell:last-child { border-right: none; }
        @media (max-width: 720px) {
          .tc-readout-grid { grid-template-columns: 1fr 1fr; }
          .tc-readout-cell:nth-child(2) { border-right: none; }
          .tc-readout-cell:nth-child(1), .tc-readout-cell:nth-child(2) {
            border-bottom: 1px solid var(--line-soft);
          }
        }

        /* Channel bank ticks rise on mount; scan line sweeps across */
        @keyframes tc-tick { from { transform: scaleY(0.12); opacity: 0; } to { transform: scaleY(1); opacity: 1; } }
        .tc-tick { transform-origin: bottom; animation: tc-tick 560ms var(--easing-default) both; }
        @keyframes tc-sweep { 0% { left: -76px; } 100% { left: 100%; } }
        .tc-sweep {
          position: absolute; top: 0; bottom: 1px; width: 76px; pointer-events: none;
          background: linear-gradient(90deg, transparent 0%, rgba(10,10,10,0.09) 84%, rgba(10,10,10,0.5) 98%, var(--fg) 100%);
          animation: tc-sweep 6.4s linear infinite;
        }

        /* Two-pane split — calendar left, brief pane right */
        /* Search */
        .tc-search {
          display: flex; align-items: center; gap: 10px;
          border: 1px solid var(--line); margin-top: 44px; padding: 0 14px;
          background: var(--bg);
        }
        .tc-search-icon { font-size: 16px; color: var(--fg-muted); }
        .tc-search-input {
          flex: 1; border: 0; background: transparent; outline: none;
          font-family: var(--font-mono); font-size: 14px; color: var(--fg);
          padding: 14px 0; letter-spacing: 0.01em;
        }
        .tc-search-input::placeholder { color: var(--fg-faint); }
        .tc-search-clear {
          border: 1px solid var(--line); background: transparent; color: var(--fg-muted);
          font-family: var(--font-mono); padding: 4px 9px; cursor: none;
          letter-spacing: 0.06em; white-space: nowrap;
        }
        .tc-search-clear:hover { background: var(--fg); color: var(--bg); }
        .tc-hl { background: var(--fg); color: var(--bg); padding: 0 1px; border-radius: 1px; }
        .tc-search-snip {
          display: block; margin-top: 5px; font-family: var(--font-mono);
          font-size: 11.5px; line-height: 1.5; color: var(--fg-muted);
          font-weight: 400; letter-spacing: 0;
        }
        .tc-search-snip .tc-hl { background: var(--line-soft); color: var(--fg); }
        .tc-search-ctx {
          line-height: 1.35; white-space: nowrap; text-transform: uppercase;
          font-size: 9px; letter-spacing: 0.06em;
        }
        .tc-search-resulthead {
          display: flex; align-items: center; justify-content: space-between;
          gap: 10px 14px; flex-wrap: wrap;
          padding: 14px 2px 4px; color: var(--fg-muted);
          border-bottom: 1px solid var(--line); margin-bottom: 2px;
        }
        /* Sort toggle — segmented control matching the brief-pane filter chips */
        .tc-sort { display: inline-flex; border: 1px solid var(--line); flex-shrink: 0; }
        .tc-sort-btn {
          background: transparent; border: 0; border-right: 1px solid var(--line);
          color: var(--fg-muted); cursor: none; white-space: nowrap;
          padding: 5px 10px; letter-spacing: 0.06em;
          transition: background 200ms var(--easing-default), color 200ms var(--easing-default);
        }
        .tc-sort-btn:last-child { border-right: 0; }
        .tc-sort-btn:hover:not(.tc-sort-btn--on) { background: var(--line-soft); }
        .tc-sort-btn--on { background: var(--fg); color: var(--bg); }

        .tc-split {
          display: grid; grid-template-columns: 384px 1fr; gap: 32px;
          align-items: start; margin-top: 20px;
        }
        .tc-cal-col { position: sticky; top: 100px; }
        @media (max-width: 1000px) {
          .tc-split { grid-template-columns: 1fr; gap: 24px; }
          .tc-cal-col { position: static; }
        }

        /* Calendar */
        .tc-cal { border: 1px solid var(--line); }
        .tc-cal-head {
          display: flex; align-items: center; justify-content: space-between;
          padding: 12px 14px; border-bottom: 1px solid var(--line-soft);
        }
        .tc-cal-nav {
          font-family: var(--font-mono); font-size: 18px; line-height: 1;
          width: 26px; height: 26px; cursor: none; color: var(--fg);
        }
        .tc-cal-wd {
          display: grid; grid-template-columns: repeat(7, 1fr);
          padding: 8px 8px 4px; gap: 3px;
        }
        .tc-cal-wd > span { text-align: center; color: var(--fg-faint); }
        .tc-cal-grid {
          display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px;
          padding: 0 8px 10px;
          animation: tc-cal-in 280ms var(--easing-default) both;
        }
        @keyframes tc-cal-in { from { opacity: 0; } to { opacity: 1; } }
        .tc-cal-cell { padding: 4px 3px; }
        .tc-cal-blank { padding: 0; }
        .tc-cal-date {
          font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.04em;
          line-height: 1; margin-bottom: 5px; display: flex; align-items: center;
          gap: 3px; font-variant-numeric: tabular-nums;
        }
        .tc-cal-today { width: 3px; height: 3px; border-radius: 50%; background: var(--fg); }
        .tc-cal-quad { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
        .tc-cal-legend { padding: 12px 14px; border-top: 1px solid var(--line-soft); }

        /* Brief pane */
        .tc-pane { border: 1px solid var(--line); animation: tc-pane-in 380ms var(--easing-default) both; }
        @keyframes tc-pane-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
        .tc-brief-head {
          display: flex; justify-content: space-between; align-items: center;
          flex-wrap: wrap; gap: 10px 24px; padding: 20px 18px 0;
        }
        .tc-brief-meta {
          padding: 8px 18px 14px; color: var(--fg-faint);
          text-transform: none; letter-spacing: 0.02em;
        }
        .tc-pane-filter {
          display: flex; gap: 6px; flex-wrap: wrap;
          padding: 0 18px 16px; border-bottom: 1px solid var(--line-soft);
        }

        /* Topic register row */
        .tc-row {
          width: 100%; text-align: left; border: none; font: inherit; cursor: none;
          background: transparent; padding: 15px 18px;
          display: grid; align-items: center; gap: 16px;
          grid-template-columns: 30px 92px 1fr 30px 50px 22px;
          grid-template-areas: "num meter head glyph tag arrow";
          transition: background 200ms var(--easing-default);
        }
        .tc-row:hover { background: var(--line-soft); }
        .tc-row-open { background: var(--line-soft); }
        .tc-c-num { grid-area: num; }
        .tc-c-meter { grid-area: meter; }
        .tc-c-head {
          grid-area: head; min-width: 0;
          overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
        }
        .tc-c-glyph { grid-area: glyph; }
        .tc-c-tag { grid-area: tag; }
        .tc-c-arrow { grid-area: arrow; justify-self: end; }
        /* Desktop: dissolve into the row grid so meter/glyph/tag keep their
           own columns. Mobile: a flex row (see the 680px block). */
        .tc-c-meta { display: contents; }
        @media (max-width: 680px) {
          .tc-row {
            grid-template-columns: 28px 1fr auto;
            grid-template-areas:
              "num  head  arrow"
              "meta meta  meta";
            gap: 10px 12px;
          }
          .tc-c-head { white-space: normal; }
          /* meter (~68px) overflowed its 28px column and collided with the
             fingerprint — lay the three out as one flex row instead. */
          .tc-c-meta {
            grid-area: meta; display: flex; align-items: center; gap: 12px;
          }
          .tc-c-tag { margin-left: auto; }
        }

        /* Accordion — animates height via grid-template-rows 0fr↔1fr */
        .tc-acc { display: grid; transition: grid-template-rows 380ms var(--easing-default); }

        /* Expanded detail */
        .tc-detail-grid { display: grid; grid-template-columns: 1fr 268px; gap: 40px; }
        .tc-readout { border: 1px solid var(--line); padding: 18px; align-self: start; }
        /* Topic body: a hairline rule splits the card bullets (TL;DR) from
           the extended commentary that follows. */
        .tc-detail-grid .post-body hr {
          border: 0; border-top: 1px solid var(--line); margin: 22px 0;
        }
        .tc-detail-grid .post-body ul + hr { margin-top: 18px; }
        @media (max-width: 860px) {
          .tc-detail-grid { grid-template-columns: 1fr; gap: 28px; }
          .tc-readout { max-width: 360px; }
        }

        /* ── Phones: vertical day-list calendar + bigger tap targets ── */
        @media (max-width: 720px) {
          /* The brief is the payload — show it first, calendar below. */
          .tc-pane-col { order: 1; }
          .tc-cal-col { order: 2; }

          /* The 7-column month grid squeezes each day's four scan slots to
             ~16px — far below a usable touch target. Collapse to one row
             per brief-day (newest first); the slots then sit ~50px wide. */
          .tc-cal-wd { display: none; }
          .tc-cal-grid {
            display: flex; flex-direction: column-reverse;
            gap: 0; padding: 0;
          }
          .tc-cal-cell:not(.tc-cal-cell--has) { display: none; }
          .tc-cal-cell--has {
            display: flex; align-items: center; gap: 14px;
            padding: 10px 14px; border-top: 1px solid var(--line-soft);
          }
          .tc-cal-date { margin: 0; flex-shrink: 0; width: 30px; font-size: 13px; }
          .tc-cal-quad {
            flex: 1; max-width: 224px;
            grid-template-columns: repeat(4, 1fr); gap: 8px;
          }

          /* Touch targets — month nav and topic filter chips */
          .tc-cal-nav {
            width: 40px; height: 40px; font-size: 22px;
            display: flex; align-items: center; justify-content: center;
          }
          .tc-pane-filter button {
            min-height: 40px; display: inline-flex; align-items: center;
          }

          /* Lift micro labels off the 9px floor for phone legibility */
          .tc-frame .micro { font-size: 10.5px; }
        }

        @media (prefers-reduced-motion: reduce) {
          .tc-tick, .tc-cal-grid, .tc-pane { animation: none; }
          .tc-sweep { animation: none; left: 32%; opacity: 0.4; }
          .tc-acc { transition: none; }
        }
      `}</style>
    </div>
  );
}

Object.assign(window, { Trendchaser });
