{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"card","type":"registry:component","title":"Card","description":"A record or a small group of related content and actions: optional media, avatar, title, status, action, meta, free content and a Details toggle; plain or double-bezel frame.","categories":["layout"],"dependencies":["@radix-ui/react-collapsible@1.1.20","@radix-ui/react-direction@1.1.4","clsx@^2.1.1","lucide-react@^1.48.0","tailwind-merge@^3.7.0"],"registryDependencies":["https://beamline.io/registry/look-prism.json"],"files":[{"path":"beamline/components/badge/badge.module.css","type":"registry:file","target":"src/ui/beamline/components/badge/badge.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Badge: a tinted label. The tone sets three local colours; the tag role lets a look outline it and ink its text\n     (pop turns badges into stickers), every other look leaves it a quiet tinted pill for dense tables. */\n  .root {\n    --pui-badge-bg: var(--pui-hover);\n    --pui-badge-fg: var(--pui-ink-2);\n    --pui-badge-dot: var(--pui-ink-3);\n    display: inline-flex;\n    flex-shrink: 0;\n    max-width: 100%;\n    align-items: center;\n    gap: var(--pui-space-1);\n    height: 22px;\n    padding-inline: var(--pui-space-2);\n    /* nesting input (O1 §14.1, as Button): a parent that holds this near its rounded corner sets --pui-nest-radius and\n       --pui-nest-inset; the corner becomes radius − inset, clamped between a floor (chip) and the part's own role radius (or --pui-nest-ceiling: a pill container sets it to the pill radius). Unset, the declaration is invalid and the own corner\n       applies. --pui-badge-radius wins over both. */\n    --_badge-nest: clamp(\n      var(--pui-radius-chip),\n      calc(var(--pui-nest-radius) - var(--pui-nest-inset, 0px)),\n      var(--pui-nest-ceiling, var(--pui-radius-pill))\n    );\n    border-radius: var(--pui-badge-radius, var(--_badge-nest, var(--pui-radius-pill)));\n    background: var(--pui-badge-bg);\n    color: var(--m-tag-ink, var(--pui-badge-fg));\n    font-size: var(--pui-text-xs);\n    font-weight: var(--pui-weight-medium);\n    font-variant-numeric: tabular-nums;\n    line-height: 1;\n    white-space: nowrap;\n    /* a tone that changes (Pending → Paid) eases its fill and ink in place; the word itself changes at once */\n    transition:\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default),\n      var(--m-transition);\n  }\n  /* in a cell too narrow for it the label ends in \"…\" (the whole label shows on hover); icon and dot stay */\n  .label {\n    min-width: 0;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    /* the clip keeps descenders: the label is one line tall at the badge's line height */\n    padding-block: 0.1em;\n  }\n  .root :where(svg) {\n    width: 14px;\n    height: 14px;\n    flex-shrink: 0;\n  }\n  .sm {\n    height: 20px;\n    padding-inline: var(--pui-space-1-5);\n    font-size: var(--pui-text-2xs);\n  }\n  .sm :where(svg) {\n    width: 12px;\n    height: 12px;\n  }\n  .accent {\n    --pui-badge-bg: var(--pui-accent-tint);\n    --pui-badge-fg: var(--pui-accent-ink);\n    --pui-badge-dot: var(--pui-accent);\n  }\n  .success {\n    --pui-badge-bg: var(--pui-green-tint);\n    --pui-badge-fg: var(--pui-green);\n    --pui-badge-dot: var(--pui-green);\n  }\n  .warning {\n    --pui-badge-bg: var(--pui-orange-tint);\n    --pui-badge-fg: var(--pui-orange);\n    --pui-badge-dot: var(--pui-orange);\n  }\n  .danger {\n    --pui-badge-bg: var(--pui-red-tint);\n    --pui-badge-fg: var(--pui-red);\n    --pui-badge-dot: var(--pui-red);\n  }\n  .info {\n    --pui-badge-bg: var(--pui-blue-tint);\n    --pui-badge-fg: var(--pui-blue);\n    --pui-badge-dot: var(--pui-blue);\n  }\n  /* a category's own colour (identity): its dot, larger than a status dot so the colour reads, ringed in the badge's\n     own ink so a pale or a dark stop shows on any fill; an icon takes the colour instead; with tint the pill takes a light\n     wash of it and the word stays in ink */\n  .root[data-color] {\n    --pui-badge-dot: var(--_label-color);\n  }\n  .root[data-color] .dot {\n    width: 8px;\n    height: 8px;\n    box-shadow: 0 0 0 1px color-mix(in oklab, currentColor 30%, transparent);\n  }\n  .root[data-color] > :where(svg) {\n    color: var(--_label-color);\n  }\n  .root[data-color=\"tint\"] {\n    --pui-badge-bg: color-mix(in oklab, var(--_label-color) 18%, var(--pui-surface));\n    --pui-badge-fg: var(--pui-ink);\n  }\n  .dot {\n    width: 6px;\n    height: 6px;\n    flex-shrink: 0;\n    border-radius: var(--pui-radius-pill);\n    background: var(--pui-badge-dot);\n    transition: background-color var(--pui-motion-out) var(--pui-ease-default);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.root {\n  composes: m-tag from global;\n}\n"},{"path":"beamline/components/badge/badge.tsx","type":"registry:file","target":"src/ui/beamline/components/badge/badge.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { BadgeDefaults } from \"./defaults\";\nexport type { BadgeDefaults } from \"./defaults\";\n\nimport type { CSSProperties, HTMLAttributes, PointerEvent, ReactNode, Ref } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./badge.module.css\";\n\nexport type BadgeTone = \"neutral\" | \"accent\" | \"success\" | \"warning\" | \"danger\" | \"info\";\nexport type BadgeSize = \"sm\" | \"md\";\n\nexport interface BadgeClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the dot part; no override by default. */\n  dot?: string;\n  /** Classes for the label part; no override by default. */\n  label?: string;\n}\n\nexport interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {\n  /** Semantic colour; pair it with a word or icon (colour is never the only signal). */\n  tone?: BadgeTone;\n  /** Height 20 / 22 px. */\n  size?: BadgeSize;\n  /** A leading icon element. */\n  icon?: ReactNode;\n  /** A leading status dot in the tone colour, instead of an icon. */\n  dot?: boolean;\n  /** A category's own colour, for a label the user matches across places (a card's \"Billing\" and the filter that\n   *  shows it): a dot in it before the word, ringed in the badge's ink so a pale or dark colour reads on any surface.\n   *  Take it from `identity(i, n)` in `@/lib/tone`, never a status tone. Omitted by default: no dot. */\n  color?: string;\n  /** With `color`, the pill takes a light tint of it as well (the word stays in ink). Default false: a neutral pill. */\n  tint?: boolean;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLSpanElement>;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: BadgeClassNames;\n}\n\n/** A short label for status, category or count; a category the user matches across places takes `color` (a dot,\n *  optionally a tint, from the identity ramp). A tone that changes (Pending → Paid) eases its colours in place;\n *  nothing else moves, so it costs nothing in a dense table. In a cell too narrow for it, the label ends in \"…\" and the\n *  whole label shows on hover. Reads scoped presentation defaults. A look may outline it (pop makes it a sticker). */\nexport function Badge({\n  tone: toneProp,\n  size: sizeProp,\n  icon,\n  dot = false,\n  color,\n  tint = false,\n  className,\n  classNames,\n  children,\n  ref,\n  onPointerEnter,\n  ...props\n}: BadgeProps) {\n  const scopeDefaults = useComponentDefaults<BadgeDefaults>(\"badge\", [\"tone\", \"size\"]);\n  const tone = toneProp ?? scopeDefaults.tone ?? \"neutral\";\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  // a label cut by a narrow cell names itself on hover; nothing is measured until someone points at it\n  const nameIfCut = (e: PointerEvent<HTMLSpanElement>) => {\n    onPointerEnter?.(e);\n    const label = e.currentTarget.querySelector<HTMLElement>(\"[data-slot=badge-label]\");\n    if (!label || props.title !== undefined) return;\n    e.currentTarget.title = label.scrollWidth > label.clientWidth + 1 ? (label.textContent ?? \"\") : \"\";\n  };\n  return (\n    <span\n      ref={ref}\n      data-slot=\"badge\"\n      data-tone={tone}\n      data-color={color ? (tint ? \"tint\" : \"dot\") : undefined}\n      onPointerEnter={nameIfCut}\n      {...props}\n      style={color ? ({ \"--_label-color\": color, ...props.style } as CSSProperties) : props.style}\n      className={cn(\n        styles.root,\n        size === \"sm\" && styles.sm,\n        tone !== \"neutral\" && styles[tone],\n        classNames?.root,\n        className,\n      )}\n    >\n      {(dot || (color && !icon)) && (\n        <span data-slot=\"badge-dot\" aria-hidden className={cn(styles.dot, classNames?.dot)} />\n      )}\n      {icon}\n      {children != null && children !== false && (\n        <span data-slot=\"badge-label\" className={cn(styles.label, classNames?.label)}>\n          {children}\n        </span>\n      )}\n    </span>\n  );\n}\n\nexport default Badge;\n"},{"path":"beamline/components/badge/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/badge/defaults.ts","content":"/** Pure allowed presentation defaults for Badge; no component or runtime imports. */\nexport interface BadgeDefaults {\n  /** Shared tone setting; omit to inherit the surrounding scope or the component default. */\n  tone?: \"danger\" | \"accent\" | \"success\" | \"warning\" | \"info\" | \"neutral\";\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\";\n}\n"},{"path":"beamline/components/bezel/bezel.module.css","type":"registry:file","target":"src/ui/beamline/components/bezel/bezel.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* the shell is a surface and the inner panel a well, so the double bezel takes the look's paint */\n  .shell {\n    --pui-gap: var(--pui-bezel-gap);\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    gap: var(--pui-gap);\n    min-width: 0;\n    padding: var(--pui-gap);\n    border-radius: var(--pui-bezel-radius);\n  }\n  .shell[data-size=\"sm\"] {\n    --pui-gap: var(--pui-space-1-5);\n    border-radius: calc(var(--pui-bezel-radius) - var(--pui-gap));\n  }\n  /* The inner radius is the outer minus the gap, plus a little: parallel curves read as one object. */\n  .inner {\n    position: relative;\n    min-width: 0;\n    min-height: 0;\n    /* grow into a shell given a height, but keep a size set on the panel (flex-basis auto, not 0) */\n    flex: 1 1 auto;\n    overflow: hidden;\n    border-radius: calc(var(--pui-bezel-radius) - var(--pui-gap) + var(--pui-space-0-5));\n  }\n  .shell[data-size=\"sm\"] > .inner {\n    border-radius: calc(var(--pui-bezel-radius) - 2 * var(--pui-gap) + var(--pui-space-0-5));\n  }\n  .inner > :is(img, video, canvas) {\n    display: block;\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n  }\n  /* interactive: the picture set into the panel leans toward the pointer (a small grow, clipped by the panel, so the\n     frame never moves) and settles back when it leaves; focus inside does the same, so the keyboard sees it too.\n     Transform only; reduced motion keeps the frame still. */\n  .shell[data-interactive] {\n    cursor: pointer;\n  }\n  .shell[data-interactive] > .inner > * {\n    transition: scale var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .shell[data-interactive]:is(:hover, :focus-within) > .inner > * {\n    scale: calc(1 + 0.04 * var(--pui-motion-travel, 1));\n    transition-duration: var(--pui-motion-in);\n  }\n  /* Nesting input (O1 §14.1): the footer (in the shell's lower corners) and the inner panel tell the controls they hold\n     their radius and the padding next to the corner; the control clamps its own concentric radius.\n     --pui-bezel-inner-inset says how far the panel's content is padded (default the card padding). */\n  .footer {\n    --pui-nest-radius: var(--pui-bezel-radius);\n    --pui-nest-inset: calc(var(--pui-gap) + var(--pui-space-1));\n  }\n  .inner {\n    --pui-nest-radius: calc(var(--pui-bezel-radius) - var(--pui-gap) + var(--pui-space-0-5));\n    --pui-nest-inset: var(--pui-bezel-inner-inset, var(--pui-pad-card));\n  }\n  .shell[data-size=\"sm\"] > .footer {\n    --pui-nest-radius: calc(var(--pui-bezel-radius) - var(--pui-gap));\n  }\n  .shell[data-size=\"sm\"] > .inner {\n    --pui-nest-radius: calc(var(--pui-bezel-radius) - 2 * var(--pui-gap) + var(--pui-space-0-5));\n  }\n  .footer {\n    display: flex;\n    align-items: center;\n    gap: var(--pui-space-3);\n    min-width: 0;\n    padding: var(--pui-space-0-5) var(--pui-space-1) var(--pui-space-1);\n  }\n  .icon {\n    display: grid;\n    place-items: center;\n    flex: none;\n    width: 40px;\n    height: 40px;\n    /* nested in the shell's corner: shell radius minus the gap and the footer's inline padding */\n    border-radius: max(0px, calc(var(--pui-bezel-radius) - var(--pui-gap) - var(--pui-space-1)));\n    color: var(--pui-ink-2);\n  }\n  .icon > svg {\n    width: 18px;\n    height: 18px;\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.shell {\n  composes: m-surface from global;\n}\n.inner {\n  composes: m-inset from global;\n}\n.icon {\n  composes: m-control from global;\n}\n"},{"path":"beamline/components/bezel/bezel.tsx","type":"registry:file","target":"src/ui/beamline/components/bezel/bezel.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { BezelDefaults } from \"./defaults\";\nexport type { BezelDefaults } from \"./defaults\";\n\nimport { type ComponentProps, type ComponentPropsWithoutRef, type ElementType, type ReactNode, type Ref } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./bezel.module.css\";\n\nexport interface BezelClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the inner part; no override by default. */\n  inner?: string;\n  /** Classes for the footer part; no override by default. */\n  footer?: string;\n}\n\nexport interface BezelProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\"> {\n  /** Sits in the shell under the panel: an icon tile with a title and a line, or actions. */\n  footer?: ReactNode;\n  /** `sm` for tiles and small previews (6 px gap), `md` for cards (10 px). */\n  size?: \"sm\" | \"md\";\n  /** Extra classes for the inner panel. */\n  innerClassName?: string;\n  /** HTML element used for the text or container. Defaults to \"div\". */\n  as?: \"div\" | \"section\" | \"article\" | \"li\";\n  /** The bezel is something to open (a template, a file, a preview): the picture set into it leans toward the pointer\n   *  (or focus inside) and the frame is a pointer target. Put the link or button inside. */\n  interactive?: boolean;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLElement>;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: BezelClassNames;\n}\n\n/**\n * A double-bezel frame: an outer shell with a hairline, a small gap, and an inset panel with its own edge, radii nested\n * so the two curves run parallel. It makes a preview, chart or media read as an object set into the surface. Static\n * CSS paint with scoped presentation defaults. Card, MetricCard and the gallery use it; wrap other content directly.\n */\nexport function Bezel({\n  footer,\n  size: sizeProp,\n  innerClassName,\n  as = \"div\",\n  interactive = false,\n  className,\n  classNames,\n  children,\n  ...props\n}: BezelProps) {\n  const scopeDefaults = useComponentDefaults<BezelDefaults>(\"bezel\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const Tag = as as ElementType;\n  return (\n    <Tag\n      data-slot=\"bezel\"\n      data-size={size}\n      data-interactive={interactive ? \"\" : undefined}\n      className={cn(styles.shell, interactive && \"reveal-host\", classNames?.root, className)}\n      {...props}\n    >\n      <div data-slot=\"bezel-inner\" className={cn(styles.inner, classNames?.inner, innerClassName)}>\n        {children}\n      </div>\n      {footer && (\n        <div data-slot=\"bezel-footer\" className={cn(styles.footer, classNames?.footer)}>\n          {footer}\n        </div>\n      )}\n    </Tag>\n  );\n}\n\n/** An icon tile for a Bezel footer: a small bezel of its own. */\nexport function BezelIcon({ className, ...props }: ComponentProps<\"span\">) {\n  return <span data-slot=\"bezel-icon\" aria-hidden className={cn(styles.icon, className)} {...props} />;\n}\n\nexport default Bezel;\n"},{"path":"beamline/components/bezel/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/bezel/defaults.ts","content":"/** Pure allowed presentation defaults for Bezel; no component or runtime imports. */\nexport interface BezelDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\";\n}\n"},{"path":"beamline/components/card/card.module.css","type":"registry:file","target":"src/ui/beamline/components/card/card.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* The card is a size container: its padding, its header and where its media sits follow the room it is given (a\n     dashboard tile, a sidebar, a wide row), never the window. It also guards the containment trap: a contained box has\n     no width of its own, so a card left content-sized in a flex row takes a sensible width instead of collapsing. */\n  .root {\n    container-type: inline-size;\n    display: flex;\n    flex-direction: column;\n    min-width: min(16rem, 100%);\n  }\n\n  /* interactive: the card opens something. Pointed at, it lights one rung (a tint laid over the look's own fill, never\n     a shadow jump) at once and lets go over --pui-motion-out; pressed, it sinks a little at once; its media leans in\n     inside its clip. Reduced motion keeps the light and drops the movement. */\n  .root[data-interactive] {\n    cursor: pointer;\n    transition:\n      var(--m-transition),\n      box-shadow var(--pui-motion-out) var(--pui-ease-default),\n      scale var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .root[data-interactive]:is(:hover, :focus-within) {\n    box-shadow:\n      var(--m-surface-edge),\n      inset 0 0 0 100vmax color-mix(in oklab, var(--pui-ink) 3%, transparent);\n    transition-duration: 0ms;\n  }\n  .root[data-interactive]:active:not(:has(:is(button, [role=\"button\"], input, select, textarea):active)) {\n    scale: calc(1 - 0.012 * var(--pui-motion-travel, 1));\n    transition-duration: 0ms;\n  }\n  .root[data-interactive] .media > * {\n    transition: scale var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .root[data-interactive]:is(:hover, :focus-within) .media > * {\n    scale: calc(1 + 0.03 * var(--pui-motion-travel, 1));\n    transition-duration: var(--pui-motion-in);\n  }\n\n  /* Room steps for the padding, read by the parts inside (a container cannot query itself): tight below 20rem, the\n     card padding from there, the panel padding from 42rem. */\n  .room {\n    --_pad: var(--pui-space-3);\n  }\n  @container (width >= 20rem) {\n    .room {\n      --_pad: var(--pui-pad-card);\n    }\n  }\n  @container (width >= 42rem) {\n    .room {\n      --_pad: var(--pui-pad-panel);\n    }\n  }\n  .body {\n    padding: var(--_pad);\n  }\n\n  /* The header is a grid of the parts it has (React marks them): avatar · heading · aside (status and action). In a\n     card narrower than 20rem the status leaves the aside and sits under the title, so the title keeps the room (it\n     ranks first); the action stays at the end of the first line. */\n  .head {\n    display: grid;\n    grid-template-columns: [heading] minmax(0, 1fr);\n    align-items: start;\n    column-gap: var(--pui-space-3);\n  }\n  .head[data-aside] {\n    grid-template-columns: [heading] minmax(0, 1fr) [aside] auto;\n  }\n  .head[data-avatar] {\n    grid-template-columns: [avatar] auto [heading] minmax(0, 1fr);\n  }\n  .head[data-avatar][data-aside] {\n    grid-template-columns: [avatar] auto [heading] minmax(0, 1fr) [aside] auto;\n  }\n  .avatar {\n    grid-column: avatar;\n    grid-row: span 2;\n  }\n  .heading {\n    grid-column: heading;\n    grid-row: 1;\n  }\n  .aside {\n    display: flex;\n    grid-column: aside;\n    grid-row: 1;\n    align-items: center;\n    gap: var(--pui-space-2);\n  }\n  @container (width < 20rem) {\n    .aside {\n      display: contents;\n    }\n    .status {\n      grid-column: heading;\n      grid-row: 2;\n      justify-self: start;\n      margin-top: var(--pui-space-1-5);\n    }\n    .action {\n      grid-column: aside;\n      grid-row: 1;\n    }\n    .head[data-avatar]:not([data-action]) {\n      grid-template-columns: [avatar] auto [heading] minmax(0, 1fr);\n    }\n    .head:not([data-avatar]):not([data-action]) {\n      grid-template-columns: [heading] minmax(0, 1fr);\n    }\n  }\n  /* an action that waits for attention (revealAction): it keeps its place, unseen until the card is pointed at or\n     holds focus; on touch it shows */\n  /* above a link stretched over an interactive card, so the action stays its own target */\n  .action {\n    position: relative;\n    z-index: 1;\n    display: flex;\n  }\n\n  /* Media beside the body when the room is wide (layout auto, from 42rem; or always with layout row): the picture fills\n     its column at any height the body needs, cropped, never stretched. */\n  .frame {\n    display: flex;\n    flex: 1;\n    flex-direction: column;\n  }\n  @container (width >= 42rem) {\n    .frame[data-layout=\"auto\"][data-media] {\n      display: grid;\n      grid-template-columns: minmax(14rem, 40%) minmax(0, 1fr);\n    }\n  }\n  .frame[data-layout=\"row\"][data-media] {\n    display: grid;\n    grid-template-columns: minmax(10rem, 40%) minmax(0, 1fr);\n  }\n  @container (width >= 42rem) {\n    .frame[data-layout=\"auto\"][data-media] > .footer {\n      grid-column: 1 / -1;\n    }\n    .frame[data-layout=\"auto\"][data-media] > .media {\n      position: relative;\n      min-height: 10rem;\n    }\n    .frame[data-layout=\"auto\"][data-media] > .media > :is(img, video, canvas, picture) {\n      position: absolute;\n      inset: 0;\n      width: 100%;\n      height: 100%;\n      object-fit: cover;\n    }\n  }\n  .frame[data-layout=\"row\"][data-media] > .footer {\n    grid-column: 1 / -1;\n  }\n  .frame[data-layout=\"row\"][data-media] > .media {\n    position: relative;\n    min-height: 8rem;\n  }\n  .frame[data-layout=\"row\"][data-media] > .media > :is(img, video, canvas, picture) {\n    position: absolute;\n    inset: 0;\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n  }\n\n  /* The details toggle: quiet ink that brightens on hover (at once; it leaves over --pui-motion-out); its chevron turns\n     as the panel opens. */\n  .detailsTrigger {\n    display: inline-flex;\n    align-items: center;\n    gap: var(--pui-space-1);\n    justify-self: start;\n    margin-inline-start: calc(var(--pui-space-1) * -1);\n    padding-inline: var(--pui-space-1);\n    border-radius: var(--pui-radius-chip);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    font-weight: var(--pui-weight-medium);\n    transition: color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .detailsTrigger:hover {\n    color: var(--pui-ink);\n    transition-duration: 0ms;\n  }\n  .detailsChevron {\n    width: var(--pui-icon-sm);\n    height: var(--pui-icon-sm);\n    transition: rotate var(--pui-duration-standard) var(--pui-ease-default);\n  }\n  .detailsTrigger[data-state=\"open\"] .detailsChevron {\n    rotate: 180deg;\n  }\n\n  /* Nesting input (O1 §14.1): the header (when it sits in the top corners) and the footer tell the controls they hold\n     the card's radius and the padding next to its corners; the control clamps its own concentric radius. */\n  .corner,\n  .footer {\n    --pui-nest-radius: var(--pui-radius-card);\n  }\n  .corner {\n    --pui-nest-inset: var(--_pad);\n  }\n  .footer {\n    --pui-nest-inset: var(--pui-space-3);\n    display: flex;\n    flex-wrap: wrap;\n    align-items: center;\n    gap: var(--pui-space-2);\n    padding: var(--pui-space-3) var(--_pad);\n    border-top: 1px solid var(--pui-line-soft);\n  }\n}\n"},{"path":"beamline/components/card/card.tsx","type":"registry:file","target":"src/ui/beamline/components/card/card.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { Badge, type BadgeTone } from \"../badge/badge\";\nimport { Bezel } from \"../bezel/bezel\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"../collapsible/collapsible\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./card.module.css\";\n\nexport interface CardClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the header part; no override by default. */\n  header?: string;\n  /** Classes for the avatar part; no override by default. */\n  avatar?: string;\n  /** Classes for the heading part; no override by default. */\n  heading?: string;\n  /** Classes for the title part; no override by default. */\n  title?: string;\n  /** Classes for the description part; no override by default. */\n  description?: string;\n  /** Classes for the meta part; no override by default. */\n  meta?: string;\n  /** Classes for the actions part; no override by default. */\n  actions?: string;\n  /** Classes for the status part; no override by default. */\n  status?: string;\n  /** Classes for the details part; no override by default. */\n  details?: string;\n  /** Classes for the details trigger part; no override by default. */\n  detailsTrigger?: string;\n  /** Classes for the details chevron part; no override by default. */\n  detailsChevron?: string;\n  /** Classes for the details content part; no override by default. */\n  detailsContent?: string;\n  /** Classes for the body part; no override by default. */\n  body?: string;\n  /** Classes for the media part; no override by default. */\n  media?: string;\n  /** Classes for the frame footer part; no override by default. */\n  frameFooter?: string;\n  /** Classes for the footer part; no override by default. */\n  footer?: string;\n}\n\nexport interface CardProps extends Omit<ComponentProps<\"article\">, \"title\"> {\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: CardClassNames;\n  /** plain: one surface. bezel: a shell with the media set into an inset panel. */\n  frame?: \"plain\" | \"bezel\";\n  /** Heading shown above the content. Defaults to no heading. */\n  title?: ReactNode;\n  /** Supporting text beneath the label or heading. Defaults to no supporting text. */\n  description?: ReactNode;\n  /** An image, chart or preview on top. */\n  media?: ReactNode;\n  /** One control at the header's end: a menu, a button. */\n  action?: ReactNode;\n  /** An avatar or icon before the title. */\n  avatar?: ReactNode;\n  /** A short fact under the description: \"72% of plan\". */\n  meta?: ReactNode;\n  /** A status pill: text, or { label, tone }. */\n  status?: string | { label: string; tone?: BadgeTone };\n  /** Extra detail behind a \"Details\" toggle. */\n  details?: ReactNode;\n  /** Text on the details toggle. Defaults to \"Details\". */\n  detailsLabel?: string;\n  /** Controlled open state; omit to use defaultOpen. */\n  open?: boolean;\n  /** Initial open state when uncontrolled. Defaults to false. */\n  defaultOpen?: boolean;\n  /** Reports open state after a change; no callback by default. */\n  onOpenChange?: (open: boolean) => void;\n  /** Free content under the header. */\n  children?: ReactNode;\n  /** Actions or a summary line at the bottom, set off by a hairline seam inside the same frame. */\n  footer?: ReactNode;\n  /** The title's heading level, so cards nest under the page's own headings (2 directly under a page title). */\n  headingLevel?: 2 | 3 | 4 | 5 | 6;\n  /** Where media sits: on top (stack), beside the body (row), or by the room (auto: beside it from 42rem). */\n  layout?: \"auto\" | \"stack\" | \"row\";\n  /** The action waits for attention: unseen until the card is pointed at or holds focus (always shown on touch). For\n   *  grids of cards where a menu on every one is noise. */\n  revealAction?: boolean;\n  /** The whole card opens something: it lights a rung under the pointer, presses in, and its media leans in. Put the\n   *  link or button inside (a title link stretched over the card is the usual way); its focus ring marks the card. */\n  interactive?: boolean;\n}\n\n/** A record or a small group of related content and actions. Everything is optional: give it a title and it is a\n *  header card; add media, a status, details or your own children as the content needs. */\nexport function Card({\n  frame = \"plain\",\n  title,\n  description,\n  media,\n  action,\n  avatar,\n  meta,\n  status,\n  details,\n  detailsLabel: detailsLabelProp,\n  open,\n  defaultOpen,\n  onOpenChange,\n  children,\n  footer,\n  headingLevel = 3,\n  layout = \"auto\",\n  revealAction = false,\n  interactive = false,\n  className,\n  classNames,\n  ...props\n}: CardProps) {\n  const i18n = useMessages(\"card\", englishMessages);\n  const detailsLabel = detailsLabelProp === undefined ? i18n(\"details\") : detailsLabelProp;\n\n  const Title = `h${headingLevel}` as \"h3\";\n  const pill = typeof status === \"string\" ? { label: status, tone: \"neutral\" as BadgeTone } : status;\n  const head = (title || description || action || avatar || meta || pill) && (\n    <div\n      data-slot=\"card-header\"\n      data-avatar={avatar ? \"\" : undefined}\n      data-aside={pill || action ? \"\" : undefined}\n      data-action={action ? \"\" : undefined}\n      className={cn(styles.head, frame === \"plain\" && !media && styles.corner, classNames?.header)}\n    >\n      {avatar && (\n        <span data-slot=\"card-avatar\" className={cn(styles.avatar, classNames?.avatar)}>\n          {avatar}\n        </span>\n      )}\n      <div data-slot=\"card-heading\" className={cn(styles.heading, \"grid gap-0.5\", classNames?.heading)}>\n        {title && (\n          <Title\n            data-slot=\"card-title\"\n            className={cn(\"truncate text-base leading-snug font-medium text-ink\", classNames?.title)}\n          >\n            {title}\n          </Title>\n        )}\n        {description && (\n          <p data-slot=\"card-description\" className={cn(\"text-sm leading-snug text-ink-3\", classNames?.description)}>\n            {description}\n          </p>\n        )}\n        {meta && (\n          <p data-slot=\"card-meta\" className={cn(\"mt-1 font-numeric text-xs text-ink-2\", classNames?.meta)}>\n            {meta}\n          </p>\n        )}\n      </div>\n      {(pill || action) && (\n        <div data-slot=\"card-actions\" className={cn(styles.aside, classNames?.actions)}>\n          {pill && (\n            <Badge tone={pill.tone ?? \"neutral\"} size=\"sm\" className={cn(styles.status, classNames?.status)}>\n              {pill.label}\n            </Badge>\n          )}\n          {action && (\n            <span data-slot=\"card-action\" className={cn(styles.action, revealAction && \"reveal\")}>\n              {action}\n            </span>\n          )}\n        </div>\n      )}\n    </div>\n  );\n  const body = (\n    <>\n      {head}\n      {children}\n      {details && (\n        <Collapsible open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange} className={classNames?.details}>\n          <CollapsibleTrigger\n            data-slot=\"card-details-trigger\"\n            className={cn(styles.detailsTrigger, classNames?.detailsTrigger)}\n          >\n            {detailsLabel}\n            <ChevronDown\n              aria-hidden\n              data-slot=\"card-details-chevron\"\n              className={cn(styles.detailsChevron, classNames?.detailsChevron)}\n            />\n          </CollapsibleTrigger>\n          <CollapsibleContent>\n            <div data-slot=\"card-details-content\" className={cn(\"pt-2\", classNames?.detailsContent)}>\n              {details}\n            </div>\n          </CollapsibleContent>\n        </Collapsible>\n      )}\n    </>\n  );\n  if (frame === \"bezel\") {\n    return (\n      <Bezel\n        as=\"article\"\n        data-slot=\"card\"\n        data-frame=\"bezel\"\n        interactive={interactive}\n        className={cn(\"reveal-host @container min-w-[min(16rem,100%)]\", classNames?.root, className)}\n        classNames={{ inner: classNames?.media, footer: classNames?.frameFooter }}\n        innerClassName={cn(!media && \"p-[var(--pui-pad-card)]\")}\n        footer={\n          <div data-slot=\"card-body\" className={cn(\"grid gap-3 px-1.5 pt-1 pb-1\", classNames?.body)}>\n            {body}\n            {footer && (\n              <div\n                data-slot=\"card-footer\"\n                className={cn(\n                  \"-mx-1.5 flex flex-wrap items-center gap-2 border-t border-line-soft px-1.5 pt-2.5\",\n                  classNames?.footer,\n                )}\n              >\n                {footer}\n              </div>\n            )}\n          </div>\n        }\n        {...props}\n      >\n        {media}\n      </Bezel>\n    );\n  }\n  return (\n    <article\n      data-slot=\"card\"\n      data-frame=\"plain\"\n      data-interactive={interactive ? \"\" : undefined}\n      className={cn(styles.root, \"m-surface reveal-host overflow-hidden rounded-card\", classNames?.root, className)}\n      {...props}\n    >\n      <div\n        data-slot=\"card-frame\"\n        data-layout={layout}\n        data-media={media ? \"\" : undefined}\n        className={cn(styles.frame, styles.room)}\n      >\n        {media && (\n          <div data-slot=\"card-media\" className={cn(styles.media, \"overflow-hidden\", classNames?.media)}>\n            {media}\n          </div>\n        )}\n        {/* stretched to its row (equal-height cards): content stays at the top, the footer at the bottom (trial t11) */}\n        <div data-slot=\"card-body\" className={cn(styles.body, \"grid flex-1 content-start gap-3\", classNames?.body)}>\n          {body}\n        </div>\n        {footer && (\n          <div data-slot=\"card-footer\" className={cn(styles.footer, classNames?.footer)}>\n            {footer}\n          </div>\n        )}\n      </div>\n    </article>\n  );\n}\n\nexport default Card;\n"},{"path":"beamline/components/card/messages.ts","type":"registry:file","target":"src/ui/beamline/components/card/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy; stable keys are the public translation API. */\nexport const english = {\n  details: \"Details\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/collapsible/collapsible.module.css","type":"registry:file","target":"src/ui/beamline/components/collapsible/collapsible.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* The region opens to the content's measured height over --pui-duration-standard and closes over --pui-motion-out;\n     the content fades with it (in over the opening, out a little faster than the height), so the words never sit\n     clipped mid-line while the box moves. Reduced motion makes both instant (every duration token is 1 ms). */\n  .content {\n    overflow: hidden;\n  }\n  .content[data-state=\"open\"] {\n    animation: open var(--pui-duration-standard) var(--pui-ease-default);\n  }\n  .content[data-state=\"closed\"] {\n    animation: close var(--pui-motion-out) var(--pui-ease-default);\n  }\n  @keyframes open {\n    from {\n      height: 0;\n      opacity: 0;\n    }\n    to {\n      height: var(--radix-collapsible-content-height, auto);\n      opacity: 1;\n    }\n  }\n  @keyframes close {\n    from {\n      height: var(--radix-collapsible-content-height, auto);\n      opacity: 1;\n    }\n    60% {\n      opacity: 0;\n    }\n    to {\n      height: 0;\n      opacity: 0;\n    }\n  }\n}\n"},{"path":"beamline/components/collapsible/collapsible.tsx","type":"registry:file","target":"src/ui/beamline/components/collapsible/collapsible.tsx","content":"\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport * as RadixCollapsible from \"@radix-ui/react-collapsible\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./collapsible.module.css\";\n\n/** One region that opens and closes in place. Controlled (open, onOpenChange) or uncontrolled (defaultOpen). */\nexport function Collapsible(props: ComponentProps<typeof RadixCollapsible.Root>) {\n  return <RadixCollapsible.Root data-slot=\"collapsible\" {...props} />;\n}\n\n/** The control that toggles it; pass asChild to use a Button. */\nexport function CollapsibleTrigger(props: ComponentProps<typeof RadixCollapsible.Trigger>) {\n  return <RadixCollapsible.Trigger data-slot=\"collapsible-trigger\" {...props} />;\n}\n\n/** The region. Its height follows the content opening and closing and the content fades with it; with reduced motion\n *  it switches at once. */\nexport function CollapsibleContent({ className, ...props }: ComponentProps<typeof RadixCollapsible.Content>) {\n  return (\n    <RadixCollapsible.Content data-slot=\"collapsible-content\" {...props} className={cn(styles.content, className)} />\n  );\n}\n\nexport default Collapsible;\n"},{"path":"beamline/lib/utils.ts","type":"registry:file","target":"src/ui/beamline/lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n/* The type roles (utilities.css) and the 2xs step are sizes, not text colours: without this, tailwind-merge reads\n   `text-overline` as a colour and drops it next to `text-ink-3`. A role and a size step still replace each other.\n   Likewise `font-title` is the look's title weight, not a family: read as a family it dropped `font-display` beside it,\n   and every merged page title lost the look's display face. */\nconst twMerge = extendTailwindMerge({\n  extend: {\n    classGroups: {\n      \"font-weight\": [{ font: [\"title\"] }],\n      \"font-family\": [{ font: [\"display\", \"numeric\", \"figure\"] }],\n      \"font-size\": [\n        {\n          text: [\n            \"2xs\",\n            \"display\",\n            \"title\",\n            \"heading\",\n            \"subheading\",\n            \"body\",\n            \"body-sm\",\n            \"reading\",\n            \"label\",\n            \"caption\",\n            \"overline\",\n            \"numeric\",\n            \"code\",\n          ],\n        },\n      ],\n    },\n  },\n});\n\n/** Merge class names, letting later Tailwind utilities win over earlier ones. */\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\n/** A small seeded random generator (LCG): the same seed gives the same sequence, for stable sample data and jitter. */\nexport function rng(seed = 1) {\n  let s = seed >>> 0 || 1;\n  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 2 ** 32;\n}\n\nexport const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n"}],"docs":"Card is in src/ui/beamline/components/card/card.tsx. The source is in src/ui/beamline (ui/beamline in a project without src/). After Tailwind, import its styles/beamline.css once, then styles/look-prism.css in your app stylesheet (use relative paths). Keep the shared base before all chosen looks. Wrap your app in <UIRoot preset={prism}> with UIRoot from its runtime/appearance and prism from its looks/prism.  shadcn replaces files when asked; use the Beamline managed-source CLI from the start for three-way upgrades that preserve edits.","meta":{"free":true,"version":"0.2.0-rc.42","library":"https://beamline.io/components/card"}}