{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"error-template","type":"registry:ui","title":"Error template","description":"The pages people land on when something goes wrong, each with the way out that fits: search, ask for access, retry with a reference, wait out maintenance, reconnect or get a new link.","dependencies":["@radix-ui/react-slot","clsx","lucide-react","tailwind-merge"],"registryDependencies":["https://caliperui.site/r/styles.json"],"files":[{"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/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/error-template.tsx","type":"registry:ui","target":"components/ui/error-template.tsx","content":"\"use client\";\n\nimport { Hourglass, RotateCw, Search as SearchIcon, WifiOff } from \"lucide-react\";\nimport { useEffect, useId, useRef, useState, useSyncExternalStore, type ReactNode } from \"react\";\nimport { BlockActions, BlockEyebrow, BlockLead, BlockRoot, BlockTitle, type BlockAction } from \"./block-concept\";\nimport type { BlockNavLink } from \"./block-nav\";\nimport { Button } from \"./button\";\nimport { CopyButton } from \"./copy-button\";\nimport { Field } from \"./field\";\nimport { Input } from \"./input\";\nimport { TemplatePage, type TemplateFooter } from \"./template-page\";\nimport { Textarea } from \"./textarea\";\nimport \"./error-template.css\";\n\nexport type ErrorKind = \"404\" | \"403\" | \"500\" | \"503\" | \"offline\" | \"expired\";\n\nconst copy: Record<ErrorKind, { eyebrow: string; title: string; lead: string }> = {\n  \"404\": { eyebrow: \"Page not found\", title: \"That page isn't here\", lead: \"The link may be old, or the page may have moved. Search, or start from one of these.\" },\n  \"403\": { eyebrow: \"No access\", title: \"You don't have access to this yet\", lead: \"This page belongs to a workspace you're not a member of. Ask its owner, or switch to the account that has access.\" },\n  \"500\": { eyebrow: \"Something broke\", title: \"This page didn't load\", lead: \"It's our fault, not yours. Nothing you did was lost. Try again, and if it keeps happening, send us the reference below.\" },\n  \"503\": { eyebrow: \"Planned maintenance\", title: \"We're upgrading the database\", lead: \"Everything you saved is safe. The site is read-only until the work finishes.\" },\n  offline: { eyebrow: \"No connection\", title: \"You're offline\", lead: \"Check your Wi-Fi or mobile data. This page will notice when you're back.\" },\n  expired: { eyebrow: \"Link expired\", title: \"That link has expired\", lead: \"For your security, links in our emails work for 30 minutes. Send yourself a new one.\" },\n};\n\nconst online = {\n  subscribe(notify: () => void) {\n    window.addEventListener(\"online\", notify);\n    window.addEventListener(\"offline\", notify);\n    return () => {\n      window.removeEventListener(\"online\", notify);\n      window.removeEventListener(\"offline\", notify);\n    };\n  },\n  get: () => navigator.onLine,\n  server: () => true,\n};\n\nfunction useMinutesLeft(until?: string) {\n  const [now, setNow] = useState(() => Date.now());\n  useEffect(() => {\n    if (!until) return;\n    const timer = window.setInterval(() => setNow(Date.now()), 30_000);\n    return () => window.clearInterval(timer);\n  }, [until]);\n  return until ? Math.max(0, Math.ceil((Date.parse(until) - now) / 60_000)) : null;\n}\n\nconst clock = (iso: string) => new Intl.DateTimeFormat(\"en-GB\", { hour: \"2-digit\", minute: \"2-digit\", timeZoneName: \"short\" }).format(new Date(iso));\n\n/**\n * The pages people land on when something goes wrong, each with the way\n * out that fits: search for a missing page, ask for access, retry with a\n * reference to quote, wait out maintenance with a time, notice when the\n * connection returns, or send a fresh link. Built only from the Free blocks.\n */\nexport function ErrorTemplate({\n  kind,\n  brand,\n  nav,\n  signIn,\n  primary,\n  title,\n  lead,\n  actions,\n  suggestions = [],\n  onSearch,\n  signedInAs,\n  onRequestAccess,\n  reference,\n  onRetry,\n  statusHref,\n  until,\n  onResend,\n  isOnline,\n  footer,\n  className,\n}: {\n  kind: ErrorKind;\n  brand: string;\n  nav?: readonly BlockNavLink[];\n  signIn?: BlockAction;\n  primary?: BlockAction;\n  title?: string;\n  lead?: string;\n  /** Usually \"Go home\" and a quiet \"Go back\". */\n  actions?: { primary?: BlockAction; secondary?: BlockAction };\n  /** Places people usually wanted, for 404. */\n  suggestions?: readonly { label: string; href: string; description?: string }[];\n  /** 404: adds a search; called with the query. */\n  onSearch?: (query: string) => void;\n  /** 403: the account that was refused, e.g. \"dana@forma.studio\". */\n  signedInAs?: string;\n  /** 403: asks the owner. May return a promise; a thrown error is shown. */\n  onRequestAccess?: (note: string) => void | Promise<void>;\n  /** 500: an ID support can look up. */\n  reference?: string;\n  /** 500, 503 and offline: try the page again. */\n  onRetry?: () => void | Promise<void>;\n  /** 500 and 503: the public status page. */\n  statusHref?: string;\n  /** 503: when maintenance is expected to end, as an ISO date-time. */\n  until?: string;\n  /** expired: sends a new link. */\n  onResend?: () => void | Promise<void>;\n  /** offline: override the browser's online state, e.g. in a preview. */\n  isOnline?: boolean;\n  footer?: TemplateFooter;\n  className?: string;\n}) {\n  const text = copy[kind];\n  const browserOnline = useSyncExternalStore(online.subscribe, online.get, online.server);\n  const connected = isOnline ?? browserOnline;\n  const minutes = useMinutesLeft(kind === \"503\" ? until : undefined);\n  const figure: ReactNode =\n    kind === \"offline\" ? <WifiOff aria-hidden=\"true\" /> : kind === \"expired\" ? <Hourglass aria-hidden=\"true\" /> : kind;\n\n  return (\n    <TemplatePage brand={brand} nav={nav} signIn={signIn} primary={primary} footer={footer} className={className}>\n      <BlockRoot width=\"narrow\">\n        <div data-slot=\"error-page\" data-kind={kind}>\n          <p data-slot=\"error-figure\" aria-hidden=\"true\">{figure}</p>\n          <div className=\"grid gap-5\">\n            <BlockEyebrow>{text.eyebrow}</BlockEyebrow>\n            <BlockTitle as=\"h1\" size={2} className=\"max-w-[16ch]\">\n              {kind === \"offline\" && connected ? \"You're back online\" : (title ?? text.title)}\n            </BlockTitle>\n            <BlockLead>\n              {kind === \"offline\" && connected ? \"The connection is back. Reload to pick up where you left off.\" : (lead ?? text.lead)}\n              <span className=\"sr-only\">{kind === \"offline\" || kind === \"expired\" ? \"\" : ` Error ${kind}.`}</span>\n            </BlockLead>\n          </div>\n\n          {kind === \"404\" && onSearch && <SearchForm onSearch={onSearch} />}\n          {kind === \"403\" && <AccessRequest signedInAs={signedInAs} onRequestAccess={onRequestAccess} />}\n          {kind === \"500\" && <Retry onRetry={onRetry} reference={reference} statusHref={statusHref} />}\n          {kind === \"503\" && (\n            <div data-slot=\"error-panel\" role=\"status\">\n              {until ? (\n                <p className=\"m-0 text-[15px]\">\n                  {minutes && minutes > 0 ? (\n                    <>\n                      Back in about <b className=\"font-medium\">{minutes} {minutes === 1 ? \"minute\" : \"minutes\"}</b>, at {clock(until)}.\n                    </>\n                  ) : (\n                    \"The work should be finished. Try again.\"\n                  )}\n                </p>\n              ) : (\n                <p className=\"m-0 text-[15px]\">We’ll be back shortly.</p>\n              )}\n              <div className=\"flex flex-wrap gap-2\">\n                {onRetry && <RetryButton onRetry={onRetry} label=\"Check again\" />}\n                {statusHref && <a href={statusHref} data-slot=\"block-button\" data-kind=\"secondary\">Follow on the status page</a>}\n              </div>\n            </div>\n          )}\n          {kind === \"offline\" && (\n            <div data-slot=\"error-panel\" role=\"status\" data-online={connected || undefined}>\n              <p className=\"m-0 flex items-center gap-2 text-[15px]\">\n                <span data-slot=\"error-dot\" aria-hidden=\"true\" />\n                {connected ? \"Connected\" : \"Waiting for a connection…\"}\n              </p>\n              {onRetry && <RetryButton onRetry={onRetry} label={connected ? \"Reload page\" : \"Try anyway\"} />}\n            </div>\n          )}\n          {kind === \"expired\" && <Resend onResend={onResend} />}\n\n          {(actions?.primary || actions?.secondary) && <BlockActions primary={actions.primary} secondary={actions.secondary} />}\n\n          {suggestions.length > 0 && (\n            <nav aria-label=\"Suggested pages\" data-slot=\"error-suggestions\">\n              <ul>\n                {suggestions.map((item) => (\n                  <li key={item.href}>\n                    <a href={item.href}>\n                      <span className=\"text-[16px] font-medium\">{item.label}</span>\n                      {item.description && <span className=\"text-[14px] text-[var(--k-muted)]\">{item.description}</span>}\n                    </a>\n                  </li>\n                ))}\n              </ul>\n            </nav>\n          )}\n        </div>\n      </BlockRoot>\n    </TemplatePage>\n  );\n}\n\nfunction SearchForm({ onSearch }: { onSearch: (query: string) => void }) {\n  const [query, setQuery] = useState(\"\");\n  return (\n    <form\n      role=\"search\"\n      className=\"flex max-w-md flex-wrap gap-2\"\n      onSubmit={(event) => {\n        event.preventDefault();\n        if (query.trim()) onSearch(query.trim());\n      }}\n    >\n      <Input aria-label=\"Search the site\" type=\"search\" placeholder=\"Search the site\" value={query} onChange={(event) => setQuery(event.target.value)} leading={<SearchIcon aria-hidden=\"true\" className=\"size-4\" />} className=\"w-auto min-w-0 flex-1 basis-56\" />\n      <Button type=\"submit\" variant=\"secondary\">Search</Button>\n    </form>\n  );\n}\n\nfunction RetryButton({ onRetry, label = \"Try again\" }: { onRetry: () => void | Promise<void>; label?: string }) {\n  const [busy, setBusy] = useState(false);\n  return (\n    <Button\n      loading={busy}\n      onClick={async () => {\n        setBusy(true);\n        try {\n          await onRetry();\n        } finally {\n          setBusy(false);\n        }\n      }}\n    >\n      <RotateCw aria-hidden=\"true\" />\n      {label}\n    </Button>\n  );\n}\n\nfunction Retry({ onRetry, reference, statusHref }: { onRetry?: () => void | Promise<void>; reference?: string; statusHref?: string }) {\n  const [tries, setTries] = useState(0);\n  const [busy, setBusy] = useState(false);\n  return (\n    <div data-slot=\"error-panel\">\n      {reference && (\n        <div className=\"flex flex-wrap items-center gap-3\">\n          <span className=\"text-[13px] text-[var(--k-muted)]\">Reference</span>\n          <code data-slot=\"error-reference\">{reference}</code>\n          <CopyButton value={reference} label=\"Copy reference\" variant=\"secondary\" />\n        </div>\n      )}\n      <div className=\"flex flex-wrap items-center gap-3\">\n        {onRetry && (\n          <Button\n            loading={busy}\n            onClick={async () => {\n              setBusy(true);\n              try {\n                await onRetry();\n              } catch {\n                setTries((count) => count + 1);\n              } finally {\n                setBusy(false);\n              }\n            }}\n          >\n            <RotateCw aria-hidden=\"true\" />\n            Try again\n          </Button>\n        )}\n        {statusHref && <a href={statusHref} className=\"text-[14px] underline underline-offset-4\">Check system status</a>}\n      </div>\n      <p role=\"status\" className=\"m-0 min-h-5 text-[13px] text-[var(--k-muted)]\">\n        {tries > 0 && `Still not working after ${tries} ${tries === 1 ? \"try\" : \"tries\"}. ${tries > 1 && reference ? \"Send the reference to support and we'll look.\" : \"Give it a minute.\"}`}\n      </p>\n    </div>\n  );\n}\n\nfunction AccessRequest({ signedInAs, onRequestAccess }: { signedInAs?: string; onRequestAccess?: (note: string) => void | Promise<void> }) {\n  const [note, setNote] = useState(\"\");\n  const [state, setState] = useState<\"idle\" | \"sending\" | \"sent\">(\"idle\");\n  const [error, setError] = useState<string>();\n  const done = useRef<HTMLParagraphElement>(null);\n  const id = useId();\n  useEffect(() => {\n    if (state === \"sent\") done.current?.focus();\n  }, [state]);\n  if (!onRequestAccess) return signedInAs ? <p className=\"m-0 text-[15px] text-[var(--k-muted)]\">Signed in as <b className=\"font-medium text-[var(--k-ink)]\">{signedInAs}</b>.</p> : null;\n  if (state === \"sent\")\n    return (\n      <div data-slot=\"error-panel\">\n        <p ref={done} tabIndex={-1} role=\"status\" className=\"m-0 text-[15px] outline-none\">\n          Request sent. The owner will get an email, and you’ll hear back at <b className=\"font-medium\">{signedInAs ?? \"your address\"}</b>.\n        </p>\n      </div>\n    );\n  return (\n    <form\n      data-slot=\"error-panel\"\n      aria-labelledby={`${id}-title`}\n      onSubmit={async (event) => {\n        event.preventDefault();\n        setState(\"sending\");\n        setError(undefined);\n        try {\n          await onRequestAccess(note.trim());\n          setState(\"sent\");\n        } catch (reason) {\n          setError(reason instanceof Error ? reason.message : \"The request did not send. Try again.\");\n          setState(\"idle\");\n        }\n      }}\n    >\n      <p id={`${id}-title`} className=\"m-0 text-[15px]\">\n        Signed in as <b className=\"font-medium\">{signedInAs}</b>\n      </p>\n      <Field label=\"Note to the owner\" optional error={error} description=\"Say what you need it for.\">\n        {(props) => <Textarea {...props} value={note} onChange={(event) => setNote(event.target.value)} disabled={state === \"sending\"} rows={2} />}\n      </Field>\n      <div className=\"flex flex-wrap gap-2\">\n        <Button type=\"submit\" loading={state === \"sending\"}>Request access</Button>\n      </div>\n    </form>\n  );\n}\n\nfunction Resend({ onResend }: { onResend?: () => void | Promise<void> }) {\n  const [wait, setWait] = useState(0);\n  const [busy, setBusy] = useState(false);\n  const [sent, setSent] = useState(false);\n  useEffect(() => {\n    if (wait <= 0) return;\n    const timer = window.setTimeout(() => setWait((left) => left - 1), 1000);\n    return () => window.clearTimeout(timer);\n  }, [wait]);\n  if (!onResend) return null;\n  return (\n    <div data-slot=\"error-panel\">\n      <div className=\"flex flex-wrap items-center gap-3\">\n        <Button\n          loading={busy}\n          disabled={wait > 0}\n          onClick={async () => {\n            setBusy(true);\n            try {\n              await onResend();\n              setSent(true);\n              setWait(30);\n            } finally {\n              setBusy(false);\n            }\n          }}\n        >\n          {wait > 0 ? `Send again in 0:${String(wait).padStart(2, \"0\")}` : sent ? \"Send another link\" : \"Send a new link\"}\n        </Button>\n      </div>\n      <p role=\"status\" className=\"m-0 min-h-5 text-[13px] text-[var(--k-muted)]\">{sent ? \"A new link is on its way. It works for 30 minutes.\" : \"\"}</p>\n    </div>\n  );\n}\n"},{"path":"ui/field.tsx","type":"registry:ui","target":"components/ui/field.tsx","content":"\"use client\";\nimport { CircleAlert, CircleCheck, LoaderCircle } from \"lucide-react\";\nimport { useId, type ReactNode } from \"react\";\nimport { Label } from \"./label\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/field-message.css\";\nimport \"./shared/loading-indicator.css\";\nimport \"./field.css\";\n\nexport type FieldControlProps = {\n  id: string;\n  \"aria-describedby\"?: string;\n  \"aria-invalid\"?: true;\n  \"aria-required\"?: true;\n};\n\nexport const isEmail = (value: string) => /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value.trim());\n\n/**\n * A label, a control and one message line wired together for assistive\n * technology. Hint, async status and error share the same reserved line and\n * rewrite it in place, so validation never pushes the next field down.\n */\nexport function Field({\n  label,\n  description,\n  error,\n  status,\n  statusTone = \"pending\",\n  count,\n  required,\n  optional,\n  action,\n  className,\n  children,\n}: {\n  label: string;\n  /** Hint shown on the message line until a status or error replaces it. */\n  description?: ReactNode;\n  /** Replaces the hint and is announced when it appears. */\n  error?: string;\n  /** An async result such as “Checking availability…” or “Available”. */\n  status?: string;\n  statusTone?: \"pending\" | \"success\";\n  /** Right-aligned counter on the message line, e.g. “6 / 40”. */\n  count?: string;\n  required?: boolean;\n  /** Adds \"(optional)\" after the label. */\n  optional?: boolean;\n  /** Something aligned with the label, such as a \"Forgot password?\" link. */\n  action?: ReactNode;\n  className?: string;\n  children: (props: FieldControlProps) => ReactNode;\n}) {\n  const id = useId();\n  const message = `${id}-message`;\n  const hasMessage = Boolean(error || status || description || count);\n  const tone = error ? \"error\" : status ? statusTone : \"hint\";\n  return (\n    <div data-slot=\"field\" className={cn(\"grid gap-1.5\", className)}>\n      <div className=\"flex items-baseline justify-between gap-3\">\n        <Label htmlFor={id}>\n          {label}\n          {optional && <span className=\"ml-1 font-normal text-muted-foreground\">(optional)</span>}\n        </Label>\n        {action}\n      </div>\n      {children({\n        id,\n        \"aria-describedby\": hasMessage ? message : undefined,\n        \"aria-invalid\": error ? true : undefined,\n        \"aria-required\": required ? true : undefined,\n      })}\n      <p id={message} data-slot=\"field-message\" data-tone={tone} aria-live={error ? \"assertive\" : \"polite\"} role={error ? \"alert\" : undefined}>\n        {tone === \"error\" && <CircleAlert aria-hidden=\"true\" />}\n        {tone === \"success\" && <CircleCheck aria-hidden=\"true\" />}\n        {tone === \"pending\" && <LoaderCircle data-slot=\"loading-indicator\" aria-hidden=\"true\" />}\n        <span data-slot=\"field-error\" className=\"min-w-0 flex-1\">{error ?? status ?? description}</span>\n        {count && <span data-slot=\"field-count\">{count}</span>}\n      </p>\n    </div>\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/input.tsx","type":"registry:ui","target":"components/ui/input.tsx","content":"import type { ComponentProps, ReactNode } from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/input.css\";\nimport \"./shared/input-2.css\";\nimport \"./shared/input-group.css\";\nimport \"./input.css\";\n\nconst fieldText =\n  \"w-full min-w-0 text-base text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed md:text-sm\";\n\nexport type InputProps = ComponentProps<\"input\"> & {\n  /** Fixed text or an icon before the value, e.g. \"caliper.app/\". */\n  leading?: ReactNode;\n  /** Text, an icon or a status after the value, e.g. a spinner or a count. */\n  trailing?: ReactNode;\n};\n\n/**\n * A recessed text field. Fields sit slightly below the paper (an inner shade\n * and a 3:1 ring); buttons stand on it, so what you fill and what you press\n * never look alike.\n */\nexport function Input({ className, leading, trailing, ...props }: InputProps) {\n  if (leading === undefined && trailing === undefined) {\n    return (\n      <input\n        data-slot=\"input\"\n        className={cn(\"flex h-[var(--control-height)] rounded-button px-3\", fieldText, className)}\n        {...props}\n      />\n    );\n  }\n  return (\n    <span\n      data-slot=\"input-group\"\n      data-disabled={props.disabled || undefined}\n      data-invalid={props[\"aria-invalid\"] === true || props[\"aria-invalid\"] === \"true\" || undefined}\n      data-readonly={props.readOnly || undefined}\n      className={cn(\"flex h-[var(--control-height)] w-full min-w-0 items-center gap-2 rounded-button px-3\", className)}\n    >\n      {leading !== undefined && <span data-slot=\"input-affix\">{leading}</span>}\n      <input data-slot=\"input-bare\" className={cn(\"h-full flex-1 bg-transparent\", fieldText)} {...props} />\n      {trailing !== undefined && <span data-slot=\"input-trailing\">{trailing}</span>}\n    </span>\n  );\n}\n"},{"path":"ui/label.tsx","type":"registry:ui","target":"components/ui/label.tsx","content":"import type { ComponentProps } from \"react\";\nimport { cn } from \"./lib/cn\";\n\nexport function Label({ className, ...props }: ComponentProps<\"label\">) {\n  return (\n    <label\n      data-slot=\"label\"\n      className={cn(\n        \"inline-block max-w-full text-sm leading-5 font-medium [overflow-wrap:anywhere]\",\n        className,\n      )}\n      {...props}\n    />\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/textarea.tsx","type":"registry:ui","target":"components/ui/textarea.tsx","content":"import type { ComponentProps } from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/input-2.css\";\nimport \"./textarea.css\";\n\n/** A recessed multi-line field that grows with the draft from 3 to 8 lines, then scrolls. */\nexport function Textarea({ className, ...props }: ComponentProps<\"textarea\">) {\n  return (\n    <textarea\n      data-slot=\"textarea\"\n      className={cn(\n        \"block min-h-[88px] max-h-[200px] w-full min-w-0 resize-none rounded-button px-3 py-[9px] text-base leading-[22px] text-foreground outline-none [field-sizing:content] placeholder:text-muted-foreground disabled:cursor-not-allowed md:text-sm md:leading-[22px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\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/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/error-template.css","type":"registry:file","target":"components/ui/error-template.css","content":"/* error-template.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Error pages */\n  [data-slot=\"error-page\"] { display: grid; justify-items: start; gap: 28px; }\n  [data-slot=\"error-figure\"] {\n    margin: 0 0 -8px; font-family: var(--k-display-font); font-weight: var(--k-display-weight); font-size: clamp(88px, 18cqi, 200px); line-height: 0.82;\n    letter-spacing: -0.05em; color: var(--k-hi); font-variant-numeric: tabular-nums;\n  }\n  [data-slot=\"error-figure\"] svg { width: clamp(64px, 10cqi, 112px); height: clamp(64px, 10cqi, 112px); stroke-width: 1.25; }\n  [data-slot=\"error-panel\"] { display: grid; gap: 14px; width: 100%; max-width: 560px; padding: 20px; border-radius: var(--k-radius-lg); background: var(--k-surface); box-shadow: 0 0 0 1px var(--k-line); }\n  [data-slot=\"error-reference\"] { padding: 3px 8px; border-radius: 8px; background: var(--k-sunken); font-family: var(--font-mono, monospace); font-size: 13px; }\n  [data-slot=\"error-dot\"] { width: 9px; height: 9px; border-radius: 999px; background: var(--k-muted); animation: caliper-pulse-soft var(--duration-loading) var(--ease-in-out) infinite alternate; }\n  [data-slot=\"error-panel\"][data-online] [data-slot=\"error-dot\"] { background: #1f7a44; animation: none; }\n  [data-slot=\"error-suggestions\"] { width: 100%; }\n  [data-slot=\"error-suggestions\"] ul { display: grid; margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--k-line-strong); }\n  [data-slot=\"error-suggestions\"] a { display: grid; gap: 2px; padding: 14px 0; border-bottom: 1px solid var(--k-line); color: inherit; text-decoration: none; transition: padding var(--duration-normal) var(--ease-out); }\n  [data-slot=\"error-suggestions\"] a:hover { padding-left: 8px; }\n  [data-slot=\"error-suggestions\"] a:focus-visible { outline: 2px solid var(--k-focus, var(--k-hi)); outline-offset: 2px; border-radius: 4px; }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"error-dot\"] { animation: none; }\n  }\n}\n"},{"path":"ui/shared/field-message.css","type":"registry:file","target":"components/ui/shared/field-message.css","content":"/* Shared by date-input.tsx, field.tsx, scrub-field.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"field-message\"] {\n    display: flex;\n    align-items: flex-start;\n    gap: 6px;\n    min-height: 18px;\n    margin: 0;\n    font-size: 12px;\n    line-height: 18px;\n    color: var(--muted-foreground);\n  }\n  [data-slot=\"field-message\"] svg {\n    flex: none;\n    width: 14px;\n    height: 14px;\n    margin-top: 2px;\n  }\n  [data-slot=\"field-message\"][data-tone=\"error\"] {\n    color: var(--danger);\n  }\n  [data-slot=\"field-message\"][data-tone=\"success\"] {\n    color: var(--success);\n  }\n}\n"},{"path":"ui/field.css","type":"registry:file","target":"components/ui/field.css","content":"/* field.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"field-count\"] {\n    margin-left: auto;\n    color: var(--muted-foreground);\n    font-variant-numeric: tabular-nums;\n    font-size: 12px;\n    font-variant-numeric: tabular-nums;\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/input.css","type":"registry:file","target":"components/ui/shared/input.css","content":"/* Shared by combobox.tsx, input.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"input\"] {\n    transition-property: background-color, color, border-color;\n    transition-duration: var(--duration-fast);\n    transition-timing-function: var(--ease-out);\n  }\n\n  /* Recessed to fill. One ring for every field: 3:1 at rest, darker on\n     hover, a 2px ember ring with a soft halo on focus, danger when invalid.\n     Disabled and read-only drop into the sunken fill and stay legible. */\n  [data-slot=\"input\"] {\n    border: 0;\n    background-color: var(--field);\n    box-shadow: var(--shadow-field);\n    transition: box-shadow var(--duration-fast) var(--ease-out);\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"input\"] {\n      transition: none;\n    }\n  }\n}\n"},{"path":"ui/shared/input-2.css","type":"registry:file","target":"components/ui/shared/input-2.css","content":"/* Shared by combobox.tsx, input.tsx, select-menu.tsx, select.tsx, textarea.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  @media (hover: hover) {\n    :is([data-slot=\"input\"], [data-slot=\"textarea\"], [data-slot=\"select\"]):hover:enabled:not(:focus-visible):not([aria-invalid=\"true\"]) {\n      box-shadow: var(--shadow-field-hover);\n    }\n  }\n  :is([data-slot=\"input\"], [data-slot=\"textarea\"], [data-slot=\"select\"]):focus-visible {\n    outline: none;\n    box-shadow: var(--shadow-field-focus);\n    transition: none;\n  }\n  :is([data-slot=\"input\"], [data-slot=\"textarea\"], [data-slot=\"select\"])[aria-invalid=\"true\"] {\n    box-shadow: var(--shadow-field-invalid);\n  }\n  :is([data-slot=\"input\"], [data-slot=\"textarea\"], [data-slot=\"select\"])[aria-invalid=\"true\"]:focus-visible {\n    box-shadow:\n      var(--shadow-field-invalid),\n      0 0 0 4px color-mix(in srgb, var(--danger) 18%, transparent);\n  }\n  :is([data-slot=\"input\"], [data-slot=\"textarea\"], [data-slot=\"select\"]):disabled {\n    background-color: var(--sunken);\n    box-shadow: var(--shadow-field-quiet);\n    color: var(--muted-foreground);\n  }\n  :is([data-slot=\"input\"], [data-slot=\"textarea\"])[readonly] {\n    background-color: var(--sunken);\n    box-shadow: var(--shadow-field-quiet);\n  }\n}\n"},{"path":"ui/shared/input-group.css","type":"registry:file","target":"components/ui/shared/input-group.css","content":"/* Shared by input.tsx, money-field.tsx, phone-field.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  @media (hover: hover) {\n    :is([data-slot=\"input-group\"], [data-slot=\"money-field\"], [data-slot=\"phone-field-control\"]):not([data-disabled]):not([data-invalid]):hover:not(:focus-within) {\n      box-shadow: var(--shadow-field-hover);\n    }\n  }\n  :is([data-slot=\"input-group\"], [data-slot=\"money-field\"], [data-slot=\"phone-field-control\"]):focus-within {\n    outline: none;\n    box-shadow: var(--shadow-field-focus);\n    transition: none;\n  }\n}\n"},{"path":"ui/input.css","type":"registry:file","target":"components/ui/input.css","content":"/* input.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Recessed to fill. One ring for every field: 3:1 at rest, darker on\n     hover, a 2px ember ring with a soft halo on focus, danger when invalid.\n     Disabled and read-only drop into the sunken fill and stay legible. */\n  [data-slot=\"input-group\"] {\n    border: 0;\n    background-color: var(--field);\n    box-shadow: var(--shadow-field);\n    transition: box-shadow var(--duration-fast) var(--ease-out);\n  }\n  [data-slot=\"input-group\"][data-invalid] {\n    box-shadow: var(--shadow-field-invalid);\n  }\n  [data-slot=\"input-group\"][data-invalid]:focus-within {\n    box-shadow:\n      var(--shadow-field-invalid),\n      0 0 0 4px color-mix(in srgb, var(--danger) 18%, transparent);\n  }\n  [data-slot=\"input-group\"][data-disabled] {\n    background-color: var(--sunken);\n    box-shadow: var(--shadow-field-quiet);\n    color: var(--muted-foreground);\n  }\n  [data-slot=\"input-group\"][data-readonly] {\n    background-color: var(--sunken);\n    box-shadow: var(--shadow-field-quiet);\n  }\n  [data-slot=\"input-bare\"] {\n    border: 0;\n    box-shadow: none;\n    outline: none;\n  }\n  [data-slot=\"input-affix\"] {\n    display: inline-flex;\n    align-items: center;\n    height: 20px;\n    padding-inline-end: 10px;\n    margin-inline-end: 2px;\n    color: var(--muted-foreground);\n    box-shadow: 1px 0 0 var(--border);\n    white-space: nowrap;\n  }\n  [data-slot=\"input-affix\"]:dir(rtl) {\n    box-shadow: -1px 0 0 var(--border);\n  }\n  [data-slot=\"input-affix\"]:has(> svg:only-child) {\n    padding-inline-end: 0;\n    box-shadow: none;\n  }\n  [data-slot=\"input-trailing\"] {\n    display: inline-flex;\n    align-items: center;\n    gap: 6px;\n    color: var(--muted-foreground);\n    white-space: nowrap;\n    font-size: 12px;\n  }\n  :is([data-slot=\"input-affix\"], [data-slot=\"input-trailing\"]) svg {\n    width: 16px;\n    height: 16px;\n  }\n  [data-slot=\"docs-toolbar\"] [data-slot=\"input-group\"] { max-width: 24rem; }\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"},{"path":"ui/textarea.css","type":"registry:file","target":"components/ui/textarea.css","content":"/* textarea.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"textarea\"] {\n    transition-property: background-color, color, border-color;\n    transition-duration: var(--duration-fast);\n    transition-timing-function: var(--ease-out);\n  }\n\n  /* Recessed to fill. One ring for every field: 3:1 at rest, darker on\n     hover, a 2px ember ring with a soft halo on focus, danger when invalid.\n     Disabled and read-only drop into the sunken fill and stay legible. */\n  [data-slot=\"textarea\"] {\n    border: 0;\n    background-color: var(--field);\n    box-shadow: var(--shadow-field);\n    transition: box-shadow var(--duration-fast) var(--ease-out);\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"textarea\"] {\n      transition: none;\n    }\n  }\n}\n"}],"categories":["free-pages"],"meta":{"tier":"free","license":"MIT","family":"reading-proof","docs":"/docs/components/error-template"}}