// Onrail UI — React primitive layer over brand/components.css.
//
// Each primitive is a thin wrapper: composes a className, spreads extra
// props, forwards refs. NO new visual logic — all paint comes from the
// CSS layer. Adding a new primitive here without a backing CSS class is
// a design-system smell.
//
// Load as:
//   <script type="text/babel" src="lib/onrail-ui.jsx"></script>
//
// Components are exported to `window` so other Babel-transpiled scripts
// in the same page can use them (each <script type="text/babel"> gets
// its own scope after Babel transpilation).

const { forwardRef } = React;

// Merge className strings, dropping nullish.
const cn = (...xs) => xs.filter(Boolean).join(" ");

// ──────────────────────────────────────────────────────────────
// TYPE
// ──────────────────────────────────────────────────────────────

// Sans headline. <Display size="sm|md|lg|xl" /> — md is default.
// `color` is an inline-style escape hatch for non-default text color
// (mirrors <Signal>, <Num>, <Money>).
const Display = forwardRef(function Display(
  { as: As = "h1", size = "md", color, className, style, ...rest }, ref
) {
  return <As ref={ref}
    className={cn("display", size !== "md" && `display-${size}`, className)}
    style={color ? { color, ...style } : style}
    {...rest} />;
});

// Running text. <Body lede /> for the larger marketing/hero subhead.
// <Body size="sm" /> for the smaller dense-dashboard label
// (13.5px, --text color — primary content inside panel bodies).
// `color` is an inline-style escape hatch for non-default text color.
const Body = forwardRef(function Body(
  { as: As = "p", lede, size, color, className, style, ...rest }, ref
) {
  return <As ref={ref}
    className={cn("body", lede && "body-lede", size && `body-${size}`, className)}
    style={color ? { color, ...style } : style}
    {...rest} />;
});

// Mono caps mini-label.
//   <Eyebrow tone="muted|faint|brand" />   - neutral ladder or semantic accent
//   <Eyebrow rule>SECTION</Eyebrow>        - 22px hairline kicker
// `color` is an inline-style escape hatch for arbitrary tokens.
const Eyebrow = forwardRef(function Eyebrow(
  { tone, rule, color, className, style, ...rest }, ref
) {
  return <span ref={ref}
    data-tone={tone}
    className={cn("eyebrow", rule && "eyebrow-rule", className)}
    style={color ? { color, ...style } : style}
    {...rest} />;
});

// Tabular mono number. <Num size="sm|md|lg|xl" weight="regular" color="..." />
// `weight="regular"` drops to 400 for dashboard meta-text (default 500).
// `color` is an inline-style escape hatch (accepts any CSS color or token,
// e.g. "var(--text-faint)", "var(--brand)", "#fff") — mirrors the
// pattern on <Signal>. Default is full --text via .num CSS.
const Num = forwardRef(function Num(
  { as: As = "span", size = "md", weight, color, className, style, ...rest }, ref
) {
  return (
    <As
      ref={ref}
      className={cn("num", `num-${size}`, weight === "regular" && "num-regular", className)}
      style={color ? { color, ...style } : style}
      {...rest}
    />
  );
});

// Mono separator dot — `·` in --text-ghost. Use between items in a
// meta cluster (livedot + eyebrow · count, footer meta segments).
//   <Dot />              — default tone "ghost"
//   <Dot tone="faint" />
const Dot = forwardRef(function Dot(
  { tone, color, className, style, ...rest }, ref
) {
  return (
    <span
      ref={ref}
      aria-hidden="true"
      data-tone={tone}
      className={cn("dot", className)}
      style={color ? { color, ...style } : style}
      {...rest}
    >·</span>
  );
});

// ──────────────────────────────────────────────────────────────
// SIGNAL — bold colored mono text (type chips, deltas, status)
// ──────────────────────────────────────────────────────────────

// Inline colored mono text — deltas, percent strings, signals next
// to a number. For pill-shaped status/type indicators use <Chip>.
// tone:   "up" | "down" | "warn" | "flat" | "brand"   (drives color)
// color:  explicit override — pass a token like "var(--chart-2)" for arbitrary hues
const Signal = forwardRef(function Signal(
  { tone, color, className, style, ...rest }, ref
) {
  return (
    <span
      ref={ref}
      data-tone={tone}
      className={cn("signal", className)}
      style={color ? { color, ...style } : style}
      {...rest}
    />
  );
});

// ──────────────────────────────────────────────────────────────
// TAG — small all-caps mono type label
// ──────────────────────────────────────────────────────────────
// Lightweight alternative to <Chip> for dense data contexts (table
// rows, activity feeds) where a filled pill would dominate. Three
// primitives now cover the color-signaling space:
//   <Signal tone=>     semantic financial indicator (up/down/warn/flat/brand)
//   <Tag color=>       lightweight type category (text only, no pill)
//   <Chip color=>      filled type/status pill (tinted bg + border)
//
// API:
//   <Tag>UNCATEGORIZED</Tag>                      — default text color
//   <Tag color="var(--chart-2)">FUNDED</Tag>      — palette accent
//   <Tag color="var(--brand-fg)">LIVE</Tag>       — brand emphasis (theme-aware)
//
// No tone prop. Type categories are arbitrary colors from the chart
// palette; the semantic tone vocabulary (up/down/warn) doesn't apply.
const Tag = forwardRef(function Tag(
  { color, className, style, ...rest }, ref
) {
  return (
    <span
      ref={ref}
      className={cn("tag", className)}
      style={color ? { color, ...style } : style}
      {...rest}
    />
  );
});

// ──────────────────────────────────────────────────────────────
// SURFACES
// ──────────────────────────────────────────────────────────────

// Hairline-bordered card.
//
//   surface  "default" (solid --surface-panel) | "glass" (translucent
//            surface-deep + blur + heavy soft shadow, for floating
//            marketing/auth panels)
//   density  "md" (default — compact, dashboard widgets) | "lg" (more
//            interior breathing room, for marketing/auth surfaces).
//            Cascades to .panel-head / .panel-body / .panel-foot.
//   overflow "hidden" → clips body content to the rounded corners.
//            Opt-in because most panels don't need it (auth/marketing
//            surfaces want their shadow to extend past the edge). Pass
//            when the body holds full-bleed media — maps, tables,
//            scrollable lists — so it tucks cleanly under the radius.
const Panel = forwardRef(function Panel(
  { surface, density, overflow, className, ...rest }, ref
) {
  return (
    <div
      ref={ref}
      className={cn(
        "panel",
        surface === "glass" && "panel-glass",
        density === "lg" && "panel-lg",
        overflow === "hidden" && "panel-clip",
        className
      )}
      {...rest}
    />
  );
});

// Panel sub-parts — auto-handle padding + hairline divider. Compose:
//   <Panel>
//     <PanelHead>...</PanelHead>                       // left/right slots
//     <PanelHead title="…" subtitle="…" />             // stacked heading shorthand
//     <PanelBody>...</PanelBody>                       // grows to fill (flex:1)
//     <PanelFoot>...</PanelFoot>
//   </Panel>
//
// PanelHead is a flex row (space-between). Pass two children for the
// dashboard-style "label + meta" head, OR pass `title`/`subtitle` for
// the marketing-style "stacked heading + lede" head. Both forms can
// coexist: title+subtitle render as the left child, any extra children
// render to the right.
//
// `flat` drops the bottom hairline — use when the head reads as part
// of the same surface as the body (full-bleed map/chart/imagery panels),
// so the tile looks like one continuous panel rather than head ┃ body.
const PanelHead = forwardRef(function PanelHead(
  { flat, className, title, subtitle, children, ...rest }, ref
) {
  const heading = (title || subtitle) && (
    <div className="panel-head-titles">
      {title && <h2 className="panel-head-title">{title}</h2>}
      {subtitle && <p className="panel-head-subtitle">{subtitle}</p>}
    </div>
  );
  return (
    <div ref={ref} className={cn("panel-head", flat && "panel-head-flat", className)} {...rest}>
      {heading}
      {children}
    </div>
  );
});
const PanelBody = forwardRef(function PanelBody(
  { stack, className, style, ...rest }, ref
) {
  // `stack` shorthand: turns PanelBody into a flex column with gap.
  // Removes the per-child marginBottom / wrapper-div pattern that
  // crops up in any panel whose body is a vertical stack of widgets.
  //   stack         → flex column, gap 12  (the .panel-body-stack default)
  //   stack={16}    → flex column, gap 16  (override via custom property)
  //
  // Layout lives in .panel-body-stack (brand/components.css); the
  // component only adds the class and, for a custom gap, tunnels the
  // value through the --panel-body-stack-gap CSS custom property.
  const stackStyle = (typeof stack === "number")
    ? { "--panel-body-stack-gap": `${stack}px`, ...style }
    : style;
  return (
    <div
      ref={ref}
      className={cn("panel-body", stack && "panel-body-stack", className)}
      style={stackStyle}
      {...rest}
    />
  );
});
const PanelFoot = forwardRef(function PanelFoot(
  { className, ...rest }, ref
) {
  return <div ref={ref} className={cn("panel-foot", className)} {...rest} />;
});

// Divided list item — 2-col grid (content | meta). Used by activity
// feeds, reserve/borrower lists, anything hairline-divided that ISN'T
// tabular data (use <Table> for that).
const ListItem = forwardRef(function ListItem(
  { as: As = "div", className, ...rest }, ref
) {
  return <As ref={ref} className={cn("list-item", className)} {...rest} />;
});

// Stacked list item — label/hint on top, value full-width below.
// Sibling to <ListItem>; same hairline-divided rhythm but vertical
// layout. Use for property/details panels (loan details, transaction
// details) where values can be long-form (addresses, hashes,
// descriptive text) so a 2-col layout would crowd them.
//
//   <StackedItem label="Origination date">
//     <Num size="sm">Aug 12, 2025</Num>
//   </StackedItem>
//
//   <StackedItem label="Property" hint="FL">
//     <Body as="span" size="sm">4322 Washington St, Orlando, FL 32801</Body>
//   </StackedItem>
//
// label   string → wraps in <Eyebrow tone="faint">. Pass a ReactNode
//         for full control of the label area.
// hint    optional inline supplement to the label (e.g. unit, state
//         code) — string wraps in <Num size="sm" weight="regular">,
//         ReactNode passes through.
// children   the value content (any shape: hash, num, money, body…).

const StackedItem = forwardRef(function StackedItem(
  { as: As = "div", label, hint, className, children, ...rest }, ref
) {
  const labelNode = typeof label === "string"
    ? <Eyebrow tone="faint">{label}</Eyebrow>
    : label;
  const hintNode = typeof hint === "string"
    ? <Num size="sm" weight="regular" color="var(--text-faint)">{hint}</Num>
    : hint;
  return (
    <As ref={ref} className={cn("stacked-item", className)} {...rest}>
      <div className="stacked-item-label">
        {labelNode}
        {hintNode}
      </div>
      <div className="stacked-item-value">{children}</div>
    </As>
  );
});

// ──────────────────────────────────────────────────────────────
// FORM
// ──────────────────────────────────────────────────────────────

// Field wrapper — pair with a <input>, optional <Icon> / suffix slot
// as children. Focus state is on the wrapper (`:focus-within`).
const Field = forwardRef(function Field(
  { className, ...rest }, ref
) {
  return <div ref={ref} className={cn("field", className)} {...rest} />;
});

// ──────────────────────────────────────────────────────────────
// BUTTONS
// ──────────────────────────────────────────────────────────────

// Primary green by default. variant is a single string:
//   "neutral" | "danger" | "warn" | "info"          — filled
//   "ghost-neutral" | "ghost-up" | "ghost-danger"
//   | "ghost-warn" | "ghost-info"                   — outlined
//   "ghost" is accepted as an alias for "ghost-neutral".
// size="sm" for the smaller header pill, block for full-width.
// `loading` hides the label and shows a centered spinner; while
// loading, the button is also disabled (loading implies disabled).
const Button = forwardRef(function Button(
  { variant, size, block, loading, disabled, className, type = "button", ...rest }, ref
) {
  // Decompose variant into outlined-or-not and a color suffix.
  // "ghost-danger" → ghost + danger; "ghost-neutral" / "ghost" → ghost only.
  const isGhost = typeof variant === "string" && variant.startsWith("ghost");
  let color = isGhost ? variant.slice(5).replace(/^-/, "") : variant;
  // ghost-neutral is just ghost — no extra color class to layer on.
  if (isGhost && (color === "" || color === "neutral")) color = "";
  return (
    <button
      ref={ref}
      type={type}
      disabled={disabled || loading}
      className={cn(
        "btn",
        isGhost && "btn-ghost",
        color && `btn-${color}`,
        size && `btn-${size}`,
        block && "btn-block",
        loading && "btn-loading",
        className,
      )}
      {...rest}
    />
  );
});

// ──────────────────────────────────────────────────────────────
// BITS
// ──────────────────────────────────────────────────────────────

// Pulsing brand dot. Lives in base.css as .live-dot.
const LiveDot = ({ size = 8, className, style, ...rest }) => (
  <span
    className={cn("live-dot", className)}
    style={{ width: size, height: size, ...style }}
    {...rest}
  />
);

// USDC mark — Circle's two-tone treatment. Always blue+white regardless
// of theme (we put a solid white circle behind the path; the path uses
// even-odd fill so the $ + side arcs cut through as holes).
const USDCMark = ({ size = 14, color = "var(--usdc)", className, style, ...rest }) => (
  <svg
    width={size} height={size} viewBox="0 0 32 32"
    aria-label="USDC" role="img"
    className={cn("usdc-mark", className)}
    style={style}
    {...rest}
  >
    <circle cx="16" cy="16" r="16" fill="#FFFFFF" />
    <path
      fillRule="evenodd"
      fill={color}
      d="M16 0c8.837 0 16 7.163 16 16s-7.163 16-16 16S0 24.837 0 16 7.163 0 16 0zm3.352 5.56c-.244-.12-.488 0-.548.243-.061.061-.061.122-.061.243v.85l.01.104a.86.86 0 00.355.503c4.754 1.7 7.192 6.98 5.424 11.653-.914 2.55-2.925 4.491-5.424 5.402-.244.121-.365.303-.365.607v.85l.005.088a.45.45 0 00.36.397c.061 0 .183 0 .244-.06a10.895 10.895 0 007.13-13.717c-1.096-3.46-3.778-6.07-7.13-7.162zm-6.46-.06c-.061 0-.183 0-.244.06a10.895 10.895 0 00-7.13 13.717c1.096 3.4 3.717 6.01 7.13 7.102.244.121.488 0 .548-.243.061-.06.061-.122.061-.243v-.85l-.01-.08c-.042-.169-.199-.362-.355-.466-4.754-1.7-7.192-6.98-5.424-11.653.914-2.55 2.925-4.491 5.424-5.402.244-.121.365-.303.365-.607v-.85l-.005-.088a.45.45 0 00-.36-.397zm3.535 3.156h-.915l-.088.008c-.2.04-.346.212-.4.478v1.396l-.207.032c-1.708.304-2.778 1.483-2.778 2.942 0 2.002 1.218 2.791 3.778 3.095 1.707.303 2.255.668 2.255 1.639 0 .97-.853 1.638-2.011 1.638-1.585 0-2.133-.667-2.316-1.578-.06-.242-.244-.364-.427-.364h-1.036l-.079.007a.413.413 0 00-.347.418v.06l.033.18c.29 1.424 1.266 2.443 3.197 2.734v1.457l.008.088c.04.198.213.344.48.397h.914l.088-.008c.2-.04.346-.212.4-.477V21.34l.207-.04c1.713-.362 2.84-1.601 2.84-3.177 0-2.124-1.28-2.852-3.84-3.156-1.829-.243-2.194-.728-2.194-1.578 0-.85.61-1.396 1.828-1.396 1.097 0 1.707.364 2.011 1.275a.458.458 0 00.427.303h.975l.079-.006a.413.413 0 00.348-.419v-.06l-.037-.173a3.04 3.04 0 00-2.706-2.316V9.142l-.008-.088c-.04-.199-.213-.345-.48-.398z"
    />
  </svg>
);

// Onrail mark — 4 stacked bars, top 2 in `top` color, bottom 2 in `accent`.
// Defaults to currentColor + brand mint so it inherits theme.
// Bars are centered in the viewBox (7px padding top + bottom) so the
// visible mark optically aligns with adjacent text in the wordmark.
const Mark = ({ size = 24, top = "currentColor", accent = "var(--brand)", className, style, ...rest }) => (
  <svg width={size} height={size} viewBox="0 0 56 56"
    className={cn("mark", className)}
    style={style}
    aria-hidden="true"
    {...rest}
  >
    <rect x="6" y="7"  width="44" height="6" rx="1" fill={top}/>
    <rect x="6" y="19" width="44" height="6" rx="1" fill={top}/>
    <rect x="6" y="31" width="44" height="6" rx="1" fill={accent}/>
    <rect x="6" y="43" width="44" height="6" rx="1" fill={accent}/>
  </svg>
);

// Onrail wordmark — text label + mark in a tight lockup.
//   <Wordmark />                          → "Onrail" + mark
//   <Wordmark label="Underwriter" />      → "Underwriter" + mark
//   <Wordmark size={28} />                → scale up
//
// color drives the text + top two mark bars; accent drives the bottom two.
//
// The mark is translated down by a small px amount to optically center
// with the text (the text's empty descender space pulls its visual
// center upward — see _wordmarkMarkNudge). Pass `nudge={false}` to
// disable, or override entirely with style on the parent.
const _wordmarkMarkNudge = (size) =>
  size < 13 ? 0 :
  size < 47 ? 1 :
                3;

const Wordmark = ({
  size = 20, color, accent, label = "Onrail",
  nudge = true, className, style, ...rest
}) => {
  const off = nudge ? _wordmarkMarkNudge(size) : 0;
  return (
    <div
      className={cn("wordmark", className)}
      style={{
        "--wordmark-size": `${size}px`,
        ...(color != null ? { color } : null),
        ...style,
      }}
      {...rest}
    >
      <span>{label}</span>
      <Mark
        size={size}
        top={color}
        accent={accent}
        style={off ? { transform: `translateY(${off}px)` } : undefined}
      />
    </div>
  );
};

// ──────────────────────────────────────────────────────────────
// AUTH PROVIDER ICONS — Google, Apple, X, Farcaster
// ──────────────────────────────────────────────────────────────
//
// All inline SVG — no external dependencies. Default mode is monochrome
// (currentColor) so they sit cleanly inside ProviderButton's ghost shell.
// GoogleIcon optionally renders the official 4-color treatment via
// `mono={false}` — use that on light hero panels where the brand color
// is part of the trust signal.

function GoogleIcon({ size = 18, mono = true }) {
  if (mono) {
    return (
      <svg width={size} height={size} viewBox="0 0 18 18" fill="currentColor" aria-hidden="true">
        <path d="M9 7.5v3h4.2c-.18 1.08-1.32 3.18-4.2 3.18A4.68 4.68 0 0 1 4.32 9 4.68 4.68 0 0 1 9 4.32c1.5 0 2.52.66 3.1 1.22l2.1-2.04C12.9 2.34 11.1 1.5 9 1.5A7.5 7.5 0 1 0 9 16.5c4.32 0 7.2-3.03 7.2-7.3 0-.48-.06-.84-.12-1.2H9Z"/>
      </svg>
    );
  }
  return (
    <svg width={size} height={size} viewBox="0 0 18 18" aria-hidden="true">
      <path d="M16.5 9.2c0-.6-.06-1.16-.16-1.7H9v3.22h4.2c-.18.96-.74 1.78-1.6 2.32v1.92h2.58c1.5-1.4 2.32-3.42 2.32-5.76Z" fill="#4285F4"/>
      <path d="M9 16.5c2.16 0 3.96-.72 5.28-1.94l-2.58-1.92c-.72.48-1.62.76-2.7.76-2.08 0-3.84-1.4-4.46-3.28H1.86v2.06A7.5 7.5 0 0 0 9 16.5Z" fill="#34A853"/>
      <path d="M4.54 10.12a4.5 4.5 0 0 1 0-2.86V5.2H1.86a7.5 7.5 0 0 0 0 6.74l2.68-1.82Z" fill="#FBBC05"/>
      <path d="M9 4.34c1.18 0 2.22.4 3.04 1.2l2.28-2.28A7.2 7.2 0 0 0 9 1.5 7.5 7.5 0 0 0 1.86 5.2l2.68 2.06C5.16 5.74 6.92 4.34 9 4.34Z" fill="#EA4335"/>
    </svg>
  );
}

function AppleIcon({ size = 18 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 18 18" fill="currentColor" aria-hidden="true">
      <path d="M13.4 9.5c-.02-1.9 1.55-2.82 1.62-2.86-.88-1.3-2.26-1.48-2.76-1.5-1.18-.12-2.3.7-2.9.7-.6 0-1.52-.68-2.5-.66C5.6 5.2 4.4 5.9 3.75 7c-1.32 2.3-.34 5.7.95 7.56.64.92 1.4 1.94 2.4 1.9.96-.04 1.32-.62 2.48-.62 1.16 0 1.48.62 2.5.6 1.04-.02 1.7-.92 2.32-1.84.74-1.06 1.04-2.1 1.06-2.16-.02-.02-2.04-.78-2.06-3.1ZM11.5 3.9c.52-.64.88-1.52.78-2.4-.76.04-1.68.52-2.22 1.14-.48.56-.92 1.46-.8 2.32.84.06 1.7-.42 2.24-1.06Z"/>
    </svg>
  );
}

function XIcon({ size = 16 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
      <path d="M12.4 1.5h2.34l-5.12 5.85L15.66 14.5h-4.7L7.3 9.7l-4.2 4.8H.76l5.48-6.26L.34 1.5H5.16l3.32 4.4 3.92-4.4Zm-.82 11.6h1.3L4.5 2.82H3.1l8.48 10.28Z"/>
    </svg>
  );
}

function FarcasterIcon({ size = 18 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 18 18" fill="none" aria-hidden="true">
      <path d="M4 3h10v2H4V3Zm0 4h10M4 11h10M4 15h10" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/>
      <path d="M3 5l1 6m11-6l-1 6" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/>
    </svg>
  );
}

function WalletGlyph({ size = 16 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M19 11V7a2 2 0 0 0-2-2H5a2 2 0 0 0 0 4h14a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7"/>
      <circle cx="16" cy="13" r="1.5" fill="currentColor"/>
    </svg>
  );
}

function MailGlyph({ size = 14 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <rect x="1.5" y="3" width="11" height="8" rx="1"/>
      <path d="M1.5 4.5l5.5 3.5 5.5-3.5"/>
    </svg>
  );
}

function LockGlyph({ size = 14 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <rect x="2.5" y="6.5" width="9" height="6" rx="1"/>
      <path d="M4.5 6.5V4.5a2.5 2.5 0 0 1 5 0v2"/>
    </svg>
  );
}

// House glyph — outlined roof + walls + door. For property /
// collateral contexts (watermark on property hero cards, inline icon
// on a "physical asset" trust signal). Stroke only so it inherits
// currentColor cleanly. Use larger sizes (32–96) for hero watermarks.
function HouseGlyph({ size = 16 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
      stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M3 11l9-7 9 7" />
      <path d="M5 9.5V20h14V9.5" />
      <path d="M10 20v-6h4v6" />
    </svg>
  );
}

// Small caret/arrow glyph — used by <FieldAction> as the default
// suffix icon (e.g. inline "Continue →" inside an email field).
function CaretRight({ size = 12 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" aria-hidden="true">
      <path d="M2.5 6h7m-2.5-2.5L9.5 6 7 8.5" />
    </svg>
  );
}

// FIELD ACTION — subtle inline button used as a suffix slot inside a
// <Field> (e.g. "Continue →" inside an email input). Borderless,
// muted-text, brightens on hover. Auto-appends a CaretRight glyph;
// pass `icon={<X />}` for a custom one, or `icon={false}` for none.

const FieldAction = forwardRef(function FieldAction(
  { children, icon, type = "button", className, ...rest }, ref
) {
  return (
    <button
      ref={ref}
      type={type}
      className={cn("field-action", className)}
      {...rest}
    >
      {children}
      {icon === false ? null : (icon || <CaretRight />)}
    </button>
  );
});

// ARROW LINK — anchor-shaped twin of <FieldAction>. Subtle navigation
// link used in dashboard card heads ("View all →", "View loans →").
// Auto-appends a CaretRight glyph; pass `icon={<X />}` for a custom one,
// or `icon={false}` for none. Shares chrome with .field-action via a
// joint CSS rule, so hover/focus stay in lockstep.
//
//   <ArrowLink href="loans.html">View loans</ArrowLink>
//   <ArrowLink href="#" icon={false}>Read more</ArrowLink>

const ArrowLink = forwardRef(function ArrowLink(
  { href = "#", children, icon, className, ...rest }, ref
) {
  return (
    <a
      ref={ref}
      href={href}
      className={cn("arrow-link", className)}
      {...rest}
    >
      {children}
      {icon === false ? null : (icon || <CaretRight />)}
    </a>
  );
});

// ──────────────────────────────────────────────────────────────
// PROVIDER BUTTON — square ghost button for OAuth/wallet providers
// ──────────────────────────────────────────────────────────────
//
// <ProviderButton provider="google" />
// <ProviderButton icon={<WalletGlyph />} aria-label="Connect wallet" />
//
// Provider shortcuts: "google" / "apple" / "x" / "farcaster" auto-pick
// the right icon. Pass `icon` explicitly for anything else.

const PROVIDER_ICONS = {
  google:    GoogleIcon,
  apple:     AppleIcon,
  x:         XIcon,
  farcaster: FarcasterIcon,
};

const ProviderButton = forwardRef(function ProviderButton(
  { provider, icon, mono = true, className, style, ...rest }, ref
) {
  const Icon = provider ? PROVIDER_ICONS[provider] : null;
  const iconNode = icon || (Icon ? <Icon mono={mono} /> : null);
  const title = rest.title || (provider ? `Continue with ${provider[0].toUpperCase() + provider.slice(1)}` : undefined);
  return (
    <button
      ref={ref}
      type="button"
      title={title}
      className={cn("provider-btn", className)}
      style={style}
      {...rest}
    >
      {iconNode}
    </button>
  );
});

// ──────────────────────────────────────────────────────────────
// DIVIDER — hairline rule with optional centered label
// ──────────────────────────────────────────────────────────────
//
// <Divider />                 — bare hairline
// <Divider>or</Divider>       — hairline · mono caps label · hairline

const Divider = ({ children, className, style, ...rest }) => (
  <div
    className={cn("divider", className)}
    style={style}
    {...rest}
  >
    {children == null
      ? null
      : <span className="divider-label">{children}</span>}
  </div>
);

// ──────────────────────────────────────────────────────────────
// VIGNETTE — readability backdrop for floating content
// ──────────────────────────────────────────────────────────────
//
//   <Vignette>            wraps content; anchor defaults to "left"
//     <Hero />
//   </Vignette>
//
// A soft radial mask of --surface that fades to transparent. Use when
// floating text/UI over an ambient background (network map, imagery)
// to give legibility without breaking the edge-to-edge atmosphere.
// All visual atoms (gradient stops, position, z-index) live in
// .vignette-backdrop (brand/components.css); the component only sets
// the anchor data-attribute and renders the backdrop + children.
//
// anchor   "left" | "center" | "right"   where the densest part sits
//          (default "left" — matches hero-on-left compositions)
//
// Renders as a fragment: a .vignette-backdrop sibling + the children.
// The backdrop positions to the nearest positioned ancestor, so place
// <Vignette> inside whatever frame should bound it.

const Vignette = ({ anchor = "left", children }) => (
  <>
    <div aria-hidden="true" className="vignette-backdrop" data-anchor={anchor} />
    {children}
  </>
);

// ──────────────────────────────────────────────────────────────
// TICKER — scrolling marquee with optional sticky label tab
// ──────────────────────────────────────────────────────────────
//
// Column-driven (preferred — minimal page code):
//
//   <Ticker
//     label="Recent originations"
//     items={ORIGINATIONS}
//     columns={[
//       { key: "amt", tone: "strong" },
//       "loc",
//       "type",
//       "term",
//       { key: "apy", tone: "brand", suffix: " APY" },
//       { key: "ago", tone: "faint" },
//     ]}
//   />
//
// columns         Array<string | ColumnSpec>. A string is shorthand
//                 for { key }. ColumnSpec:
//                   key      field on item
//                   tone     "default" | "strong" | "brand" | "faint"
//                            | "ghost" | "muted"   (default: "default")
//                   prefix   string before the value
//                   suffix   string after  the value
//                   format   (val, item) => ReactNode — full override
//                   style    CSSProperties — escape-hatch merged last
// bullet          leading glyph; defaults to a brand-stroke check.
//                 Pass `false` to disable, or any ReactNode to replace.
// label           optional left-side sticky tab (gets a live-dot + eyebrow)
// renderItem      (item, index) => ReactNode — full escape hatch; wins
//                 over `columns` when both are supplied.
// speed           "slow" | "normal" | "fast"  → 180 / 120 / 60s per loop
//                 (default "normal"). Pass a number for an exact seconds
//                 value if you need to tune.
// gap             px gap between rows (default 48)
//
// The marquee duplicates items to make seamless looping work via the
// `ticker-track` keyframes in brand/components.css.

const TICKER_DEFAULT_BULLET = (
  <svg width="11" height="11" viewBox="0 0 11 11" fill="none"
    stroke="var(--brand)" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <path d="M2 5.5l2.5 2.5L9 3" />
  </svg>
);

// Column tone: a built-in token resolves to a `data-tone` attribute
// on the column span; any other string is treated as a raw CSS color
// and tunneled inline (the escape hatch for per-row chart-palette
// accents).
const TICKER_TONE_VALUES = new Set([
  "strong", "muted", "subtle", "faint", "ghost",
  "up", "down", "warn", "flat", "brand",
]);

const TickerRow = ({ item, columns, bullet }) => (
  <span className="ticker-row">
    {bullet !== false && (bullet ?? TICKER_DEFAULT_BULLET)}
    {columns.map((c, i) => {
      const spec = typeof c === "string" ? { key: c } : c;
      const { key, tone, prefix, suffix, format, style } = spec;
      const raw = item[key];
      const rendered = format ? format(raw, item) : raw;
      const isToneToken = tone && TICKER_TONE_VALUES.has(tone);
      const arbitraryColor = tone && !isToneToken ? tone : null;
      const colorStyle = arbitraryColor ? { color: arbitraryColor, ...style } : style;
      return (
        <React.Fragment key={key + ":" + i}>
          {i > 0 && <Dot />}
          <span
            className="ticker-col"
            data-tone={isToneToken ? tone : undefined}
            style={colorStyle}
          >
            {prefix}{rendered}{suffix}
          </span>
        </React.Fragment>
      );
    })}
  </span>
);

const TICKER_SPEEDS = { slow: 180, normal: 120, fast: 60 };

const Ticker = ({ label, items = [], columns, renderItem, bullet, speed = "normal", gap = 48, labelOffset, className, style, ...rest }) => {
  if (!items.length) return null;
  const speedSec = typeof speed === "number" ? speed : (TICKER_SPEEDS[speed] ?? TICKER_SPEEDS.normal);
  const render = renderItem
    ? renderItem
    : columns
      ? (item) => <TickerRow item={item} columns={columns} bullet={bullet} />
      : (item) => item;
  // Tune the track via CSS variables — gap + animation-duration. The
  // label-aware left padding lives in CSS (.ticker:has(.ticker-label)
  // > .ticker-track) and defaults to 220px; consumers can override
  // per-instance via the `labelOffset` prop, tunneled through
  // --ticker-label-offset.
  const trackStyle = {
    "--ticker-gap": `${gap}px`,
    "--ticker-duration": `${speedSec}s`,
    ...(labelOffset != null ? { "--ticker-label-offset": `${labelOffset}px` } : null),
  };
  return (
    <div className={cn("ticker", className)} style={style} {...rest}>
      {label && (
        <div className="ticker-label">
          <LiveDot size={6} />
          <span className="eyebrow">{label}</span>
        </div>
      )}
      <div className="ticker-track" style={trackStyle}>
        {[...items, ...items].map((item, i) => (
          <React.Fragment key={i}>
            {render(item, i)}
          </React.Fragment>
        ))}
      </div>
    </div>
  );
};

// ──────────────────────────────────────────────────────────────
// CHIP — filled status/type pill
// ──────────────────────────────────────────────────────────────

// Filled status/type pill. Default chip is neutral; pass `color`
// with a token reference (`var(--up)`, `var(--brand)`, `var(--chart-N)`,
// etc.) to tint. The .chip-tinted class derives bg + border from
// currentColor via color-mix.
const Chip = forwardRef(function Chip(
  { color, className, style, ...rest }, ref
) {
  const colorStyle = color ? { color, ...style } : style;
  return (
    <span
      ref={ref}
      className={cn("chip", color && "chip-tinted", className)}
      style={colorStyle}
      {...rest}
    />
  );
});

// ──────────────────────────────────────────────────────────────
// TABLE
// ──────────────────────────────────────────────────────────────
//
// Thin wrapper — emit a <table className="table">. Inside, use
// standard <thead><tr><th>...</th></tr></thead><tbody>... Standard
// HTML — align via `data-align="right"`/"center" on th/td; sortable
// header via `data-sortable="true"` + `aria-sort="ascending|descending"`.
//
// <TableSortArrow /> — the chevron icon for sortable headers.
//
// <Table dense> — tighter row padding for very long tables.

const Table = forwardRef(function Table(
  { dense, className, ...rest }, ref
) {
  return <table ref={ref} className={cn("table", dense && "dense", className)} {...rest} />;
});

const TableSortArrow = ({ size = 9, ...rest }) => (
  <svg
    width={size} height={size} viewBox="0 0 9 9" fill="none"
    stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"
    className="sort-arrow" aria-hidden="true" {...rest}
  >
    <path d="M4.5 2v5M2.5 4.5l2-2 2 2" />
  </svg>
);

// Quiet section-header row for grouped tables. Drops in between tbody
// rows to label the next batch (e.g. covenants by category, loans by
// status, transactions by month). Renders as a <tr> with a single
// colSpan'd cell — no hover tint, no bottom border, breathing room
// above (see .table tr.table-group in components.css).
//
//   <Table>
//     <thead>…</thead>
//     <tbody>
//       <TableGroup label="Borrower" colSpan={5} />
//       <tr>…borrower rows…</tr>
//       <TableGroup label="LTV" colSpan={5} />
//       <tr>…LTV rows…</tr>
//     </tbody>
//   </Table>
//
// label    string → wraps in <Eyebrow tone="muted">; pass children for
//          custom content (a livedot + label, an action button, etc.).
// colSpan  must match the table's column count.

const TableGroup = forwardRef(function TableGroup(
  { label, colSpan, className, children, ...rest }, ref
) {
  return (
    <tr ref={ref} className={cn("table-group", className)} {...rest}>
      <td colSpan={colSpan}>
        {children ?? (label ? <Eyebrow tone="muted">{label}</Eyebrow> : null)}
      </td>
    </tr>
  );
});

// ──────────────────────────────────────────────────────────────
// HASH LINK — truncated mono link with title + copy on click
// ──────────────────────────────────────────────────────────────

const truncMid = (s, head = 6, tail = 4) =>
  !s ? "" : s.length > head + tail + 1 ? `${s.slice(0, head)}...${s.slice(-tail)}` : s;

const HashLink = forwardRef(function HashLink(
  { value, href, head, tail, copy = true, className, onClick, ...rest }, ref
) {
  const display = truncMid(value, head ?? 6, tail ?? 4);
  const handleClick = (e) => {
    onClick?.(e);
    if (copy && !href && navigator.clipboard) {
      e.preventDefault();
      navigator.clipboard.writeText(value).catch(() => {});
    }
  };
  return (
    <a
      ref={ref}
      href={href || "#"}
      title={value}
      onClick={handleClick}
      className={cn("hash-link", className)}
      {...rest}
    >
      {display}
    </a>
  );
});

// ──────────────────────────────────────────────────────────────
// PAGINATION
// ──────────────────────────────────────────────────────────────
//
// Controlled. Renders: prev arrow | page numbers w/ ellipsis |
// next arrow | optional rows-per-page select.
//
// page             current page (1-indexed)
// pages            total page count
// onPageChange     (newPage) => void
// pageSize         current page size (optional)
// pageSizeOptions  [10, 20, 50, ...] (optional)
// onPageSizeChange (newSize) => void
// siblings         pages either side of current to show (default 1)

function buildPageList(page, pages, siblings = 1) {
  if (pages <= 7) return Array.from({ length: pages }, (_, i) => i + 1);
  const start = Math.max(2, page - siblings);
  const end   = Math.min(pages - 1, page + siblings);
  const list  = [1];
  if (start > 2) list.push("…");
  for (let i = start; i <= end; i++) list.push(i);
  if (end < pages - 1) list.push("…");
  list.push(pages);
  return list;
}

const PaginationArrow = ({ dir, ...rest }) => (
  <svg width="11" height="11" viewBox="0 0 11 11" fill="none"
    stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"
    aria-hidden="true" {...rest}
  >
    {dir === "prev"
      ? <path d="M7 2.5L3.5 5.5L7 8.5" />
      : <path d="M4 2.5L7.5 5.5L4 8.5" />}
  </svg>
);

const Pagination = forwardRef(function Pagination(
  { page, pages, onPageChange,
    pageSize, pageSizeOptions, onPageSizeChange,
    siblings = 1, compact = false, total,
    className, ...rest }, ref
) {
  // Compact mode renders a "X-Y of Z" range string between prev/next
  // arrows instead of page numbers. Use for very long lists where
  // jumping to page 87 isn't a meaningful affordance.
  const showRange = compact && pageSize != null;
  const lo = showRange ? (page - 1) * pageSize + 1 : 0;
  const hi = showRange ? Math.min(page * pageSize, total ?? pages * pageSize) : 0;
  const totalLabel = total != null ? total.toLocaleString() : (pages * (pageSize ?? 0)).toLocaleString();

  const pageList = compact ? null : buildPageList(page, pages, siblings);
  const go = (p) => p >= 1 && p <= pages && p !== page && onPageChange?.(p);
  return (
    <div ref={ref} className={cn("pagination", className)} {...rest}>
      <button className="pg-btn" disabled={page <= 1} onClick={() => go(page - 1)} aria-label="Previous page">
        <PaginationArrow dir="prev" />
      </button>
      {compact ? (
        <span className="pg-range">
          <Num size="sm" weight="regular">{lo}–{hi}</Num>
          <span className="pg-range-sep">of</span>
          <Num size="sm" weight="regular">{totalLabel}</Num>
        </span>
      ) : (
        pageList.map((p, i) =>
          p === "…"
            ? <span key={`e-${i}`} className="pg-ellipsis">…</span>
            : <button
                key={p}
                className="pg-btn"
                data-active={p === page}
                onClick={() => go(p)}
                aria-current={p === page ? "page" : undefined}
              >
                {p}
              </button>
        )
      )}
      <button className="pg-btn" disabled={page >= pages} onClick={() => go(page + 1)} aria-label="Next page">
        <PaginationArrow dir="next" />
      </button>
      {pageSizeOptions && pageSize != null && (
        <>
          <span className="pg-divider" />
          <select
            className="pg-size"
            value={pageSize}
            onChange={(e) => onPageSizeChange?.(Number(e.target.value))}
            aria-label="Rows per page"
          >
            {pageSizeOptions.map((n) => (
              <option key={n} value={n}>{n} / page</option>
            ))}
          </select>
        </>
      )}
    </div>
  );
});

// ──────────────────────────────────────────────────────────────
// PROGRESS BAR — horizontal value bar, recolored by hue
// ──────────────────────────────────────────────────────────────
//
// <ProgressBar value={42} hue="chart-2" thickness={4} />
//
// value      0..max (default max=100)
// hue        accent name (chart-1..7 or chart-up/chart-down/chart-warn)
// thickness  px height (default 4)

const ProgressBar = forwardRef(function ProgressBar(
  { value, max = 100, hue, thickness = 4, className, style, ...rest }, ref
) {
  const pct = Math.max(0, Math.min(100, (value / max) * 100));
  const color = _hueVar(hue);
  // Tunnel hue + thickness + percent through CSS custom properties.
  // All layout (display, position, width, height, border-radius,
  // background mix, overflow) lives in `.progress` and `.progress-fill`
  // (brand/components.css). The component never writes inline atoms.
  const progressStyle = {
    "--progress-hue": color,
    "--progress-thickness": `${thickness}px`,
    "--progress-pct": `${pct}%`,
    ...style,
  };
  return (
    <span
      ref={ref}
      className={cn("progress", className)}
      role="progressbar"
      aria-valuenow={value}
      aria-valuemin={0}
      aria-valuemax={max}
      style={progressStyle}
      {...rest}
    >
      <span aria-hidden="true" className="progress-fill" />
    </span>
  );
});

// ──────────────────────────────────────────────────────────────
// CHARTS
// ──────────────────────────────────────────────────────────────
//
// Hand-built SVG. No chart library. Shared visual contract:
//   · Hairline dashed gridlines at color-mix(currentColor 8% transparent)
//   · Mono caps axis labels at --text-subtle
//   · 1.5px lines, 8% area fill, tonal bars (hue top → 40% mix bottom)
//   · Hue prop → accent palette (mint/teal/sage/sky/amber/lilac/rose)
//   · All charts size to their container via viewBox + preserveAspectRatio
//
// Token resolution: hues stay as CSS variables so theme flips repaint.

// Resolve a hue prop to a CSS var. Accepts:
//   · undefined / null  → fallback (charts default to --chart-default)
//   · number N          → var(--chart-N), cycling 1..7
//   · "chart-N"         → var(--chart-N)
//   · "chart-up"/"chart-down"/"chart-default"/"chart-warn" → chart-semantic
//   · "up"/"down"/"warn"/"brand" → accent-palette semantic aliases
//   · any other string  → var(--<name>)  (accent palette: mint, sage, ...)
const _hueVar = (h, fallback = "var(--chart-default)") => {
  if (h === undefined || h === null) return fallback;
  if (typeof h === "number") return `var(--chart-${((h - 1) % 10) + 1})`;
  return `var(--${h})`;
};

// ─── <Sparkline> — inline line, no axes ──────────────────────
//
// data: number[] OR {x?, y}[]
// w/h:  pixel size (default 80×24)
// hue:  accent name (default brand)
// area: render filled area under line (default true)

const Sparkline = forwardRef(function Sparkline(
  { data = [], w = 80, h = 24, hue, area = true,
    className, style, ...rest }, ref
) {
  if (!data.length) return null;
  const ys = data.map(d => typeof d === "number" ? d : d.y);
  const min = Math.min(...ys), max = Math.max(...ys), span = max - min || 1;
  const stepX = ys.length > 1 ? (w - 2) / (ys.length - 1) : 0;
  const pts = ys.map((y, i) => [1 + i * stepX, h - 1 - ((y - min) / span) * (h - 2)]);
  const line = pts.map((p, i) => `${i ? "L" : "M"}${p[0].toFixed(2)} ${p[1].toFixed(2)}`).join(" ");
  const fill = `${line} L${pts[pts.length-1][0].toFixed(2)} ${h-1} L${pts[0][0].toFixed(2)} ${h-1} Z`;
  const color = _hueVar(hue);
  // SVG fill/stroke values are data-driven geometry attributes,
  // not CSS visual atoms — they stay inline on the SVG. The
  // wrapper chrome (display: inline-block, vertical-align nudge)
  // lives in `.sparkline` (brand/components.css).
  return (
    <svg
      ref={ref}
      viewBox={`0 0 ${w} ${h}`}
      preserveAspectRatio="none"
      width={w} height={h}
      className={cn("sparkline", className)}
      style={style}
      {...rest}
    >
      {area && (
        <path d={fill}
          fill={`color-mix(in srgb, ${color} 12%, transparent)`} />
      )}
      <path d={line}
        fill="none"
        stroke={color}
        strokeWidth="1.5"
        strokeLinecap="round"
        strokeLinejoin="round"
        vectorEffect="non-scaling-stroke" />
    </svg>
  );
});

// ─── <BarChart> — horizontal or vertical bars ───────────────
//
// data: [{ label, value, hue? }, …]
// orientation: "horizontal" (default) | "vertical"
// hue:  top-level default hue for ALL bars (overrides auto-assign).
//       Pass when you want single-color single-series; omit for
//       categorical (each bar gets chart-1, chart-2, ... by index).
//       Per-bar `hue` on a data point overrides this.
// max:  axis cap (default = max(value))
// w/h:  container size; defaults are sensible for inline usage
// showAxis: render category + value axes (default true)
// valueFmt: (n) => string  custom number formatter
// gap:  px between bars (default 14 horizontal / 16 vertical)

const BarChart = forwardRef(function BarChart(
  { data = [], orientation = "horizontal", hue, max, w = 360, h,
    showAxis = true, valueFmt = (n) => String(n), gap = 14,
    className, style, ...rest }, ref
) {
  if (!data.length) return null;
  const cap = max ?? Math.max(...data.map(d => d.value));
  const horizontal = orientation === "horizontal";
  // Per-bar color resolution: explicit per-bar hue → top-level hue →
  // auto-assign chart-N by index.
  const colorFor = (d, i) => _hueVar(d.hue ?? hue ?? (i + 1));

  // Default heights — horizontal grows per row, vertical caps at 220.
  const rowH = 24, barT = 8;
  const H = h ?? (horizontal ? data.length * (rowH + gap) - gap + (showAxis ? 18 : 0) : 220);
  const valueGutter = horizontal && showAxis ? 56 : 0;
  const labelGutter = horizontal && showAxis ? 110 : 0;

  if (horizontal) {
    return (
      <svg
        ref={ref}
        viewBox={`0 0 ${w} ${H}`}
        preserveAspectRatio="xMidYMid meet"
        className={cn("chart bar-chart", className)}
        style={style}
        {...rest}
      >
        {data.map((d, i) => {
          const color = colorFor(d, i);
          const trackX = labelGutter;
          const trackW = w - labelGutter - valueGutter;
          const barW = (d.value / cap) * trackW;
          const y = i * (rowH + gap) + 4;
          return (
            <g key={d.label}>
              {showAxis && (
                <text x={labelGutter - 10} y={y + barT + 1}
                  textAnchor="end" dominantBaseline="middle" className="chart-axis-text">
                  {d.label}
                </text>
              )}
              {/* Track */}
              <rect x={trackX} y={y} width={trackW} height={barT} rx={barT/2}
                fill={`color-mix(in srgb, ${color} 12%, transparent)`} />
              {/* Bar */}
              <rect x={trackX} y={y} width={barW} height={barT} rx={barT/2}
                fill={color} />
              {showAxis && (
                <text x={w - 6} y={y + barT + 1}
                  textAnchor="end" dominantBaseline="middle"
                  className="chart-axis-text chart-axis-text-strong">
                  {valueFmt(d.value)}
                </text>
              )}
            </g>
          );
        })}
      </svg>
    );
  }

  // Vertical
  const top = 18, bottom = showAxis ? 22 : 6, left = 6, right = 6;
  const plotH = H - top - bottom;
  const plotW = w - left - right;
  const slot = plotW / data.length;
  const barW = Math.max(8, slot - gap);
  return (
    <svg
      ref={ref}
      viewBox={`0 0 ${w} ${H}`}
      preserveAspectRatio="xMidYMid meet"
      className={cn("chart bar-chart", className)}
      style={style}
      {...rest}
    >
      {/* Hairline dashed gridlines: 0/50/100% of cap */}
      {[0, 0.5, 1].map((t) => {
        const y = top + (1 - t) * plotH;
        return <line key={t} x1={left} x2={w-right} y1={y} y2={y}
          stroke="color-mix(in srgb, currentColor 8%, transparent)"
          strokeDasharray="2 3" strokeWidth="1" vectorEffect="non-scaling-stroke" />;
      })}
      {data.map((d, i) => {
        const color = colorFor(d, i);
        const x = left + i * slot + (slot - barW) / 2;
        const barH = (d.value / cap) * plotH;
        const y = top + plotH - barH;
        // Tonal gradient: full hue top → 40% mix bottom
        const gradId = `bar-grad-${i}-${Math.round(Math.random()*1e6)}`;
        return (
          <g key={d.label}>
            <defs>
              <linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
                <stop offset="0%"  stopColor={color} />
                <stop offset="100%" stopColor={color} stopOpacity="0.45" />
              </linearGradient>
            </defs>
            <rect x={x} y={y} width={barW} height={barH} rx={2}
              fill={`url(#${gradId})`} />
            {/* Value above */}
            <text x={x + barW/2} y={y - 6} textAnchor="middle"
              className="chart-axis-text chart-axis-text-strong">
              {valueFmt(d.value)}
            </text>
            {showAxis && (
              <text x={x + barW/2} y={H - 6} textAnchor="middle"
                className="chart-axis-text">{d.label}</text>
            )}
          </g>
        );
      })}
    </svg>
  );
});

// ─── <LineChart> — single-series line, time axis ────────────
//
// data:    [{ x, y }, …]      x can be number or string (categorical)
// hue:     accent name
// w/h:     container size
// gridY:   gridline count (default 4)
// area:    fill area under line (default true)
// valueFmt:(n) => string

const LineChart = forwardRef(function LineChart(
  { data = [], hue, w = 540, h = 200, gridY = 4, area = true,
    valueFmt = (n) => String(n),
    className, style, ...rest }, ref
) {
  if (!data.length) return null;
  const top = 12, bottom = 22, left = 36, right = 12;
  const plotW = w - left - right;
  const plotH = h - top - bottom;
  const ys = data.map(d => d.y);
  const min = 0;  // anchor to 0 for finance-style "growth" charts
  const max = Math.max(...ys);
  const stepX = data.length > 1 ? plotW / (data.length - 1) : 0;
  const pts = data.map((d, i) => [
    left + i * stepX,
    top + plotH - ((d.y - min) / (max - min || 1)) * plotH
  ]);
  const line = pts.map((p, i) => `${i ? "L" : "M"}${p[0].toFixed(2)} ${p[1].toFixed(2)}`).join(" ");
  const fill = `${line} L${pts[pts.length-1][0].toFixed(2)} ${top + plotH} L${pts[0][0].toFixed(2)} ${top + plotH} Z`;
  const color = _hueVar(hue);

  // Y-axis ticks (gridY divisions, evenly spaced from 0 → max)
  const yTicks = Array.from({ length: gridY + 1 }, (_, i) => (max / gridY) * i);

  // X-axis labels — every nth point so we don't overcrowd
  const xStride = Math.max(1, Math.round(data.length / 6));

  return (
    <svg
      ref={ref}
      viewBox={`0 0 ${w} ${h}`}
      preserveAspectRatio="xMidYMid meet"
      className={cn("chart line-chart", className)}
      style={style}
      {...rest}
    >
      {/* Gridlines */}
      {yTicks.map((t, i) => {
        const y = top + plotH - (t / (max || 1)) * plotH;
        return (
          <g key={i}>
            <line x1={left} x2={w - right} y1={y} y2={y}
              stroke="color-mix(in srgb, currentColor 8%, transparent)"
              strokeDasharray="2 3" strokeWidth="1" vectorEffect="non-scaling-stroke" />
            <text x={left - 8} y={y} textAnchor="end" dominantBaseline="middle"
              className="chart-axis-text">
              {valueFmt(t)}
            </text>
          </g>
        );
      })}
      {/* Area + line */}
      {area && <path d={fill} fill={`color-mix(in srgb, ${color} 10%, transparent)`} />}
      <path d={line} fill="none" stroke={color} strokeWidth="1.5"
        strokeLinecap="round" strokeLinejoin="round" vectorEffect="non-scaling-stroke" />
      {/* X labels */}
      {data.map((d, i) => (i % xStride === 0 || i === data.length - 1) ? (
        <text key={i} x={pts[i][0]} y={h - 6} textAnchor="middle" className="chart-axis-text">
          {d.x}
        </text>
      ) : null)}
    </svg>
  );
});

// ─── <DonutChart> — composition with center value ───────────
//
// data:     [{ label, value, hue? }, …]
// size:     px diameter (default 96)
// thick:    stroke thickness (default 10)
// center:   ReactNode | { value, label } — what goes in the hole
// trackHue: hue for the background track (defaults to first slice's hue)
// gap:      px gap between slices, drawn in --surface (default 2)
// legend:   "none" | "right" | "bottom"   (default: "right" when 2+ slices)
// valueFmt: (n, total) => string   for legend values
//
// Adjacent slices read as distinct via two mechanisms:
//   1. accent palette has varied lightness (tokens.css)
//   2. small gap in --surface between slices (this component)

const DonutChart = forwardRef(function DonutChart(
  { data = [], size = 96, thick = 16, center, trackHue,
    gap = 2, legend, valueFmt,
    className, style, ...rest }, ref
) {
  if (!data.length) return null;
  const total = data.reduce((s, d) => s + d.value, 0);
  const r = (size - thick) / 2;
  const c = 2 * Math.PI * r;
  const trackColor = _hueVar(trackHue || data[0].hue);
  const multiSlice = data.length > 1;

  // Default legend on when there are 2+ slices.
  const legendPos = legend ?? (multiSlice ? "right" : "none");
  const fmt = valueFmt || ((n) => `${((n / total) * 100).toFixed(1)}%`);

  // Compute slice arcs. We accumulate `cursor` to set strokeDashoffset
  // for each slice. For multi-slice donuts, subtract a small arc length
  // from each segment so a surface-colored gap separates them.
  // Auto-assign chart palette when a slice has no explicit hue — apps
  // can just pass `{ label, value }` and get balanced peer colors.
  const gapArc = multiSlice ? Math.min(gap, c / data.length / 4) : 0;
  let cursor = 0;
  const slices = data.map((d, i) => {
    const frac = d.value / total;
    const len = Math.max(0, frac * c - gapArc);
    const offset = -cursor;
    cursor += frac * c;
    return { ...d, frac, len, offset, color: _hueVar(d.hue ?? (i + 1)) };
  });

  // Center slot — either a custom ReactNode (passed through) or the
  // structured {value, label} shape (uses the .donut-chart-center-*
  // slot classes, which have their own visual contract distinct from
  // <Num>/<Eyebrow>).
  const centerNode = center == null ? null : (
    <div className="donut-chart-center">
      {React.isValidElement(center) ? center : (
        <>
          <span className="donut-chart-center-value">{center.value}</span>
          {center.label && (
            <span className="donut-chart-center-label">{center.label}</span>
          )}
        </>
      )}
    </div>
  );

  const donut = (
    <div className="donut-chart-donut" style={{ "--donut-size": `${size}px` }}>
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
        {/* Track — neutral, so slice gaps reveal background not "another color" */}
        <circle cx={size/2} cy={size/2} r={r} fill="none"
          stroke={`color-mix(in srgb, ${trackColor} 10%, transparent)`}
          strokeWidth={thick} />
        {slices.map((s, i) => (
          <circle key={i} cx={size/2} cy={size/2} r={r} fill="none"
            stroke={s.color} strokeWidth={thick}
            strokeDasharray={`${s.len} ${c - s.len}`}
            strokeDashoffset={s.offset}
            transform={`rotate(-90 ${size/2} ${size/2})`}
            strokeLinecap="butt" />
        ))}
      </svg>
      {centerNode}
    </div>
  );

  if (legendPos === "none") {
    return (
      <div ref={ref} className={cn("donut-chart", "donut-chart-legend-none", className)} style={style} {...rest}>
        {donut}
      </div>
    );
  }

  const legendEl = (
    <ul className="donut-chart-legend">
      {slices.map((s) => (
        <li key={s.label} className="donut-chart-legend-row">
          <span className="donut-chart-legend-label-group">
            <span aria-hidden="true" className="donut-chart-legend-swatch"
              style={{ "--donut-swatch": s.color }} />
            <span className="donut-chart-legend-label">{s.label}</span>
          </span>
          <Num size="sm" color="var(--text-muted)">{fmt(s.value, total)}</Num>
        </li>
      ))}
    </ul>
  );

  return (
    <div
      ref={ref}
      className={cn("donut-chart", `donut-chart-legend-${legendPos}`, className)}
      style={style}
      {...rest}
    >
      {donut}
      {legendEl}
    </div>
  );
});

// ──────────────────────────────────────────────────────────────
// MONEY — currency mark + tnum amount
// ──────────────────────────────────────────────────────────────
//
// <Money amount={1234000} />                       → mark + 1,234,000
// <Money amount="1,234,000" />                     → mark + literal string
// <Money amount={null} />                          → "—" with faint color
// <Money amount={123} size="lg" />                 → larger mark + num size
// <Money amount={123} mark={false} />              → number only (no mark)
// <Money amount={123} currency="USDC" />           → USDC mark (default)
//
// Auto-strips a leading "$" from string amounts so the mark carries the
// currency signal. Pass `format="compact"` for $1.2M-style shortening
// (purely visual; the original value is preserved in title attr).

const _fmtCompact = (n) => {
  const abs = Math.abs(n);
  if (abs >= 1e9) return (n / 1e9).toFixed(1).replace(/\.0$/, "") + "B";
  if (abs >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
  if (abs >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "K";
  return String(n);
};
const _fmtFull = (n) => n.toLocaleString("en-US");

const Money = forwardRef(function Money(
  { amount, currency = "USDC", mark = true, size = "md", format = "full",
    color, className, style, ...rest }, ref
) {
  if (amount == null || amount === "—") {
    // Em-dash branch — just a .num + .money-empty; no inline-flex
    // since there's no mark+amount pair to lay out.
    return (
      <span ref={ref}
        className={cn("num", size !== "md" && `num-${size}`, "money-empty", className)}
        style={color ? { color, ...style } : style}
        {...rest}>—</span>
    );
  }
  const display = typeof amount === "number"
    ? (format === "compact" ? _fmtCompact(amount) : _fmtFull(amount))
    : String(amount).replace(/^\$/, "");
  const markSize = size === "xl" ? 18 : size === "lg" ? 16 : size === "sm" ? 12 : 14;
  return (
    <span
      ref={ref}
      className={cn("num", size !== "md" && `num-${size}`, "money", className)}
      style={color ? { color, ...style } : style}
      title={typeof amount === "number" ? `${currency} ${_fmtFull(amount)}` : undefined}
      {...rest}
    >
      {mark && currency === "USDC" && <USDCMark size={markSize} />}
      {display}
    </span>
  );
});

// ──────────────────────────────────────────────────────────────
// KPI CELL + STRIP
// ──────────────────────────────────────────────────────────────
//
// <KpiCell label value delta deltaLabel tone primary />
//   label       small caps label
//   value       headline number (string or ReactNode)
//   delta       small mono trend value, e.g. "+4.2%"
//   deltaLabel  faint suffix, e.g. "vs. last month"
//   tone        "up" | "down" | "warn" | "flat"   (delta color)
//   primary     bumps value to num-xl, adds slightly more padding
//   trend       optional Sparkline component instance (or any node)
//
// <KpiStrip>{cells}</KpiStrip>
//   Wraps KpiCells in a hairline-bounded grid row. Children are
//   auto-divided by vertical hairlines; the first child has no left
//   border. Pass `columns` to override the default 1.2fr 1fr 1fr 1fr 1fr.

const KpiCell = forwardRef(function KpiCell(
  { label, value, delta, deltaLabel, tone = "flat", primary, trend,
    className, style, ...rest }, ref
) {
  return (
    <div
      ref={ref}
      className={cn("kpi-cell", primary && "kpi-cell-primary", className)}
      style={style}
      {...rest}
    >
      <Eyebrow>{label}</Eyebrow>
      <Num size={primary ? "xl" : "lg"}>{value}</Num>
      {delta != null && (
        <Signal tone={tone}>
          <span>{delta}</span>
          {deltaLabel && (
            <span className="kpi-cell-delta-label">{deltaLabel}</span>
          )}
        </Signal>
      )}
      {trend && <div className="kpi-cell-trend">{trend}</div>}
    </div>
  );
});

const KpiStrip = forwardRef(function KpiStrip(
  { inset = "0 64px", minCellWidth = 200, className, style, children, ...rest }, ref
) {
  // Cells flow naturally via flex-wrap + flex-grow:
  //   · 1 cell  → takes the full width
  //   · N cells → equal share of the row
  //   · When per-cell width would drop below `minCellWidth`, cells
  //     wrap to a new row and the new row's cells grow to fill it too
  //     (no leftover space, no broken hairlines).
  // Hairlines come from a 1px gap with the strip's --line background
  // showing through. The strip's top/bottom borders live in CSS
  // (.kpi-strip) so the component never writes inline border atoms.
  return (
    <div
      ref={ref}
      className={cn("kpi-strip", className)}
      style={{
        margin: inset,
        "--kpi-cell-min-width": `${minCellWidth}px`,
        ...style
      }}
      {...rest}
    >
      {children}
    </div>
  );
});

// ──────────────────────────────────────────────────────────────
// KPI STRIP QUIET — chrome-less sibling of KpiStrip
// ──────────────────────────────────────────────────────────────
//
// <KpiStripQuiet items={[
//   { value: "9.12%",  label: "Current portfolio APY" },
//   { value: "$1.99B", label: "AUM" },
// ]} />
//
// No dividers, no eyebrows, no deltas — just value over label,
// repeated in a loose row. Used for hero-adjacent trust signals
// where <KpiStrip>'s instrumented look would be too heavy.
// Page owns outer positioning (wrap in a div for margin); the
// strip is a closed box.

const KpiStripQuiet = ({ items = [] }) => (
  <div className="kpi-strip-quiet">
    {items.map(({ value, label }) => (
      <div key={label} className="kpi-stat-quiet">
        <span className="kpi-stat-quiet-value">{value}</span>
        <span className="kpi-stat-quiet-label">{label}</span>
      </div>
    ))}
  </div>
);

// ──────────────────────────────────────────────────────────────
// METER CELL — small bar + percent for table cells with zone color
// ──────────────────────────────────────────────────────────────
//
// <MeterCell value={42} />
// <MeterCell value={72} zones={[
//   { max: 70, hue: "chart-up" },
//   { max: 90, hue: "chart-warn" },
//   { max: Infinity, hue: "chart-down" },
// ]} />
// <MeterCell value={42} format={(v) => v.toFixed(1) + "%"} width={120} />
//
// Default zones: <70 up, <=90 warn, >90 down. Override via `zones` prop.

const DEFAULT_METER_ZONES = [
  { max: 70,        hue: "chart-up" },
  { max: 90,        hue: "chart-warn" },
  { max: Infinity,  hue: "chart-down" },
];

const MeterCell = forwardRef(function MeterCell(
  { value, max = 100, zones = DEFAULT_METER_ZONES,
    format = (v) => `${v}%`, width = 124,
    className, style, ...rest }, ref
) {
  const zone = zones.find(z => value <= z.max) || zones[zones.length - 1];
  // Thin layout wrapper. Composes <ProgressBar> for the bar and
  // <Num size="sm"> inside a `.meter-cell-value` slot for the
  // numeric column. The bar's geometry and color contract live
  // in ProgressBar; MeterCell owns only flex layout + width.
  const cellStyle = { "--meter-width": `${width}px`, ...style };
  return (
    <div
      ref={ref}
      className={cn("meter-cell", className)}
      style={cellStyle}
      {...rest}
    >
      <ProgressBar value={value} max={max} hue={zone.hue} />
      <span className="meter-cell-value">
        <Num size="sm">{format(value)}</Num>
      </span>
    </div>
  );
});

// ──────────────────────────────────────────────────────────────
// MODAL — centered dialog over a scrim
// ──────────────────────────────────────────────────────────────
//
// Built on the native <dialog> element so focus trap, Esc-to-close
// and a11y semantics come for free. Backdrop is styled via the
// ::backdrop pseudo-element in components.css.
//
//   const [open, setOpen] = React.useState(false);
//   <Modal open={open} onClose={() => setOpen(false)}
//          title="Pool covenants"
//          subtitle="12 active rules — last evaluated 2s ago">
//     …body…
//   </Modal>
//
// open       boolean — controlled.
// onClose    fired on backdrop click, Esc, or close-button.
// title      stacked heading shorthand (renders in modal-head).
// subtitle   lede under the title.
// width      modal-inner width in px (default 720).
// footer     optional ReactNode rendered in a hairline-divided foot.
// bodyPad    set false to drop modal-body padding (for full-bleed
//            tables / lists that own their own interior).

const ModalCloseIcon = ({ size = 14 }) => (
  <svg width={size} height={size} viewBox="0 0 14 14" fill="none"
    stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true">
    <path d="M3.5 3.5l7 7M10.5 3.5l-7 7" />
  </svg>
);

const Modal = forwardRef(function Modal(
  { open, onClose, title, subtitle, footer, width = 720, bodyPad = true,
    className, children, ...rest }, ref
) {
  const dialogRef = React.useRef(null);
  React.useImperativeHandle(ref, () => dialogRef.current);

  // Sync open prop → native showModal/close.
  React.useEffect(() => {
    const dlg = dialogRef.current;
    if (!dlg) return;
    if (open && !dlg.open) dlg.showModal();
    if (!open && dlg.open) dlg.close();
  }, [open]);

  // Catch native close (Esc, form method=dialog, etc.) so the
  // controlled open state stays in sync.
  React.useEffect(() => {
    const dlg = dialogRef.current;
    if (!dlg) return;
    const handler = () => onClose?.();
    dlg.addEventListener("close", handler);
    return () => dlg.removeEventListener("close", handler);
  }, [onClose]);

  // Backdrop click dismisses. Native <dialog> emits clicks on the
  // dialog itself when the backdrop is clicked; inner content stops
  // propagation at .modal-inner via the e.target identity check.
  const onBackdropClick = (e) => {
    if (e.target === dialogRef.current) onClose?.();
  };

  return (
    <dialog
      ref={dialogRef}
      className={cn("modal", className)}
      onClick={onBackdropClick}
      {...rest}
    >
      <div className="modal-inner" style={{ "--modal-width": `${width}px` }}>
        {(title || subtitle) && (
          <div className="modal-head">
            <div className="modal-head-titles">
              {title && <h2 className="modal-title">{title}</h2>}
              {subtitle && <p className="modal-subtitle">{subtitle}</p>}
            </div>
            <button
              type="button"
              className="modal-close"
              aria-label="Close"
              onClick={() => onClose?.()}
            >
              <ModalCloseIcon />
            </button>
          </div>
        )}
        <div className={cn("modal-body", !bodyPad && "modal-body-bare")}>
          {children}
        </div>
        {footer && <div className="modal-foot">{footer}</div>}
      </div>
    </dialog>
  );
});

// ──────────────────────────────────────────────────────────────
// INFO TIP — small ⓘ icon with a hover/focus tooltip
// ──────────────────────────────────────────────────────────────
//
//   <span>Avg LTV <InfoTip text="Weighted by outstanding principal." /></span>
//
// Sits inline next to a label and reveals a small bubble on hover or
// keyboard focus. Pure-CSS show/hide (see .info-tip in components.css);
// the trigger is a real <button> for keyboard reachability + aria.
//
// text     plain string OR ReactNode (rich content allowed).
// side     "top" (default) | "bottom" — vertical position of the bubble.
// size     icon size in px (default 12).

const InfoIcon = ({ size = 12 }) => (
  <svg width={size} height={size} viewBox="0 0 12 12" fill="none"
    stroke="currentColor" strokeWidth="1.3" aria-hidden="true">
    <circle cx="6" cy="6" r="5" />
    <path d="M6 5.4v3" strokeLinecap="round" />
    <circle cx="6" cy="3.6" r="0.6" fill="currentColor" stroke="none" />
  </svg>
);

const InfoTip = forwardRef(function InfoTip(
  { text, side = "top", size = 12, className, ...rest }, ref
) {
  // aria-label only when text is a plain string; otherwise rely on
  // visible tooltip content.
  const ariaLabel = typeof text === "string" ? text : undefined;
  return (
    <span
      ref={ref}
      tabIndex={0}
      role="button"
      aria-label={ariaLabel}
      className={cn("info-tip", `info-tip-${side}`, className)}
      {...rest}
    >
      <InfoIcon size={size} />
      <span className="info-tip-bubble" role="tooltip">{text}</span>
    </span>
  );
});

// ──────────────────────────────────────────────────────────────
// APP SHELL — useTheme, ThemeToggle, Header, Footer
// ──────────────────────────────────────────────────────────────

// `useTheme` syncs a "dark" | "light" state to `data-theme` on the
// document root. SSR caveat: the hook returns `initial` on first
// render — actual DOM reads/writes happen inside useEffect. Apps
// hydrating from a cookie should pass the resolved value as `initial`.
function useTheme(initial = "light") {
  const [theme, setTheme] = React.useState(initial);
  React.useEffect(() => {
    if (typeof document === "undefined") return;
    const root = document.documentElement;
    const prev = root.getAttribute("data-theme");
    root.setAttribute("data-theme", theme);
    return () => {
      if (prev === null) root.removeAttribute("data-theme");
      else root.setAttribute("data-theme", prev);
    };
  }, [theme]);
  return [theme, setTheme];
}

// Sun + moon glyphs for the toggle slots. Stroke uses currentColor
// so the CSS rule that lives under `.theme-toggle[aria-checked=...]`
// can recolor whichever icon sits under the thumb.
const ThemeSunIcon = () => (
  <svg width="12" height="12" viewBox="0 0 13 13" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true">
    <circle cx="6.5" cy="6.5" r="2.4" />
    <path d="M6.5 1v1.3M6.5 10.7V12M1 6.5h1.3M10.7 6.5H12M2.7 2.7l.9.9M9.4 9.4l.9.9M2.7 10.3l.9-.9M9.4 3.6l.9-.9" />
  </svg>
);
const ThemeMoonIcon = () => (
  <svg width="12" height="12" viewBox="0 0 13 13" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <path d="M10.5 7.8a4.2 4.2 0 0 1-5.3-5.3 4.5 4.5 0 1 0 5.3 5.3z" />
  </svg>
);

function ThemeToggle({ theme, onChange }) {
  const isDark = theme === "dark";
  return (
    <button
      type="button"
      role="switch"
      aria-checked={isDark}
      aria-label={`Switch to ${isDark ? "light" : "dark"} mode`}
      onClick={() => onChange(isDark ? "light" : "dark")}
      className="theme-toggle"
    >
      <span aria-hidden="true" className="theme-toggle-thumb" />
      <span className="theme-toggle-icon" data-side="sun">
        <ThemeSunIcon />
      </span>
      <span className="theme-toggle-icon" data-side="moon">
        <ThemeMoonIcon />
      </span>
    </button>
  );
}

function Header({
  nav = [],
  active,
  theme,
  onThemeChange,
  cta,
  label = "Onrail",
  surface,         // "bare" → no borderBottom; for floating-over-hero screens
  linkComponent: LinkComponent,  // optional — wraps each nav <a> for client-side routing
}) {
  return (
    <header className="header" data-surface={surface === "bare" ? "bare" : undefined}>
      <div className="header-cluster header-cluster-left">
        <Wordmark size={20} color="var(--text)" accent="var(--brand)" label={label} />
        <nav className="header-nav">
          {nav.map((l) => {
            const isActive = l.label === active;
            const linkProps = {
              className: "header-nav-link",
              "data-active": isActive ? "true" : undefined,
            };
            const linkContent = (
              <>
                {isActive && <span aria-hidden="true" className="header-nav-active-dot" />}
                {l.label}
              </>
            );
            if (LinkComponent) {
              return (
                <LinkComponent key={l.label} href={l.href || "#"} {...linkProps}>
                  {linkContent}
                </LinkComponent>
              );
            }
            return (
              <a key={l.label} href={l.href || "#"} {...linkProps}>
                {linkContent}
              </a>
            );
          })}
        </nav>
      </div>
      <div className="header-cluster header-cluster-right">
        <ThemeToggle theme={theme} onChange={onThemeChange} />
        {cta && (
          <button className="btn btn-sm" onClick={cta.onClick}>
            {cta.label}
          </button>
        )}
      </div>
    </header>
  );
}

// Footer — three-slot generic footer.
//
//   <Footer
//     left={…}      // ReactNode — far-left content
//     center={…}    // ReactNode — middle slot
//     right={…}     // ReactNode — far-right content
//   />
//
// All slots optional. No built-in defaults — app supplies content.
function Footer({ left, center, right }) {
  return (
    <footer className="footer">
      <div className="footer-slot footer-slot-left">{left}</div>
      <div className="footer-slot footer-slot-center">{center}</div>
      <div className="footer-slot footer-slot-right">{right}</div>
    </footer>
  );
}

Object.assign(window, {
  Display, Body, Eyebrow, Num, Dot, Signal, Tag,
  Panel, PanelHead, PanelBody, PanelFoot,
  ListItem, StackedItem, Field, Button,
  LiveDot, USDCMark, Money,
  Mark, Wordmark,
  GoogleIcon, AppleIcon, XIcon, FarcasterIcon, WalletGlyph, MailGlyph, LockGlyph, HouseGlyph,
  FieldAction, ArrowLink,
  ProviderButton, Divider, Vignette, Ticker,
  Chip, Table, TableSortArrow, TableGroup,
  HashLink,
  Pagination,
  ProgressBar, MeterCell,
  Sparkline, BarChart, LineChart, DonutChart,
  KpiCell, KpiStrip, KpiStripQuiet,
  useTheme, ThemeToggle, Header, Footer,
  Modal, InfoTip,
});
