{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"blog-post-template","type":"registry:ui","title":"Blog post template","description":"One article at a reading measure: a progress line under the header, an outline that marks where you are, linkable headings, the author and three related posts.","dependencies":["@radix-ui/react-slot","clsx","lucide-react","tailwind-merge"],"registryDependencies":["https://caliperui.site/r/styles.json"],"files":[{"path":"ui/avatar.tsx","type":"registry:ui","target":"components/ui/avatar.tsx","content":"\"use client\";\n\nimport { createContext, useContext, useState, type ComponentProps } from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./avatar.css\";\n\nexport type AvatarProps = Omit<ComponentProps<\"span\">, \"children\"> & {\n  name: string;\n  src?: string;\n  alt?: string;\n  /** \"square\" for a company, workspace or integration: a coloured mark instead of a person's orb or photo. */\n  shape?: \"circle\" | \"square\";\n  /** Shown as a shape at the corner: filled, half, dash or ring. */\n  presence?: \"online\" | \"away\" | \"busy\" | \"offline\";\n};\n\n/** Resolves a person's photo from their name when an Avatar has no `src`: your user directory, or sample faces in a demo.\n    Return undefined for anyone without one and they keep their orb. */\nexport const AvatarPhotos = createContext<((name: string) => string | undefined) | null>(null);\n\n/** The same name always gets the same orb (six glass orbs; the first follows the accent). */\nexport function avatarTone(name: string) {\n  let hash = 7;\n  for (const char of name.trim().toLowerCase()) hash = (hash * 31 + char.codePointAt(0)!) >>> 0;\n  return hash % 6;\n}\n\nfunction AvatarContent({ src, initials }: { src?: string; initials: string }) {\n  const [failed, setFailed] = useState(false);\n  return (\n    <>\n      <span aria-hidden=\"true\">{initials}</span>\n      {src && !failed && (\n        <img\n          src={src}\n          alt=\"\"\n          aria-hidden=\"true\"\n          width={32}\n          height={32}\n          decoding=\"async\"\n          className=\"absolute inset-0 size-full object-cover\"\n          ref={(image) => {\n            // A cached or pre-hydration failure may precede the error listener.\n            if (image?.complete && image.naturalWidth === 0) setFailed(true);\n          }}\n          onError={() => setFailed(true)}\n        />\n      )}\n    </>\n  );\n}\n\nexport function Avatar({\n  name,\n  src,\n  alt = name,\n  shape = \"circle\",\n  presence,\n  className,\n  ...props\n}: AvatarProps) {\n  const resolve = useContext(AvatarPhotos);\n  const photo = src ?? (shape === \"circle\" ? resolve?.(name) : undefined);\n  const words = name.trim().normalize(\"NFC\").split(/\\s+/u);\n  const initials =\n    (Array.from(words[0] ?? \"\")[0] ?? \"?\") +\n    (words.length > 1 ? (Array.from(words[words.length - 1]!)[0] ?? \"\") : \"\");\n\n  return (\n    <span\n      data-slot=\"avatar\"\n      data-tone={avatarTone(name)}\n      data-shape={shape === \"square\" ? \"square\" : undefined}\n      className={cn(\n        \"relative inline-flex size-8 shrink-0 items-center justify-center text-xs font-semibold\",\n        shape === \"square\" ? \"rounded-[8px]\" : \"rounded-full\",\n        className,\n      )}\n      {...props}\n      role={alt ? \"img\" : undefined}\n      aria-label={alt || undefined}\n      aria-hidden={!alt || undefined}\n    >\n      <AvatarContent\n        key={photo ?? \"\"}\n        src={photo}\n        initials={initials.toUpperCase()}\n      />\n      {presence && <span data-slot=\"avatar-presence\" data-presence={presence} aria-hidden=\"true\" />}\n    </span>\n  );\n}\n"},{"path":"ui/block-concept.tsx","type":"registry:ui","target":"components/ui/block-concept.tsx","content":"import { ArrowRight } from \"lucide-react\";\nimport type { ComponentProps, ElementType, ReactNode } from \"react\";\nimport { cn } from \"./lib/cn\";\nimport { StageScope } from \"./stage-scope\";\nimport \"./block-concept.css\";\n\n/**\n * Marketing blocks share one voice: monochrome from the system tokens, Geist\n * display with two-tone titles, ink actions, ruled sections and ember only for\n * focus. Blocks follow the page theme, and the Caliper UI controls inside them too.\n */\n\nexport type BlockAction = { label: string; href?: string; onClick?: () => void };\n\n/**\n * A full-bleed band. The section is the container, so everything inside lays\n * out by the block's own width; `block-inner` holds the measure and rhythm.\n * `backdrop` paints behind the content edge to edge (grid lines, a tone).\n */\nexport function BlockRoot({\n  as: Tag = \"section\",\n  width = \"default\",\n  backdrop,\n  tone,\n  className,\n  innerClassName,\n  children,\n  ...props\n}: Omit<ComponentProps<\"section\">, \"ref\"> & {\n  /** The band's element; a site footer is a footer. */\n  as?: \"section\" | \"footer\" | \"div\";\n  /** default 1240px · narrow 960px · wide 1440px. */\n  width?: \"default\" | \"narrow\" | \"wide\";\n  backdrop?: ReactNode;\n  /** A band tone: the page colour or the sunken colour. */\n  tone?: \"page\" | \"sunken\" | \"ink\";\n  innerClassName?: string;\n}) {\n  return (\n    <Tag data-slot=\"block\" data-tone={tone} className={cn(\"@container relative isolate overflow-clip\", className)} {...props}>\n      {backdrop}\n      <div data-slot=\"block-inner\" data-width={width} className={innerClassName}>\n        {children}\n      </div>\n    </Tag>\n  );\n}\n\nexport function BlockEyebrow({ className, ...props }: ComponentProps<\"p\">) {\n  return <p data-slot=\"block-eyebrow\" className={className} {...props} />;\n}\n\n/** A display heading in the block face. size 1 is the hero headline. */\nexport function BlockTitle({\n  as: Tag = \"h2\",\n  size = 2,\n  className,\n  ...props\n}: ComponentProps<\"h2\"> & { as?: \"h1\" | \"h2\" | \"h3\"; size?: 1 | 2 | 3 }) {\n  return <Tag data-slot=\"block-title\" data-size={size} className={className} {...props} />;\n}\n\n/** The second tone of a title: these words land in grey. */\nexport function BlockEm({ className, ...props }: ComponentProps<\"em\">) {\n  return <em data-slot=\"block-em\" className={className} {...props} />;\n}\n\nexport function BlockLead({ className, ...props }: ComponentProps<\"p\">) {\n  return <p data-slot=\"block-lead\" className={className} {...props} />;\n}\n\n/**\n * The section opening every block shares: eyebrow, title and lead. `split`\n * sets the lead beside the title on wide blocks; `center` stacks it centred.\n */\nexport function BlockHeader({\n  eyebrow,\n  title,\n  lead,\n  align = \"start\",\n  aside,\n  headingLevel = 2,\n  className,\n}: {\n  eyebrow?: ReactNode;\n  title: ReactNode;\n  lead?: ReactNode;\n  align?: \"start\" | \"center\" | \"split\";\n  /** 1 when the block opens the page, e.g. a pricing page. */\n  headingLevel?: 1 | 2;\n  /** Sits at the end of the header row, e.g. a period toggle. */\n  aside?: ReactNode;\n  className?: string;\n}) {\n  return (\n    <header data-slot=\"block-header\" data-align={align} className={className}>\n      <div data-slot=\"block-header-title\">\n        {eyebrow && <BlockEyebrow>{eyebrow}</BlockEyebrow>}\n        <BlockTitle as={headingLevel === 1 ? \"h1\" : \"h2\"}>{title}</BlockTitle>\n      </div>\n      {(lead || aside) && (\n        <div data-slot=\"block-header-aside\">\n          {lead && <BlockLead>{lead}</BlockLead>}\n          {aside}\n        </div>\n      )}\n    </header>\n  );\n}\n\nexport function BlockActions({\n  primary,\n  secondary,\n  className,\n  block = false,\n}: {\n  primary?: BlockAction;\n  secondary?: BlockAction;\n  className?: string;\n  /** Full-width actions, e.g. on phones. */\n  block?: boolean;\n}) {\n  if (!primary && !secondary) return null;\n  return (\n    <div data-slot=\"block-actions\" className={cn(\"flex flex-wrap gap-3\", block && \"@max-[520px]:grid @max-[520px]:grid-cols-1\", className)}>\n      {primary && <BlockButton action={primary} kind=\"primary\" arrow />}\n      {secondary && <BlockButton action={secondary} kind=\"secondary\" />}\n    </div>\n  );\n}\n\nexport function BlockButton({ action, kind = \"primary\", arrow = false, link: Link = \"a\" }: { action: BlockAction; kind?: \"primary\" | \"secondary\"; arrow?: boolean; /** The element for a link action, e.g. your router's Link. */ link?: ElementType }) {\n  const shared = { \"data-slot\": \"block-button\", \"data-kind\": kind } as const;\n  const content = (\n    <>\n      {action.label}\n      {arrow && <ArrowRight aria-hidden=\"true\" data-slot=\"block-button-arrow\" className=\"size-4\" />}\n    </>\n  );\n  return action.href ? (\n    <Link href={action.href} {...shared}>\n      {content}\n    </Link>\n  ) : (\n    <button type=\"button\" onClick={action.onClick} {...shared}>\n      {content}\n    </button>\n  );\n}\n\nexport function BlockCard({ className, ...props }: ComponentProps<\"div\">) {\n  return <div data-slot=\"block-card\" className={className} {...props} />;\n}\n\nexport function BlockChip({ className, ...props }: ComponentProps<\"span\">) {\n  return <span data-slot=\"block-chip\" className={className} {...props} />;\n}\n\n/**\n * A product artefact on a stage: a dotted tonal panel holding a framed window, with an optional floating note that overlaps the\n * window's corner on wide blocks and sits beneath it on narrow ones.\n */\nexport function BlockStage({\n  title,\n  floating,\n  bare = false,\n  className,\n  children,\n}: {\n  /** Shown in the window bar, e.g. \"halden.app/close\". */\n  title?: string;\n  floating?: ReactNode;\n  /** No window frame: the children bring their own surfaces. */\n  bare?: boolean;\n  className?: string;\n  children: ReactNode;\n}) {\n  return (\n    <div data-slot=\"block-stage\" className={className}>\n      {bare ? (\n        <StageScope>{children}</StageScope>\n      ) : (\n        <div data-slot=\"block-window\">\n          <div data-slot=\"block-window-bar\" aria-hidden={title ? undefined : true}>\n            <span data-slot=\"block-window-dots\" aria-hidden=\"true\"><i /><i /><i /></span>\n            {title && <span data-slot=\"block-window-title\">{title}</span>}\n          </div>\n          <div data-slot=\"block-window-body\"><StageScope>{children}</StageScope></div>\n        </div>\n      )}\n      {floating && <div data-slot=\"block-float\">{floating}</div>}\n    </div>\n  );\n}\n\n/** Company names set as type in a quiet row; swap in SVG marks in production. */\nexport function BlockLogos({ label, logos, className }: { label?: string; logos: readonly string[]; className?: string }) {\n  return (\n    <div data-slot=\"block-logos\" className={className}>\n      {label && <p data-slot=\"block-logos-label\">{label}</p>}\n      <ul aria-label={label ?? \"Customers\"}>\n        {logos.map((logo) => (\n          <li key={logo}>{logo}</li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n\n/** Initials from a name, in the block's highlight tones. */\nexport function BlockInitials({ name, size = 36, tone, className }: { name: string; size?: number; tone?: number; className?: string }) {\n  const words = name.trim().split(/\\s+/u);\n  const initials = ((words[0]?.[0] ?? \"\") + (words.length > 1 ? (words[words.length - 1]?.[0] ?? \"\") : \"\")).toUpperCase();\n  return (\n    <span data-slot=\"block-initials\" data-tone={tone === undefined ? undefined : tone % 4} aria-hidden=\"true\" className={className} style={{ width: size, height: size, fontSize: Math.round(size / 2.8) }}>\n      {initials}\n    </span>\n  );\n}\n\nexport type BlockStat = { value: ReactNode; label: string };\n"},{"path":"ui/block-nav.tsx","type":"registry:ui","target":"components/ui/block-nav.tsx","content":"\"use client\";\n\nimport { Menu, X } from \"lucide-react\";\nimport { useEffect, useId, useRef, useState, type ElementType, type ReactNode } from \"react\";\nimport { BlockButton, type BlockAction } from \"./block-concept\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/site-header.css\";\nimport \"./block-nav.css\";\n\nexport type BlockNavLink = { label: string; href: string; current?: boolean };\n\n// Whole class names, so Tailwind sees them.\nconst folds = {\n  860: { nav: \"@[860px]:block\", row: \"@[860px]:flex\", menu: \"@[860px]:hidden\" },\n  1180: { nav: \"@[1180px]:block\", row: \"@[1180px]:flex\", menu: \"@[1180px]:hidden\" },\n};\n\n/**\n * The site header page templates share. It sits on the page colour, gains a\n * hairline and frost once the page scrolls, keeps the primary action in\n * reach, and on narrow pages folds the links into a menu disclosure that\n * closes with Escape and returns focus to its button.\n */\nexport function BlockNav({\n  brand,\n  brandHref = \"/\",\n  brandLabel,\n  logo,\n  nav = [],\n  signIn,\n  primary,\n  aside,\n  link: Link = \"a\",\n  foldAt = 860,\n  className,\n}: {\n  brand: string;\n  brandHref?: string;\n  /** The home link's accessible name when the visible brand is short, e.g. \"Halden home\". */\n  brandLabel?: string;\n  /** A mark set before the brand name; the name stays the accessible label. */\n  logo?: ReactNode;\n  nav?: readonly BlockNavLink[];\n  signIn?: BlockAction;\n  primary?: BlockAction;\n  /** Extra controls before the actions, e.g. a theme switch or search. */\n  aside?: ReactNode;\n  /** The element for the brand and nav links, e.g. your router's Link, so moving between pages stays in the app. */\n  link?: ElementType;\n  /** The header width (px) the links and actions need to sit in one row; narrower, they fold into the menu. 1180 suits six or more links beside extra controls. */\n  foldAt?: keyof typeof folds;\n  className?: string;\n}) {\n  const id = useId();\n  const sentinel = useRef<HTMLDivElement>(null);\n  const menuButton = useRef<HTMLButtonElement>(null);\n  const [scrolled, setScrolled] = useState(false);\n  const [menu, setMenu] = useState(false);\n  const fold = folds[foldAt];\n\n  useEffect(() => {\n    const top = sentinel.current;\n    if (!top) return;\n    const observer = new IntersectionObserver(([entry]) => setScrolled(!entry!.isIntersecting));\n    observer.observe(top);\n    return () => observer.disconnect();\n  }, []);\n\n  return (\n    <>\n      <div ref={sentinel} aria-hidden=\"true\" className=\"absolute top-0 h-px w-px\" />\n      <header data-slot=\"site-header\" data-scrolled={scrolled || undefined} className={cn(\"sticky top-0 z-20\", className)}>\n        <div data-slot=\"block-nav-row\">\n          <Link href={brandHref} aria-label={brandLabel} data-slot=\"block-nav-brand\" className={cn(logo && \"inline-flex items-center gap-2.5\")}>{logo}{brand}</Link>\n          {nav.length > 0 && (\n            <nav aria-label=\"Site\" className={cn(\"hidden\", fold.nav)}>\n              <ul data-slot=\"block-nav-list\">\n                {nav.map((item) => (\n                  <li key={item.href}>\n                    <Link href={item.href} aria-current={item.current ? \"page\" : undefined} data-slot=\"block-nav-link\">{item.label}</Link>\n                  </li>\n                ))}\n              </ul>\n            </nav>\n          )}\n          <span className={cn(\"ml-auto hidden items-center gap-2\", fold.row)}>\n            {aside}\n            {signIn && <BlockButton action={signIn} kind=\"secondary\" link={Link} />}\n            {primary && <BlockButton action={primary} link={Link} />}\n          </span>\n          {(nav.length > 0 || signIn) && (\n            <button\n              ref={menuButton}\n              type=\"button\"\n              aria-expanded={menu}\n              aria-controls={`${id}-menu`}\n              onClick={() => setMenu((open) => !open)}\n              // Focus stays on the button after it opens the menu, so Escape has to work here too.\n              onKeyDown={(event) => event.key === \"Escape\" && setMenu(false)}\n              data-slot=\"block-button\"\n              data-kind=\"secondary\"\n              className={cn(\"ml-auto !min-h-11 !px-4\", fold.menu)}\n            >\n              {menu ? <X aria-hidden=\"true\" className=\"size-4\" /> : <Menu aria-hidden=\"true\" className=\"size-4\" />}\n              Menu\n            </button>\n          )}\n        </div>\n        <div\n          id={`${id}-menu`}\n          hidden={!menu}\n          data-slot=\"site-menu\"\n          className={cn(\"absolute inset-x-0 top-full grid gap-1 border-b border-x border-border bg-[var(--elevated-glass)] px-5 pt-2 pb-6 backdrop-blur-[20px] backdrop-saturate-[1.4] [box-shadow:var(--shadow-overlay),inset_0_1px_0_var(--edge-light)]\", fold.menu)}\n          onKeyDown={(event) => {\n            if (event.key === \"Escape\") {\n              setMenu(false);\n              menuButton.current?.focus();\n            }\n          }}\n        >\n          {nav.map((item) => (\n            <Link key={item.href} href={item.href} aria-current={item.current ? \"page\" : undefined} onClick={() => setMenu(false)} className=\"flex h-12 items-center border-b border-[var(--k-line)] text-[17px]\">\n              {item.label}\n            </Link>\n          ))}\n          {menu && aside && <div className=\"mt-4 flex flex-wrap items-center gap-2\">{aside}</div>}\n          <span className=\"mt-4 grid gap-2\">\n            {signIn && <BlockButton action={signIn} kind=\"secondary\" link={Link} />}\n            {primary && <BlockButton action={primary} link={Link} />}\n          </span>\n        </div>\n      </header>\n    </>\n  );\n}\n"},{"path":"ui/blog-post-template.tsx","type":"registry:ui","target":"components/ui/blog-post-template.tsx","content":"\"use client\";\n\nimport { ArrowRight, Link2 } from \"lucide-react\";\nimport { useState, type ReactNode } from \"react\";\nimport { Avatar } from \"./avatar\";\nimport { BlockEyebrow, BlockLead, BlockRoot, BlockStage, BlockTitle, type BlockAction } from \"./block-concept\";\nimport type { BlockNavLink } from \"./block-nav\";\nimport { CopyButton } from \"./copy-button\";\nimport { TemplateOutline, TemplatePage, useReadingProgress, useScrollSpy, type TemplateFooter } from \"./template-page\";\nimport \"./shared/article-prose.css\";\nimport \"./blog-post-template.css\";\n\nexport type BlogArticleSection = {\n  /** The anchor, e.g. \"why-we-moved\". */\n  id: string;\n  title: string;\n  body: ReactNode;\n};\n\nexport type BlogArticleRelated = { title: string; href: string; category?: string; date?: string; excerpt?: string };\n\nexport type BlogArticle = {\n  category: string;\n  title: string;\n  /** The standfirst: one or two sentences under the title. */\n  lead: string;\n  /** ISO date, e.g. \"2026-10-07\". */\n  date: string;\n  readingMinutes: number;\n  author: { name: string; role: string; href?: string; bio?: string };\n  cover?: ReactNode;\n  coverTitle?: string;\n  /** Short caption under the cover, e.g. its source. */\n  coverCaption?: string;\n  sections: readonly BlogArticleSection[];\n  /** Where this article lives, for the copy-link button. */\n  url?: string;\n};\n\nconst formatDate = (iso: string) => new Intl.DateTimeFormat(\"en-GB\", { day: \"numeric\", month: \"long\", year: \"numeric\", timeZone: \"UTC\" }).format(new Date(`${iso}T00:00:00Z`));\n\n/**\n * One article at a reading measure: a progress line that follows the\n * reader, an outline that marks the current section, headings you can link\n * to, the author at the end and three related posts. Built only from the\n * Free blocks; the footer carries the newsletter.\n */\nexport function BlogPostTemplate({\n  brand,\n  nav,\n  signIn,\n  primary,\n  post,\n  related = [],\n  next,\n  footer,\n  className,\n}: {\n  brand: string;\n  nav?: readonly BlockNavLink[];\n  signIn?: BlockAction;\n  primary?: BlockAction;\n  post: BlogArticle;\n  related?: readonly BlogArticleRelated[];\n  /** A closing action under the article, e.g. \"Try the close checklist\". */\n  next?: BlockAction;\n  footer?: TemplateFooter;\n  className?: string;\n}) {\n  const [article, setArticle] = useState<HTMLElement | null>(null);\n  const progress = useReadingProgress(article);\n  const active = useScrollSpy(post.sections.map((section) => section.id));\n  const url = post.url ?? \"\";\n\n  return (\n    <TemplatePage brand={brand} nav={nav} signIn={signIn} primary={primary} footer={footer} className={className}>\n      <div data-slot=\"reading-progress\" aria-hidden=\"true\">\n        <span style={{ transform: `scaleX(${progress})` }} />\n      </div>\n      <article ref={setArticle}>\n        <BlockRoot as=\"div\" innerClassName=\"!pb-10\">\n          <div data-slot=\"article-layout\">\n          <span aria-hidden=\"true\" />\n          <header className=\"grid min-w-0 gap-6\">\n            <BlockEyebrow className=\"flex-wrap\">\n              <span className=\"whitespace-nowrap\">{post.category}</span>\n              <span aria-hidden=\"true\">·</span>\n              <time dateTime={post.date} className=\"whitespace-nowrap\">{formatDate(post.date)}</time>\n              <span aria-hidden=\"true\">·</span>\n              <span className=\"whitespace-nowrap\">{post.readingMinutes} min read</span>\n            </BlockEyebrow>\n            <BlockTitle as=\"h1\" size={2} className=\"max-w-[18ch]\">{post.title}</BlockTitle>\n            <BlockLead className=\"!max-w-[56ch]\">{post.lead}</BlockLead>\n            <div className=\"flex flex-wrap items-center justify-between gap-4 border-t border-[var(--k-line)] pt-5\">\n              <span className=\"flex items-center gap-3\">\n                <Avatar name={post.author.name} alt=\"\" className=\"size-10 text-sm\" />\n                <span className=\"grid\">\n                  <span className=\"text-sm font-medium\">{post.author.name}</span>\n                  <span className=\"text-[13px] text-[var(--k-muted)]\">{post.author.role}</span>\n                </span>\n              </span>\n              {url && (\n                <CopyButton value={url} label=\"Copy link to this article\" display=\"Link copied\" variant=\"secondary\">\n                  <Link2 aria-hidden=\"true\" />\n                  Copy link\n                </CopyButton>\n              )}\n            </div>\n          </header>\n          </div>\n        </BlockRoot>\n        {post.cover && (\n          <BlockRoot width=\"default\" as=\"div\" innerClassName=\"!py-0\">\n            <figure className=\"m-0 grid gap-3\">\n              <BlockStage title={post.coverTitle}>{post.cover}</BlockStage>\n              {post.coverCaption && <figcaption className=\"text-[13px] text-[var(--k-muted)]\">{post.coverCaption}</figcaption>}\n            </figure>\n          </BlockRoot>\n        )}\n        <BlockRoot width=\"default\" as=\"div\" innerClassName=\"!pb-[clamp(48px,7cqi,88px)]\">\n          <div data-slot=\"article-layout\">\n            <TemplateOutline items={post.sections} active={active} />\n            <div data-slot=\"article-body\">\n              {post.sections.map((section) => (\n                <section key={section.id} id={section.id} aria-labelledby={`${section.id}-title`}>\n                  <h2 id={`${section.id}-title`} data-slot=\"article-heading\">\n                    {section.title}\n                    <a href={`#${section.id}`} data-slot=\"article-anchor\" aria-label={`Link to “${section.title}”`}>\n                      <Link2 aria-hidden=\"true\" />\n                    </a>\n                  </h2>\n                  <div data-slot=\"article-prose\">{section.body}</div>\n                </section>\n              ))}\n              <aside data-slot=\"article-author\" aria-label=\"About the author\">\n                <Avatar name={post.author.name} alt=\"\" className=\"size-14 text-base\" />\n                <div className=\"grid gap-1\">\n                  <p className=\"m-0 text-[15px] font-medium\">\n                    {post.author.href ? <a href={post.author.href} className=\"underline-offset-4 hover:underline\">{post.author.name}</a> : post.author.name}\n                  </p>\n                  <p className=\"m-0 text-[13px] text-[var(--k-muted)]\">{post.author.role}</p>\n                  {post.author.bio && <p className=\"m-0 mt-2 max-w-[56ch] text-[15px] leading-relaxed text-[var(--k-muted)]\">{post.author.bio}</p>}\n                </div>\n              </aside>\n              {next && (\n                <a data-slot=\"article-next\" href={next.href} onClick={next.onClick}>\n                  {next.label}\n                  <ArrowRight aria-hidden=\"true\" />\n                </a>\n              )}\n            </div>\n          </div>\n        </BlockRoot>\n      </article>\n      {related.length > 0 && (\n        <BlockRoot tone=\"sunken\" aria-labelledby=\"related-title\">\n          <h2 id=\"related-title\" data-slot=\"block-title\" data-size=\"3\" className=\"!mb-8\">Keep reading</h2>\n          <ul data-slot=\"article-related\">\n            {related.slice(0, 3).map((item) => (\n              <li key={item.href}>\n                <a href={item.href}>\n                  {(item.category || item.date) && (\n                    <span className=\"text-[12px] uppercase tracking-[0.08em] text-[var(--k-muted)]\">\n                      {item.category}\n                      {item.category && item.date && \" · \"}\n                      {item.date && <time dateTime={item.date}>{formatDate(item.date)}</time>}\n                    </span>\n                  )}\n                  <span data-slot=\"article-related-title\">{item.title}</span>\n                  {item.excerpt && <span className=\"text-[15px] leading-relaxed text-[var(--k-muted)]\">{item.excerpt}</span>}\n                </a>\n              </li>\n            ))}\n          </ul>\n        </BlockRoot>\n      )}\n    </TemplatePage>\n  );\n}\n"},{"path":"ui/button.tsx","type":"registry:ui","target":"components/ui/button.tsx","content":"\"use client\";\n\nimport { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport { LoaderCircle } from \"lucide-react\";\nimport {\n  useLayoutEffect,\n  useRef,\n  useState,\n  type ComponentProps,\n  type ReactNode,\n} from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/loading-indicator.css\";\nimport \"./button.css\";\n\n/* Colour, material and press live in button.css keyed on data-variant, so a\n   consumer's utility classes still override them. Raised to press: the\n   button sinks 1px into an inner shade instead of scaling. */\nconst variants = {\n  default: \"\",\n  secondary: \"\",\n  ghost: \"\",\n  destructive: \"\",\n};\n\nconst sizes = {\n  sm: \"h-8 gap-1.5 rounded-full px-3 text-[13px] max-md:min-h-11 [@media(pointer:coarse)]:min-h-11\",\n  md: \"h-[var(--control-height)] gap-2 rounded-full px-3.5 text-sm\",\n  lg: \"h-11 gap-2 rounded-full px-[18px] text-[15px]\",\n  icon: \"size-[var(--control-height)] rounded-full p-0\",\n};\n\nexport type ButtonProps = ComponentProps<\"button\"> & {\n  variant?: keyof typeof variants;\n  size?: keyof typeof sizes;\n  asChild?: boolean;\n  /** Shows a spinner over the label and swallows clicks while keeping focus and width. */\n  loading?: boolean;\n  /** Animate a meaningful label/icon change, such as saving → saved. */\n  feedbackKey?: string;\n};\n\nexport function Button({\n  className,\n  variant = \"default\",\n  size = \"md\",\n  asChild = false,\n  loading = false,\n  feedbackKey,\n  disabled,\n  \"aria-disabled\": ariaDisabled,\n  children,\n  onClick,\n  onClickCapture,\n  tabIndex,\n  type = \"button\",\n  ...props\n}: ButtonProps) {\n  const Component = asChild ? Slot : \"button\";\n  const nativeDisabled = Boolean(disabled);\n  const blocked =\n    nativeDisabled ||\n    loading ||\n    ariaDisabled === true ||\n    ariaDisabled === \"true\";\n\n  return (\n    <Component\n      data-slot=\"button\"\n      data-variant={variant}\n      data-size={size}\n      type={asChild ? undefined : type}\n      className={cn(\n        \"relative inline-flex shrink-0 items-center justify-center whitespace-nowrap border border-transparent font-medium tracking-[-0.006em] outline-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none aria-disabled:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n        variants[variant],\n        sizes[size],\n        className,\n      )}\n      disabled={asChild ? undefined : nativeDisabled}\n      aria-disabled={nativeDisabled || ariaDisabled ? true : undefined}\n      aria-busy={loading || undefined}\n      tabIndex={asChild && nativeDisabled ? -1 : tabIndex}\n      {...props}\n      onKeyDown={(event) => {\n        if (blocked && (event.key === \"Enter\" || event.key === \" \")) {\n          event.preventDefault();\n        }\n        props.onKeyDown?.(event);\n      }}\n      onClickCapture={(event) => {\n        if (blocked) {\n          event.preventDefault();\n          event.stopPropagation();\n          return;\n        }\n        onClickCapture?.(event);\n      }}\n      onClick={onClick}\n    >\n      {loading && asChild && (\n        <LoaderCircle data-slot=\"loading-indicator\" aria-hidden=\"true\" />\n      )}\n      {loading && !asChild && feedbackKey === undefined && (\n        <span data-slot=\"button-busy\" aria-hidden=\"true\">\n          <LoaderCircle data-slot=\"loading-indicator\" />\n        </span>\n      )}\n      <Slottable>\n        {loading && !asChild && feedbackKey === undefined ? (\n          <span data-slot=\"button-label\" data-hidden=\"true\">\n            {children}\n          </span>\n        ) : feedbackKey !== undefined && !asChild ? (\n          <ButtonFeedback stateKey={feedbackKey}>\n            {loading && (\n              <LoaderCircle data-slot=\"loading-indicator\" aria-hidden=\"true\" />\n            )}\n            {children}\n          </ButtonFeedback>\n        ) : (\n          children\n        )}\n      </Slottable>\n    </Component>\n  );\n}\n\nfunction ButtonFeedback({\n  stateKey,\n  children,\n}: {\n  stateKey: string;\n  children: ReactNode;\n}) {\n  const content = useRef<HTMLSpanElement>(null);\n  const [initialKey] = useState(stateKey);\n  const [width, setWidth] = useState<number>();\n  useLayoutEffect(() => {\n    const element = content.current;\n    if (!element) return;\n    const measure = () => setWidth(element.offsetWidth);\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, [stateKey]);\n  return (\n    <span data-slot=\"button-feedback\" style={{ width }}>\n      <span\n        key={stateKey}\n        ref={content}\n        data-slot=\"button-feedback-label\"\n        data-enter={stateKey !== initialKey}\n      >\n        {children}\n      </span>\n    </span>\n  );\n}\n"},{"path":"ui/copy-button.tsx","type":"registry:ui","target":"components/ui/copy-button.tsx","content":"\"use client\";\n\nimport { Check, Copy } from \"lucide-react\";\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { Button, type ButtonProps } from \"./button\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/action-label.css\";\nimport \"./copy-button.css\";\n\n/** \"rv_live_8Kq2mZt7XcP1vW9yL3nB4f2a\" → \"rv_live_8K…4f2a\". */\nexport function shortenValue(value: string, head = 10, tail = 4) {\n  return value.length <= head + tail + 1 ? value : `${value.slice(0, head)}…${value.slice(-tail)}`;\n}\n\nasync function writeClipboard(value: string) {\n  try {\n    await navigator.clipboard.writeText(value);\n    return true;\n  } catch {\n    // Older or locked-down browsers: fall back to a selected textarea.\n    const area = document.createElement(\"textarea\");\n    area.value = value;\n    area.setAttribute(\"readonly\", \"\");\n    area.style.position = \"fixed\";\n    area.style.opacity = \"0\";\n    document.body.appendChild(area);\n    area.select();\n    let ok = false;\n    try {\n      ok = document.execCommand(\"copy\");\n    } catch {\n      ok = false;\n    }\n    area.remove();\n    return ok;\n  }\n}\n\n/**\n * Copies a value and confirms what was copied, shortened in mono, on a small\n * ticket above the button. If the browser blocks the clipboard, the ticket\n * offers the value selected so it can be copied by hand.\n */\nexport function CopyButton({\n  value,\n  label,\n  display,\n  iconOnly = false,\n  variant = \"secondary\",\n  size = \"sm\",\n  resetAfterMs = 1800,\n  onCopy,\n  className,\n  children,\n}: {\n  /** The text written to the clipboard. */\n  value: string;\n  /** Accessible name, e.g. \"Copy live secret key\". */\n  label: string;\n  /** What the ticket shows; defaults to the value shortened in the middle. */\n  display?: string;\n  iconOnly?: boolean;\n  variant?: ButtonProps[\"variant\"];\n  size?: Exclude<ButtonProps[\"size\"], \"icon\">;\n  resetAfterMs?: number;\n  onCopy?: (ok: boolean) => void;\n  className?: string;\n  /** Visible label; defaults to “Copy”. */\n  children?: ReactNode;\n}) {\n  const [state, setState] = useState<\"idle\" | \"copied\" | \"failed\">(\"idle\");\n  const fallback = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    if (state !== \"copied\") return;\n    const timer = setTimeout(() => setState(\"idle\"), resetAfterMs);\n    return () => clearTimeout(timer);\n  }, [state, resetAfterMs]);\n\n  useEffect(() => {\n    if (state === \"failed\") fallback.current?.select();\n  }, [state]);\n\n  async function copy() {\n    const ok = await writeClipboard(value);\n    setState(ok ? \"copied\" : \"failed\");\n    onCopy?.(ok);\n  }\n\n  const copied = state === \"copied\";\n  const iconClass = { sm: \"size-8 rounded-control max-md:size-11\", md: undefined, lg: \"size-11 rounded-touch\" }[size];\n\n  return (\n    <span\n      data-slot=\"copy-button\"\n      className={cn(\"relative inline-flex\", className)}\n      onKeyDown={(event) => {\n        if (event.key === \"Escape\" && state !== \"idle\") setState(\"idle\");\n      }}\n    >\n      <span data-slot=\"copy-ticket\" data-state={state} role={state === \"failed\" ? \"alert\" : \"status\"} aria-live={state === \"failed\" ? \"assertive\" : \"polite\"}>\n        {copied && (\n          <span data-slot=\"copy-ticket-body\">\n            <Check aria-hidden=\"true\" />\n            Copied <span className=\"font-mono text-[11px] opacity-75\">{display ?? shortenValue(value)}</span>\n          </span>\n        )}\n        {state === \"failed\" && (\n          <span data-slot=\"copy-ticket-body\" data-tone=\"error\">\n            <span>Couldn’t copy. The browser blocked the clipboard; press ⌘C or Ctrl+C.</span>\n            <input\n              ref={fallback}\n              readOnly\n              value={value}\n              aria-label={`${label}: value to copy by hand`}\n              onBlur={() => setState(\"idle\")}\n              className=\"w-full rounded-[6px] bg-background px-2 py-1 font-mono text-xs text-foreground outline-none\"\n            />\n          </span>\n        )}\n      </span>\n      <Button\n        variant={variant}\n        size={iconOnly ? \"icon\" : size}\n        aria-label={label}\n        data-copied={copied}\n        className={iconOnly ? iconClass : undefined}\n        onClick={copy}\n      >\n        <span data-slot=\"copy-icon\" data-copied={copied} aria-hidden=\"true\">\n          <Copy />\n          <Check />\n        </span>\n        {!iconOnly && (\n          <span data-slot=\"action-labels\">\n            <span data-slot=\"action-label\" data-active={!copied}>{children ?? \"Copy\"}</span>\n            <span data-slot=\"action-label\" data-active={copied}>Copied</span>\n          </span>\n        )}\n      </Button>\n    </span>\n  );\n}\n"},{"path":"ui/footer-block.tsx","type":"registry:ui","target":"components/ui/footer-block.tsx","content":"\"use client\";\n\nimport { ArrowRight, Check } from \"lucide-react\";\nimport { useEffect, useId, useRef, useState, type ReactNode } from \"react\";\nimport { BlockRoot } from \"./block-concept\";\nimport { cn } from \"./lib/cn\";\nimport \"./footer-block.css\";\n\nexport type BlockFooterColumn = { title: string; links: readonly { label: string; href: string }[] };\n\n/** Sizes the wordmark so the name spans the band exactly, whatever its length. */\nfunction Wordmark({ children }: { children: string }) {\n  const ref = useRef<HTMLParagraphElement>(null);\n  useEffect(() => {\n    const node = ref.current;\n    const text = node?.firstElementChild as HTMLElement | null;\n    if (!node || !text) return;\n    // Scale from the current size, so a fitted wordmark measures as fitted and stops.\n    const fit = () => {\n      // Layout width, not the drawn box: inside a scaled preview or a transformed parent the drawn box is smaller.\n      const width = text.offsetWidth;\n      const size = parseFloat(getComputedStyle(node).fontSize);\n      if (!width || !size) return;\n      const next = Math.floor((size * node.clientWidth) / width);\n      if (Math.abs(next - size) >= 1) node.style.setProperty(\"--wordmark-size\", `${next}px`);\n    };\n    fit();\n    const observer = new ResizeObserver(() => requestAnimationFrame(fit));\n    observer.observe(node);\n    document.fonts?.ready.then(fit);\n    return () => observer.disconnect();\n  }, [children]);\n  return (\n    <p ref={ref} data-slot=\"footer-wordmark\" aria-hidden=\"true\">\n      <span>{children}</span>\n    </p>\n  );\n}\n\nconst isEmail = (value: string) => /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/u.test(value.trim());\n\n/**\n * The page's last word: what the company is, where everything lives, an\n * optional newsletter that confirms in place, the legal line, and the name\n * set as a wordmark across the full width of the band.\n */\nexport function FooterBlock({\n  brand,\n  tagline,\n  columns,\n  newsletter,\n  legal,\n  status,\n  wordmark = true,\n  className,\n}: {\n  brand: string;\n  tagline?: string;\n  columns: readonly BlockFooterColumn[];\n  /** An email sign-up. `onSubmit` may return a promise; the field confirms when it settles. */\n  newsletter?: { label: string; note?: string; onSubmit?: (email: string) => void | Promise<void> };\n  /** e.g. \"© 2026 Halden Ltd · Privacy · Terms\". */\n  legal?: ReactNode;\n  /** A live system status line, e.g. { label: \"All systems normal\", href: \"https://status…\" }. */\n  status?: { label: string; href: string };\n  /** Set the brand as a full-width wordmark at the foot. */\n  wordmark?: boolean;\n  className?: string;\n}) {\n  const id = useId();\n  const [email, setEmail] = useState(\"\");\n  const [state, setState] = useState<\"idle\" | \"invalid\" | \"busy\" | \"done\">(\"idle\");\n\n  async function submit(event: React.FormEvent) {\n    event.preventDefault();\n    if (!isEmail(email)) {\n      setState(\"invalid\");\n      return;\n    }\n    setState(\"busy\");\n    await newsletter?.onSubmit?.(email.trim());\n    setState(\"done\");\n  }\n\n  return (\n    <BlockRoot as=\"footer\" tone=\"sunken\" className={className} innerClassName=\"!pb-0\">\n      <div data-slot=\"footer-block\" className=\"grid gap-[clamp(40px,6cqi,72px)]\">\n        <div className=\"grid gap-12 @[900px]:grid-cols-[minmax(0,1fr)_minmax(0,1.6fr)]\">\n          <div className=\"grid content-start gap-6\">\n            <p data-slot=\"footer-brand\">{brand}</p>\n            {tagline && <p className=\"max-w-[36ch] text-[15px] leading-6 text-[var(--k-muted)]\">{tagline}</p>}\n            {newsletter &&\n              (state === \"done\" ? (\n                <p role=\"status\" className=\"flex items-center gap-2 text-[15px]\">\n                  <Check aria-hidden=\"true\" className=\"size-4 text-[var(--k-hi)]\" />\n                  Check {email.trim()} to confirm.\n                </p>\n              ) : (\n                <form noValidate onSubmit={submit} className=\"grid max-w-[400px] gap-2\">\n                  <label htmlFor={`${id}-email`} className=\"text-sm font-medium\">{newsletter.label}</label>\n                  <div data-slot=\"footer-field\" data-invalid={state === \"invalid\" || undefined}>\n                    <input\n                      id={`${id}-email`}\n                      type=\"email\"\n                      autoComplete=\"email\"\n                      placeholder=\"you@company.com\"\n                      value={email}\n                      aria-invalid={state === \"invalid\" || undefined}\n                      aria-describedby={`${id}-note`}\n                      onChange={(event) => {\n                        setEmail(event.currentTarget.value);\n                        if (state === \"invalid\") setState(\"idle\");\n                      }}\n                    />\n                    <button type=\"submit\" disabled={state === \"busy\"} aria-label={state === \"busy\" ? \"Subscribing\" : \"Subscribe\"}>\n                      <ArrowRight aria-hidden=\"true\" className=\"size-4\" />\n                    </button>\n                  </div>\n                  <p id={`${id}-note`} role={state === \"invalid\" ? \"alert\" : undefined} className={cn(\"min-h-5 text-[13px]\", state === \"invalid\" ? \"text-[var(--k-danger)]\" : \"text-[var(--k-muted)]\")}>\n                    {state === \"invalid\" ? \"Enter an email address like name@company.com.\" : newsletter.note}\n                  </p>\n                </form>\n              ))}\n          </div>\n          <nav aria-label=\"Footer\" className=\"grid grid-cols-2 gap-x-6 gap-y-10 @[640px]:grid-cols-[repeat(var(--cols),minmax(0,1fr))]\" style={{ [\"--cols\" as string]: Math.min(columns.length, 4) }}>\n            {columns.map((column) => (\n              <div key={column.title} className=\"grid content-start gap-4\">\n                <h2 data-slot=\"footer-heading\">{column.title}</h2>\n                <ul className=\"m-0 grid list-none gap-2.5 p-0\">\n                  {column.links.map((link) => (\n                    <li key={link.label}><a href={link.href} data-slot=\"footer-block-link\">{link.label}</a></li>\n                  ))}\n                </ul>\n              </div>\n            ))}\n          </nav>\n        </div>\n        <div className=\"flex flex-wrap items-center justify-between gap-x-8 gap-y-3 border-t border-[var(--k-line-strong)] pt-6 text-[13px] text-[var(--k-muted)]\">\n          {legal && <div>{legal}</div>}\n          {status && (\n            <a href={status.href} className=\"inline-flex items-center gap-2 hover:text-[var(--k-ink)]\">\n              <i aria-hidden=\"true\" data-slot=\"footer-status-dot\" />\n              {status.label}\n            </a>\n          )}\n        </div>\n        {wordmark && (\n          <Wordmark>{brand}</Wordmark>\n        )}\n      </div>\n    </BlockRoot>\n  );\n}\n"},{"path":"ui/lib/cn.ts","type":"registry:ui","target":"components/ui/lib/cn.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\nconst twMerge = extendTailwindMerge({\n  extend: { theme: { radius: [\"control\", \"surface\", \"overlay\"], shadow: [\"overlay\"] } },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"},{"path":"ui/stage-scope.tsx","type":"registry:ui","target":"components/ui/stage-scope.tsx","content":"\"use client\";\n\nimport { createContext, useContext, type ReactNode } from \"react\";\n\n/**\n * True inside a block stage: a frame shown there is a demonstration on a page\n * that already has its own main landmark, so it should not add a second one.\n */\nconst InStage = createContext(false);\n\nexport function StageScope({ children }: { children: ReactNode }) {\n  return <InStage.Provider value={true}>{children}</InStage.Provider>;\n}\n\nexport const useInStage = () => useContext(InStage);\n"},{"path":"ui/template-page.tsx","type":"registry:ui","target":"components/ui/template-page.tsx","content":"\"use client\";\n\nimport { useEffect, useId, useState, type ComponentProps, type ReactNode } from \"react\";\nimport type { BlockAction } from \"./block-concept\";\nimport { BlockNav, type BlockNavLink } from \"./block-nav\";\nimport { FooterBlock } from \"./footer-block\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/skip-link.css\";\nimport \"./template-page.css\";\n\nexport type TemplateFooter = Omit<ComponentProps<typeof FooterBlock>, \"brand\" | \"className\"> & { brand?: string };\n\n/**\n * The frame every Free page template shares: a skip link, the site header,\n * one main region that can take focus, and the footer block. Pages put\n * their bands inside.\n */\nexport function TemplatePage({\n  brand,\n  nav = [],\n  signIn,\n  primary,\n  footer,\n  className,\n  children,\n}: {\n  brand: string;\n  nav?: readonly BlockNavLink[];\n  signIn?: BlockAction;\n  primary?: BlockAction;\n  footer?: TemplateFooter;\n  className?: string;\n  children: ReactNode;\n}) {\n  const id = useId();\n  return (\n    <div data-slot=\"block\" className={cn(\"@container relative\", className)}>\n      <a href={`#${id}-main`} data-slot=\"skip-link\">Skip to content</a>\n      <BlockNav brand={brand} nav={nav} signIn={signIn} primary={primary} />\n      <main id={`${id}-main`} tabIndex={-1} className=\"outline-none\">\n        {children}\n      </main>\n      {footer && <FooterBlock {...footer} brand={footer.brand ?? brand} />}\n    </div>\n  );\n}\n\n/** The nearest element that scrolls, or null for the window. */\nexport function scrollParent(element: HTMLElement | null): HTMLElement | null {\n  for (let node = element?.parentElement; node; node = node.parentElement) {\n    const { overflowY } = getComputedStyle(node);\n    if ((overflowY === \"auto\" || overflowY === \"scroll\") && node.scrollHeight > node.clientHeight) return node;\n  }\n  return null;\n}\n\n/**\n * Which section the reader is in: the last heading that has passed a line\n * just under the sticky header. Works in the window or a scrolling panel.\n */\nexport function useScrollSpy(ids: readonly string[], offset = 120) {\n  const [active, setActive] = useState(ids[0] ?? \"\");\n  const key = ids.join(\"|\");\n  useEffect(() => {\n    const first = ids[0] ? document.getElementById(ids[0]) : null;\n    if (!first) return;\n    const scroller = scrollParent(first);\n    const target: HTMLElement | Window = scroller ?? window;\n    const update = () => {\n      const top = scroller ? scroller.getBoundingClientRect().top : 0;\n      let current = ids[0]!;\n      for (const id of ids) {\n        const element = document.getElementById(id);\n        if (element && element.getBoundingClientRect().top - top <= offset) current = id;\n      }\n      setActive(current);\n    };\n    update();\n    target.addEventListener(\"scroll\", update, { passive: true });\n    window.addEventListener(\"resize\", update);\n    return () => {\n      target.removeEventListener(\"scroll\", update);\n      window.removeEventListener(\"resize\", update);\n    };\n    // ids are compared by value through `key`.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [key, offset]);\n  return active;\n}\n\n/** How far through an element the reader is, 0 to 1. */\nexport function useReadingProgress(element: HTMLElement | null) {\n  const [progress, setProgress] = useState(0);\n  useEffect(() => {\n    if (!element) return;\n    const scroller = scrollParent(element);\n    const target: HTMLElement | Window = scroller ?? window;\n    const update = () => {\n      const rect = element.getBoundingClientRect();\n      const top = scroller ? scroller.getBoundingClientRect().top : 0;\n      const view = scroller ? scroller.clientHeight : window.innerHeight;\n      const travel = rect.height - view;\n      setProgress(travel <= 0 ? 1 : Math.min(1, Math.max(0, (top - rect.top) / travel)));\n    };\n    update();\n    target.addEventListener(\"scroll\", update, { passive: true });\n    window.addEventListener(\"resize\", update);\n    return () => {\n      target.removeEventListener(\"scroll\", update);\n      window.removeEventListener(\"resize\", update);\n    };\n  }, [element]);\n  return progress;\n}\n\n/** Jumps to a section inside whatever scrolls, then moves focus to its heading. */\nexport function goToSection(id: string) {\n  const element = document.getElementById(id);\n  if (!element) return;\n  const smooth = !window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  element.scrollIntoView({ behavior: smooth ? \"smooth\" : \"auto\", block: \"start\" });\n  const heading = element.matches(\"h1, h2, h3\") ? element : element.querySelector<HTMLElement>(\"h2, h3\");\n  heading?.setAttribute(\"tabindex\", \"-1\");\n  heading?.focus({ preventScroll: true });\n}\n\n/**\n * The page outline: numbered when `numbered`, with the reader's section\n * marked as current. On narrow pages it folds into a disclosure.\n */\nexport function TemplateOutline({\n  items,\n  active,\n  label = \"On this page\",\n  numbered = false,\n  className,\n}: {\n  items: readonly { id: string; title: string }[];\n  active: string;\n  label?: string;\n  numbered?: boolean;\n  className?: string;\n}) {\n  const list = (\n    <ol data-slot=\"template-outline-list\" data-numbered={numbered || undefined}>\n      {items.map((item, index) => (\n        <li key={item.id}>\n          <a\n            href={`#${item.id}`}\n            aria-current={active === item.id ? \"location\" : undefined}\n            onClick={(event) => {\n              event.preventDefault();\n              goToSection(item.id);\n              history.replaceState(null, \"\", `#${item.id}`);\n            }}\n          >\n            {numbered && <span data-slot=\"template-outline-number\">{index + 1}.</span>}\n            {item.title}\n          </a>\n        </li>\n      ))}\n    </ol>\n  );\n  return (\n    <nav aria-label={label} data-slot=\"template-outline\" className={className}>\n      <p data-slot=\"template-outline-label\">{label}</p>\n      {list}\n      <details data-slot=\"template-outline-fold\">\n        <summary>{label}</summary>\n        {list}\n      </details>\n    </nav>\n  );\n}\n"},{"path":"ui/avatar.css","type":"registry:file","target":"components/ui/avatar.css","content":"/* avatar.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Avatars: a light glass orb, two soft hues with a specular highlight and a deeper lower edge. Six tones, assigned per\n     person, so a row of people reads as a calm spectrum; dark initials read on every tone at 9:1 or better. */\n  [data-slot=\"avatar\"] {\n    --av-a: #9ec5ff;\n    --av-b: #c4b2ff;\n    color: #161616;\n    background:\n      radial-gradient(circle at 30% 24%, rgb(255 255 255 / 80%), rgb(255 255 255 / 0%) 36%),\n      radial-gradient(circle at 76% 84%, color-mix(in oklab, var(--av-b) 78%, #3b2a6b), transparent 58%),\n      linear-gradient(140deg, var(--av-a) 12%, var(--av-b) 88%);\n    box-shadow: inset 0 0 0 1px rgb(255 255 255 / 34%), inset 0 -4px 8px rgb(0 0 0 / 8%);\n  }\n  [data-slot=\"avatar\"] > span:first-child {\n    position: relative;\n    text-shadow: 0 1px 0 rgb(255 255 255 / 35%);\n  }\n  [data-slot=\"avatar\"] img {\n    border-radius: inherit;\n  }\n  [data-slot=\"avatar\"][data-tone=\"1\"] { --av-a: #a3f0c8; --av-b: #7fd3e6; }\n  [data-slot=\"avatar\"][data-tone=\"2\"] { --av-a: #c9adff; --av-b: #ffb2d8; }\n  [data-slot=\"avatar\"][data-tone=\"3\"] { --av-a: #ffd6a0; --av-b: #ffab86; }\n  [data-slot=\"avatar\"][data-tone=\"4\"] { --av-a: #ffb4a6; --av-b: #ff9ec6; }\n  [data-slot=\"avatar\"][data-tone=\"5\"] { --av-a: #b8c6ff; --av-b: #9fe1f0; }\n  /* Company marks (shape=\"square\"): a solid hue from the name with white initials, 4.7:1 or better on every hue. */\n  [data-slot=\"avatar\"][data-shape=\"square\"] {\n    --mark-h: 255;\n    color: #ffffff;\n    background: oklch(0.52 0.15 var(--mark-h));\n    box-shadow: inset 0 0 0 1px rgb(255 255 255 / 12%);\n  }\n  [data-slot=\"avatar\"][data-shape=\"square\"] > span:first-child { text-shadow: none; }\n  [data-slot=\"avatar\"][data-shape=\"square\"][data-tone=\"1\"] { --mark-h: 295; }\n  [data-slot=\"avatar\"][data-shape=\"square\"][data-tone=\"2\"] { --mark-h: 2; }\n  [data-slot=\"avatar\"][data-shape=\"square\"][data-tone=\"3\"] { --mark-h: 50; }\n  [data-slot=\"avatar\"][data-shape=\"square\"][data-tone=\"4\"] { --mark-h: 150; }\n  [data-slot=\"avatar\"][data-shape=\"square\"][data-tone=\"5\"] { --mark-h: 195; }\n  [data-slot=\"avatar-presence\"] {\n    position: absolute;\n    right: -3px;\n    bottom: -3px;\n    width: 10px;\n    height: 10px;\n    border-radius: 50%;\n    box-shadow: 0 0 0 2px var(--background);\n  }\n  [data-slot=\"avatar-presence\"][data-presence=\"online\"] { background: var(--success); }\n  [data-slot=\"avatar-presence\"][data-presence=\"away\"] { background: linear-gradient(90deg, var(--warning) 50%, var(--background) 50%); box-shadow: 0 0 0 2px var(--background), inset 0 0 0 1.5px var(--warning); }\n  [data-slot=\"avatar-presence\"][data-presence=\"busy\"] { background: radial-gradient(var(--background) 0 0) center / 5px 1.5px no-repeat, var(--danger); }\n  [data-slot=\"avatar-presence\"][data-presence=\"offline\"] { background: var(--background); box-shadow: 0 0 0 2px var(--background), inset 0 0 0 1.5px var(--muted-foreground); }\n}\n"},{"path":"ui/block-concept.css","type":"registry:file","target":"components/ui/block-concept.css","content":"/* block-concept.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* A site header used outside a block still needs the block tokens. */\n  [data-slot=\"block\"] {\n    --k-display-font: var(--font-display-block, var(--font-sans, system-ui, sans-serif));\n    --k-display-weight: 700;\n    --k-display-case: none;\n    --k-display-track: -0.03em;\n    --k-display-stretch: normal;\n    --k-display-lh: 1.04;\n    /* Monochrome, from the system tokens, so blocks follow the page theme. */\n    --k-bg: var(--background, #fff); --k-surface: var(--elevated, #fff); --k-sunken: var(--surface, #f7f7f8); --k-ink: var(--foreground, #18181b); --k-muted: var(--muted-foreground, #5d5d66); --k-line: var(--border, #e4e4e8);\n    --k-line-strong: color-mix(in oklab, var(--border-strong, #8a8a94) 55%, var(--k-line));\n    /* The grey second line of a two-tone title; 3:1 or better as large text in both themes. */\n    --k-faint: color-mix(in oklab, var(--k-muted) 70%, var(--k-bg));\n    --k-accent: var(--k-ink); --k-accent-ink: var(--k-bg); --k-hi: var(--accent-text, var(--k-ink)); --k-hi-soft: var(--accent-soft, var(--k-sunken));\n    --k-focus: var(--brand, #bb4400); --k-danger: var(--danger, #c2272d);\n    --k-stage: var(--surface, #f7f7f8); --k-stage-dot: color-mix(in oklab, var(--k-ink) 14%, var(--k-stage));\n    --k-shadow: none;\n    --k-radius: 12px; --k-radius-lg: 16px; --k-radius-xl: 20px; --k-btn-radius: 999px;\n    --k-tile-1: color-mix(in oklab, var(--k-ink) 5%, var(--k-bg)); --k-tile-2: color-mix(in oklab, var(--k-ink) 8%, var(--k-bg)); --k-tile-3: color-mix(in oklab, var(--k-ink) 11%, var(--k-bg)); --k-tile-4: color-mix(in oklab, var(--k-ink) 14%, var(--k-bg));\n    background: var(--k-bg);\n    color: var(--k-ink);\n    font-family: var(--font-sans, system-ui, sans-serif);\n  }\n  /* Band tones inside a concept: a sunken band, or an inverse ink band. */\n  [data-slot=\"block\"][data-tone=\"sunken\"] { background: var(--k-sunken); }\n\n  [data-slot=\"block-inner\"] {\n    position: relative;\n    z-index: 1;\n    margin-inline: auto;\n    max-inline-size: 1240px;\n    padding-inline: clamp(20px, 5cqi, 64px);\n    padding-block: clamp(64px, 10cqi, 128px);\n  }\n  [data-slot=\"block-inner\"][data-width=\"narrow\"] { max-inline-size: 960px; }\n  [data-slot=\"block-inner\"][data-width=\"wide\"] { max-inline-size: 1440px; }\n\n  /* ── Type ─────────────────────────────────────────── */\n  [data-slot=\"block-eyebrow\"] {\n    display: inline-flex;\n    align-items: center;\n    gap: 8px;\n    margin: 0;\n    font-size: 14px;\n    line-height: 20px;\n    font-weight: 500;\n    color: var(--k-muted);\n  }\n\n  [data-slot=\"block-title\"] {\n    margin: 0;\n    font-family: var(--k-display-font);\n    font-weight: var(--k-display-weight);\n    font-stretch: var(--k-display-stretch);\n    font-variation-settings: \"wdth\" var(--k-display-wdth, 100);\n    text-transform: var(--k-display-case);\n    letter-spacing: var(--k-display-track);\n    font-optical-sizing: auto;\n    text-wrap: balance;\n    color: var(--k-ink);\n  }\n  [data-slot=\"block-title\"][data-size=\"1\"] { font-size: clamp(40px, 5cqi, 64px); line-height: 1.04; font-weight: 700; letter-spacing: -0.048em; }\n  [data-slot=\"block-title\"][data-size=\"2\"] { font-size: clamp(30px, 3.4cqi, 40px); line-height: 1.1; letter-spacing: -0.04em; }\n  [data-slot=\"block-title\"][data-size=\"3\"] { font-size: clamp(19px, 1.7cqi, 22px); line-height: 1.2; font-weight: 600; letter-spacing: -0.015em; }\n  /* Two-tone titles: the emphasised words land in grey. */\n  [data-slot=\"block-em\"] { font-style: normal; color: var(--k-faint); }\n\n  [data-slot=\"block-lead\"] {\n    margin: 0;\n    max-width: 50ch;\n    font-size: clamp(16px, 1.3cqi, 18px);\n    line-height: 1.6;\n    color: var(--k-muted);\n    text-wrap: pretty;\n  }\n\n  /* The shared section opening. */\n  [data-slot=\"block-header\"] {\n    display: grid;\n    gap: 20px 48px;\n    margin-bottom: clamp(36px, 5cqi, 64px);\n  }\n  [data-slot=\"block-header-title\"] { display: grid; gap: 18px; justify-items: start; }\n  [data-slot=\"block-header-aside\"] { display: grid; gap: 16px; align-content: end; justify-items: start; }\n  [data-slot=\"block-header\"][data-align=\"center\"] { justify-items: center; text-align: center; }\n  [data-slot=\"block-header\"][data-align=\"center\"] :is([data-slot=\"block-header-title\"], [data-slot=\"block-header-aside\"]) { justify-items: center; }\n  [data-slot=\"block-header\"][data-align=\"center\"] [data-slot=\"block-title\"] { max-width: 18ch; }\n  @container (min-width: 900px) {\n    [data-slot=\"block-header\"][data-align=\"split\"] { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); align-items: end; }\n    [data-slot=\"block-header\"][data-align=\"split\"] [data-slot=\"block-header-aside\"] { justify-items: start; padding-bottom: 6px; }\n  }\n\n  /* ── Buttons ──────────────────────────────────────── */\n  [data-slot=\"block-button\"] {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    gap: 8px;\n    min-height: 40px;\n    padding: 0 16px;\n    border: 0;\n    border-radius: var(--k-btn-radius);\n    font: 500 14px/1 var(--font-sans, system-ui, sans-serif);\n    letter-spacing: -0.005em;\n    text-decoration: none;\n    white-space: nowrap;\n    cursor: pointer;\n    transition: background-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out), translate var(--duration-normal) var(--ease-settle);\n  }\n  [data-slot=\"block-button\"][data-kind=\"primary\"] {\n    background: var(--k-accent);\n    color: var(--k-accent-ink);\n    box-shadow: inset 0 1px 0 rgb(255 255 255 / 14%), 0 1px 2px rgb(0 0 0 / 20%);\n  }\n  [data-slot=\"block-button\"][data-kind=\"secondary\"] {\n    background: color-mix(in srgb, var(--k-surface) 60%, transparent);\n    color: var(--k-ink);\n    box-shadow: inset 0 0 0 1px var(--k-line-strong);\n  }\n  [data-slot=\"block-button-arrow\"] { transition: translate var(--duration-normal) var(--ease-settle); }\n  @media (hover: hover) {\n    [data-slot=\"block-button\"][data-kind=\"primary\"]:hover { background: color-mix(in srgb, var(--k-accent) 86%, var(--k-bg)); }\n    [data-slot=\"block-button\"][data-kind=\"secondary\"]:hover { background: var(--k-sunken); box-shadow: inset 0 0 0 1px var(--k-ink); }\n    [data-slot=\"block-button\"]:hover [data-slot=\"block-button-arrow\"] { translate: 3px 0; }\n  }\n  [data-slot=\"block-button\"]:active { translate: 0 var(--motion-press-travel); }\n  [data-slot=\"block-button\"]:focus-visible { outline: 2px solid var(--k-focus); outline-offset: 3px; }\n\n  [data-slot=\"block-card\"] {\n    border-radius: var(--k-radius-lg);\n    background: var(--k-surface);\n    box-shadow: 0 0 0 1px var(--k-line);\n  }\n  [data-slot=\"block-chip\"] {\n    display: inline-flex;\n    align-items: center;\n    gap: 6px;\n    min-height: 28px;\n    padding: 0 12px;\n    border-radius: 999px;\n    font-size: 13px;\n    font-weight: 500;\n    background: var(--k-hi-soft);\n    color: var(--k-ink);\n    box-shadow: inset 0 0 0 1px var(--k-line);\n    text-decoration: none;\n  }\n  /* People in blocks are the library's light glass orbs (as Avatar), one pastel tone per person. */\n  [data-slot=\"block-initials\"] {\n    --av-a: #9ec5ff;\n    --av-b: #c4b2ff;\n    display: inline-grid;\n    flex: none;\n    place-items: center;\n    border-radius: 999px;\n    font-weight: 600;\n    color: #161616;\n    background:\n      radial-gradient(circle at 30% 24%, rgb(255 255 255 / 80%), rgb(255 255 255 / 0%) 36%),\n      radial-gradient(circle at 76% 84%, color-mix(in oklab, var(--av-b) 78%, #3b2a6b), transparent 58%),\n      linear-gradient(140deg, var(--av-a) 12%, var(--av-b) 88%);\n    box-shadow: inset 0 0 0 1px rgb(255 255 255 / 34%), inset 0 -4px 8px rgb(0 0 0 / 8%);\n  }\n  [data-slot=\"block-initials\"][data-tone=\"1\"] { --av-a: #a3f0c8; --av-b: #7fd3e6; }\n  [data-slot=\"block-initials\"][data-tone=\"2\"] { --av-a: #c9adff; --av-b: #ffb2d8; }\n  [data-slot=\"block-initials\"][data-tone=\"3\"] { --av-a: #ffd6a0; --av-b: #ffab86; }\n\n  /* ── Stage: a product artefact framed on a tonal panel ─ */\n  [data-slot=\"block-stage\"] {\n    position: relative;\n    padding: clamp(14px, 4.5cqi, 64px);\n    padding-bottom: 0;\n    border-radius: var(--k-radius-xl);\n    background-color: var(--k-stage);\n    background-image: radial-gradient(var(--k-stage-dot) 1px, transparent 1.2px);\n    background-size: 16px 16px;\n    box-shadow: inset 0 0 0 1px var(--k-line);\n    overflow: hidden;\n  }\n  [data-slot=\"block-window\"] {\n    position: relative;\n    overflow: hidden;\n    border-radius: var(--k-radius-lg) var(--k-radius-lg) 0 0;\n    background: var(--k-surface);\n    box-shadow: 0 0 0 1px var(--k-line), var(--k-shadow);\n  }\n  [data-slot=\"block-window-bar\"] {\n    display: flex; align-items: center; gap: 12px;\n    height: 40px; padding: 0 16px;\n    border-bottom: 1px solid var(--k-line);\n    background: color-mix(in srgb, var(--k-sunken) 55%, var(--k-surface));\n  }\n  [data-slot=\"block-window-dots\"] { display: none; }\n  [data-slot=\"block-window-dots\"] i { width: 10px; height: 10px; border-radius: 999px; background: var(--k-line-strong); }\n  [data-slot=\"block-window-title\"] {\n    flex: 1 1 0; min-width: 0; width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n    text-align: left;\n    font-family: var(--font-mono, monospace); font-size: 12px; color: var(--k-muted);\n  }\n  [data-slot=\"block-window-body\"] { padding: clamp(14px, 2.6cqi, 28px); }\n  [data-slot=\"block-float\"] {\n    position: relative;\n    z-index: 2;\n    margin: 12px 0 14px;\n  }\n  @container (min-width: 760px) {\n    /* Over the window bar's empty right end, so it never hides a row. */\n    [data-slot=\"block-float\"] {\n      position: absolute;\n      top: clamp(12px, 2.4cqi, 36px);\n      right: clamp(12px, 2.4cqi, 36px);\n      margin: 0;\n      max-width: 320px;\n    }\n  }\n  [data-slot=\"block-float\"] > * {\n    border-radius: var(--k-radius);\n    background: var(--k-surface);\n    box-shadow: 0 0 0 1px var(--k-line), var(--k-shadow);\n  }\n\n  /* ── Logos ────────────────────────────────────────── */\n  [data-slot=\"block-logos\"] { display: grid; gap: 18px; }\n  [data-slot=\"block-logos-label\"] { margin: 0; font-size: 13px; color: var(--k-muted); }\n  [data-slot=\"block-logos\"] ul { display: flex; flex-wrap: wrap; align-items: center; gap: 14px clamp(24px, 4.5cqi, 56px); margin: 0; padding: 0; list-style: none; }\n  [data-slot=\"block-logos\"] li {\n    font-family: var(--k-display-font);\n    font-weight: var(--k-display-weight);\n    font-stretch: var(--k-display-stretch);\n    text-transform: var(--k-display-case);\n    font-size: clamp(18px, 2cqi, 24px);\n    letter-spacing: calc(var(--k-display-track) / 2);\n    color: var(--k-muted);\n    white-space: nowrap;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"block-button\"], [data-slot=\"block-button-arrow\"] { transition: none; }\n    [data-slot=\"block-button\"]:active { translate: none; }\n    [data-slot=\"block-button\"]:hover [data-slot=\"block-button-arrow\"] { translate: none; }\n  }\n  @media print {\n    footer[data-slot=\"block\"] { display: none !important; }\n    [data-slot=\"block\"] { background: #fff; }\n  }\n}\n"},{"path":"ui/shared/site-header.css","type":"registry:file","target":"components/ui/shared/site-header.css","content":"/* Shared by block-nav.tsx, site-header.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"site-header\"] {\n    background: color-mix(in srgb, var(--background) 92%, transparent);\n    border-bottom: 1px solid transparent;\n    transition: border-color var(--duration-normal) var(--ease-out);\n  }\n  [data-slot=\"site-header\"][data-sticky=\"true\"] {\n    position: sticky;\n    top: 0;\n    -webkit-backdrop-filter: blur(10px);\n    backdrop-filter: blur(10px);\n  }\n  [data-slot=\"site-header\"][data-scrolled=\"true\"] {\n    border-bottom-color: var(--border);\n  }\n  [data-slot=\"site-menu\"] {\n    margin: 0;\n    padding: 0;\n    list-style: none;\n  }\n  [data-slot=\"site-menu\"] a {\n    display: grid;\n    gap: 0.125rem;\n    padding: 0.5rem 0.75rem;\n    border-radius: var(--radius-control);\n    text-decoration: none;\n    color: inherit;\n    outline: none;\n    transition: background-color var(--duration-fast) var(--ease-out);\n  }\n  [data-slot=\"site-menu\"] a:hover,\n  [data-slot=\"site-menu\"] a:focus-visible {\n    background: var(--muted);\n  }\n  [data-slot=\"site-menu\"] a:focus-visible {\n    outline: 2px solid var(--ring);\n    outline-offset: -2px;\n  }\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"site-header\"] {\n      transition: none;\n    }\n  }\n  /* A site header used outside a block still needs the block tokens. */\n  [data-slot=\"site-header\"] {\n    --k-display-font: var(--font-display-block, var(--font-sans, system-ui, sans-serif));\n    --k-display-weight: 700;\n    --k-display-case: none;\n    --k-display-track: -0.03em;\n    --k-display-stretch: normal;\n    --k-display-lh: 1.04;\n    /* Monochrome, from the system tokens, so blocks follow the page theme. */\n    --k-bg: var(--background, #fff); --k-surface: var(--elevated, #fff); --k-sunken: var(--surface, #f7f7f8); --k-ink: var(--foreground, #18181b); --k-muted: var(--muted-foreground, #5d5d66); --k-line: var(--border, #e4e4e8);\n    --k-line-strong: color-mix(in oklab, var(--border-strong, #8a8a94) 55%, var(--k-line));\n    /* The grey second line of a two-tone title; 3:1 or better as large text in both themes. */\n    --k-faint: color-mix(in oklab, var(--k-muted) 70%, var(--k-bg));\n    --k-accent: var(--k-ink); --k-accent-ink: var(--k-bg); --k-hi: var(--accent-text, var(--k-ink)); --k-hi-soft: var(--accent-soft, var(--k-sunken));\n    --k-focus: var(--brand, #bb4400); --k-danger: var(--danger, #c2272d);\n    --k-stage: var(--surface, #f7f7f8); --k-stage-dot: color-mix(in oklab, var(--k-ink) 14%, var(--k-stage));\n    --k-shadow: none;\n    --k-radius: 12px; --k-radius-lg: 16px; --k-radius-xl: 20px; --k-btn-radius: 999px;\n    --k-tile-1: color-mix(in oklab, var(--k-ink) 5%, var(--k-bg)); --k-tile-2: color-mix(in oklab, var(--k-ink) 8%, var(--k-bg)); --k-tile-3: color-mix(in oklab, var(--k-ink) 11%, var(--k-bg)); --k-tile-4: color-mix(in oklab, var(--k-ink) 14%, var(--k-bg));\n    background: var(--k-bg);\n    color: var(--k-ink);\n    font-family: var(--font-sans, system-ui, sans-serif);\n  }\n\n  /* Landing: header gains a hairline once scrolled; phone action bar. */\n  [data-slot=\"site-header\"] { background: var(--k-bg); transition: box-shadow var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out); }\n  [data-slot=\"site-header\"][data-scrolled] {\n    background: color-mix(in srgb, var(--k-bg) 84%, transparent);\n    -webkit-backdrop-filter: blur(var(--blur-overlay, 20px)) saturate(1.2); backdrop-filter: blur(var(--blur-overlay, 20px)) saturate(1.2);\n    box-shadow: 0 1px 0 var(--k-line);\n  }\n  [data-slot=\"site-menu\"][hidden] { display: none; }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"site-header\"] { transition: none; }\n  }\n  @media print {\n    [data-slot=\"site-header\"] { display: none !important; }\n  }\n}\n"},{"path":"ui/block-nav.css","type":"registry:file","target":"components/ui/block-nav.css","content":"/* block-nav.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* ── Site nav (shared by page templates) ─────────── */\n  [data-slot=\"block-nav-row\"] {\n    display: flex; align-items: center; gap: 28px;\n    max-inline-size: 1240px; height: 68px; margin-inline: auto;\n    padding-inline: clamp(20px, 5cqi, 64px);\n  }\n  [data-slot=\"block-nav-brand\"] {\n    font-family: var(--k-display-font); font-weight: var(--k-display-weight); font-stretch: var(--k-display-stretch);\n    font-variation-settings: \"wdth\" var(--k-display-wdth, 100);\n    text-transform: var(--k-display-case); letter-spacing: calc(var(--k-display-track) / 2);\n    font-size: 22px; line-height: 1; color: var(--k-ink); text-decoration: none; white-space: nowrap;\n  }\n  [data-slot=\"block-nav-brand\"]:focus-visible, [data-slot=\"block-nav-link\"]:focus-visible { outline: 2px solid var(--k-focus); outline-offset: 4px; border-radius: 4px; }\n  [data-slot=\"block-nav-list\"] { display: flex; gap: 4px; margin: 0; padding: 0; list-style: none; }\n  [data-slot=\"block-nav-link\"] {\n    display: inline-flex; align-items: center; height: 36px; padding: 0 12px; border-radius: var(--k-btn-radius);\n    font-size: 14px; color: var(--k-muted); text-decoration: none;\n    transition: color var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out);\n  }\n  [data-slot=\"block-nav-link\"]:hover, [data-slot=\"block-nav-link\"][aria-current=\"page\"] { color: var(--k-ink); background: color-mix(in srgb, var(--k-ink) 6%, transparent); }\n  [data-slot=\"site-header\"] [data-slot=\"block-button\"] { min-height: 40px; padding-inline: 16px; font-size: 14px; }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"block-nav-link\"] { transition: none; }\n  }\n  [data-slot=\"site-menu\"] [data-slot=\"block-button\"] { justify-content: center; }\n}\n"},{"path":"ui/shared/article-prose.css","type":"registry:file","target":"components/ui/shared/article-prose.css","content":"/* Shared by about-template.tsx, blog-post-template.tsx, legal-template.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Outline beside the article; a disclosure on narrow pages. */\n  [data-slot=\"article-layout\"] { display: grid; gap: 32px; }\n  @container (min-width: 960px) { [data-slot=\"article-layout\"] { grid-template-columns: 220px minmax(0, 1fr); gap: 64px; } }\n\n  /* Article prose at a reading measure. */\n  [data-slot=\"article-body\"] { display: grid; gap: clamp(36px, 5cqi, 56px); min-width: 0; max-width: 68ch; }\n  [data-slot=\"article-body\"] > section { display: grid; gap: 16px; scroll-margin-top: 96px; }\n  [data-slot=\"article-heading\"] {\n    position: relative; display: flex; align-items: baseline; gap: 10px; margin: 0;\n    font-family: var(--k-display-font); font-weight: var(--k-display-weight); font-size: clamp(26px, 3cqi, 34px); line-height: 1.12; letter-spacing: calc(var(--k-display-track) / 2);\n  }\n  [data-slot=\"article-heading\"]:focus { outline: none; }\n  [data-slot=\"article-anchor\"] { display: inline-grid; place-items: center; flex: none; width: 28px; height: 28px; border-radius: 8px; color: var(--k-muted); opacity: 0; transition: opacity var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out); }\n  [data-slot=\"article-anchor\"] svg { width: 16px; height: 16px; }\n  [data-slot=\"article-heading\"]:hover [data-slot=\"article-anchor\"], [data-slot=\"article-anchor\"]:focus-visible { opacity: 1; }\n  [data-slot=\"article-anchor\"]:hover { background: var(--k-sunken); color: var(--k-ink); }\n  [data-slot=\"article-anchor\"]:focus-visible { outline: 2px solid var(--k-focus, var(--k-hi)); outline-offset: 2px; }\n  @media (hover: none) { [data-slot=\"article-anchor\"] { opacity: 0.6; } }\n  [data-slot=\"article-prose\"] { display: grid; gap: 16px; font-size: 17px; line-height: 1.7; color: color-mix(in srgb, var(--k-ink) 86%, var(--k-muted)); }\n  [data-slot=\"article-prose\"] p, [data-slot=\"article-prose\"] ul, [data-slot=\"article-prose\"] ol, [data-slot=\"article-prose\"] blockquote, [data-slot=\"article-prose\"] figure { margin: 0; }\n  [data-slot=\"article-prose\"] ul, [data-slot=\"article-prose\"] ol { display: grid; gap: 8px; padding-left: 1.25em; }\n  [data-slot=\"article-prose\"] ul { list-style: disc; }\n  [data-slot=\"article-prose\"] ol { list-style: decimal; }\n  [data-slot=\"article-prose\"] li::marker { color: var(--k-hi); }\n  [data-slot=\"article-prose\"] a { color: var(--k-ink); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--k-hi) 60%, transparent); text-underline-offset: 3px; }\n  [data-slot=\"article-prose\"] blockquote { padding-left: 20px; border-left: 2px solid var(--k-hi); font-family: var(--k-display-font); font-size: clamp(20px, 2.2cqi, 24px); line-height: 1.4; color: var(--k-ink); }\n  [data-slot=\"article-prose\"] code { padding: 1px 6px; border-radius: 8px; background: var(--k-sunken); font-family: var(--font-mono, monospace); font-size: 0.86em; }\n  [data-slot=\"article-prose\"] strong { font-weight: 600; color: var(--k-ink); }\n  @media print {\n    [data-slot=\"article-anchor\"] { display: none !important; }\n    [data-slot=\"article-layout\"] { display: block; }\n  }\n}\n"},{"path":"ui/blog-post-template.css","type":"registry:file","target":"components/ui/blog-post-template.css","content":"/* blog-post-template.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Reading progress: a 2px ember line under the sticky header. */\n  [data-slot=\"reading-progress\"] { position: sticky; top: 68px; z-index: 19; height: 2px; margin-bottom: -2px; pointer-events: none; }\n  [data-slot=\"reading-progress\"] > span { display: block; height: 100%; background: var(--k-hi); transform-origin: left center; }\n  [data-slot=\"article-author\"] { display: flex; align-items: flex-start; gap: 16px; padding-top: 28px; border-top: 1px solid var(--k-line-strong); }\n  [data-slot=\"article-next\"] {\n    display: inline-flex; align-items: center; justify-self: start; gap: 10px; min-height: 48px; padding: 0 20px; border-radius: var(--k-btn-radius);\n    background: var(--k-accent); color: var(--k-accent-ink); font-size: 15px; font-weight: 500; text-decoration: none;\n  }\n  [data-slot=\"article-next\"] svg { width: 16px; height: 16px; transition: translate var(--duration-normal) var(--ease-out); }\n  [data-slot=\"article-next\"]:hover svg { translate: 3px 0; }\n  [data-slot=\"article-next\"]:focus-visible { outline: 2px solid var(--k-focus, var(--k-hi)); outline-offset: 3px; }\n  [data-slot=\"article-related\"] { display: grid; gap: 16px; margin: 0; padding: 0; list-style: none; }\n  @container (min-width: 760px) { [data-slot=\"article-related\"] { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; } }\n  [data-slot=\"article-related\"] a {\n    display: grid; align-content: start; gap: 10px; height: 100%; padding: 22px; border-radius: var(--k-radius-lg); background: var(--k-surface); box-shadow: 0 0 0 1px var(--k-line);\n    color: inherit; text-decoration: none; transition: box-shadow var(--duration-fast) var(--ease-out), translate var(--duration-normal) var(--ease-out);\n  }\n  [data-slot=\"article-related\"] a:hover { box-shadow: 0 0 0 1px var(--k-line-strong), var(--k-shadow); translate: 0 -2px; }\n  [data-slot=\"article-related\"] a:focus-visible { outline: 2px solid var(--k-focus, var(--k-hi)); outline-offset: 3px; }\n  [data-slot=\"article-related-title\"] { font-family: var(--k-display-font); font-size: 22px; line-height: 1.2; letter-spacing: calc(var(--k-display-track) / 2); }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"article-related\"] a:hover { translate: none; }\n  }\n}\n"},{"path":"ui/shared/loading-indicator.css","type":"registry:file","target":"components/ui/shared/loading-indicator.css","content":"/* Shared by button.tsx, confirm-morph.tsx, expanding-button-group.tsx, field.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"loading-indicator\"] {\n    animation: caliper-spin var(--duration-loading) var(--ease-linear) infinite;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"loading-indicator\"] {\n      animation: none;\n    }\n  }\n}\n"},{"path":"ui/button.css","type":"registry:file","target":"components/ui/button.css","content":"/* button.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"button\"] {\n    transition-property: background-color, color, border-color;\n    transition-duration: var(--duration-fast);\n    transition-timing-function: var(--ease-out);\n  }\n\n  /* Button material. Raised to press: a lit top edge and a small lift; the\n     press travels 1px down and swaps the lift for an inner shade. Only\n     translate animates (release on the settle curve); shadows swap. */\n  [data-slot=\"button\"] {\n    --b-bg: var(--raised);\n    --b-bg-hover: var(--raised-hover);\n    --b-bg-press: var(--raised-press);\n    --b-fg: var(--foreground);\n    --b-shadow: var(--shadow-raised);\n    --b-shadow-press: var(--shadow-raised-press);\n    background-color: var(--b-bg);\n    color: var(--b-fg);\n    box-shadow: var(--b-shadow);\n    transition-property: background-color, color, border-color, translate, scale;\n    transition-duration:\n      var(--duration-fast), var(--duration-fast), var(--duration-fast),\n      var(--duration-normal), var(--duration-normal);\n    transition-timing-function:\n      var(--ease-out), var(--ease-out), var(--ease-out), var(--ease-settle),\n      var(--ease-settle);\n  }\n  [data-slot=\"button\"][data-variant=\"default\"] {\n    --b-bg: var(--primary);\n    --b-bg-hover: var(--primary-hover);\n    --b-bg-press: var(--primary-press);\n    --b-fg: var(--primary-foreground);\n    --b-shadow: var(--shadow-accent);\n    --b-shadow-press: var(--shadow-accent-press);\n  }\n  [data-slot=\"button\"][data-variant=\"destructive\"] {\n    --b-bg: var(--danger-fill);\n    --b-bg-hover: var(--danger-fill-hover);\n    --b-bg-press: var(--danger-fill-press);\n    --b-fg: var(--danger-fill-foreground);\n    --b-shadow: var(--shadow-danger);\n    --b-shadow-press: var(--shadow-danger-press);\n  }\n  [data-slot=\"button\"][data-variant=\"ghost\"] {\n    --b-bg: transparent;\n    --b-bg-hover: var(--muted);\n    --b-bg-press: var(--muted);\n    --b-fg: var(--muted-foreground);\n    --b-shadow: none;\n    --b-shadow-press: none;\n  }\n  @media (hover: hover) {\n    [data-slot=\"button\"]:hover {\n      background-color: var(--b-bg-hover);\n    }\n    [data-slot=\"button\"][data-variant=\"ghost\"]:hover {\n      color: var(--foreground);\n    }\n  }\n  [data-slot=\"button\"]:active:not(:disabled):not([aria-disabled=\"true\"]):not(\n      [aria-busy=\"true\"]\n    ) {\n    translate: 0 var(--motion-press-travel);\n    scale: var(--motion-press-scale);\n    background-color: var(--b-bg-press);\n    box-shadow: var(--b-shadow-press);\n    transition-duration:\n      var(--duration-fast), var(--duration-fast), var(--duration-fast), 90ms,\n      90ms;\n    transition-timing-function: var(--ease-out);\n  }\n  [data-slot=\"button\"]:is(:disabled, [aria-disabled=\"true\"]):not([aria-busy=\"true\"]) {\n    background-color: var(--sunken);\n    color: var(--muted-foreground);\n  }\n  [data-slot=\"button\"][data-variant=\"ghost\"]:is(:disabled, [aria-disabled=\"true\"]):not(\n      [aria-busy=\"true\"]\n    ) {\n    background-color: transparent;\n    box-shadow: none;\n  }\n  /* Loading keeps the exact width: the label stays in flow but invisible. */\n  [data-slot=\"button-label\"] {\n    display: inline-flex;\n    align-items: center;\n    gap: inherit;\n  }\n  /* Opacity, not visibility, so the label still names the button. */\n  [data-slot=\"button-label\"][data-hidden=\"true\"] {\n    opacity: 0;\n  }\n  [data-slot=\"button-busy\"] {\n    position: absolute;\n    inset: 0;\n    display: grid;\n    place-items: center;\n  }\n  [data-slot=\"button-feedback\"] {\n    display: inline-flex;\n    align-items: center;\n    gap: inherit;\n    overflow: clip;\n    transition: width var(--duration-normal) var(--ease-out);\n  }\n  [data-slot=\"button-feedback-label\"] {\n    display: inline-flex;\n    flex-shrink: 0;\n    align-items: center;\n    gap: inherit;\n    width: max-content;\n  }\n  [data-slot=\"button-feedback-label\"][data-enter=\"true\"] {\n    animation: caliper-feedback-enter var(--duration-normal) var(--ease-out);\n  }\n\n  [data-slot=\"button\"][aria-busy=\"true\"] {\n    pointer-events: none;\n  }\n  [data-slot=\"button\"][data-action-phase=\"error\"] {\n    color: var(--danger);\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"button\"],\n    [data-slot=\"button-feedback\"] {\n      transition: none;\n    }\n    [data-slot=\"button\"]:active:not(:disabled):not([aria-disabled=\"true\"]):not(\n        [aria-busy=\"true\"]\n      ) {\n      translate: none;\n    }\n    [data-slot=\"button-feedback-label\"],\n    [data-slot=\"button-feedback-label\"][data-enter=\"true\"] {\n      animation: none;\n    }\n  }\n\n  /* Toggle: pressed is recessed, as a toggled tool in a button group is. */\n  /* Four attributes, so it wins over the group and toolbar pressed rules in any stylesheet order. */\n  [data-slot=\"button\"][data-variant=\"ghost\"][data-toggle][aria-pressed=\"true\"] {\n    --b-bg: var(--raised-press);\n    --b-bg-hover: var(--raised-press);\n    --b-bg-press: var(--raised-press);\n    --b-fg: var(--foreground);\n    --b-shadow: var(--shadow-sunken);\n    --b-shadow-press: var(--shadow-sunken);\n  }\n}\n"},{"path":"ui/shared/action-label.css","type":"registry:file","target":"components/ui/shared/action-label.css","content":"/* Shared by action-button.tsx, copy-button.tsx, hold-to-confirm.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Action button: every phase's label shares one grid cell, so the button\n     is always as wide as its widest label and never shifts. */\n  [data-slot=\"action-labels\"] {\n    display: inline-grid;\n    justify-items: center;\n  }\n  [data-slot=\"action-label\"] {\n    grid-area: 1 / 1;\n    display: inline-flex;\n    align-items: center;\n    gap: 8px;\n    visibility: hidden;\n    opacity: 0;\n    transition: opacity var(--duration-fast) var(--ease-out), visibility 0s linear var(--duration-fast);\n  }\n  [data-slot=\"action-label\"][data-active=\"true\"] {\n    visibility: visible;\n    opacity: 1;\n    transition: opacity var(--duration-fast) var(--ease-out), visibility 0s;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"action-label\"],\n    [data-slot=\"action-label\"][data-active=\"true\"] {\n      transition: none !important;\n    }\n  }\n}\n"},{"path":"ui/copy-button.css","type":"registry:file","target":"components/ui/copy-button.css","content":"/* copy-button.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Copy button: the icon swaps in place; a small inverse ticket says what\n     was copied. A failure keeps the ticket open with the value selected. */\n  [data-slot=\"copy-icon\"] {\n    position: relative;\n    display: inline-grid;\n    width: 16px;\n    height: 16px;\n  }\n  [data-slot=\"copy-icon\"] svg {\n    grid-area: 1 / 1;\n    transition:\n      opacity var(--duration-fast) var(--ease-out),\n      scale var(--duration-normal) var(--ease-settle);\n  }\n  [data-slot=\"copy-icon\"] svg:last-child,\n  [data-slot=\"copy-icon\"][data-copied=\"true\"] svg:first-child {\n    opacity: 0;\n    scale: 0.6;\n  }\n  [data-slot=\"copy-icon\"][data-copied=\"true\"] svg:last-child {\n    opacity: 1;\n    scale: 1;\n  }\n  [data-slot=\"copy-ticket\"] {\n    position: absolute;\n    right: 0;\n    bottom: calc(100% + 8px);\n    z-index: 30;\n    pointer-events: none;\n  }\n  [data-slot=\"copy-ticket-body\"] {\n    display: inline-flex;\n    align-items: center;\n    gap: 8px;\n    padding: 6px 10px;\n    border-radius: 8px;\n    background: var(--primary);\n    color: var(--primary-foreground);\n    font-size: 12px;\n    line-height: 16px;\n    font-weight: 500;\n    white-space: nowrap;\n    box-shadow: var(--shadow-feedback);\n    animation: caliper-ticket-in var(--duration-normal) var(--ease-out);\n  }\n  [data-slot=\"copy-ticket-body\"] svg {\n    width: 14px;\n    height: 14px;\n  }\n  [data-slot=\"copy-ticket-body\"][data-tone=\"error\"] {\n    display: grid;\n    width: min(320px, calc(100vw - 32px));\n    gap: 8px;\n    padding: 10px;\n    white-space: normal;\n    background: var(--elevated);\n    color: var(--danger);\n    box-shadow: var(--shadow-overlay), inset 0 0 0 1px var(--border);\n    pointer-events: auto;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"copy-icon\"] svg {\n      transition: none;\n    }\n    [data-slot=\"copy-ticket-body\"] {\n      animation: none;\n    }\n  }\n  @keyframes caliper-ticket-in {\n    from {\n      opacity: 0;\n      translate: 0 4px;\n    }\n  }\n}\n"},{"path":"ui/footer-block.css","type":"registry:file","target":"components/ui/footer-block.css","content":"/* footer-block.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* ── Footer ───────────────────────────────────────── */\n  [data-slot=\"footer-brand\"] {\n    margin: 0; font-family: var(--k-display-font); font-weight: var(--k-display-weight); font-stretch: var(--k-display-stretch);\n    font-variation-settings: \"wdth\" var(--k-display-wdth, 100); text-transform: var(--k-display-case);\n    letter-spacing: calc(var(--k-display-track) / 2); font-size: 28px; line-height: 1;\n  }\n  /* A label, not code: Geist at the label step (DESIGN-SYSTEM.md keeps mono for code, commands and keycaps). */\n  [data-slot=\"footer-heading\"] { margin: 0; font-size: 13px; line-height: 18px; font-weight: 500; color: var(--k-muted); }\n  [data-slot=\"footer-block-link\"] { font-size: 15px; color: var(--k-ink); text-decoration: none; text-underline-offset: 4px; }\n  [data-slot=\"footer-block-link\"]:hover { text-decoration: underline; text-decoration-color: var(--k-hi); }\n  [data-slot=\"footer-block-link\"]:focus-visible { outline: 2px solid var(--k-focus); outline-offset: 3px; border-radius: 3px; }\n  [data-slot=\"footer-field\"] {\n    display: flex; align-items: center; gap: 6px; height: 50px; padding: 5px 5px 5px 16px;\n    border-radius: var(--k-btn-radius); background: var(--k-surface); box-shadow: inset 0 0 0 1px var(--k-line-strong);\n    transition: box-shadow var(--duration-fast) var(--ease-out);\n  }\n  [data-slot=\"footer-field\"]:focus-within { box-shadow: inset 0 0 0 2px var(--k-focus); }\n  [data-slot=\"footer-field\"][data-invalid] { box-shadow: inset 0 0 0 2px var(--k-danger); }\n  [data-slot=\"footer-field\"] input { flex: 1; min-width: 0; border: 0; background: transparent; color: var(--k-ink); font-size: 15px; outline: none; }\n  [data-slot=\"footer-field\"] input::placeholder { color: var(--k-muted); }\n  [data-slot=\"footer-field\"] button {\n    display: grid; place-items: center; width: 40px; height: 40px; flex: none; border-radius: calc(var(--k-btn-radius) - 4px);\n    background: var(--k-accent); color: var(--k-accent-ink); cursor: pointer;\n  }\n  [data-slot=\"footer-field\"] button:focus-visible { outline: 2px solid var(--k-focus); outline-offset: 2px; }\n  [data-slot=\"footer-field\"] button:disabled { opacity: 0.6; cursor: progress; }\n  [data-slot=\"footer-status-dot\"] { width: 8px; height: 8px; border-radius: 999px; background: var(--success, #0b7643); box-shadow: 0 0 0 3px color-mix(in srgb, var(--success, #0b7643) 22%, transparent); }\n  [data-slot=\"footer-wordmark\"] {\n    margin: 0 0 -0.16em; overflow: hidden;\n    font-family: var(--k-display-font); font-weight: var(--k-display-weight); font-stretch: var(--k-display-stretch);\n    font-variation-settings: \"wdth\" var(--k-display-wdth, 100); text-transform: var(--k-display-case);\n    font-size: var(--wordmark-size, clamp(72px, 21cqi, 300px)); line-height: 0.82; letter-spacing: -0.06em; white-space: nowrap;\n    color: var(--k-ink); user-select: none;\n  }\n  [data-slot=\"footer-wordmark\"] > span { display: inline-block; padding-right: 0.04em; }\n  [data-slot=\"footer-wordmark\"] { font-weight: 700; }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"footer-field\"] { transition: none; }\n  }\n}\n"},{"path":"ui/shared/skip-link.css","type":"registry:file","target":"components/ui/shared/skip-link.css","content":"/* Shared by changelog-template.tsx, docs-template.tsx, landing-template.tsx, pricing-template.tsx, site-header.tsx, template-page.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"skip-link\"] {\n    position: absolute;\n    top: 0.5rem;\n    left: 0.5rem;\n    z-index: 50;\n    padding: 0.5rem 0.75rem;\n    border-radius: var(--radius-control);\n    background: var(--foreground);\n    color: var(--background);\n    font-size: 0.875rem;\n    transform: translateY(-200%);\n  }\n  [data-slot=\"skip-link\"]:focus {\n    transform: none;\n    outline: 2px solid var(--ring);\n    outline-offset: 2px;\n  }\n  [data-slot=\"skip-link\"] {\n    position: absolute; left: 12px; top: -48px; z-index: 50; padding: 8px 12px; border-radius: 12px;\n    background: var(--foreground); color: var(--background); font-size: 13px; text-decoration: none;\n  }\n  [data-slot=\"skip-link\"]:focus-visible { top: 12px; outline: 2px solid var(--ring); outline-offset: 2px; }\n  @media print {\n    [data-slot=\"skip-link\"] { display: none !important; }\n  }\n}\n"},{"path":"ui/template-page.css","type":"registry:file","target":"components/ui/template-page.css","content":"/* template-page.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"template-outline\"] { min-width: 0; }\n  [data-slot=\"template-outline-label\"] { display: none; margin: 0 0 12px; font-family: var(--font-mono, monospace); font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--k-muted); }\n  [data-slot=\"template-outline\"] > [data-slot=\"template-outline-list\"] { display: none; }\n  @container (min-width: 960px) {\n    [data-slot=\"template-outline\"] { position: sticky; top: 96px; align-self: start; }\n    [data-slot=\"template-outline-label\"], [data-slot=\"template-outline\"] > [data-slot=\"template-outline-list\"] { display: block; }\n    [data-slot=\"template-outline-fold\"] { display: none; }\n  }\n  [data-slot=\"template-outline-list\"] { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; }\n  [data-slot=\"template-outline-list\"] a {\n    position: relative; display: flex; gap: 8px; padding: 6px 0 6px 14px; font-size: 14px; line-height: 20px; color: var(--k-muted); text-decoration: none;\n    transition: color var(--duration-fast) var(--ease-out);\n  }\n  [data-slot=\"template-outline-list\"] a::before { content: \"\"; position: absolute; left: 0; top: 6px; bottom: 6px; width: 2px; border-radius: 2px; background: var(--k-line); transition: background-color var(--duration-normal) var(--ease-out); }\n  [data-slot=\"template-outline-list\"] a:hover { color: var(--k-ink); }\n  [data-slot=\"template-outline-list\"] a[aria-current] { color: var(--k-ink); }\n  [data-slot=\"template-outline-list\"] a[aria-current]::before { background: var(--k-hi); }\n  [data-slot=\"template-outline-list\"] a:focus-visible { outline: 2px solid var(--k-focus, var(--k-hi)); outline-offset: 2px; border-radius: 4px; }\n  [data-slot=\"template-outline-number\"] { flex: none; min-width: 1.6em; font-variant-numeric: tabular-nums; }\n  [data-slot=\"template-outline-fold\"] { border-radius: var(--k-radius); background: var(--k-surface); box-shadow: 0 0 0 1px var(--k-line); }\n  [data-slot=\"template-outline-fold\"] > summary { display: flex; align-items: center; justify-content: space-between; min-height: 48px; padding: 0 16px; font-size: 14px; font-weight: 500; cursor: pointer; list-style: none; }\n  [data-slot=\"template-outline-fold\"] > summary::-webkit-details-marker { display: none; }\n  [data-slot=\"template-outline-fold\"] > summary::after { content: \"+\"; font-size: 18px; color: var(--k-muted); }\n  [data-slot=\"template-outline-fold\"][open] > summary::after { content: \"–\"; }\n  [data-slot=\"template-outline-fold\"] > summary:focus-visible { outline: 2px solid var(--k-focus, var(--k-hi)); outline-offset: 2px; border-radius: var(--k-radius); }\n  [data-slot=\"template-outline-fold\"] > [data-slot=\"template-outline-list\"] { padding: 0 16px 12px; }\n  @media print {\n    [data-slot=\"template-outline\"] { display: none !important; }\n  }\n}\n"}],"categories":["free-pages"],"meta":{"tier":"free","license":"MIT","family":"reading-proof","docs":"/docs/components/blog-post-template"}}