{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "annals",
  "type": "registry:block",
  "title": "Annals",
  "description": "Changelog section block: release entries on a scroll-linked spine with sticky version and date, change items grouped into colored New / Improved / Fixed / Deprecated chips, procedural screenshot plates and a collapsible \"Show N more changes\". Multi-select kind filters collapse non-matching items with height animation, search highlights matches with <mark>, the newest release wears a single-pulse Latest badge, and a Subscribe button morphs into an inline email field with async pending/success/error states.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://crucibleui.com/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/default/sections/annals/annals.tsx",
      "type": "registry:block",
      "target": "components/crucible/annals.tsx",
      "content": "\"use client\";\n\nimport {\n  AnimatePresence,\n  motion,\n  useMotionValue,\n  useSpring,\n  type Transition,\n} from \"motion/react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\n\n/* ---------------------------------------------------------------- types */\n\n/** The four change kinds, in the order groups render. */\nexport type AnnalsKind = \"new\" | \"improved\" | \"fixed\" | \"deprecated\";\n\n/** One line in a release. */\nexport interface AnnalsItem {\n  /** Which group (and chip color) the change belongs to. */\n  kind: AnnalsKind;\n  /** The change itself, one sentence. Search matches and highlights within it. */\n  text: string;\n}\n\n/** Optional screenshot plate for a release. */\nexport interface AnnalsMedia {\n  /** Image URL. Omit for a procedural gradient \"screenshot\" placeholder seeded from the version. */\n  src?: string;\n  /** Accessible description of the image. @default \"<title> preview\" */\n  alt?: string;\n  /** Small caption under the plate. */\n  caption?: string;\n}\n\n/** One release in the changelog. Pass entries newest first. */\nexport interface AnnalsEntry {\n  /** Version label, e.g. `\"v3.8.0\"`. Also the stable key unless `id` is set. */\n  version: string;\n  /** Release date — ISO `YYYY-MM-DD` is formatted (\"Oct 8, 2026\"); anything else renders as-is. */\n  date: string;\n  /** Release headline. */\n  title: string;\n  /** Optional one-line summary under the headline. */\n  summary?: string;\n  /** The changes. Rendered grouped by kind: New, Improved, Fixed, Deprecated. */\n  items: AnnalsItem[];\n  /** Screenshot plate. `true` renders the procedural placeholder. */\n  media?: AnnalsMedia | true;\n  /** Stable identity. @default version */\n  id?: string;\n}\n\n/** Imperative verbs exposed through the `ref` prop. */\nexport interface AnnalsHandle {\n  /** Replay the designed intro (header rise, spine draw, entry cascade, Latest pulse). */\n  replay: () => void;\n  /** Clear the kind filters and the search query. */\n  clearFilters: () => void;\n  /** Focus the search field. */\n  focusSearch: () => void;\n}\n\nexport interface AnnalsProps extends Omit<React.ComponentPropsWithoutRef<\"section\">, \"children\" | \"title\"> {\n  /** Releases, newest first. The first entry carries the \"Latest\" badge. Defaults to a seeded sample changelog. */\n  entries?: AnnalsEntry[];\n  /** Section headline (`<h2>`). @default \"What's new\" */\n  title?: React.ReactNode;\n  /** Small label above the headline. @default \"Changelog\" */\n  eyebrow?: React.ReactNode;\n  /** Supporting copy under the headline. */\n  description?: React.ReactNode;\n  /**\n   * Called with the email from the inline subscribe field. Return a Promise to\n   * hold the pending state — resolve shows \"Subscribed\", reject shows the\n   * rejection message (or a generic one) inline.\n   */\n  onSubscribe?: (email: string) => void | Promise<unknown>;\n  /** Href of the RSS ghost link; `false` hides it. @default \"/changelog/rss.xml\" */\n  rssHref?: string | false;\n  /** Items shown per release before \"Show N more changes\" (ignored while searching). @default 4 */\n  collapseAfter?: number;\n  /** Render media plates (real or procedural). @default true */\n  showMedia?: boolean;\n  /** Show the search field in the toolbar. @default true */\n  searchable?: boolean;\n  /**\n   * Scroll container that drives the spine's progress beam. Auto-detects the\n   * nearest scrollable ancestor (or the window) when omitted; pass a ref to\n   * pin it explicitly.\n   */\n  scroller?: React.RefObject<HTMLElement | null>;\n  /** Backdrop painted behind the sticky toolbar so entries scroll beneath it cleanly. Match your surface. @default \"rgb(10 10 10 / 0.86)\" */\n  stickyBackground?: string;\n  /** Accent hex: \"New\" chips, the spine beam, Latest badge, subscribe button, marks and focus rings. @default \"#bef264\" */\n  accentColor?: string;\n  /** Imperative handle (`replay()`, `clearFilters()`, `focusSearch()`). React 19 ref-as-prop. */\n  ref?: React.Ref<AnnalsHandle>;\n  /** Extra classes for the `<section>` root. */\n  className?: string;\n}\n\n/* ---------------------------------------------------------------- constants */\n\nconst DEFAULT_ACCENT = \"#bef264\";\nconst EASE: [number, number, number, number] = [0.22, 1, 0.36, 1];\nconst SPRING: Transition = { type: \"spring\", stiffness: 380, damping: 36, mass: 0.8 };\nconst INSTANT: Transition = { duration: 0 };\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/;\nconst KINDS: AnnalsKind[] = [\"new\", \"improved\", \"fixed\", \"deprecated\"];\nconst KIND_LABEL: Record<AnnalsKind, string> = {\n  new: \"New\",\n  improved: \"Improved\",\n  fixed: \"Fixed\",\n  deprecated: \"Deprecated\",\n};\nconst KIND_COLOR: Record<AnnalsKind, string> = {\n  new: \"var(--annals-accent)\",\n  improved: \"#7dd3fc\",\n  fixed: \"#fda4af\",\n  deprecated: \"#a3a3a3\",\n};\nconst MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\n\nconst DEFAULT_ENTRIES: AnnalsEntry[] = [\n  {\n    version: \"v3.8.0\",\n    date: \"2026-10-08\",\n    title: \"Shared views and a faster command bar\",\n    summary: \"Save any filtered table as a view and hand it to a teammate as a link.\",\n    media: true,\n    items: [\n      { kind: \"new\", text: \"Shared views — save filters, sort and column layout, then share them by link.\" },\n      { kind: \"new\", text: \"The command bar now searches settings, teammates and recent exports.\" },\n      { kind: \"new\", text: \"Keyboard shortcut sheet: press ? anywhere to see every binding.\" },\n      { kind: \"improved\", text: \"Command bar opens 40% faster on workspaces with more than 10k records.\" },\n      { kind: \"improved\", text: \"Column widths persist per view instead of per browser.\" },\n      { kind: \"fixed\", text: \"The date picker skipped the 31st when moving between months.\" },\n      { kind: \"fixed\", text: \"CSV exports now keep leading zeros in postcode and ID columns.\" },\n      { kind: \"fixed\", text: \"Tooltips no longer flicker when a row re-renders underneath them.\" },\n      { kind: \"deprecated\", text: \"The v1 webhook payload format — it stops sending on January 15, 2027.\" },\n    ],\n  },\n  {\n    version: \"v3.7.2\",\n    date: \"2026-09-24\",\n    title: \"Sync reliability patch\",\n    items: [\n      { kind: \"fixed\", text: \"Offline edits made on two devices no longer overwrite each other on reconnect.\" },\n      { kind: \"fixed\", text: \"Large attachments resume uploading after a dropped connection.\" },\n      { kind: \"improved\", text: \"Conflict banners now show who made the competing change and when.\" },\n      { kind: \"fixed\", text: \"The sync indicator stayed amber after a successful retry.\" },\n    ],\n  },\n  {\n    version: \"v3.7.0\",\n    date: \"2026-09-10\",\n    title: \"Automations, round two\",\n    summary: \"Branches, run history and smarter retries for every workflow.\",\n    media: true,\n    items: [\n      { kind: \"new\", text: \"Conditional branches — route a run down different steps based on any field.\" },\n      { kind: \"new\", text: \"Run history with per-step timings and the exact payload each step received.\" },\n      { kind: \"improved\", text: \"Retry backoff is configurable per step, from 10 seconds to 6 hours.\" },\n      { kind: \"improved\", text: \"The step picker groups actions by app and remembers your last five.\" },\n      { kind: \"fixed\", text: \"Scheduled runs fired twice when the workspace time zone changed.\" },\n      { kind: \"deprecated\", text: \"The “Daily digest” template — rebuild it with the new schedule trigger.\" },\n    ],\n  },\n  {\n    version: \"v3.6.0\",\n    date: \"2026-08-20\",\n    title: \"Granular permissions\",\n    summary: \"Custom roles down to the field, with an exportable audit trail.\",\n    items: [\n      { kind: \"new\", text: \"Custom roles with per-field read and write access.\" },\n      { kind: \"new\", text: \"Audit log export to CSV or JSON for any date range.\" },\n      { kind: \"improved\", text: \"Invites can pre-assign a role and a set of projects.\" },\n      { kind: \"improved\", text: \"Guests see a clear notice instead of an empty page on restricted views.\" },\n      { kind: \"fixed\", text: \"Removing a member now revokes their API tokens immediately.\" },\n      { kind: \"fixed\", text: \"Role names longer than 24 characters were truncated in the sidebar.\" },\n    ],\n  },\n  {\n    version: \"v3.5.1\",\n    date: \"2026-08-02\",\n    title: \"Polish and small fixes\",\n    items: [\n      { kind: \"improved\", text: \"Empty states now suggest a next step based on the view type.\" },\n      { kind: \"fixed\", text: \"Dragging a card to the last column scrolled the board back to the start.\" },\n      { kind: \"fixed\", text: \"Mentions inside code blocks no longer send notifications.\" },\n    ],\n  },\n];\n\n/* ---------------------------------------------------------------- helpers */\n\n/** FNV-1a string hash → 32-bit seed. */\nfunction hashString(s: string): number {\n  let h = 0x811c9dc5;\n  for (let i = 0; i < s.length; i++) {\n    h ^= s.charCodeAt(i);\n    h = Math.imul(h, 0x01000193);\n  }\n  return h >>> 0;\n}\n\n/** Seeded PRNG — deterministic on server and client. */\nfunction mulberry32(seed: number) {\n  let a = seed >>> 0;\n  return () => {\n    a = (a + 0x6d2b79f5) >>> 0;\n    let t = a;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\n/** Black or white text, whichever reads against the accent fill. */\nfunction readableOn(hex: string): string {\n  const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());\n  if (!m) return \"#0a0a0a\";\n  let s = m[1];\n  if (s.length === 3) s = s.replace(/./g, (c) => c + c);\n  const n = parseInt(s, 16);\n  const lum = 0.2126 * ((n >> 16) & 255) + 0.7152 * ((n >> 8) & 255) + 0.0722 * (n & 255);\n  return lum > 140 ? \"#0a0a0a\" : \"#ffffff\";\n}\n\n/** Deterministic date label (no locale/time-zone drift between server and client). */\nfunction formatDate(date: string): string {\n  const m = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(date);\n  if (!m) return date;\n  const month = MONTHS[Number(m[2]) - 1];\n  return month ? `${month} ${Number(m[3])}, ${m[1]}` : date;\n}\n\nfunction escapeRegExp(s: string) {\n  return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/** Wrap case-insensitive matches of `query` in `<mark>`. */\nfunction highlight(text: string, query: string): React.ReactNode {\n  if (!query) return text;\n  const parts = text.split(new RegExp(`(${escapeRegExp(query)})`, \"gi\"));\n  if (parts.length === 1) return text;\n  return parts.map((part, i) =>\n    i % 2 === 1 ? (\n      <mark\n        key={i}\n        className=\"rounded-[3px] bg-[color-mix(in_oklab,var(--annals-accent)_30%,transparent)] px-[1px] text-white\"\n      >\n        {part}\n      </mark>\n    ) : (\n      <React.Fragment key={i}>{part}</React.Fragment>\n    )\n  );\n}\n\n/** Nearest scrollable ancestor, or null for the window. */\nfunction findScrollParent(el: HTMLElement | null): HTMLElement | null {\n  let node = el?.parentElement ?? null;\n  while (node && node !== document.body && node !== document.documentElement) {\n    const oy = getComputedStyle(node).overflowY;\n    if ((oy === \"auto\" || oy === \"scroll\" || oy === \"overlay\") && node.scrollHeight > node.clientHeight + 1) {\n      return node;\n    }\n    node = node.parentElement;\n  }\n  return null;\n}\n\n/* ---------------------------------------------------------------- glyphs */\n\nfunction SearchGlyph({ className }: { className?: string }) {\n  return (\n    <svg aria-hidden viewBox=\"0 0 20 20\" className={className} fill=\"none\">\n      <circle cx=\"9\" cy=\"9\" r=\"5.25\" stroke=\"currentColor\" strokeWidth=\"1.6\" />\n      <path d=\"m13 13 3.5 3.5\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n    </svg>\n  );\n}\n\nfunction RssGlyph({ className }: { className?: string }) {\n  return (\n    <svg aria-hidden viewBox=\"0 0 20 20\" className={className} fill=\"none\">\n      <path d=\"M4.5 9a6.5 6.5 0 0 1 6.5 6.5M4.5 4.5a11 11 0 0 1 11 11\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n      <circle cx=\"5.25\" cy=\"14.75\" r=\"1.25\" fill=\"currentColor\" />\n    </svg>\n  );\n}\n\nfunction ArrowGlyph({ className }: { className?: string }) {\n  return (\n    <svg aria-hidden viewBox=\"0 0 20 20\" className={className} fill=\"none\">\n      <path d=\"M4.5 10h11m0 0-4.5-4.5M15.5 10 11 14.5\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\nfunction CheckGlyph({ className }: { className?: string }) {\n  return (\n    <svg aria-hidden viewBox=\"0 0 20 20\" className={className} fill=\"none\">\n      <path d=\"m4.75 10.5 3.5 3.5 7-8\" stroke=\"currentColor\" strokeWidth=\"1.9\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\nfunction CloseGlyph({ className }: { className?: string }) {\n  return (\n    <svg aria-hidden viewBox=\"0 0 20 20\" className={className} fill=\"none\">\n      <path d=\"m6 6 8 8M14 6l-8 8\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n    </svg>\n  );\n}\n\nfunction ChevronGlyph({ className }: { className?: string }) {\n  return (\n    <svg aria-hidden viewBox=\"0 0 20 20\" className={className} fill=\"none\">\n      <path d=\"m6 8 4 4 4-4\" stroke=\"currentColor\" strokeWidth=\"1.7\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\nfunction Spinner({ className }: { className?: string }) {\n  return (\n    <svg aria-hidden viewBox=\"0 0 24 24\" className={cn(\"motion-safe:animate-spin\", className)} fill=\"none\">\n      <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" strokeOpacity=\"0.3\" strokeWidth=\"3\" />\n      <path d=\"M21 12a9 9 0 0 0-9-9\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" />\n    </svg>\n  );\n}\n\n/* ---------------------------------------------------------------- pieces */\n\nfunction KindChip({ kind, className }: { kind: AnnalsKind; className?: string }) {\n  const color = KIND_COLOR[kind];\n  return (\n    <span\n      className={cn(\n        \"inline-flex h-[22px] items-center gap-1.5 rounded-full px-2 text-[11px] font-medium tracking-wide\",\n        className\n      )}\n      style={{\n        color,\n        backgroundColor: `color-mix(in oklab, ${color} 12%, transparent)`,\n        boxShadow: `inset 0 0 0 1px color-mix(in oklab, ${color} 26%, transparent)`,\n      }}\n    >\n      <span aria-hidden className=\"size-1.5 rounded-full\" style={{ backgroundColor: color }} />\n      {KIND_LABEL[kind]}\n    </span>\n  );\n}\n\n/** Procedural \"screenshot\": a seeded faux app window on a soft accent-lit gradient. */\nfunction MediaPlate({ seedKey, label }: { seedKey: string; label: string }) {\n  const art = React.useMemo(() => {\n    const rand = mulberry32(hashString(seedKey));\n    const variant = Math.floor(rand() * 3);\n    const gx = 15 + Math.round(rand() * 70);\n    const gy = 10 + Math.round(rand() * 40);\n    const hue = Math.round(180 + rand() * 120);\n    const bars = Array.from({ length: 5 }, () => 40 + Math.round(rand() * 50));\n    const points = Array.from({ length: 9 }, (_, i) => {\n      const y = 70 - i * 5 - rand() * 22;\n      return `${(i / 8) * 100},${Math.max(8, Math.min(92, y))}`;\n    });\n    const rows = Array.from({ length: 5 }, () => [30 + rand() * 40, 15 + rand() * 25, 10 + rand() * 15]);\n    return { variant, gx, gy, hue, bars, points, rows, active: Math.floor(rand() * 3) };\n  }, [seedKey]);\n\n  return (\n    <div\n      role=\"img\"\n      aria-label={label}\n      className=\"relative aspect-[16/9] w-full overflow-hidden rounded-xl border border-white/[0.08] bg-[#0d0d10]\"\n      style={{\n        backgroundImage: `radial-gradient(70% 90% at ${art.gx}% ${art.gy}%, color-mix(in oklab, var(--annals-accent) 22%, transparent), transparent 70%), radial-gradient(60% 70% at ${100 - art.gx}% 100%, hsl(${art.hue} 70% 55% / 0.16), transparent 70%)`,\n      }}\n    >\n      {/* faux window */}\n      <div className=\"absolute inset-x-[7%] top-[12%] bottom-0 overflow-hidden rounded-t-lg border border-b-0 border-white/[0.1] bg-[#0b0b0d]/90 shadow-[0_-12px_40px_-12px_rgba(0,0,0,0.6)]\">\n        <div className=\"flex h-[9%] min-h-[14px] items-center gap-[3px] border-b border-white/[0.06] px-[2.5%]\">\n          {[0, 1, 2].map((i) => (\n            <span key={i} className=\"aspect-square h-[34%] rounded-full bg-white/15\" />\n          ))}\n          <span className=\"mx-auto h-[38%] w-[28%] rounded-full bg-white/[0.06]\" />\n        </div>\n        <div className=\"flex h-[91%]\">\n          <div className=\"flex w-[20%] flex-col gap-[7%] border-r border-white/[0.06] p-[3%] pt-[5%]\">\n            {art.bars.map((w, i) => (\n              <span\n                key={i}\n                className=\"h-[5px] rounded-full\"\n                style={{\n                  width: `${w}%`,\n                  backgroundColor: i === 1 ? \"color-mix(in oklab, var(--annals-accent) 70%, transparent)\" : \"rgb(255 255 255 / 0.1)\",\n                }}\n              />\n            ))}\n          </div>\n          <div className=\"flex flex-1 flex-col gap-[5%] p-[3.5%]\">\n            <div className=\"flex items-center justify-between\">\n              <span className=\"h-[7px] w-[30%] rounded-full bg-white/20\" />\n              <span className=\"h-[10px] w-[14%] rounded-full\" style={{ backgroundColor: \"var(--annals-accent)\", opacity: 0.85 }} />\n            </div>\n            {art.variant === 0 && (\n              <div className=\"grid flex-1 grid-cols-3 gap-[4%]\">\n                {[0, 1, 2].map((i) => (\n                  <div\n                    key={i}\n                    className=\"rounded-md border bg-white/[0.025] p-[8%]\"\n                    style={{\n                      borderColor:\n                        i === art.active ? \"color-mix(in oklab, var(--annals-accent) 55%, transparent)\" : \"rgb(255 255 255 / 0.07)\",\n                    }}\n                  >\n                    <span className=\"block h-[5px] w-3/5 rounded-full bg-white/20\" />\n                    <span className=\"mt-[14%] block h-[4px] w-full rounded-full bg-white/[0.08]\" />\n                    <span className=\"mt-[8%] block h-[4px] w-4/5 rounded-full bg-white/[0.08]\" />\n                  </div>\n                ))}\n              </div>\n            )}\n            {art.variant === 1 && (\n              <div className=\"flex-1 rounded-md border border-white/[0.07] bg-white/[0.02] p-[3%]\">\n                <svg viewBox=\"0 0 100 100\" preserveAspectRatio=\"none\" className=\"h-full w-full overflow-visible\">\n                  <polyline\n                    points={art.points.join(\" \")}\n                    fill=\"none\"\n                    stroke=\"var(--annals-accent)\"\n                    strokeWidth=\"1.6\"\n                    vectorEffect=\"non-scaling-stroke\"\n                    strokeLinejoin=\"round\"\n                  />\n                  <polyline\n                    points={`0,100 ${art.points.join(\" \")} 100,100`}\n                    fill=\"var(--annals-accent)\"\n                    opacity=\"0.08\"\n                    stroke=\"none\"\n                  />\n                </svg>\n              </div>\n            )}\n            {art.variant === 2 && (\n              <div className=\"flex flex-1 flex-col justify-between rounded-md border border-white/[0.07] bg-white/[0.02] p-[3%]\">\n                {art.rows.map((r, i) => (\n                  <div key={i} className=\"flex items-center gap-[4%]\">\n                    <span\n                      className=\"size-[6px] shrink-0 rounded-full\"\n                      style={{ backgroundColor: i === art.active ? \"var(--annals-accent)\" : \"rgb(255 255 255 / 0.18)\" }}\n                    />\n                    <span className=\"h-[4px] rounded-full bg-white/[0.14]\" style={{ width: `${r[0]}%` }} />\n                    <span className=\"h-[4px] rounded-full bg-white/[0.07]\" style={{ width: `${r[1]}%` }} />\n                    <span className=\"ml-auto h-[4px] rounded-full bg-white/[0.07]\" style={{ width: `${r[2]}%` }} />\n                  </div>\n                ))}\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\ntype SubscribeMode = \"closed\" | \"open\" | \"pending\" | \"done\";\n\nfunction SubscribeControl({\n  onSubscribe,\n  reducedMotion,\n  accentInk,\n  uid,\n}: {\n  onSubscribe?: (email: string) => void | Promise<unknown>;\n  reducedMotion: boolean;\n  accentInk: string;\n  uid: string;\n}) {\n  const [mode, setMode] = React.useState<SubscribeMode>(\"closed\");\n  const [email, setEmail] = React.useState(\"\");\n  const [error, setError] = React.useState<string | null>(null);\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const buttonRef = React.useRef<HTMLButtonElement>(null);\n  const restoreFocus = React.useRef(false);\n  const pendingRef = React.useRef(false);\n  const errorId = `${uid}-sub-error`;\n  // Controls live inside the clipped capsule, so their focus ring is drawn inset.\n  const innerRing = \"outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white/75\";\n\n  React.useEffect(() => {\n    if (mode === \"open\") inputRef.current?.focus({ preventScroll: true });\n    if (mode === \"closed\" && restoreFocus.current) {\n      restoreFocus.current = false;\n      buttonRef.current?.focus({ preventScroll: true });\n    }\n  }, [mode]);\n\n  const close = () => {\n    if (pendingRef.current) return;\n    restoreFocus.current = true;\n    setError(null);\n    setMode(\"closed\");\n  };\n\n  const submit = async (event: React.FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    if (pendingRef.current) return;\n    const trimmed = email.trim();\n    if (!EMAIL_RE.test(trimmed)) {\n      setError(trimmed ? \"Enter a valid email address.\" : \"Enter your email address.\");\n      inputRef.current?.focus();\n      return;\n    }\n    setError(null);\n    pendingRef.current = true;\n    setMode(\"pending\");\n    try {\n      await onSubscribe?.(trimmed);\n      pendingRef.current = false;\n      setMode(\"done\");\n    } catch (err) {\n      pendingRef.current = false;\n      setMode(\"open\");\n      setError(err instanceof Error && err.message ? err.message : \"Something went wrong. Try again.\");\n    }\n  };\n\n  const width = mode === \"closed\" ? 112 : mode === \"done\" ? 140 : 260;\n  const fade: Transition = reducedMotion ? { duration: 0.12 } : { duration: 0.18, ease: EASE };\n\n  return (\n    <div className=\"relative min-w-0 flex-1 @2xl:flex-none\">\n      <motion.div\n        initial={false}\n        animate={{ width }}\n        transition={reducedMotion ? INSTANT : SPRING}\n        className={cn(\n          \"relative h-9 max-w-full overflow-hidden rounded-full\",\n          mode === \"closed\" ? \"\" : \"bg-white/[0.04] ring-1 ring-inset\",\n          error ? \"ring-rose-300/60\" : \"ring-white/10 focus-within:ring-[var(--annals-accent)]\",\n          mode === \"done\" && \"bg-[color-mix(in_oklab,var(--annals-accent)_12%,transparent)] ring-[color-mix(in_oklab,var(--annals-accent)_35%,transparent)]\"\n        )}\n      >\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {mode === \"closed\" && (\n            <motion.button\n              key=\"btn\"\n              ref={buttonRef}\n              type=\"button\"\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={fade}\n              onClick={() => setMode(\"open\")}\n              className={cn(\n                \"absolute inset-0 inline-flex items-center justify-center gap-1.5 rounded-full text-sm font-medium transition-[filter] hover:brightness-110\",\n                innerRing\n              )}\n              style={{ backgroundColor: \"var(--annals-accent)\", color: accentInk }}\n            >\n              Subscribe\n            </motion.button>\n          )}\n          {(mode === \"open\" || mode === \"pending\") && (\n            <motion.form\n              key=\"form\"\n              noValidate\n              onSubmit={submit}\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={fade}\n              onKeyDown={(e) => {\n                if (e.key === \"Escape\") {\n                  e.stopPropagation();\n                  close();\n                }\n              }}\n              className=\"absolute inset-0 flex items-center gap-1 pl-3.5 pr-1\"\n            >\n              <label htmlFor={`${uid}-sub-email`} className=\"sr-only\">\n                Email address for release updates\n              </label>\n              <input\n                ref={inputRef}\n                id={`${uid}-sub-email`}\n                type=\"email\"\n                inputMode=\"email\"\n                autoComplete=\"email\"\n                placeholder=\"you@company.com\"\n                value={email}\n                disabled={mode === \"pending\"}\n                onChange={(e) => {\n                  setEmail(e.target.value);\n                  if (error) setError(null);\n                }}\n                aria-invalid={error ? true : undefined}\n                aria-describedby={error ? errorId : undefined}\n                className=\"h-full min-w-0 flex-1 bg-transparent text-sm text-white placeholder:text-white/35 focus:outline-none disabled:opacity-60\"\n              />\n              <button\n                type=\"button\"\n                onClick={close}\n                disabled={mode === \"pending\"}\n                aria-label=\"Cancel subscribing\"\n                className={cn(\n                  \"grid size-7 shrink-0 place-items-center rounded-full text-white/45 transition-colors hover:bg-white/[0.06] hover:text-white disabled:opacity-40\",\n                  innerRing\n                )}\n              >\n                <CloseGlyph className=\"size-4\" />\n              </button>\n              <button\n                type=\"submit\"\n                disabled={mode === \"pending\"}\n                aria-label={mode === \"pending\" ? \"Subscribing\" : \"Subscribe\"}\n                className={cn(\n                  \"grid size-7 shrink-0 place-items-center rounded-full transition-[filter] hover:brightness-110\",\n                  innerRing\n                )}\n                style={{ backgroundColor: \"var(--annals-accent)\", color: accentInk }}\n              >\n                {mode === \"pending\" ? <Spinner className=\"size-3.5\" /> : <ArrowGlyph className=\"size-4\" />}\n              </button>\n            </motion.form>\n          )}\n          {mode === \"done\" && (\n            <motion.div\n              key=\"done\"\n              initial={{ opacity: 0, scale: reducedMotion ? 1 : 0.92 }}\n              animate={{ opacity: 1, scale: 1 }}\n              transition={fade}\n              className=\"absolute inset-0 flex items-center justify-center gap-1.5 text-sm font-medium\"\n              style={{ color: \"var(--annals-accent)\" }}\n            >\n              <CheckGlyph className=\"size-4\" />\n              Subscribed\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </motion.div>\n      <p role=\"status\" className=\"sr-only\">\n        {mode === \"done\" ? \"Subscribed to release updates.\" : mode === \"pending\" ? \"Subscribing…\" : \"\"}\n      </p>\n      <AnimatePresence>\n        {error && (\n          <motion.p\n            id={errorId}\n            role=\"alert\"\n            initial={{ opacity: 0, y: reducedMotion ? 0 : -4 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0 }}\n            transition={fade}\n            className=\"absolute right-0 top-full z-20 mt-1.5 whitespace-nowrap rounded-md bg-neutral-900 px-2 py-1 text-xs text-rose-300 ring-1 ring-white/10\"\n          >\n            {error}\n          </motion.p>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\n/* ---------------------------------------------------------------- main */\n\n/**\n * Annals — a changelog section block. Releases run down a spine whose accent\n * beam fills as you scroll, igniting each release's node as it passes; at\n * wide sizes the version and date sit in a sticky left rail. Each release\n * groups its changes under colored kind chips (New / Improved / Fixed /\n * Deprecated), can carry a screenshot plate (procedural placeholder when no\n * image is given) and folds long lists behind \"Show N more changes\". The\n * sticky toolbar's multi-select kind toggles and search field collapse\n * non-matching items with height animation and highlight matches with\n * `<mark>`; the newest release wears a \"Latest\" badge that pulses once in\n * view. A Subscribe button morphs into an inline email field with async\n * pending / success / error states. Reduced motion: final layout, crossfades\n * only, no pulse.\n */\nexport function Annals({\n  entries = DEFAULT_ENTRIES,\n  title = \"What's new\",\n  eyebrow = \"Changelog\",\n  description = \"New features, improvements and fixes — shipped every two weeks.\",\n  onSubscribe,\n  rssHref = \"/changelog/rss.xml\",\n  collapseAfter = 4,\n  showMedia = true,\n  searchable = true,\n  scroller,\n  stickyBackground = \"rgb(10 10 10 / 0.86)\",\n  accentColor = DEFAULT_ACCENT,\n  ref,\n  className,\n  style,\n  ...props\n}: AnnalsProps) {\n  const reducedMotion = useReducedMotion();\n  const uid = React.useId().replace(/[:]/g, \"\");\n  const headingId = `${uid}-heading`;\n  const accentInk = readableOn(accentColor);\n\n  const rootRef = React.useRef<HTMLElement>(null);\n  const listRef = React.useRef<HTMLDivElement>(null);\n  const toolbarRef = React.useRef<HTMLDivElement>(null);\n  const searchRef = React.useRef<HTMLInputElement>(null);\n  const nodeRefs = React.useRef(new Map<string, HTMLSpanElement>());\n\n  const [kinds, setKinds] = React.useState<AnnalsKind[]>([]);\n  const [query, setQuery] = React.useState(\"\");\n  const [expanded, setExpanded] = React.useState<Record<string, boolean>>({});\n  const [litCount, setLitCount] = React.useState(0);\n  const [toolbarH, setToolbarH] = React.useState(64);\n  const [announcement, setAnnouncement] = React.useState(\"\");\n\n  /* ---- intro: once in view; replay() snaps back then plays again ---- */\n  const [intro, setIntro] = React.useState<\"waiting\" | \"reset\" | \"play\">(\"waiting\");\n  const [run, setRun] = React.useState(0);\n  React.useEffect(() => {\n    const el = rootRef.current;\n    if (!el) return;\n    const observer = new IntersectionObserver(\n      (obs) => {\n        if (obs.some((o) => o.isIntersecting)) {\n          setIntro((s) => (s === \"waiting\" ? \"play\" : s));\n          observer.disconnect();\n        }\n      },\n      { rootMargin: \"0px 0px -12% 0px\" }\n    );\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, []);\n  React.useEffect(() => {\n    if (intro !== \"reset\") return;\n    const id = window.requestAnimationFrame(() => setIntro(\"play\"));\n    return () => window.cancelAnimationFrame(id);\n  }, [intro]);\n  const shown = reducedMotion || intro === \"play\";\n  const snap = intro === \"reset\";\n  const rise = (delay: number) => ({\n    initial: false as const,\n    animate: { opacity: shown ? 1 : 0, y: shown || reducedMotion ? 0 : 12 },\n    transition: reducedMotion || snap ? INSTANT : { duration: 0.55, ease: EASE, delay },\n  });\n\n  const replay = React.useCallback(() => {\n    setIntro(\"reset\");\n    setRun((r) => r + 1);\n  }, []);\n  const clearFilters = React.useCallback(() => {\n    setKinds([]);\n    setQuery(\"\");\n  }, []);\n  React.useImperativeHandle(\n    ref,\n    () => ({ replay, clearFilters, focusSearch: () => searchRef.current?.focus() }),\n    [replay, clearFilters]\n  );\n\n  /* ---- filtering ---- */\n  const q = query.trim();\n  const ql = q.toLowerCase();\n  const kindSet = new Set(kinds);\n\n  const filtered = entries.map((entry, index) => {\n      const key = entry.id ?? entry.version;\n      const titleHit =\n        !!ql &&\n        (entry.title.toLowerCase().includes(ql) ||\n          entry.version.toLowerCase().includes(ql) ||\n          (entry.summary?.toLowerCase().includes(ql) ?? false));\n      const withIndex = entry.items.map((item, i) => ({ item, i }));\n      const kindOk = withIndex.filter(({ item }) => kindSet.size === 0 || kindSet.has(item.kind));\n      const matches = titleHit || !ql ? kindOk : kindOk.filter(({ item }) => item.text.toLowerCase().includes(ql));\n      const ordered = KINDS.flatMap((k) => matches.filter(({ item }) => item.kind === k));\n      return { entry, index, key, ordered, visible: ordered.length > 0 || (!ql && kindSet.size === 0) };\n  });\n\n  const kindCounts: Record<AnnalsKind, number> = { new: 0, improved: 0, fixed: 0, deprecated: 0 };\n    for (const entry of entries) {\n      const titleHit = !!ql && (entry.title.toLowerCase().includes(ql) || entry.version.toLowerCase().includes(ql));\n      for (const item of entry.items) {\n        if (!ql || titleHit || item.text.toLowerCase().includes(ql)) kindCounts[item.kind]++;\n      }\n    }\n\n  const visibleEntries = filtered.filter((f) => f.visible);\n  const totalVisibleItems = visibleEntries.reduce((n, f) => n + f.ordered.length, 0);\n  const filtering = kindSet.size > 0 || !!q;\n\n  // Announce result counts after the user changes filters (debounced for typing).\n  React.useEffect(() => {\n    const id = window.setTimeout(() => {\n      setAnnouncement(\n        !filtering\n          ? \"\"\n          : totalVisibleItems === 0\n          ? \"No changes match.\"\n          : `${totalVisibleItems} ${totalVisibleItems === 1 ? \"change\" : \"changes\"} in ${visibleEntries.length} ${\n              visibleEntries.length === 1 ? \"release\" : \"releases\"\n            }.`\n      );\n    }, filtering ? 350 : 0);\n    return () => window.clearTimeout(id);\n  }, [filtering, totalVisibleItems, visibleEntries.length]);\n\n  const toggleKind = (kind: AnnalsKind) =>\n    setKinds((prev) => (prev.includes(kind) ? prev.filter((k) => k !== kind) : [...prev, kind]));\n\n  /* ---- sticky offsets: toolbar height drives the rail's sticky top ---- */\n  React.useEffect(() => {\n    const el = toolbarRef.current;\n    if (!el || typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(() => setToolbarH(Math.round(el.getBoundingClientRect().height)));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n\n  /* ---- scroll-linked spine progress + node ignition ---- */\n  const progress = useMotionValue(0);\n  const smooth = useSpring(progress, { stiffness: 220, damping: 34, mass: 0.6 });\n  const visibleKeys = visibleEntries.map((f) => f.key).join(\"|\");\n  React.useEffect(() => {\n    const list = listRef.current;\n    if (!list) return;\n    const target = scroller?.current ?? findScrollParent(rootRef.current);\n    const keys = visibleKeys ? visibleKeys.split(\"|\") : [];\n    let raf = 0;\n    const measure = () => {\n      raf = 0;\n      const vp = target ? target.getBoundingClientRect() : { top: 0, height: window.innerHeight };\n      const line = vp.top + vp.height * 0.4;\n      const rect = list.getBoundingClientRect();\n      const p = rect.height > 0 ? (line - rect.top) / rect.height : 0;\n      progress.set(Math.max(0, Math.min(1, p)));\n      let lit = 0;\n      for (const key of keys) {\n        const node = nodeRefs.current.get(key);\n        if (node && node.getBoundingClientRect().top + 5 <= line) lit++;\n        else break;\n      }\n      setLitCount(lit);\n    };\n    const schedule = () => {\n      if (!raf) raf = window.requestAnimationFrame(measure);\n    };\n    measure();\n    const scrollTarget: HTMLElement | Window = target ?? window;\n    scrollTarget.addEventListener(\"scroll\", schedule, { passive: true });\n    window.addEventListener(\"resize\", schedule);\n    const ro = typeof ResizeObserver !== \"undefined\" ? new ResizeObserver(schedule) : null;\n    ro?.observe(list);\n    return () => {\n      if (raf) window.cancelAnimationFrame(raf);\n      scrollTarget.removeEventListener(\"scroll\", schedule);\n      window.removeEventListener(\"resize\", schedule);\n      ro?.disconnect();\n    };\n  }, [scroller, visibleKeys, progress]);\n  const beamScale = reducedMotion ? progress : smooth;\n\n  /* ---- styles ---- */\n  const focusRing =\n    \"outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--annals-accent)] focus-visible:outline-solid\";\n  const heightT: Transition = reducedMotion\n    ? { height: INSTANT, opacity: { duration: 0.15 } }\n    : { height: { type: \"spring\", stiffness: 420, damping: 40 }, opacity: { duration: 0.2 } };\n\n  const latestKey = filtered[0]?.key;\n\n  return (\n    <section\n      ref={rootRef}\n      aria-labelledby={headingId}\n      className={cn(\"@container relative w-full text-white\", className)}\n      style={\n        {\n          \"--annals-accent\": accentColor,\n          \"--annals-top\": `${toolbarH + 20}px`,\n          ...style,\n        } as React.CSSProperties\n      }\n      {...props}\n    >\n      <div className=\"mx-auto w-full max-w-5xl px-5 pb-16 pt-10 @lg:px-8 @lg:pt-14\">\n        {/* ---------- header ---------- */}\n        <motion.header\n          {...rise(0)}\n          className=\"flex flex-col gap-5 @2xl:flex-row @2xl:items-end @2xl:justify-between\"\n        >\n          <div className=\"max-w-xl\">\n            <p className=\"text-xs font-medium uppercase tracking-[0.16em]\" style={{ color: \"var(--annals-accent)\" }}>\n              {eyebrow}\n            </p>\n            <h2 id={headingId} className=\"mt-2 text-3xl font-semibold tracking-tight text-white @lg:text-4xl\">\n              {title}\n            </h2>\n            {description ? <p className=\"mt-3 text-[15px] leading-relaxed text-white/55\">{description}</p> : null}\n          </div>\n          <div className=\"flex w-full flex-nowrap items-center justify-start gap-2 @2xl:justify-end @2xl:w-[340px] @2xl:shrink-0\">\n            {rssHref !== false && (\n              <a\n                href={rssHref}\n                className={cn(\n                  \"-ml-3 inline-flex h-9 shrink-0 items-center gap-1.5 rounded-full px-3 text-sm text-white/55 @2xl:ml-0 transition-colors hover:bg-white/[0.05] hover:text-white\",\n                  focusRing\n                )}\n              >\n                <RssGlyph className=\"size-4\" />\n                RSS\n              </a>\n            )}\n            <SubscribeControl\n              onSubscribe={onSubscribe}\n              reducedMotion={reducedMotion}\n              accentInk={accentInk}\n              uid={uid}\n            />\n          </div>\n        </motion.header>\n\n        {/* ---------- toolbar ---------- */}\n        <motion.div\n          ref={toolbarRef}\n          {...rise(0.08)}\n          className=\"z-20 -mx-5 mt-8 border-b border-white/[0.07] px-5 py-3 backdrop-blur-md @lg:-mx-8 @lg:px-8 @2xl:sticky @2xl:top-0\"\n          style={{ backgroundColor: stickyBackground }}\n        >\n          <div className=\"flex flex-col gap-3 @2xl:flex-row @2xl:items-center\">\n            <div\n              role=\"group\"\n              aria-label=\"Filter by change type\"\n              className=\"flex flex-wrap items-center gap-1.5 @2xl:flex-nowrap\"\n            >\n              {KINDS.map((kind) => {\n                const on = kindSet.has(kind);\n                const color = KIND_COLOR[kind];\n                return (\n                  <button\n                    key={kind}\n                    type=\"button\"\n                    aria-pressed={on}\n                    onClick={() => toggleKind(kind)}\n                    className={cn(\n                      \"inline-flex h-8 shrink-0 items-center gap-2 rounded-full px-3 text-[13px] font-medium transition-[background-color,color,box-shadow] duration-200\",\n                      on ? \"text-white\" : \"text-white/55 hover:bg-white/[0.06] hover:text-white/85\",\n                      focusRing\n                    )}\n                    style={{\n                      backgroundColor: on ? `color-mix(in oklab, ${color} 14%, transparent)` : \"rgb(255 255 255 / 0.03)\",\n                      boxShadow: on\n                        ? `inset 0 0 0 1px color-mix(in oklab, ${color} 45%, transparent)`\n                        : \"inset 0 0 0 1px rgb(255 255 255 / 0.07)\",\n                    }}\n                  >\n                    <span\n                      aria-hidden\n                      className=\"size-2 rounded-full transition-opacity duration-200\"\n                      style={{ backgroundColor: color, opacity: on ? 1 : 0.55 }}\n                    />\n                    {KIND_LABEL[kind]}\n                    <span className={cn(\"text-xs tabular-nums\", on ? \"text-white/60\" : \"text-white/35\")}>\n                      {kindCounts[kind]}\n                    </span>\n                  </button>\n                );\n              })}\n              <AnimatePresence initial={false}>\n                {filtering && (\n                  <motion.button\n                    key=\"clear\"\n                    type=\"button\"\n                    initial={{ opacity: 0 }}\n                    animate={{ opacity: 1 }}\n                    exit={{ opacity: 0 }}\n                    transition={{ duration: 0.15 }}\n                    onClick={() => {\n                      clearFilters();\n                      searchRef.current?.focus();\n                    }}\n                    className={cn(\n                      \"h-8 shrink-0 rounded-full px-2.5 text-[13px] text-white/45 underline-offset-4 hover:text-white hover:underline\",\n                      focusRing\n                    )}\n                  >\n                    Clear\n                  </motion.button>\n                )}\n              </AnimatePresence>\n            </div>\n            {searchable && (\n              <div className=\"relative @2xl:ml-auto @2xl:w-64\">\n                <SearchGlyph className=\"pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-white/35\" />\n                <label htmlFor={`${uid}-search`} className=\"sr-only\">\n                  Search changes\n                </label>\n                <input\n                  ref={searchRef}\n                  id={`${uid}-search`}\n                  type=\"search\"\n                  value={query}\n                  onChange={(e) => setQuery(e.target.value)}\n                  onKeyDown={(e) => {\n                    if (e.key === \"Escape\" && query) {\n                      e.preventDefault();\n                      setQuery(\"\");\n                    }\n                  }}\n                  placeholder=\"Search changes\"\n                  autoComplete=\"off\"\n                  className=\"h-9 w-full rounded-lg bg-white/[0.03] pl-9 pr-3 text-sm text-white ring-1 ring-inset ring-white/[0.08] transition-shadow placeholder:text-white/35 focus:outline-none focus:ring-[var(--annals-accent)] [&::-webkit-search-cancel-button]:hidden\"\n                />\n              </div>\n            )}\n          </div>\n          <p role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n            {announcement}\n          </p>\n        </motion.div>\n\n        {/* ---------- releases on the spine ---------- */}\n        <div\n          ref={listRef}\n          className=\"relative mt-10 [--rail-x:6px] @2xl:[--rail-x:10.75rem]\"\n        >\n          {/* track + scroll-linked beam */}\n          <motion.div\n            key={`track-${run}`}\n            aria-hidden\n            initial={false}\n            animate={{ scaleY: shown ? 1 : 0 }}\n            transition={reducedMotion || snap ? INSTANT : { duration: 0.8, ease: EASE, delay: 0.1 }}\n            className=\"absolute bottom-0 top-1 w-px origin-top bg-white/[0.08] [mask-image:linear-gradient(to_bottom,black_85%,transparent)]\"\n            style={{ left: \"var(--rail-x)\" }}\n          />\n          <motion.div\n            aria-hidden\n            className=\"absolute bottom-0 top-1 w-[2px] origin-top rounded-full\"\n            style={{\n              left: \"calc(var(--rail-x) - 0.5px)\",\n              scaleY: beamScale,\n              backgroundImage:\n                \"linear-gradient(to bottom, color-mix(in oklab, var(--annals-accent) 10%, transparent), var(--annals-accent))\",\n              boxShadow: \"0 0 10px color-mix(in oklab, var(--annals-accent) 45%, transparent)\",\n              opacity: shown ? 1 : 0,\n            }}\n          />\n\n          <AnimatePresence initial={false}>\n            {visibleEntries.map((f, vIndex) => {\n              const { entry, key, ordered } = f;\n              const isLatest = key === latestKey;\n              const isOpen = !!expanded[key] || !!q;\n              const shownItems = isOpen ? ordered : ordered.slice(0, Math.max(0, collapseAfter));\n              const hiddenCount = ordered.length - shownItems.length;\n              const canCollapse = !q && ordered.length > collapseAfter;\n              const groups = KINDS.map((k) => ({ kind: k, rows: shownItems.filter(({ item }) => item.kind === k) })).filter(\n                (g) => g.rows.length > 0\n              );\n              const lit = vIndex < litCount;\n              const listId = `${uid}-items-${f.index}`;\n              const iso = /^\\d{4}-\\d{2}-\\d{2}/.test(entry.date) ? entry.date.slice(0, 10) : undefined;\n              const media = showMedia && entry.media ? (entry.media === true ? {} : entry.media) : null;\n              const cascade = Math.min(vIndex, 5) * 0.07;\n\n              return (\n                <motion.article\n                  key={key}\n                  aria-labelledby={`${uid}-title-${f.index}`}\n                  initial={{ height: 0, opacity: 0 }}\n                  animate={{ height: \"auto\", opacity: 1 }}\n                  exit={{ height: 0, opacity: 0 }}\n                  transition={heightT}\n                  className=\"relative [overflow-y:clip]\"\n                >\n                  <motion.div\n                    key={`in-${run}`}\n                    {...rise(0.16 + cascade)}\n                    className=\"relative pb-14 pl-8 @2xl:grid @2xl:grid-cols-[8.5rem_minmax(0,1fr)] @2xl:gap-x-[4.5rem] @2xl:pl-0\"\n                  >\n                    {/* node */}\n                    <span\n                      ref={(el) => {\n                        if (el) nodeRefs.current.set(key, el);\n                        else nodeRefs.current.delete(key);\n                      }}\n                      aria-hidden\n                      className=\"absolute top-[7px] size-[11px] rounded-full border transition-[background-color,border-color,box-shadow] duration-300\"\n                      style={{\n                        left: \"calc(var(--rail-x) - 5px)\",\n                        backgroundColor: lit ? \"var(--annals-accent)\" : \"rgb(10 10 10)\",\n                        borderColor: lit ? \"var(--annals-accent)\" : \"rgb(255 255 255 / 0.22)\",\n                        boxShadow: lit\n                          ? \"0 0 0 4px color-mix(in oklab, var(--annals-accent) 16%, transparent), 0 0 14px color-mix(in oklab, var(--annals-accent) 55%, transparent)\"\n                          : \"0 0 0 4px rgb(10 10 10)\",\n                      }}\n                    />\n\n                    {/* rail: version + date (sticky at wide sizes) */}\n                    <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1.5 @2xl:sticky @2xl:top-[var(--annals-top)] @2xl:flex-col @2xl:items-end @2xl:self-start @2xl:text-right\">\n                      <span className=\"font-mono text-sm font-medium tabular-nums text-white\">\n                        {highlight(entry.version, q)}\n                      </span>\n                      <time dateTime={iso} className=\"text-[13px] tabular-nums text-white/45\">\n                        {formatDate(entry.date)}\n                      </time>\n                      {isLatest && <LatestBadge play={shown && !snap} reducedMotion={reducedMotion} run={run} />}\n                    </div>\n\n                    {/* body */}\n                    <div className=\"mt-3 min-w-0 @2xl:mt-0\">\n                      <h3\n                        id={`${uid}-title-${f.index}`}\n                        className=\"text-xl font-semibold leading-snug tracking-tight text-white @lg:text-2xl\"\n                      >\n                        {highlight(entry.title, q)}\n                      </h3>\n                      {entry.summary ? (\n                        <p className=\"mt-2 text-[15px] leading-relaxed text-white/55\">{highlight(entry.summary, q)}</p>\n                      ) : null}\n\n                      {media ? (\n                        <figure className=\"mt-5\">\n                          {media.src ? (\n                            // eslint-disable-next-line @next/next/no-img-element\n                            <img\n                              src={media.src}\n                              alt={media.alt ?? `${entry.title} preview`}\n                              className=\"aspect-[16/9] w-full rounded-xl border border-white/[0.08] object-cover\"\n                              loading=\"lazy\"\n                            />\n                          ) : (\n                            <MediaPlate seedKey={entry.version} label={media.alt ?? `${entry.title} preview`} />\n                          )}\n                          {media.caption ? (\n                            <figcaption className=\"mt-2 text-xs text-white/40\">{media.caption}</figcaption>\n                          ) : null}\n                        </figure>\n                      ) : null}\n\n                      <div id={listId} className=\"mt-5\">\n                        <AnimatePresence initial={false}>\n                          {groups.map((g) => (\n                            <motion.div\n                              key={g.kind}\n                              initial={{ height: 0, opacity: 0 }}\n                              animate={{ height: \"auto\", opacity: 1 }}\n                              exit={{ height: 0, opacity: 0 }}\n                              transition={heightT}\n                              className=\"[overflow-y:clip]\"\n                            >\n                              <div className=\"border-t border-white/[0.06] py-3.5 @lg:grid @lg:grid-cols-[7.5rem_minmax(0,1fr)] @lg:gap-x-4\">\n                                <div className=\"mb-2 @lg:mb-0 @lg:pt-px\">\n                                  <KindChip kind={g.kind} />\n                                </div>\n                                <ul className=\"flex flex-col\">\n                                  <AnimatePresence initial={false}>\n                                    {g.rows.map(({ item, i }) => (\n                                      <motion.li\n                                        key={i}\n                                        initial={{ height: 0, opacity: 0 }}\n                                        animate={{ height: \"auto\", opacity: 1 }}\n                                        exit={{ height: 0, opacity: 0 }}\n                                        transition={heightT}\n                                        className=\"[overflow-y:clip]\"\n                                      >\n                                        <p className=\"relative py-1 pl-4 text-[14px] leading-relaxed text-white/70\">\n                                          <span\n                                            aria-hidden\n                                            className=\"absolute left-0 top-[0.85rem] size-1 rounded-full bg-white/25\"\n                                          />\n                                          {highlight(item.text, q)}\n                                        </p>\n                                      </motion.li>\n                                    ))}\n                                  </AnimatePresence>\n                                </ul>\n                              </div>\n                            </motion.div>\n                          ))}\n                        </AnimatePresence>\n                      </div>\n\n                      {canCollapse && (hiddenCount > 0 || expanded[key]) ? (\n                        <button\n                          type=\"button\"\n                          aria-expanded={!!expanded[key]}\n                          aria-controls={listId}\n                          onClick={() => setExpanded((s) => ({ ...s, [key]: !s[key] }))}\n                          className={cn(\n                            \"mt-1 inline-flex h-8 items-center gap-1.5 rounded-full border border-white/[0.08] bg-white/[0.02] pl-3 pr-2.5 text-[13px] text-white/65 transition-colors hover:border-white/15 hover:text-white\",\n                            focusRing\n                          )}\n                        >\n                          {expanded[key] ? (\n                            \"Show fewer changes\"\n                          ) : (\n                            <span className=\"tabular-nums\">\n                              Show {hiddenCount} more {hiddenCount === 1 ? \"change\" : \"changes\"}\n                            </span>\n                          )}\n                          <ChevronGlyph\n                            className={cn(\n                              \"size-4 transition-transform duration-200\",\n                              expanded[key] && \"rotate-180\"\n                            )}\n                          />\n                        </button>\n                      ) : null}\n                    </div>\n                  </motion.div>\n                </motion.article>\n              );\n            })}\n          </AnimatePresence>\n\n          {/* empty state */}\n          <AnimatePresence initial={false}>\n            {visibleEntries.length === 0 && (\n              <motion.div\n                key=\"empty\"\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                exit={{ opacity: 0 }}\n                transition={{ duration: 0.2 }}\n                className=\"ml-8 flex flex-col items-start gap-3 rounded-xl border border-dashed border-white/10 px-5 py-8 @2xl:ml-[12.75rem]\"\n              >\n                <p className=\"text-sm text-white/70\">\n                  No changes match{q ? <> “<span className=\"text-white\">{q}</span>”</> : \" these filters\"}.\n                </p>\n                <button\n                  type=\"button\"\n                  onClick={() => {\n                    clearFilters();\n                    searchRef.current?.focus();\n                  }}\n                  className={cn(\n                    \"inline-flex h-8 items-center rounded-full px-3 text-[13px] font-medium transition-[filter] hover:brightness-110\",\n                    focusRing\n                  )}\n                  style={{ backgroundColor: \"var(--annals-accent)\", color: accentInk }}\n                >\n                  Clear filters\n                </button>\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nfunction LatestBadge({ play, reducedMotion, run }: { play: boolean; reducedMotion: boolean; run: number }) {\n  return (\n    <span className=\"relative inline-flex\">\n      {!reducedMotion && play && (\n        <motion.span\n          key={run}\n          aria-hidden\n          className=\"absolute inset-0 rounded-full\"\n          style={{ boxShadow: \"0 0 0 1.5px var(--annals-accent)\" }}\n          initial={{ opacity: 0.8, scale: 1 }}\n          animate={{ opacity: 0, scale: 1.6 }}\n          transition={{ duration: 1.1, ease: \"easeOut\", delay: 0.55 }}\n        />\n      )}\n      <span\n        className=\"relative inline-flex h-[22px] items-center gap-1.5 rounded-full px-2 text-[11px] font-semibold uppercase tracking-[0.08em]\"\n        style={{\n          color: \"var(--annals-accent)\",\n          backgroundColor: \"color-mix(in oklab, var(--annals-accent) 14%, transparent)\",\n          boxShadow: \"inset 0 0 0 1px color-mix(in oklab, var(--annals-accent) 40%, transparent)\",\n        }}\n      >\n        <span aria-hidden className=\"size-1.5 rounded-full\" style={{ backgroundColor: \"var(--annals-accent)\" }} />\n        Latest\n      </span>\n    </span>\n  );\n}\n"
    }
  ]
}