@import "tailwindcss";

/* Inter Variable, self-hosted under /static/fonts/ to eliminate the
   cross-origin font swap on hard-reload. base.html and the wizard layout
   both load styles.css, so the @font-face lives here once. The roman file
   is preloaded from each layout's <head>. */
@font-face {
  font-family: 'Inter var';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/static/fonts/InterVariable.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter var';
  font-style: italic;
  font-weight: 100 900;
  font-display: swap;
  src: url('/static/fonts/InterVariable-Italic.woff2') format('woff2');
}

/* ─── EXPERIMENT (theme-strip branch) ─────────────────────────────────
 * Drop the bespoke breadbox-light / breadbox-dark OKLCH palettes and
 * fall back to daisyUI's stock light + dark themes. Goal: see whether
 * the v1 admin "drift" feel comes from custom theme tokens diverging
 * from canonical daisy. Decide keep/revert after visual comparison
 * against the pre-strip screenshots under
 * .claude/sandbox-screens/theme-before/.
 *
 * Known dependents that hardcode oklch values against the old palette
 * (kept untouched in this PR — flagged here for the follow-up if we
 * decide to keep the strip):
 *   - .bb-card dark-mode lift (color-mix on var(--color-base-100))
 *   - .bb-comment-bubble dark-mode background mix
 *   - .bb-tx-avatar--uncategorized
 *   - .bb-tx-amount--income (hardcoded green)
 *   - body bg, focus rings, ::selection, scrollbar (oklch hex tied to
 *     the old palette — may look slightly off under daisy stock)
 *   - .select::picker(select) dark-mode block (daisyUI v5 bug fix)
 * ─────────────────────────────────────────────────────────────────── */
@plugin "daisyui" {
  themes: light --default, dark --prefersdark;
}

/* ─── shadcn-flavored daisyUI themes ──────────────────────────────────
 * Faithful port of the shadcn/ui new-york default palette (oklch source
 * values from shadcn-ui/ui@apps/v4/app/globals.css) onto daisyUI 5's
 * token model. Near-zero-chroma grays, inverted primary (dark stamp in
 * light, light stamp in dark), red destructive.
 *
 * Theme names are kept as `light` / `dark` so they override daisyUI's
 * built-ins — the existing theme-toggle in base.html writes
 * data-theme="light"/"dark" verbatim, no markup changes needed.
 *
 * Semantic colors that shadcn doesn't define (warning/success/info) are
 * aliased to Tailwind palette tokens — same shades shadcn users compose
 * inline (amber/emerald/sky 500 → 400 in dark). This keeps daisy
 * components like alert-warning / badge-success rendering without
 * inventing a new tone vocabulary.
 * ──────────────────────────────────────────────────────────────── */
@plugin "daisyui/theme" {
  name: "light";
  default: true;
  prefersdark: false;
  color-scheme: light;

  /* Mintlify-style tinted-neutral palette, warmed at the brand egg-yolk
     hue (gold ~95). The base grays carry a faint WARM cast (chroma
     ~0.004–0.008 at hue ~95) so the UI reads as a warm off-white; base-100
     stays pure white to keep the content surface crisp. Primary is a
     near-black warm neutral, NOT gold: a solid gold CTA collided with the
     amber warning color, so gold is reserved for brand identity (the logo)
     plus the warm chrome tint, and actions use a crisp dark button. */
  --color-base-100: oklch(1 0 0);
  --color-base-200: oklch(0.971 0.004 95);
  --color-base-300: oklch(0.923 0.008 95);
  --color-base-content: oklch(0.16 0.01 95);

  --color-primary: oklch(0.205 0.012 95);
  --color-primary-content: oklch(0.985 0 0);

  --color-secondary: oklch(0.971 0.004 95);
  --color-secondary-content: oklch(0.205 0.012 95);

  --color-accent: oklch(0.971 0.004 95);
  --color-accent-content: oklch(0.205 0.012 95);

  --color-neutral: oklch(0.205 0.012 95);
  --color-neutral-content: oklch(0.985 0 0);

  --color-info: var(--color-sky-500);
  --color-info-content: oklch(0.985 0 0);

  --color-success: var(--color-emerald-500);
  --color-success-content: oklch(0.145 0 0);

  --color-warning: var(--color-amber-500);
  --color-warning-content: oklch(0.145 0 0);

  --color-error: oklch(0.577 0.245 27.325);
  --color-error-content: oklch(0.985 0 0);

  /* Radii matched to the Mintlify dashboard/docs (measured from
     app.mintlify.com → buttons 12px, and docs.breadbox.sh → .card 16px,
     small elements 6px): small controls (checkbox/toggle/badge) 6px,
     fields (button/input/select/tab) 12px, boxes (card/modal/alert) 16px. */
  --radius-selector: 0.375rem;
  --radius-field: 0.75rem;
  --radius-box: 1rem;

  --size-selector: 0.25rem;
  --size-field: 0.25rem;

  --border: 1px;
  --depth: 0;
  --noise: 1;
}

@plugin "daisyui/theme" {
  name: "dark";
  default: false;
  prefersdark: true;
  color-scheme: dark;

  /* Dark counterpart: same faint WARM cast on the dark grays. Primary is a
     warm near-white (shadcn-style inverted dark primary, with dark text) —
     gold stays brand-only, not the action color. */
  --color-base-100: oklch(0.15 0.006 95);
  --color-base-200: oklch(0.205 0.008 95);
  --color-base-300: oklch(0.269 0.01 95);
  --color-base-content: oklch(0.985 0.003 95);

  --color-primary: oklch(0.922 0.008 95);
  --color-primary-content: oklch(0.205 0.012 95);

  --color-secondary: oklch(0.269 0.01 95);
  --color-secondary-content: oklch(0.985 0.003 95);

  --color-accent: oklch(0.371 0.012 95);
  --color-accent-content: oklch(0.985 0 0);

  --color-neutral: oklch(0.97 0.004 95);
  --color-neutral-content: oklch(0.205 0.012 95);

  --color-info: var(--color-sky-400);
  --color-info-content: oklch(0.145 0 0);

  --color-success: var(--color-emerald-400);
  --color-success-content: oklch(0.145 0 0);

  --color-warning: var(--color-amber-400);
  --color-warning-content: oklch(0.145 0 0);

  --color-error: oklch(0.704 0.191 22.216);
  --color-error-content: oklch(0.145 0 0);

  /* Radii matched to the Mintlify dashboard/docs (measured from
     app.mintlify.com → buttons 12px, and docs.breadbox.sh → .card 16px,
     small elements 6px): small controls (checkbox/toggle/badge) 6px,
     fields (button/input/select/tab) 12px, boxes (card/modal/alert) 16px. */
  --radius-selector: 0.375rem;
  --radius-field: 0.75rem;
  --radius-box: 1rem;

  --size-selector: 0.25rem;
  --size-field: 0.25rem;

  --border: 1px;
  --depth: 0;
  --noise: 1;
}

/* Force-include classes that Tailwind's content scanner can't see
   because they live inside dynamic strings (Alpine :class, runtime
   templ expressions, JS className builders).
   Add new entries here whenever a class only exists at runtime —
   prefer this over chasing arbitrary-value scanner bugs. */
@source inline("max-w-[768px] max-w-[390px]");
/* Developer Mode reporter — arbitrary z-index/size only present in base.html. */
@source inline("z-[9998] z-[9999] w-[min(92vw,24rem)] max-h-[85vh] max-h-32");
/* Global prompt modal (base.html) — heights, expand transition, discard
   overlay, overscroll: arbitrary/uncommon classes Tailwind can't see in
   base.html or behind Alpine :class bindings. */
@source inline("h-[55vh] h-[78vh] max-h-[60vh] max-h-[82vh] transition-[max-width] backdrop-blur-sm bg-base-100/85 rounded-[inherit] z-20");

/* Utility: hide Alpine.js elements until initialized */
[x-cloak] { display: none !important; }

/* ─── Native browser polish ──────────────────────────────────────────
 * Two top-level hooks the platform now offers for free:
 *
 *   color-scheme: light dark
 *     Tells the browser to render native widgets (scrollbars, form
 *     controls, <dialog> backdrops, input UA styles) with dark-mode
 *     awareness driven by prefers-color-scheme. Without this they
 *     stay light against our dark theme. Safe everywhere.
 *
 *   @view-transition { navigation: auto }
 *     Opt-in to cross-document View Transitions on same-origin nav.
 *     Browsers that support it (Chrome 126+, Safari 18.2+) cross-fade
 *     between full page loads automatically; others do regular nav.
 *     Doesn't disable bfcache or the iOS swipe-back gesture — the spec
 *     layers transitions on top of native navigation.
 *
 *     Reduced-motion guard zeros out the animations rather than turning
 *     the at-rule off, which is the pattern recommended by the spec.
 * ──────────────────────────────────────────────────────────────── */
:root { color-scheme: light dark; }

@view-transition { navigation: auto; }

/* ─── View Transitions chrome polish ────────────────────────────────
 * Default cross-document transition fades the WHOLE viewport, which
 * makes the persistent sidebar/navbar look like they "flashed" too.
 * Giving them a view-transition-name tells the browser "this is the
 * same element on both pages" and confines the fade to <main>.
 *
 * Tighter duration + iOS-standard ease so the fade feels coincident
 * with a server-rendered MPA's <100ms TTFB rather than a slow 250ms
 * default that reads as artificial delay.
 *
 * NOTE: We deliberately do NOT name the active sidebar link as its
 * own group. With a name, the browser interpolates the old and new
 * bounding boxes, which makes the highlight visibly glide across the
 * column from the previous link to the new one. Sounds delightful in
 * theory; in practice it's distracting, especially when the two
 * active links are far apart or the glide crosses category headers.
 * Without a name, the active state cross-fades inside the sidebar
 * group — old highlight fades out in place, new one fades in in
 * place. Calmer.
 * ──────────────────────────────────────────────────────────── */
.bb-sidebar       { view-transition-name: app-sidebar; }
.bb-mobile-navbar { view-transition-name: app-navbar; }
.bb-progress-bar  { view-transition-name: app-progress; }

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 180ms;
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}

/* Universal reduced-motion guard. Honors the OS-level "Reduce Motion"
 * toggle (iOS Accessibility, macOS Accessibility, Windows Animations,
 * Linux GNOME Animations) by collapsing every transition/animation to
 * a near-zero duration and disabling smooth scroll. Recommended pattern
 * from web.dev / Eric Bailey — scoped to *::before/::after to catch
 * pseudo-elements, !important to override Tailwind/daisy/our own rules.
 *
 * The view-transition pseudo-elements need their own zero-out below
 * because they're not matched by the universal selector. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

/* ─── shadcn-inspired global overrides ─── */
@layer base {
  /* Ensure Lucide SVG icons don't steal clicks from interactive parents. */
  svg.lucide, svg.lucide * { pointer-events: none; }

  /*
   * Lucide icons are composed of multiple sibling <path> elements that share
   * `stroke="currentColor"`. If the inherited color carries alpha (Tailwind
   * v4's `text-COLOR/NN` syntax), every path strokes at that alpha
   * independently and overlapping strokes — the curl-arrows of `repeat`, the
   * stem of `circle-dollar-sign`, the check on `shield-check`, etc — stack
   * into visibly darker pixels at intersections.
   *
   * Strip alpha at the SVG so each stroke renders fully opaque. To dim a
   * Lucide icon, use `opacity-NN` on the <i> (Lucide preserves the class on
   * the generated <svg>), which composites the whole icon as a single layer
   * before applying transparency. `text-COLOR/NN` on Lucide icons is treated
   * the same as `text-COLOR` here — call sites that wanted dimming should
   * use `opacity-NN`.
   *
   * The explicit `/ 1` matters: in CSS relative color syntax, an omitted
   * alpha defaults to the *origin* color's alpha (not 100%), so
   * `rgb(from currentColor r g b)` alone passes the parent's alpha straight
   * through — making this rule a no-op whenever the parent sets
   * `text-COLOR/NN` (e.g. the sidebar's `.bb-sidebar-link`
   * `text-base-content/60`). `/ 1` forces opaque output.
   */
  svg.lucide { color: rgb(from currentColor r g b / 1); }

  /* Brand icons follow the same pointer-events rule as lucide — they
   * always sit inside an interactive parent (link, button, card
   * header) and shouldn't intercept clicks. */
  svg.brand, svg.brand * { pointer-events: none; }

  /* Brand marks that fill with currentColor (Plaid, MCP) must mirror the
   * svg.lucide alpha-strip above. Without it, a muted container color —
   * text-base-content/45 (settings section headers) or /65 (settings-rail
   * tabs) — bleeds its alpha into the fill, so the mark renders a softer
   * gray than the Lucide icons sitting right next to it. The baked-fill
   * marks (GitHub, Anthropic, Claude, Teller) use explicit fills and
   * ignore `color`, so this is a no-op for them. */
  svg.brand { color: rgb(from currentColor r g b / 1); }

  /* Two kinds of brand mark live here:
   *
   *   1. Monochrome marks meant to read as UI icons (Plaid, MCP). Their
   *      <path>s use fill="currentColor", so they inherit whatever color
   *      the container sets (muted section-header text, the warning tile,
   *      the active settings-rail tab) and track light/dark automatically
   *      — exactly like the Lucide icons sitting next to them. Nothing to
   *      flip here; currentColor already follows the theme.
   *
   *   2. Marks that keep their official baked-in fill because the fill IS
   *      the brand. The black-on-light ones (GitHub, Anthropic) need a
   *      flip to white in dark mode so they don't disappear on a base-100
   *      surface. Claude orange already contrasts in both modes; Teller is
   *      multi-color and stays.
   *
   * The flip must follow the *resolved* daisy theme, not the raw OS
   * preference — the theme toggle ($store.theme) can force
   * data-theme="light"/"dark" independent of prefers-color-scheme. Keying
   * only off @media (prefers-color-scheme: dark) makes the mark turn white
   * on a forced-light page when the OS is dark (white-on-white, invisible),
   * and stay black on a forced-dark page when the OS is light. So pair the
   * media query (guarded against a forced-light override) with an explicit
   * [data-theme="dark"] rule — the same shape the .select / .bb-tx-row /
   * .bb-tx-avatar dark overrides use below. */
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) svg.brand-github path,
    :root:not([data-theme="light"]) svg.brand-anthropic path { fill: #fff; }
  }
  [data-theme="dark"] svg.brand-github path,
  [data-theme="dark"] svg.brand-anthropic path { fill: #fff; }

  /* `brand-onbtn`: the GitHub mark sitting on a solid-tone button
   * (btn-primary) must track the *button's* text color, not the page
   * background. primary inverts against the surface in both themes
   * (dark button / light text in light mode, and the reverse in dark),
   * while the baked-fill flip above keys off the page theme — so the
   * unmodified mark renders black-on-black in light mode and
   * white-on-white in dark. Opt into currentColor to follow
   * primary-content instead. Mirrors the base / media / data-theme
   * shape above so it wins on specificity + source order, no !important. */
  svg.brand-github.brand-onbtn path { fill: currentColor; }
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) svg.brand-github.brand-onbtn path { fill: currentColor; }
  }
  [data-theme="dark"] svg.brand-github.brand-onbtn path { fill: currentColor; }

  /*
   * Pre-hydration size for Lucide placeholders.
   *
   * Before lucide.createIcons() swaps `<i data-lucide="x">` for `<svg>`, the
   * <i> renders as `display:inline` with no intrinsic dimensions, so Tailwind
   * sizing utilities (`w-4 h-4`, etc.) on the <i> do not produce a real box —
   * the placeholder collapses to 0×0 and reflows when the SVG arrives. Make
   * the <i> behave like an inline-block 1em×1em box so the row is the right
   * height immediately and there is no layout shift on hydration.
   *
   * Once lucide replaces the <i> with `<svg class="lucide">`, this rule no
   * longer applies (svg is not <i>), so SVG sizing remains controlled by the
   * size utilities migrated onto the SVG by lucide's clone behavior.
   */
  i[data-lucide] {
    display: inline-block;
    width: 1em;
    height: 1em;
    vertical-align: -0.125em;
  }

  html { scroll-behavior: smooth; }

  body {
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    /* Match safe area bars to content background */
    background-color: var(--color-base-200);
  }

  /* ── Themed text selection ── */
  ::selection {
    background: oklch(from var(--color-base-content) l c h / 0.15);
    color: var(--color-base-content);
  }

  /* ── Focus rings: accessible, consistent, shadcn-style ── */
  :focus-visible {
    outline: 2px solid oklch(from var(--color-base-content) l c h / 0.4);
    outline-offset: 2px;
    border-radius: inherit;
  }

  /* ── Thin scrollbars ── */
  * {
    scrollbar-width: thin;
    scrollbar-color: oklch(from var(--color-base-content) l c h / 0.25) transparent;
  }
  ::-webkit-scrollbar {
    width: 6px;
    height: 6px;
  }
  ::-webkit-scrollbar-track {
    background: transparent;
  }
  ::-webkit-scrollbar-thumb {
    background: oklch(from var(--color-base-content) l c h / 0.25);
    border-radius: 3px;
  }
  ::-webkit-scrollbar-thumb:hover {
    background: oklch(from var(--color-base-content) l c h / 0.4);
  }

  /* ── Global link transitions ── */
  a { transition: color 0.15s ease; }
}

/* ─── App-specific component classes ─── */
@layer components {

  /* Daisy `breadcrumbs` is the source of truth (see breadcrumb.templ).
     Only override: hide the scrollbar to match the prior shadcn-style
     trail, daisy keeps the system scrollbar visible by default. */
  .breadcrumbs { scrollbar-width: none; -ms-overflow-style: none; }
  .breadcrumbs::-webkit-scrollbar { display: none; }

  /* ── Cards: Mintlify-style — flat 1px border, no drop shadow ──
   * Mintlify's surfaces are border-only: a hairline defines the card
   * against the bright content column, with no shadow lift. We dropped
   * the shadcn `shadow-sm` (it read as an "off" halo on the new white
   * content surface). Radius is rounded-2xl (16px) to match Mintlify's
   * .card radius (measured on docs.breadbox.sh). Dark mode keeps the
   * color-mix surface lift below; the faint border carries elevation. */
  .bb-card {
    @apply bg-base-100 rounded-2xl border border-base-300;
    transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.2s ease;
  }

  .bb-card--interactive {
    @apply cursor-pointer;
  }

  .bb-card--interactive:hover {
    @apply bg-base-200/50;
    border-color: var(--color-base-300);
  }

  /* ── Form card primitives ── */
  /* Icon-header: colored tile + title + optional subtitle.
     Used at the top of sectioned form cards (bb-card p-0 overflow-hidden). */
  .bb-icon-header { @apply flex items-center gap-3 mb-5; }
  .bb-icon-header__tile { @apply flex items-center justify-center w-10 h-10 rounded-xl shrink-0; }
  .bb-icon-header__text { @apply min-w-0; }

  /* Color modifiers for the icon tile. Pair with .bb-icon-header__tile
     (40×40 inside form cards) or .bb-entity-header__tile (48×48 at the
     top of detail pages — see EntityHeader templ component).
     `primary` uses /8 for a slightly softer hero tint (the page-defining
     accent); the semantic tones use /10 so info/success/warning/error
     read uniformly against each other in nearby cards. */
  .bb-icon-tile--primary { @apply bg-primary/8 text-primary; }
  .bb-icon-tile--accent  { @apply bg-accent/10 text-accent; }
  .bb-icon-tile--info    { @apply bg-info/10 text-info; }
  .bb-icon-tile--success { @apply bg-success/10 text-success; }
  .bb-icon-tile--warning { @apply bg-warning/10 text-warning; }
  .bb-icon-tile--error   { @apply bg-error/10 text-error; }
  .bb-icon-tile--neutral { @apply bg-base-200 text-base-content/60; }

  /* Detail-page entity header — 40×40 tile on mobile, 48×48 at sm+
     for the top-of-page row that pairs an icon with the entity name +
     badges + meta line. The mobile shrink reclaims horizontal space
     for the title once the meta row and secondary action are hidden
     (see EntityHeader templ component). */
  .bb-entity-header__tile { @apply flex items-center justify-center w-10 h-10 sm:w-12 sm:h-12 rounded-lg sm:rounded-xl shrink-0; }

  /* Action row at the bottom of a sectioned form card.
     Left-aligned on mobile (thumb-reach), right-aligned on sm+. */
  .bb-action-row { @apply p-4 sm:p-5 border-t border-base-300 bg-base-200/20 flex items-center justify-start sm:justify-end gap-2; }

  /* ── Settings surface — see .claude/rules/settings.md ──
     The shared shape for every /settings/* tab. SettingsSection wraps
     SettingsRow children; auto-save selects and one-Save-per-section
     forms both reuse these classes.

     We chose a slightly heavier outline (border-base-300) than .bb-card
     and a flush, full-bleed header so the section reads as a discrete
     "group" the way macOS / Linear settings do — not as another card.
     The header is intentionally quiet (small icon, sm/font-semibold
     title) — the row labels carry the visual hierarchy. */
  /* === Settings: borderless gutter header + carded right content ===
     Modeled on Mintlify settings. A section is a two-column block: the
     left gutter is the section's identity (icon + title + caption), the
     right column is the working surface — usually a subtle CARD that
     groups the controls. The section itself is borderless; sections are
     separated by a full-width hairline + even vertical rhythm.

     `--full` drops the gutter so the content spans the whole width
     (overview cards, stats grids, page-level alerts). Separator + rhythm
     are applied UNCONDITIONALLY (top border + symmetric padding) rather
     than via :first-child, because sections are frequently wrapped in a
     `<div id="anchor">` for hash deep-links, which defeats sibling rules.
     The page header carries no bottom border, so the first section's top
     border reads as the line under the title. */
  /* Each section is its own inline-size container so the gutter/content
     split flips on the *available* width, not the viewport. The live
     Settings page sits in a narrow column (viewport − app sidebar −
     settings rail), so a viewport `lg:` breakpoint flipped to two columns
     while the content column was still cramped (~1030px viewport → a
     ~290px control column). A container query measures the real width, so
     the same section behaves correctly on the live page and in the
     wide-open /design sandbox. */
  .bb-settings-section {
    @apply border-t border-base-200 py-7;
    container: settings-section / inline-size;
  }
  .bb-settings-section__grid {
    @apply grid grid-cols-1 gap-x-10 gap-y-3;
  }
  /* Two-column (gutter + content) only once the section is wide enough to
     give the content column a comfortable minimum: 14rem gutter + 2.5rem
     gap + ~10rem leaves the content ≥ ~24rem before flipping. `--full`
     sections stay single-column at every width. */
  @container settings-section (min-width: 40rem) {
    .bb-settings-section:not(.bb-settings-section--full) .bb-settings-section__grid {
      grid-template-columns: 14rem minmax(0, 1fr);
    }
  }
  /* Left gutter — section identity. Quiet by design; the card on the
     right carries the working hierarchy. Top-aligned with the content. */
  .bb-settings-section__meta {
    @apply min-w-0;
  }
  .bb-settings-section__heading {
    @apply flex items-center gap-2 min-w-0;
  }
  .bb-settings-section__title {
    @apply text-sm font-semibold leading-tight;
  }
  .bb-settings-section__caption {
    @apply text-xs text-base-content/50 mt-1 leading-relaxed;
  }
  .bb-settings-section__content {
    @apply min-w-0;
  }
  /* Section primary action (e.g. "Create key") — sits above the card,
     flush-right, its top edge aligned with the gutter title. */
  .bb-settings-section__action {
    @apply flex justify-start lg:justify-end gap-2 mb-3 empty:hidden;
  }
  .bb-settings-section--danger .bb-settings-section__title {
    @apply text-error;
  }

  /* The content card — subtle elevation over the base-200 page surface.
     Rows live inside it, divided by hairlines; an optional header/footer
     brackets them. Flatter than .bb-card (no shadow) so it reads as a
     quiet container, not a raised panel. */
  .bb-settings-card {
    @apply rounded-xl border border-base-300 bg-base-100 overflow-hidden;
  }
  .bb-settings-card--danger {
    @apply border-error/25 bg-error/[0.025];
  }
  .bb-settings-card__header {
    @apply px-4 sm:px-5 py-3.5 border-b border-base-200;
  }
  .bb-settings-card__title {
    @apply text-sm font-semibold leading-tight flex items-center gap-2 flex-wrap;
  }
  .bb-settings-card__caption {
    @apply text-xs text-base-content/50 mt-1 leading-snug;
  }
  .bb-settings-card__footer {
    @apply px-4 sm:px-5 py-3 border-t border-base-200 bg-base-200/30 flex items-center justify-end gap-2;
  }

  /* Rows. Inside a card they carry horizontal padding + hairline dividers;
     `--stack` flips to full-width-below for textareas / file inputs /
     reveals. */
  .bb-settings-row {
    @apply flex items-center justify-between gap-4 px-4 sm:px-5 py-3.5 min-h-[3rem];
  }
  .bb-settings-card > .bb-settings-row + .bb-settings-row,
  .bb-settings-card__body > .bb-settings-row + .bb-settings-row,
  .bb-settings-section__body > .bb-settings-row + .bb-settings-row {
    @apply border-t border-base-200;
  }
  .bb-settings-row--stack {
    @apply flex-col items-stretch gap-2;
  }
  .bb-settings-row__main {
    @apply min-w-0 flex-1;
  }
  .bb-settings-row__label {
    @apply text-sm font-medium leading-tight flex items-center gap-2 flex-wrap;
  }
  .bb-settings-row__caption {
    @apply text-xs text-base-content/50 mt-1 leading-snug;
  }
  .bb-settings-row__control {
    @apply shrink-0 flex items-center;
  }
  .bb-settings-row--stack .bb-settings-row__control {
    @apply w-full;
  }
  /* When a row IS a link (action list rows), the whole row hovers. */
  .bb-settings-row--link {
    @apply hover:bg-base-200/50 transition-colors no-underline cursor-pointer;
  }
  /* Footer used inside Bare/full-width content (no card wrapper). */
  .bb-settings-section__footer {
    @apply flex items-center justify-end gap-2 mt-4 pt-3 border-t border-base-200;
  }

  /* === Settings shell: the rail is a true extension of the app sidebar ===
     On lg+ the rail is a STICKY, full-height base-200 column sitting flush
     against the app sidebar's right edge — same recessed surface + link
     language as .bb-sidebar, so Settings reads as the sidebar's own
     sub-navigation rather than page content. It's in flow (a sticky peer of
     the sidebar, not a fixed overlay) so it rides the page's overscroll the
     SAME way .bb-sidebar does — the two never drift apart on a rubber-band
     bounce. The topbar is offset to its right (the :has() offset at the end
     of this block). Below lg it collapses to an in-flow horizontal scroll
     strip at the top of the tab. */
  .bb-settings-layout {
    /* lg:gap-x-6 restores the 1.5rem gutter between the rail and the content
       column. The rail's -1.5rem left margin (below) makes it flush against
       the sidebar, which also pulls the content flush against the rail's
       right edge; the column-gap puts the breathing room back. It's a
       column-gap, so it only applies to the lg+ row layout — on the mobile
       flex-col stack it's the cross axis and has no effect. */
    /* min-h fills the viewport so the sticky rail has a full-height column and
       short settings pages don't float the rail short. Subtract BOTH main's
       3rem (sm:p-6 top+bottom) AND the 3.5rem in-flow sticky topbar (.bb-topbar
       is h-14) — omitting the topbar makes main a full 100dvh that stacks below
       the topbar and overscrolls the page by 3.5rem. The 6.5rem total mirrors
       the rail's -6.5rem of negative margin (-(3.5rem+1.5rem) top, -1.5rem
       bottom) so rail height (100dvh) minus its margins equals this box. */
    @apply flex flex-col lg:flex-row lg:gap-x-6 lg:min-h-[calc(100dvh-6.5rem)];
  }
  /* Mobile default: an in-flow strip above the content. The lg+ block
     below lifts it out into the fixed sidebar-extension column. */
  .bb-settings-rail {
    @apply shrink-0 mb-5;
  }
  .bb-settings-rail__list {
    @apply flex flex-row gap-1 overflow-x-auto pb-1;
  }
  .bb-settings-rail__group-label {
    @apply hidden lg:block text-[0.65rem] font-medium uppercase tracking-wide text-base-content/40 px-3 pt-5 pb-1.5;
  }
  /* Items mirror .bb-sidebar-link: muted by default, base-300 on hover,
     and a base-300 fill (no border) when active — the active background
     matches the hover background, so the row reads as "settled here"
     rather than as a separate pill. Weight (font-semibold) + a
     full-opacity icon keep the active item distinct from a transient
     hover. The transparent default border is kept so the box-model
     matches the inactive state (no layout shift). */
  .bb-settings-rail__item {
    @apply flex items-center gap-3 px-3 py-2.5 lg:py-1.5 min-h-11 lg:min-h-8 shrink-0
           rounded-lg text-sm font-medium whitespace-nowrap no-underline border border-transparent
           text-base-content/60 hover:bg-base-300 hover:text-base-content;
    transition: background-color 0.15s ease, color 0.15s ease;
  }
  .bb-settings-rail__item[data-active="true"] {
    @apply bg-base-300 text-base-content font-semibold;
  }
  .bb-settings-rail__item i, .bb-settings-rail__item svg {
    @apply w-4 h-4 opacity-50;
    transition: opacity 0.15s ease;
  }
  .bb-settings-rail__item:hover i, .bb-settings-rail__item:hover svg {
    @apply opacity-70;
  }
  .bb-settings-rail__item[data-active="true"] i, .bb-settings-rail__item[data-active="true"] svg {
    @apply opacity-100;
  }
  .bb-settings-content {
    @apply flex-1 min-w-0;
  }

  /* lg+: the rail is a sticky, full-height column — the in-flow peer of the
     app sidebar (which is itself a sticky .drawer-side). Being in flow, not
     fixed, is the whole point: it rides the page's overscroll exactly like
     .bb-sidebar, so a rubber-band bounce moves them together instead of
     freezing the rail against a bouncing page. */
  @media (min-width: 1024px) {
    .bb-settings-rail {
      position: sticky;
      top: 0;
      align-self: flex-start;
      /* Pull flush to the viewport top — cancel the 3.5rem topbar (.bb-topbar
         is h-14) + main's 1.5rem (sm:p-6) top padding so the rail's first tab
         lines up with the sidebar's brand row — and flush against the
         sidebar's right edge — cancel main's 1.5rem left padding. Lands
         exactly where the old fixed rail sat, but now in normal flow.
         The -1.5rem BOTTOM margin is the symmetric counterpart: the rail's
         sticky-constraint rect is its containing block (.bb-settings-layout),
         whose bottom edge sits 1.5rem above the document bottom because of
         main's sm:p-6 bottom padding. Pulling the margin box up by 1.5rem lets
         the border box extend through that padding to the document edge, so at
         full scroll the rail reaches the bottom in lockstep with .drawer-side
         (whose constraint rect IS the whole document) instead of leaving a
         1.5rem strip of page background below it. */
      margin: calc((3.5rem + 1.5rem) * -1) 0 -1.5rem -1.5rem;
      height: 100dvh;
      width: 14rem;
      flex: none;
      z-index: 31; /* above the sticky topbar (z-30) */
      display: flex;
      flex-direction: column;
      /* Same vertical + horizontal rhythm as .bb-sidebar (py-5 / px-3). */
      padding: 1.25rem 0.75rem;
      background-color: var(--color-base-200);
      border-right: 1px solid var(--color-base-300);
    }
    /* The list fills the rail and scrolls on its own if the sections ever
       overflow a short viewport. */
    .bb-settings-rail__list {
      flex: 1 1 auto;
      min-height: 0;
      flex-direction: column;
      gap: 0.125rem;
      overflow-x: visible;
      overflow-y: auto;
      padding-bottom: 0;
    }
    /* Offset only the topbar by the rail width so it begins to the rail's
       right. <main> is NOT offset — the rail now lives inside it in flow, and
       the daisy drawer grid already starts <main> at the sidebar's right
       edge, so the rail lands flush against the sidebar and the
       sidebar-collapse shift is handled for free (no manual `left` tracking).
       Scoped to Settings via :has() — the rail only exists there. */
    .drawer-content:has(.bb-settings-layout) .bb-topbar {
      margin-left: 14rem;
    }
  }

  /* Form inputs/selects with subtle base-200 bg that clears on focus. */
  .bb-form-input { @apply input w-full rounded-xl bg-base-200/50 focus:bg-base-100 transition-colors; }
  .bb-form-select { @apply select w-full rounded-xl bg-base-200; }

  /* Inline error alert inside a form card (pairs with alert-circle icon). */
  .bb-form-error { @apply flex items-center gap-3 rounded-xl bg-error/10 text-error px-4 py-3 text-sm; }

  /* Danger-zone card tint — combine with .bb-card for destructive-action cards. */
  .bb-danger-card { @apply border-error/20 bg-error/[0.02]; }

  /* ── Mobile Navbar — sticky so it anchors at the top while scrolling ── */
  .bb-mobile-navbar {
    @apply navbar px-2 bg-base-100;
    position: sticky;
    top: 0;
    z-index: 30;
    border-bottom: 1px solid oklch(from var(--color-base-content) l c h / 0.06);
    /* Subtle backdrop blur for depth when content scrolls underneath */
    -webkit-backdrop-filter: blur(8px);
            backdrop-filter: blur(8px);
    background-color: color-mix(in oklch, var(--color-base-100) 92%, transparent);
  }

  .bb-mobile-search-btn {
    @apply flex items-center gap-1.5 h-10 px-3 rounded-full
           text-sm text-base-content/50 bg-base-200/60 border border-base-300/50
           cursor-pointer select-none;
    transition: background-color 0.15s ease, border-color 0.15s ease;
    min-width: 44px;
    min-height: 44px;
  }

  .bb-mobile-search-btn:active {
    @apply bg-base-200 border-base-300;
    transform: scale(0.96);
  }

  .bb-mobile-search-btn i, .bb-mobile-search-btn svg {
    @apply w-4 h-4 opacity-50;
  }

  .bb-mobile-search-label {
    @apply text-xs text-base-content/35 hidden;
  }

  @media (min-width: 480px) {
    .bb-mobile-search-label {
      @apply inline;
    }
  }

  /* ── Desktop topbar ── static across pages; the lg+ counterpart of
     .bb-mobile-navbar. Sticky, hairline-bordered, lightly blurred. Holds
     the breadcrumb (left) + search / theme / profile (right). */
  .bb-topbar {
    @apply hidden lg:flex items-center justify-between gap-3 h-14 px-4 sm:px-6 bg-base-100;
    position: sticky;
    top: 0;
    z-index: 30;
    border-bottom: 1px solid oklch(from var(--color-base-content) l c h / 0.06);
    -webkit-backdrop-filter: blur(8px);
            backdrop-filter: blur(8px);
    background-color: color-mix(in oklch, var(--color-base-100) 92%, transparent);
    view-transition-name: app-topbar;
  }

  .bb-topbar-breadcrumb {
    @apply flex items-center gap-1.5 min-w-0 text-sm;
  }

  /* ⌘K search trigger — shared chrome for the topbar trigger and the
     sidebar trigger (the latter now only in the /design sandbox). The
     surface + hover + transition live here once so re-theming the trigger
     can't make the two drift; each variant's sizing/spacing/icon-size are
     deltas (.bb-topbar-search below, .bb-sidebar-search further down). */
  .bb-topbar-search,
  .bb-sidebar-search {
    @apply flex items-center rounded-lg text-sm
           bg-base-200/50 border border-base-300/50 cursor-pointer select-none;
    transition: background-color 0.15s ease, border-color 0.15s ease;
  }

  .bb-topbar-search:hover,
  .bb-sidebar-search:hover {
    @apply bg-base-200 border-base-300;
  }

  /* Topbar variant: navbar-row sizing; hidden < md so the breadcrumb keeps
     its width (⌘K + the palette still work there). */
  .bb-topbar-search {
    @apply hidden md:flex h-9 w-56 px-3 gap-2 text-base-content/40;
  }

  .bb-topbar-search svg, .bb-topbar-search i {
    @apply w-4 h-4;
  }

  /* ── Sidebar ── */
  /* Recessed surface (base-200) — the content column is the bright one
     (base-100). This is the Mintlify-style flip of the previous
     white-sidebar / gray-content arrangement. */
  .bb-sidebar {
    @apply bg-base-200 h-full w-[85vw] max-w-80 sm:w-72 lg:w-64 px-3 py-5 flex flex-col border-r border-base-300;
  }

  .bb-sidebar-brand {
    @apply text-lg font-semibold px-3 py-1 mb-5 no-underline text-base-content flex items-center gap-2;
    /* When used inside the brand+close flex row we override mb and px via !mb-0 !px-0 !py-0 */
  }

  .bb-sidebar-nav {
    @apply flex flex-col gap-0.5 flex-1 overflow-y-auto pb-4;
    scrollbar-width: thin;
    scrollbar-color: transparent transparent;
    /* Contain the iOS rubber-band — without this, dragging past either end
       of the nav list pulls the page underneath the drawer along with it. */
    overscroll-behavior: contain;
  }

  .bb-sidebar-nav:hover,
  .bb-sidebar-nav:active {
    scrollbar-color: oklch(var(--bc) / 0.15) transparent;
  }

  /* ── Mobile: iOS 26-style scroll fade gradients ── */
  @media (max-width: 1023px) {
    .bb-sidebar-nav {
      scrollbar-width: none;
      --fade-size: 2rem;
      --fade-top: 0;
      --fade-bottom: 0;
      mask-image: linear-gradient(
        to bottom,
        transparent,
        black calc(var(--fade-size) * var(--fade-top)),
        black calc(100% - var(--fade-size) * var(--fade-bottom)),
        transparent
      );
      -webkit-mask-image: linear-gradient(
        to bottom,
        transparent,
        black calc(var(--fade-size) * var(--fade-top)),
        black calc(100% - var(--fade-size) * var(--fade-bottom)),
        transparent
      );
    }
  }

  .bb-sidebar-section {
    @apply text-[0.7rem] lg:text-[0.65rem] font-medium text-base-content/40 px-3 pt-5 pb-2 lg:pb-1.5;
  }

  /* Hover/active recalibrated for the recessed (base-200) rail: hover
     darkens one step to base-300; active is a bright base-100 pill that
     pops against the gray rail and visually links to the content column
     (Mintlify pattern). Were both base-200 — invisible on a base-200 rail. */
  .bb-sidebar-link {
    @apply flex items-center gap-3 px-3 py-2.5 lg:py-1.5 min-h-11 lg:min-h-8
           shrink-0 rounded-lg text-sm font-medium
           text-base-content/60 no-underline border border-transparent
           hover:bg-base-300 hover:text-base-content;
    transition: background-color 0.15s ease, color 0.15s ease, transform 0.1s ease;
  }

  .bb-sidebar-link:active {
    transform: scale(0.98);
  }

  /* Active item: a base-300 fill (no border) — the same background as
     hover — so the row reads as "settled here" rather than as a separate
     pill. Weight (font-semibold) + a full-opacity icon distinguish
     active from a transient hover. (Label/icon stay base-content rather
     than text-primary — the brand gold is too light to read as a
     text/thin-stroke icon. The accent surfaces on buttons, filter tabs,
     focus rings and links instead.) The base .bb-sidebar-link keeps its
     transparent border so the borderless active state doesn't shift
     layout. */
  .bb-sidebar-link--active {
    @apply bg-base-300 text-base-content font-semibold;
  }

  .bb-sidebar-link i, .bb-sidebar-link svg {
    @apply w-[1.125rem] h-[1.125rem] lg:w-4 lg:h-4 opacity-50;
    transition: opacity 0.15s ease;
    /* Hint the browser to keep icons on their own compositing layer so
       the sub-pixel position stays locked when the parent
       .bb-sidebar-link triggers its `transform: scale(0.98)` :active
       state — without this, the browser re-rounds child positions and
       the icons visibly jiggle by 1px. `will-change` doesn't override
       the actual transform (so .bb-nav-external-arrow's translate-x
       animation still works). */
    will-change: transform;
    backface-visibility: hidden;
  }

  .bb-sidebar-link:hover i, .bb-sidebar-link:hover svg {
    @apply opacity-70;
  }

  .bb-sidebar-link--active i, .bb-sidebar-link--active svg {
    @apply opacity-100;
  }

  /* Label span inside every sidebar link — the hook the collapsed
     icon-rail uses to hide the text while keeping the icon. flex-1 lets it
     fill the row so any trailing badge sits at the far edge. */
  .bb-sidebar-link__label {
    @apply flex-1 truncate;
  }

  /* ── Collapse toggle: hover icon swap ──
     The footer collapse button stacks three icons in one slot and
     crossfades between them on hover so the swap reads as a morph, not a
     hard cut: panel-left at rest, panel-left-close while expanded (the
     click collapses), panel-left-open while collapsed (it expands). The
     `.bb-sidebar-collapse__icons` wrapper is exactly one nav-icon wide so
     the collapsed-rail centring geometry (which keys off a w-4 icon) is
     preserved. All rules are scoped under `.bb-sidebar-collapse` so they
     out-specify the generic `.bb-sidebar-link svg` opacity above. */
  .bb-sidebar-collapse .bb-sidebar-collapse__icons {
    @apply relative inline-flex items-center justify-center w-4 h-4 shrink-0;
  }
  .bb-sidebar-collapse .bb-sidebar-collapse__icon {
    @apply absolute inset-0;
    transition: opacity 0.18s ease, transform 0.18s ease;
  }
  /* At rest only the panel-left icon shows; the two directional cues are
     hidden. (0,2,0) out-specifies the generic `.bb-sidebar-link svg`
     opacity:.5 above.) */
  .bb-sidebar-collapse .bb-sidebar-collapse__icon--close,
  .bb-sidebar-collapse .bb-sidebar-collapse__icon--open {
    opacity: 0;
    transform: scale(0.82);
  }
  /* On hover, take full ownership of all three icons' opacity: blank them
     out first, then the state-specific rule below raises exactly one. This
     blanket (0,3,0) is what beats the generic `.bb-sidebar-link:hover svg`
     opacity:.7 (0,2,1) — without it that rule would leak a second icon in. */
  .bb-sidebar-collapse:hover .bb-sidebar-collapse__icon {
    opacity: 0;
    transform: scale(0.82);
  }
  /* Expanded + hover → panel-left-close fades in. */
  .bb-sidebar-collapse:hover .bb-sidebar-collapse__icon--close {
    opacity: 0.7;
    transform: none;
  }
  /* Collapsed + hover → panel-left-open instead (close stays hidden). */
  html[data-sidebar="collapsed"] .bb-sidebar-collapse:hover .bb-sidebar-collapse__icon--close {
    opacity: 0;
    transform: scale(0.82);
  }
  html[data-sidebar="collapsed"] .bb-sidebar-collapse:hover .bb-sidebar-collapse__icon--open {
    opacity: 0.7;
    transform: none;
  }
  /* Reduced motion: keep the swap, drop the animation. */
  @media (prefers-reduced-motion: reduce) {
    .bb-sidebar-collapse .bb-sidebar-collapse__icon {
      transition: none;
    }
  }

  /* ── Trailing loading spinner on mobile sidebar tap ──
     Injected by base.html's link-click handler. The visible spin is a
     CSS @keyframes rotate on an inline SVG (compositor-eligible so it
     survives the navigation window) — *not* daisy's
     `loading loading-spinner`, whose SMIL inside `mask-image` is
     rasterized once by Chromium/WebKit and never animates. */
  .bb-sidebar-link .bb-sidebar-loading {
    @apply inline-flex items-center justify-center w-3.5 h-3.5 shrink-0;
    color: currentColor;
  }
  .bb-sidebar-link .bb-sidebar-loading svg {
    @apply w-3.5 h-3.5 opacity-100;
    transform-origin: 50% 50%;
    animation: bb-sidebar-loading-spin 0.7s linear infinite;
    will-change: transform;
    backface-visibility: hidden;
  }
  @keyframes bb-sidebar-loading-spin {
    to { transform: rotate(360deg); }
  }

  /* ── External-link arrow (Documentation / GitHub buttons) ── */
  /* Always visible on mobile (no hover affordance there), hidden by
     default and faded in on hover at lg+. Kept inside the
     bb-sidebar-link cascade so the arrow inherits the link's icon
     colour but overrides the size + opacity rules. */
  .bb-sidebar-link .bb-nav-external-arrow,
  .bb-sidebar-link .bb-nav-external-arrow svg {
    @apply w-3.5 h-3.5 opacity-60 lg:opacity-0;
    transition: opacity 0.15s ease;
  }
  .bb-sidebar-link.group:hover .bb-nav-external-arrow,
  .bb-sidebar-link.group:hover .bb-nav-external-arrow svg {
    @apply opacity-60;
  }

  /* ── Sidebar notification badges ── */
  .bb-nav-badge {
    @apply text-[0.6rem] font-semibold leading-none tabular-nums
           min-w-[1.25rem] h-[1.25rem] flex items-center justify-center
           rounded-full shrink-0 px-1
           bg-base-content/8 text-base-content/50;
    transition: background-color 0.15s ease, color 0.15s ease;
  }

  .bb-sidebar-link:hover .bb-nav-badge {
    @apply bg-base-content/12 text-base-content/70;
  }

  .bb-sidebar-link--active .bb-nav-badge {
    @apply bg-base-content/12 text-base-content/70;
  }

  .bb-nav-badge--warn {
    @apply bg-warning/15 text-warning;
  }

  .bb-sidebar-link:hover .bb-nav-badge--warn {
    @apply bg-warning/22 text-warning;
  }

  /* Error tone — a failed workflow run lights the Workflows nav badge red.
     error is one of the vivid dark-theme tones, so the soft fill stays
     visible in both modes (unlike primary/neutral soft badges). The
     active/hover selectors are scoped to --error so they out-specify the
     generic .bb-sidebar-link--active .bb-nav-badge neutral override — the
     badge must stay red even on the /workflows page itself. */
  .bb-nav-badge--error,
  .bb-sidebar-link--active .bb-nav-badge--error {
    @apply bg-error/15 text-error;
  }

  .bb-sidebar-link:hover .bb-nav-badge--error,
  .bb-sidebar-link--active:hover .bb-nav-badge--error {
    @apply bg-error/22 text-error;
  }

  /* ── Update-available callout card ──
     A small "what's new"-style nudge pinned to the foot of the rail (above
     the Collapse button) when an admin runs an older release than the latest
     GitHub tag. A bright base-100 card on the recessed base-200 rail with a
     dismiss ✕. Part of the bespoke .bb-sidebar-* rail surface (daisy has no
     sidebar-callout primitive); the collapsed-rail hide lives with the other
     collapse rules below. */
  .bb-update-card {
    @apply relative mt-2 flex flex-col gap-1 rounded-xl border border-base-300
           bg-base-100 p-3 shadow-sm;
  }
  .bb-update-card__dismiss {
    @apply absolute right-2 top-2 inline-flex h-6 w-6 items-center justify-center
           rounded-md text-base-content/40 cursor-pointer;
    transition: background-color 0.15s ease, color 0.15s ease;
  }
  .bb-update-card__dismiss:hover {
    @apply bg-base-200 text-base-content/70;
  }
  .bb-update-card__dismiss svg { @apply h-3.5 w-3.5; }
  .bb-update-card__eyebrow {
    @apply inline-flex items-center gap-1 text-[0.7rem] font-semibold uppercase
           tracking-wide text-success;
  }
  .bb-update-card__eyebrow svg { @apply h-3.5 w-3.5; }
  .bb-update-card__title {
    @apply pr-5 text-sm font-semibold leading-tight text-base-content;
  }
  .bb-update-card__desc {
    @apply text-xs leading-snug text-base-content/60;
  }
  .bb-update-card__action {
    @apply mt-1 inline-flex items-center gap-1 text-xs font-semibold text-primary no-underline;
  }
  .bb-update-card__action:hover { @apply underline; }
  .bb-update-card__action svg {
    @apply h-3.5 w-3.5;
    transition: transform 0.15s ease;
  }
  .bb-update-card__action:hover svg { transform: translateX(2px); }

  /* ── Tag chips (pill-shaped) ── */
  /*
   * Canonical tag styling. Always pill-shaped to differentiate from status badges.
   * Color driven by --tag-color CSS variable so server- and client-rendered chips stay consistent.
   *
   * Variants:
   *   .bb-tag         — default (forms, filter panels, dense detail rows)
   *   .bb-tag-lg      — primary interactive surface (detail-page tag editor)
   *   .bb-tag-sm      — compact (transaction rows, dense lists)
   *   .bb-tag-ghost   — selector/filter state when unselected (muted)
   *   .bb-tag-remove  — inline × button, hidden until hover/focus on parent
   *
   * Usage:
   *   <span class="bb-tag" style="--tag-color:#f59e0b">
   *     <i data-lucide="inbox"></i><span>Needs Review</span>
   *   </span>
   */
  .bb-tag {
    --tag-color: oklch(from var(--color-base-content) l c h / 0.4);
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.125rem 0.5rem;
    border-radius: 9999px; /* pill */
    font-size: 0.7rem;
    font-weight: 500;
    line-height: 1.4;
    background-color: color-mix(in srgb, var(--tag-color) 10%, transparent);
    color: var(--tag-color);
    border: 1px solid color-mix(in srgb, var(--tag-color) 25%, transparent);
    white-space: nowrap;
    max-width: 100%;
  }


  .bb-tag > i, .bb-tag > svg {
    width: 0.75rem;
    height: 0.75rem;
    flex-shrink: 0;
  }

  .bb-tag-sm {
    padding: 0 0.375rem;
    font-size: 0.625rem;
    gap: 0.15rem;
  }

  .bb-tag-sm > i, .bb-tag-sm > svg {
    width: 0.65rem;
    height: 0.65rem;
  }

  /* Larger pill for primary interactive contexts (e.g. the tag editor on the
     transaction detail card). The default size suits dense lists and filter
     bars; this variant gives a comfortable tap/click target where managing
     tags is the section's main action. */
  .bb-tag-lg {
    padding: 0.3125rem 0.625rem;
    font-size: 0.8125rem;
    gap: 0.3125rem;
  }

  .bb-tag-lg > i, .bb-tag-lg > svg {
    width: 0.875rem;
    height: 0.875rem;
  }

  .bb-tag-lg .bb-tag-remove > i, .bb-tag-lg .bb-tag-remove > svg {
    width: 0.875rem;
    height: 0.875rem;
  }

  /* Below xl, transaction-row tag chips collapse into a single icon + count
     pill rendered beside the container (see data-tx-tag-mobile in tx_row.html).
     The pill renders cleanly on phones, tablets, and small desktops where the
     full chip list would otherwise crowd the meta line and overflow into the
     adjacent Category column. Chips stay in the DOM so the bulk picker's
     computeAppliedCounts() can still read them; they're just visually hidden. */
  @media (max-width: 1279.98px) {
    .bb-tx-tag-container .bb-tag {
      display: none;
    }
  }

  /* Filter/selector state: faded when unselected */
  .bb-tag-ghost {
    opacity: 0.35;
    transition: opacity 0.15s ease;
  }

  .bb-tag-ghost:hover {
    opacity: 0.7;
  }

  /* Interactive (removable) tag — cursor + hover affordance */
  .bb-tag-interactive {
    cursor: pointer;
    transition: filter 0.15s ease;
  }

  .bb-tag-interactive:hover {
    filter: brightness(1.1);
  }

  /*
   * × remove button — always lightly visible so layout stays stable, and the affordance
   * is discoverable. Stepped brightness on parent hover + button hover makes intent clear.
   *
   * Don't use opacity 0 here — the button always reserves space to keep the pill from
   * shifting as we reveal it. Color channel carries the hover feedback instead.
   */
  .bb-tag-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    margin-left: 0.125rem;
    background: transparent;
    border: 0;
    color: inherit;
    opacity: 0.4;
    cursor: pointer;
    transition: opacity 0.15s ease, background-color 0.15s ease;
    border-radius: 9999px;
  }

  .bb-tag:hover .bb-tag-remove {
    opacity: 0.7;
  }

  .bb-tag-remove:hover {
    opacity: 1;
    background-color: color-mix(in srgb, currentColor 18%, transparent);
  }

  .bb-tag-remove > i, .bb-tag-remove > svg {
    width: 0.75rem;
    height: 0.75rem;
  }

  .bb-tag-sm .bb-tag-remove > i, .bb-tag-sm .bb-tag-remove > svg {
    width: 0.65rem;
    height: 0.65rem;
  }

  /*
   * .bb-tag-reveal is deprecated in favor of the default always-visible treatment.
   * Kept as a no-op alias so existing callers don't break.
   */

  /*
   * Ghost "+ Add tag" chip — same shape/spacing as .bb-tag so it flows into the
   * wrapped tag row but uses a dashed border + muted foreground so it reads as an
   * action rather than content.
   */
  .bb-tag-add {
    background-color: transparent;
    color: oklch(from var(--color-base-content) l c h / 0.55);
    border: 1px dashed oklch(from var(--color-base-content) l c h / 0.2);
    cursor: pointer;
    transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
  }

  .bb-tag-add:hover {
    color: var(--color-base-content);
    border-color: oklch(from var(--color-base-content) l c h / 0.35);
    background-color: oklch(from var(--color-base-content) l c h / 0.05);
  }

  /* ── Filter bar ── */
  .bb-filter-bar {
    @apply flex flex-wrap items-end gap-3 mb-5;
  }

  .bb-filter-bar label {
    @apply flex flex-col gap-1 text-sm;
  }

  .bb-filter-bar select {
    @apply select select-sm w-full sm:w-auto sm:min-w-[8rem];
  }

  .bb-filter-bar input {
    @apply input input-sm w-full sm:w-auto sm:min-w-[8rem];
  }

  /* ── Amount formatting ── */
  .bb-amount {
    @apply text-right tabular-nums;
  }

  /* ── Privacy mode ──
   *
   * Client-side presentation layer (see static/js/admin/privacy.js). Sensitive
   * financial values carry `data-private="<kind>"`; the active mode lives on
   * `<html data-privacy="obfuscate|hide">`.
   *   - hide:      blur the real text via CSS — no JS rewrite.
   *   - obfuscate: privacy.js swaps the text for a matrix/hex glitch; a subtle
   *                monospace finish makes it read as synthetic at a glance.
   * `.privacy-pending` is set pre-paint (base.html) and dropped once privacy.js
   * runs its first pass, so obfuscate never flashes real values before the
   * rewrite. `user-select: none` keeps the real (blurred) text from being
   * casually selected/copied; pointer-events stay on so links still work. */
  html[data-privacy="hide"] [data-private],
  html.privacy-pending [data-private] {
    filter: blur(0.4rem);
    user-select: none;
  }

  /* Let the hide blur's halo feather out softly instead of being clipped to a
   * hard rectangular edge by a tight line-box / `truncate` wrapper. `filter:
   * blur()` paints a halo beyond the element box; an `overflow: hidden` value
   * cell (or the value's own `truncate`) was cropping that halo to a crisp
   * rectangle. Scoped to hide mode and only to a private value + its direct
   * wrapper, so real-mode truncation/ellipsis is untouched. `!important` beats
   * the Tailwind `.truncate` utility, which sits in a later cascade layer. */
  html[data-privacy="hide"] [data-private],
  html[data-privacy="hide"] :has(> [data-private]) {
    overflow: visible !important;
  }

  html[data-privacy="obfuscate"]:not(.privacy-pending) [data-private] {
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    letter-spacing: 0.02em;
    opacity: 0.92;
  }

  /* Once the initial reveal is done, privacy.js adds `.privacy-anim` to <html>
   * so mode switches ease the hide blur in/out (and the obfuscate opacity
   * shift) instead of snapping. Deliberately off during `.privacy-pending` /
   * first paint so the pre-paint blur→content swap on load stays instant. The
   * text-decode "scramble" is animated in JS; this only smooths the
   * CSS-driven properties. Respect reduced-motion. */
  @media (prefers-reduced-motion: no-preference) {
    html.privacy-anim:not(.privacy-pending) [data-private] {
      transition: filter 0.24s ease, opacity 0.24s ease;
    }
  }

  /* ── Sparkline (inline mini charts in summary pills) ── */
  svg.bb-sparkline {
    width: 64px;
    height: 24px;
    opacity: 0;
    transition: opacity 0.5s ease;
    flex-shrink: 0;
  }

  .bb-sparkline-line {
    vector-effect: non-scaling-stroke;
  }

  /* Summary pill hover lift */
  .bb-summary-pill {
    transition: transform 0.2s ease, box-shadow 0.2s ease;
  }

  .bb-summary-pill:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
  }

  /* ── Comment bubble ──
   *
   * Chat-style bubble used by the transaction-detail activity timeline for
   * comment rows. The flat top-left corner visually points at the avatar on
   * the rail without needing a CSS tail/notch. */
  .bb-comment-bubble {
    display: inline-block;
    max-width: 100%;
    padding: 0.5rem 0.875rem;
    font-size: 0.875rem;
    line-height: 1.55;
    color: color-mix(in srgb, var(--color-base-content) 88%, transparent);
    background: color-mix(in srgb, var(--color-base-200) 75%, transparent);
    border: 1px solid color-mix(in srgb, var(--color-base-300) 60%, transparent);
    border-radius: 1rem;
    border-top-left-radius: 0.375rem;
    word-break: break-word;
    overflow-wrap: anywhere;
  }
  /* Markdown inside a comment bubble is rendered server-side via
     @components.MarkdownBreaks into a nested .bb-prose, which owns all
     element styling (headings, lists, code, tables, task lists). The bubble
     only supplies the chat-tail chrome above. The one comment-specific tweak
     kept: trim the leading top margin so the bubble doesn't open with a gap. */
  .bb-comment-bubble .bb-prose > :first-child { margin-top: 0; }

  /* Dark-mode lift: in dark theme base-200 sits *below* the body bg
   * (body = base-100 = oklch 0.205, base-200 = oklch 0.145), so the
   * default base-200 comment bubble reads as a recessed hole. Bump the
   * surface and border so the bubble floats above the conversation
   * background. Light-mode untouched. */
  @media (prefers-color-scheme: dark) {
    .bb-comment-bubble {
      background: color-mix(in oklch, var(--color-base-100) 88%, white 12%);
      border-color: color-mix(in oklch, var(--color-base-300) 85%, white 15%);
    }
  }

  /* ── Activity timeline — prominent (main-content) variant ──
   * Used by the transaction-detail page to promote the activity timeline
   * out of card chrome (see internal/templates/components/timeline.templ).
   * Goal: a GitHub PR/issue conversation feel — large heading, scaled-up
   * row tiles, and comments rendered as proper bordered cards with a tail
   * pointing at the avatar on the rail. The card-mode 24px tile / 12px
   * inner-icon invariants in docs/activity-timeline.md still apply to the
   * default Timeline variant; they're scaled up here in lockstep with the
   * rail offset so alignment stays exact. */

  /* Heading event-count chip. Reads more like a GitHub tab counter than
   * a muted text suffix. */
  .bb-timeline-count {
    @apply inline-flex items-center justify-center
           text-xs font-medium tabular-nums leading-none
           px-2 py-1 rounded-full
           bg-base-200 text-base-content/70 border border-base-300;
  }

  /* Note: the size/colour/layout overrides for `.bb-timeline-prominent` are
   * defined below the closing `}` of @layer components (search for
   * "Activity timeline — prominent overrides"). They have to live outside
   * the layer because they target Tailwind utility classes (w-6, h-6,
   * leading-6, ring-4, py-2.5, …) — utilities live in @layer utilities
   * and beat anything in @layer components regardless of specificity. */

  /* ── Info grid (detail pages) ── */
  .bb-info-grid {
    @apply grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm;
  }

  .bb-info-grid dt {
    @apply font-medium text-base-content/50;
  }

  .bb-info-grid dd {
    @apply text-base-content;
  }

  /* ── Page header pattern ── */
  /* items-start on mobile so single-child buttons keep their intrinsic width
     instead of stretching edge-to-edge when the header wraps to a column. */
  .bb-page-header {
    @apply flex flex-col items-start sm:flex-row sm:items-center justify-between gap-3 mb-6;
  }

  .bb-page-title {
    @apply text-xl font-semibold tracking-tight;
  }

  /* ── Wizard / Auth pages (login, setup, error) ── */
  .bb-wizard-bg {
    /* Mobile: top-aligned with generous top padding so content isn't dead-center
       in a sea of dark background. sm+: vertically centered as before.
       overflow-x clip prevents off-screen children (e.g. the 1Password save
       button's hidden info modal) from creating a horizontal scrollbar at
       narrow widths like 320px. */
    @apply bg-base-200 min-h-screen flex items-start justify-center
           px-4 pt-10 pb-8 overflow-x-clip
           sm:items-center sm:py-4;
  }
  /* Pair with .bb-wizard-bg's overflow-x: clip — without applying it to
     <html> as well, the document root keeps the larger scrollWidth and
     mobile browsers may still produce a horizontal scrollbar. Scoped to
     wizard pages via :has() so non-wizard pages keep their default. */
  html:has(body.bb-wizard-bg) {
    overflow-x: clip;
  }

  /* ── /setup/save-key: single-line scrollable key display.
     A 64-char hex string is wider than the wizard card on every viewport,
     so we render it on one line with horizontal scroll inside the field
     rather than wrapping. Keeps the action buttons below at a predictable
     vertical position and matches the visual of password-reveal patterns
     elsewhere (GitHub recovery codes, Auth0 client secrets, etc). */
  .bb-key-display {
    @apply flex items-center min-h-13 bg-base-200/50 border border-base-300 rounded-xl
           px-4 py-3.5 font-mono text-sm leading-relaxed
           text-base-content/85
           whitespace-nowrap overflow-x-auto overscroll-x-contain;
    /* Space the glyphs out so the 64 dots/chars read as a key rather than
       a packed block — the masked bullets especially look cramped at 0. */
    letter-spacing: 0.12em;
    /* Thin, theme-aware scrollbar so it doesn't dominate the field. */
    scrollbar-width: thin;
  }
  .bb-key-display::-webkit-scrollbar {
    height: 6px;
  }
  .bb-key-display::-webkit-scrollbar-thumb {
    background-color: color-mix(in oklab, var(--color-base-content) 20%, transparent);
    border-radius: 3px;
  }

  .bb-wizard-orb {
    @apply hidden;
  }

  .bb-wizard-container {
    @apply w-full max-w-[420px];
  }

  .bb-wizard-brand {
    /* Tighten vertical gap on mobile; restore on sm+ */
    @apply flex justify-center mb-5 sm:mb-8;
  }

  .bb-wizard-brand-icon {
    @apply w-10 h-10 rounded-xl bg-primary text-primary-content
           flex items-center justify-center;
  }

  .bb-wizard-card {
    /* Tighter padding on mobile (p-5), comfortable on sm+ (p-6) */
    @apply bg-base-100 rounded-xl border border-base-300 p-5 sm:p-6;
  }

  /* ── Error pages (404, 500) ── */
  .bb-error-page {
    @apply flex flex-col items-center justify-center text-center;
    min-height: 50vh;
  }

  .bb-error-code {
    @apply text-8xl font-bold text-base-content/8 leading-none mb-2;
    letter-spacing: -0.04em;
  }

  .bb-error-icon {
    @apply w-12 h-12 rounded-xl flex items-center justify-center mb-5;
  }

  .bb-error-title {
    @apply text-lg font-semibold mb-2;
  }

  .bb-error-message {
    @apply text-sm text-base-content/50 max-w-sm mb-6 leading-relaxed;
  }

  /* ── Button polish ── */
  .btn {
    transition: background-color 0.15s ease, color 0.15s ease,
                border-color 0.15s ease, transform 0.1s ease,
                box-shadow 0.15s ease, opacity 0.15s ease !important;
  }

  .btn:active:not(:disabled) {
    transform: scale(0.97) !important;
  }

  /*
   * Removed the former shadcn `shadow-xs` override on solid buttons
   * (was box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05)). Any remaining button
   * lift is now governed solely by daisyUI's --depth theme token, so
   * flat (--depth: 0) vs lifted (--depth: 1) is a single theme switch.
   */

  /*
   * btn-outline: keep the outline visible through hover / press / focus.
   * DaisyUI's resting + hover rules for .btn-outline explicitly EXCLUDE
   * :active and :focus-visible, so on press/focus the base .btn takes over
   * and fills the button solid — the border blends into the fill and the
   * outline "disappears". We pin the outline vars (transparent-ish bg +
   * colored border + colored text) across every interactive state and use a
   * soft tint for press/hover feedback instead of a solid fill, so the
   * border never vanishes. `!important` matches the existing sanctioned
   * .btn overrides — daisy ships same-specificity rules that win on source
   * order otherwise (see .claude/rules/daisyui.md → spec'd overrides).
   */
  .btn-outline:hover,
  .btn-outline:focus-visible,
  .btn-outline:active,
  .btn-outline.btn-active {
    --btn-fg: var(--btn-color) !important;
    --btn-border: var(--btn-color) !important;
  }
  /* The hover/press FILL for outline buttons is set UNLAYERED near the
     bottom of this file (search "Outline button hover/press fill"). daisy
     paints the button background with a layered !important that a @layer
     components rule — and even an inline style — can't beat, so the fill
     has to live unlayered. */

  /* Button radius convention — see unlayered .btn-sm / .btn-xs override
     below the components layer. */

  /* ── Badge transitions ── */
  .badge {
    transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
  }

  /* ── Table row polish ── */
  .table tbody tr {
    transition: background-color 0.12s ease;
  }

  /* ── iOS-style toggle ──────────────────────────────────────────────
   * Reshapes daisyUI 5's .toggle into an Apple/iOS switch: a full-pill,
   * borderless track, a filled gray "off" state (base-300 — which maps
   * almost exactly onto Apple's #E9E9EA light / #39393D dark), an
   * Apple-green "on" state, and a white CIRCULAR knob carrying Apple's
   * two-layer drop shadow. The knob slide reuses daisy's existing grid-
   * column swap (1fr/0fr → 0fr/1fr); we only retune the easing to an
   * iOS spring curve.
   *
   * `!important` mirrors the existing .toggle/.btn/.modal-box overrides:
   * daisyUI emits `.toggle{…}` / `.toggle:checked{…}` in a component
   * sublayer that wins over our @layer components on cascade order, so
   * the declarations that must beat daisy carry !important. Color
   * modifiers (toggle-primary, toggle-success, …) stay live by
   * reassigning --bb-toggle-on instead of hard-coding the on color. */
  .toggle {
    --bb-toggle-on: #34c759; /* Apple system green */
    --bb-toggle-ease: cubic-bezier(0.32, 0.72, 0, 1);
    border-color: transparent !important;
    background-color: var(--color-base-300) !important;
    border-radius: 999px !important;
    box-shadow: none !important;
    transition:
      background-color 0.25s var(--bb-toggle-ease),
      grid-template-columns 0.25s var(--bb-toggle-ease) !important;
  }

  /* Knob: white circle + Apple's two-layer shadow, in both states. */
  .toggle::before {
    border-radius: 999px !important;
    background-color: #fff !important;
    box-shadow:
      0 3px 8px oklch(0 0 0 / 0.15),
      0 1px 1px oklch(0 0 0 / 0.16) !important;
    outline: none !important;
    transition:
      translate 0.25s var(--bb-toggle-ease),
      inset-inline-start 0.25s var(--bb-toggle-ease) !important;
  }

  /* On state — track turns green; knob stays white and slides right
     (the grid-column swap that slides it is daisy's, left intact). */
  .toggle:checked,
  .toggle[aria-checked="true"],
  .toggle:has(> input:checked) {
    background-color: var(--bb-toggle-on) !important;
  }
  .toggle:checked::before,
  .toggle[aria-checked="true"]::before,
  .toggle:has(> input:checked)::before {
    background-color: #fff !important;
  }

  /* Color modifiers keep working by re-pointing the on color. */
  .toggle-primary {
    --bb-toggle-on: var(--color-primary);
  }
  .toggle-secondary {
    --bb-toggle-on: var(--color-secondary);
  }
  .toggle-accent {
    --bb-toggle-on: var(--color-accent);
  }
  .toggle-neutral {
    --bb-toggle-on: var(--color-neutral);
  }
  .toggle-info {
    --bb-toggle-on: var(--color-info);
  }
  .toggle-success {
    --bb-toggle-on: var(--color-success);
  }
  .toggle-warning {
    --bb-toggle-on: var(--color-warning);
  }
  .toggle-error {
    --bb-toggle-on: var(--color-error);
  }

  /* ── Select/Input focus transitions ── */
  .select, .input, .textarea {
    transition: border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease !important;
  }

  /* ── Select dropdown popup (base-select appearance) ── */
  /* DaisyUI 5 uses appearance: base-select which renders a custom dropdown popup.
     In dark mode the popup and option backgrounds default to transparent,
     causing content behind to bleed through. Fix by setting explicit backgrounds.
     Paired with an explicit [data-theme="dark"] selector so the override also
     fires when the user toggles the theme on a light-OS system. */
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .select::picker(select) {
      background-color: var(--color-base-100);
      border: 1px solid var(--color-base-300);
      border-radius: 0.75rem;
      box-shadow: 0 8px 32px oklch(0 0 0 / 0.5);
      padding: 0.25rem;
    }

    :root:not([data-theme="light"]) .select option {
      color: var(--color-base-content);
      border-radius: 0.5rem;
      padding: 0.375rem 0.75rem;
    }

    :root:not([data-theme="light"]) .select option:hover {
      background-color: var(--color-base-200);
    }

    :root:not([data-theme="light"]) .select option:checked {
      background-color: var(--color-base-200);
    }
  }

  [data-theme="dark"] .select::picker(select) {
    background-color: var(--color-base-100);
    border: 1px solid var(--color-base-300);
    border-radius: 0.75rem;
    box-shadow: 0 8px 32px oklch(0 0 0 / 0.5);
    padding: 0.25rem;
  }

  [data-theme="dark"] .select option {
    color: var(--color-base-content);
    border-radius: 0.5rem;
    padding: 0.375rem 0.75rem;
  }

  [data-theme="dark"] .select option:hover {
    background-color: var(--color-base-200);
  }

  [data-theme="dark"] .select option:checked {
    background-color: var(--color-base-200);
  }

  /* DaisyUI's .modal defaults are used as-is — no per-class overrides
     here. If you ever add a non-none transform animation to <main> or
     any ancestor, the dialog will snap to that ancestor's bounds the
     moment the browser removes it from the top layer on close. Stay
     opacity-only on page-level entrance animations. */


  /* ── Transaction row hover + keyboard focus ──
   *
   * Selection state is driven by `data-tx-selected` (set imperatively by the
   * bulk store) — no per-row Alpine bindings, no transitions that fire en masse.
   *
   * --bb-row-bg mirrors the row's effective background per state so child
   * elements (e.g. .bb-tx-owner-badge) can draw a ring that blends into the
   * highlight instead of staying base-100 white. Colors approximate the
   * bg utilities below by mixing in oklab.
   */
  .bb-tx-row {
    --bb-row-bg: var(--color-base-100);
  }
  .bb-tx-row[data-tx-selected] {
    @apply bg-primary/5;
    --bb-row-bg: color-mix(in oklab, var(--color-primary) 5%, var(--color-base-100));
  }

  /*
   * Per-row checkbox label. Visibility is toggled imperatively by the bulk
   * store's `_updateRowVisibility()`. All styling lives here rather than via
   * Tailwind utilities on the element: utilities live in `@layer utilities`
   * and beat anything in `@layer components` regardless of specificity, which
   * means a naive `label.bb-tx-checkbox { display: none }` loses to `.flex`
   * on the same element.
   */
  .bb-tx-checkbox {
    display: none;
    align-items: center;
    flex-shrink: 0;
    cursor: pointer;
  }

  /*
   * While bulk-selection mode is active, the per-row checkbox is injected at
   * the start of the row. On mobile there isn't enough horizontal room to
   * also keep the decorative category avatar — the name column would lose
   * ~40px and merchant names truncate (#593). Hide the avatar slot below the
   * `sm` breakpoint so the checkbox effectively reuses that space. Tablet and
   * desktop keep both.
   */
  @media (max-width: 639px) {
    body.bb-tx-selecting .bb-tx-avatar-slot {
      display: none;
    }
  }

  /*
   * DaisyUI 5's `.checkbox` ships with `transition: all`, which animated every
   * property change (including nearby paint invalidations from unrelated CSS).
   * Clamp the transition to the properties that actually drive the check draw.
   */
  .bb-tx-row .checkbox,
  .bb-tx-checkbox .checkbox {
    transition: background-color 0.12s ease, border-color 0.12s ease;
  }

  .bb-tx-row:hover {
    @apply bg-base-200/40;
    --bb-row-bg: color-mix(in oklab, var(--color-base-200) 40%, var(--color-base-100));
  }

  .bb-tx-row--focused {
    @apply bg-primary/5;
    --bb-row-bg: color-mix(in oklab, var(--color-primary) 5%, var(--color-base-100));
    box-shadow: inset 3px 0 0 var(--color-primary);
  }

  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .bb-tx-row--focused {
      @apply bg-primary/10;
      --bb-row-bg: color-mix(in oklab, var(--color-primary) 10%, var(--color-base-100));
    }
  }

  [data-theme="dark"] .bb-tx-row--focused {
    @apply bg-primary/10;
    --bb-row-bg: color-mix(in oklab, var(--color-primary) 10%, var(--color-base-100));
  }

  /* ── Compact tx row (command palette, dashboard recents) ──
     Inline Tailwind utilities cover hover paint; this just exposes
     --bb-row-bg so the corner owner badge can track the highlight. */
  .bb-tx-row-compact {
    --bb-row-bg: var(--color-base-100);
  }
  .bb-tx-row-compact:hover {
    --bb-row-bg: color-mix(in oklab, var(--color-base-200) 40%, var(--color-base-100));
  }

  /* ── Transaction category avatar ── */
  .bb-tx-avatar {
    @apply w-9 h-9 rounded-xl flex items-center justify-center shrink-0;
    --avatar-color: oklch(from var(--color-base-content) l c h / 0.5);
    background: color-mix(in oklch, var(--avatar-color) 12%, transparent);
    color: var(--avatar-color);
  }

  .bb-tx-avatar i, .bb-tx-avatar svg {
    color: inherit;
    opacity: 0.85;
  }

  .bb-tx-avatar--sm {
    /* Size-only override — radius stays `rounded-xl` from the base rule so
     * the compact/feed tile reads as the same shape family as the default
     * 36×36 avatar (the previous `rounded-lg` here felt visibly sharper). */
    @apply w-8 h-8;
  }

  .bb-tx-avatar--sm i, .bb-tx-avatar--sm svg {
    @apply w-3.5 h-3.5;
  }

  /* Uncategorized: a neutral grey circle with a question-mark icon. Used
   * across every tx-row variant (full, compact, feed one-liner) when a
   * transaction has no category yet, so the visual signal "needs a
   * category" is consistent regardless of where the row appears. */
  .bb-tx-avatar--uncategorized {
    --avatar-color: var(--color-base-content);
    background: oklch(from var(--color-base-content) l c h / 0.06);
    color: oklch(from var(--color-base-content) l c h / 0.45);
  }

  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .bb-tx-avatar {
      background: color-mix(in oklch, var(--avatar-color) 18%, transparent);
    }
    :root:not([data-theme="light"]) .bb-tx-avatar--uncategorized {
      background: oklch(from var(--color-base-content) l c h / 0.08);
    }
  }

  [data-theme="dark"] .bb-tx-avatar {
    background: color-mix(in oklch, var(--avatar-color) 18%, transparent);
  }
  [data-theme="dark"] .bb-tx-avatar--uncategorized {
    background: oklch(from var(--color-base-content) l c h / 0.08);
  }

  /* ── Owner-identity badge on the tx avatar ──
     Sits in the bottom-right corner of `.bb-tx-avatar`, ringed in the row
     background so it punches through the parent rounding cleanly. Mirrors
     the sm+ meta-line avatar (now removed) so identity is visible on every
     breakpoint without burning horizontal space. */
  .bb-tx-owner-badge {
    @apply absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full object-cover shrink-0;
    /* Ring tracks the row's effective background (set via `--bb-row-bg` on
       `.bb-tx-row` per hover/selected/focused state) so the badge blends into
       the highlight instead of always punching with base-100 white. */
    box-shadow: 0 0 0 2px var(--bb-row-bg, var(--color-base-100, #fff));
  }

  .bb-tx-owner-badge--letter {
    /* Soft 15 % primary tint over an opaque row-background base so the
     * avatar tile never bleeds through. Base matches the same `--bb-row-bg`
     * the ring uses, so the pill, the ring, and the row hover/selected
     * highlight stay in lock-step. Text keeps the original 80 % to read
     * as a soft chip rather than a punched-out solid. */
    @apply inline-flex items-center justify-center text-primary/80;
    background-color: var(--bb-row-bg, var(--color-base-100, #fff));
    background-image: linear-gradient(
      color-mix(in srgb, var(--color-primary) 15%, transparent),
      color-mix(in srgb, var(--color-primary) 15%, transparent)
    );
  }

  .bb-tx-owner-badge--letter > span {
    @apply text-[0.5rem] font-semibold leading-none;
  }

  /* ── Transaction amount pending modifier ──
     Pending transactions are provisional, so we soften the amount glyph
     (dimmed + leading clock icon at the call site) instead of tacking a
     warning chip onto the name line. No italic / no underline — just a
     quieter number. Applied by components.Amount when Pending: true.
     The sibling --income tone (success green for negative values) and
     the base .bb-tx-amount typography rule were retired with the
     components.Amount migration — sign + color now flow through the
     component's tabular-nums + text-success classes instead. */
  .bb-tx-amount--pending {
    opacity: 0.55;
  }

  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .bb-tx-amount--pending {
      opacity: 0.65;
    }
  }
  [data-theme="dark"] .bb-tx-amount--pending {
    opacity: 0.65;
  }

  /* ── Rule category badge ── */
  .bb-rule-cat-badge {
    @apply inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full;
    --cat-color: oklch(from var(--color-base-content) l c h / 0.55);
    background: color-mix(in oklch, var(--cat-color) 10%, transparent);
    color: var(--cat-color);
  }

  .bb-rule-cat-dot {
    @apply w-1.5 h-1.5 rounded-full shrink-0;
    background: var(--cat-color);
  }

  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .bb-rule-cat-badge {
      background: color-mix(in oklch, var(--cat-color) 15%, transparent);
    }
  }
  [data-theme="dark"] .bb-rule-cat-badge {
    background: color-mix(in oklch, var(--cat-color) 15%, transparent);
  }

  /* ── Smooth collapse for filter panels ── */
  [x-collapse] {
    transition: height 0.2s ease-out, opacity 0.15s ease !important;
  }

  /* ── Mobile sidebar overlay above sticky navbar ── */
  .drawer-side {
    z-index: 40 !important;
  }

  /* ── Modal scroll containment ──────────────────────────────────────
   * When the daisy modal-box body scrolls past its top/bottom (a
   * long form in a `<dialog class="modal">`), iOS Safari rubber-bands
   * the page underneath the backdrop instead of just bouncing the
   * modal's own scroll. `contain` keeps the bounce local to the
   * modal — matches the platform feel the bespoke pickers already
   * have on their inner scroll regions (.bb-cmdk-list, etc).
   * ──────────────────────────────────────────────────────────── */
  .modal-box {
    overscroll-behavior: contain;
    /* Nova-style inset ring on modals — replaces the hard daisy shadow-2xl
       with a foreground-tinted ring + soft drop. `!important` because
       daisyUI's plugin layer emits a same-specificity `.modal-box`
       box-shadow that wins on source order otherwise. NOTE: re-added in
       this PR after #1548 inadvertently dropped it during rebase. */
    box-shadow:
      0 0 0 1px oklch(from var(--color-base-content) l c h / 0.10),
      0 10px 30px -10px oklch(0 0 0 / 0.18) !important;
  }

  /* Dark-mode modal lift. daisy 5's `.modal-box` defaults to bg-base-100,
     but in the dark theme base-100 IS the page background (oklch 0.145),
     so the modal recesses instead of lifting and the black drop shadow
     is invisible against the dark backdrop. shadcn solves this with a
     separate `--popover` token one luminance step lighter than
     `--background`; daisy 5 doesn't expose that distinction, so we lift
     the modal surface to base-200 in dark only (light-mode keeps the
     stock bg-base-100 / white). Also bump the ring + drop opacities so
     the edge stays legible against the near-black page. Same dual-
     selector pattern as the .bb-card dark boost above (manual data-theme
     + auto via prefers-color-scheme when none is set). */
  [data-theme="dark"] .modal-box {
    /* `!important` on background-color too — daisy's plugin layer emits
       `.modal-box { background: var(--color-base-100) }` at the same
       specificity, which wins source-order. */
    background-color: var(--color-base-200) !important;
    box-shadow:
      0 0 0 1px oklch(from var(--color-base-content) l c h / 0.14),
      0 20px 40px -12px oklch(0 0 0 / 0.50) !important;
  }
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme]) .modal-box {
      background-color: var(--color-base-200) !important;
      box-shadow:
        0 0 0 1px oklch(from var(--color-base-content) l c h / 0.14),
        0 20px 40px -12px oklch(0 0 0 / 0.50) !important;
    }
  }

  /* Floating menus / dropdown content — same inset-ring treatment so
     overflow popups read consistently with cards and modals. No
     !important here: callers that opt into a heavier elevation via
     Tailwind `shadow-lg`/`shadow-xl` (the daisyui.md-mandated standard
     dropdown shape, settings_modal tab picker) still win by specificity.
     Re-added in this PR after #1548 inadvertently dropped it. */
  .dropdown-content {
    box-shadow:
      0 0 0 1px oklch(from var(--color-base-content) l c h / 0.08),
      0 6px 20px -6px oklch(0 0 0 / 0.12);
  }

  /* ── Suppress WebKit's native search-clear (×) on overlay pickers ──
   * The cmdk / catpicker / tagpicker inputs are now type="search" so
   * iOS gets the proper "Search" keyboard key. The flip side is that
   * WebKit renders a small × clear button at the inline-end edge,
   * which would visually compete with the existing `esc` kbd chip
   * already rendered in the same wrap. Hide it on these three only;
   * standalone search inputs (tx quick-search, filters) keep the
   * native ×, which is genuinely useful there.
   * ──────────────────────────────────────────────────────────── */
  .bb-cmdk-input::-webkit-search-cancel-button,
  .bb-catpicker-input::-webkit-search-cancel-button {
    -webkit-appearance: none;
    appearance: none;
  }



  /* ── Mobile-friendly info grid ── */
  @media (max-width: 639px) {
    .bb-info-grid {
      @apply grid-cols-1 gap-y-3;
    }
    .bb-info-grid dt {
      @apply mb-0.5;
    }
    .bb-info-grid dd {
      @apply pb-3 border-b border-base-300/50;
    }
    .bb-info-grid dd:last-of-type {
      @apply border-b-0 pb-0;
    }
  }

  /* ── Consistent filter toggle button ── */
  .bb-filter-toggle {
    @apply flex items-center justify-between w-full px-4 sm:px-5 py-3 text-sm font-medium
           hover:bg-base-200/50 transition-colors duration-150 rounded-t-xl cursor-pointer;
  }

  .bb-filter-form {
    @apply px-4 sm:px-5 pb-4 border-t border-base-300/50;
  }

  .bb-filter-label {
    @apply flex flex-col gap-1 text-xs font-medium text-base-content/50;
  }

  .bb-filter-actions {
    @apply flex justify-end gap-2;
  }

  /* ── Table header for list view ── */
  .bb-table-header {
    @apply flex items-center gap-3 px-4 py-2 text-xs font-medium text-base-content/40
           border-b border-base-300/50 bg-base-200/30;
  }

  /* ── Date visibility (hidden in grouped, shown in list) ──
     `.bb-tx-date` is the desktop right-side date column.
     `.bb-tx-date-mobile` is the date in the mobile sub-line; we also hide
     its immediately-following `·` separator span so the sub-line doesn't
     start with an orphan dot. */
  .bb-date-group .bb-tx-date,
  .bb-date-group .bb-tx-date-mobile,
  .bb-date-group .bb-tx-date-mobile + span {
    display: none !important;
  }

  /* ── Paginator ──
     Numbered-button row is now a daisy `join` + `join-item btn` group
     (see tx_results.templ / rules.templ). Only the surrounding layout
     wrapper + per-page selector + info span keep dedicated classes. */
  .bb-paginator {
    @apply flex flex-col sm:flex-row items-center justify-between gap-3 pt-5 px-1;
  }

  .bb-paginator__info {
    @apply flex items-center gap-4;
  }

  .bb-paginator__per-page {
    @apply flex items-center gap-2;
  }

  /* ─── Command Palette (Cmd+K) ─── */
  .bb-cmdk-backdrop {
    @apply fixed inset-0 z-50;
    background: oklch(0 0 0 / 0.4);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
    touch-action: none;
    -webkit-overflow-scrolling: auto;
  }

  .bb-cmdk-dialog {
    @apply fixed z-50 bg-base-100 border border-base-300 rounded-xl overflow-hidden;
    top: min(20%, 140px);
    left: 50%;
    transform: translateX(-50%);
    width: min(95vw, 560px);
    max-height: min(70vh, 460px);
    box-shadow: 0 16px 70px oklch(0 0 0 / 0.15), 0 0 0 1px oklch(0 0 0 / 0.04);
  }

  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .bb-cmdk-dialog {
      box-shadow: 0 16px 70px oklch(0 0 0 / 0.5), 0 0 0 1px oklch(from var(--color-base-content) l c h / 0.06);
    }
  }
  [data-theme="dark"] .bb-cmdk-dialog {
    box-shadow: 0 16px 70px oklch(0 0 0 / 0.5), 0 0 0 1px oklch(from var(--color-base-content) l c h / 0.06);
  }

  .bb-cmdk-input-wrap {
    @apply flex items-center gap-3 px-4 border-b border-base-300;
  }

  .bb-cmdk-input-wrap svg,
  .bb-cmdk-input-wrap i {
    @apply w-4 h-4 text-base-content/40 shrink-0;
  }

  .bb-cmdk-input {
    @apply w-full py-3.5 text-sm bg-transparent border-none outline-none placeholder:text-base-content/40;
  }

  /* Mobile: pin to top, prevent Safari zoom on focus */
  @media (max-width: 640px) {
    .bb-cmdk-dialog {
      top: calc(env(safe-area-inset-top, 0px) + 0.5rem);
      border-radius: 0.75rem;
      width: calc(100vw - 1rem);
    }

    .bb-cmdk-input {
      font-size: 16px; /* prevents Safari auto-zoom on input focus */
    }
  }

  .bb-cmdk-input:focus {
    outline: none;
    box-shadow: none;
  }

  .bb-cmdk-list {
    @apply overflow-y-auto;
    max-height: min(55vh, 370px);
    scrollbar-width: thin;
    overscroll-behavior: contain;
  }

  .bb-cmdk-group {
    @apply py-2;
  }

  .bb-cmdk-group-label {
    @apply px-4 py-1.5 text-[0.65rem] font-medium text-base-content/45;
  }

  .bb-cmdk-item {
    @apply mx-2 rounded-lg text-sm cursor-pointer;
    transition: background-color 0.1s ease, color 0.1s ease;
    /* Default backdrop is the cmdk dialog surface (base-100). When the row
       lights up below, we re-aim --bb-row-bg so the owner badge ring blends
       into the highlight instead of staying base-100 white. */
    --bb-row-bg: var(--color-base-100);
  }

  .bb-cmdk-item:hover,
  .bb-cmdk-item[data-active="true"] {
    @apply bg-base-200 text-base-content;
    --bb-row-bg: var(--color-base-200);
  }

  .bb-cmdk-item[data-active="true"] {
    @apply bg-primary/8;
    --bb-row-bg: color-mix(in oklab, var(--color-primary) 8%, var(--color-base-100));
  }

  @media (prefers-color-scheme: dark) {
    .bb-cmdk-item[data-active="true"] {
      @apply bg-primary/12;
      --bb-row-bg: color-mix(in oklab, var(--color-primary) 12%, var(--color-base-100));
    }
  }

  .bb-cmdk-generic {
    @apply flex items-center gap-3 px-4 py-2;
  }

  .bb-cmdk-item-icon {
    @apply w-8 h-8 rounded-lg bg-base-200 flex items-center justify-center shrink-0;
  }

  .bb-cmdk-item[data-active="true"] .bb-cmdk-item-icon {
    @apply bg-primary/10;
  }

  .bb-cmdk-item-icon svg,
  .bb-cmdk-item-icon i {
    @apply w-4 h-4 text-base-content/70;
  }

  .bb-cmdk-item[data-active="true"] .bb-cmdk-item-icon svg,
  .bb-cmdk-item[data-active="true"] .bb-cmdk-item-icon i {
    @apply text-primary;
  }

  .bb-cmdk-item-label {
    @apply flex-1 min-w-0;
  }

  .bb-cmdk-item-title {
    @apply text-sm font-medium truncate;
  }

  .bb-cmdk-item-desc {
    @apply text-xs text-base-content/50 truncate;
  }

  .bb-cmdk-item-shortcut {
    @apply flex items-center gap-1 shrink-0;
  }

  /* Tx rows in the cmdk render via TxRowCompact (server-rendered fragment
     served by /-/search/transactions). The component already paints itself;
     the cmdk-item wrapper just contributes spacing + the active highlight
     handled above. Strip the inner row's own hover paint so it doesn't fight
     the cmdk active state, and drop the wrapper's default padding so the
     anchor's padding is the only one in play. */
  .bb-cmdk-item.bb-cmdk-item--tx {
    padding: 0;
  }
  .bb-cmdk-item .bb-tx-row-compact:hover {
    background-color: transparent;
  }

  .bb-cmdk-footer {
    @apply flex items-center justify-between px-4 py-2.5 border-t border-base-300 text-[0.65rem] text-base-content/40;
  }

  .bb-cmdk-empty {
    @apply flex flex-col items-center justify-center py-12 text-base-content/30;
  }

  .bb-cmdk-empty svg,
  .bb-cmdk-empty i {
    @apply w-8 h-8 mb-2 opacity-40;
  }

  /* ── Sidebar Cmd+K trigger — chrome + hover shared with .bb-topbar-search
     (see "⌘K search trigger" near the top); only the sidebar-row sizing,
     spacing, muted-text tint and smaller icon are deltas here. ── */
  .bb-sidebar-search {
    @apply gap-2.5 px-3 py-2.5 lg:py-2 mx-0 mb-3 text-base-content/35;
  }

  .bb-sidebar-search svg,
  .bb-sidebar-search i {
    @apply w-3.5 h-3.5;
  }

  .bb-sidebar-search-hint {
    @apply ml-auto flex items-center gap-0.5 text-[0.6rem] text-base-content/25;
  }
  /* The legacy `.bb-sidebar-search-hint kbd` rule was removed — the hint
     now renders via the shared `KbdCombo` templ component, which emits
     `<span class="bb-kbd-combo">` (not `<kbd>`). The kbd inversion happens
     via the `.bb-sidebar-search .bb-kbd / .bb-kbd-combo` rules near the
     shortcut-badges block above. */

  /* ── Sidebar user profile ── */
  .bb-sidebar-profile {
    @apply flex items-center gap-2.5 px-2 py-3 lg:py-2 rounded-lg;
    transition: background-color 0.15s ease;
  }

  .bb-sidebar-profile-avatar {
    @apply w-8 h-8 rounded-full object-cover shrink-0;
    transition: opacity 0.15s ease;
  }

  .bb-sidebar-profile-info {
    @apply flex-1 min-w-0;
  }

  .bb-sidebar-profile-name {
    @apply text-sm font-medium text-base-content truncate leading-tight;
  }

  .bb-sidebar-profile-role {
    @apply text-[0.65rem] text-base-content/35 leading-tight mt-0.5;
  }

  .bb-sidebar-profile-logout {
    @apply shrink-0;
  }

  .bb-sidebar-logout-btn {
    @apply flex items-center justify-center w-7 h-7 rounded-md
           text-base-content/30 cursor-pointer border-0 bg-transparent;
    transition: background-color 0.15s ease, color 0.15s ease;
  }

  .bb-sidebar-logout-btn:hover {
    @apply bg-base-200 text-base-content/60;
  }

  .bb-sidebar-logout-btn svg, .bb-sidebar-logout-btn i {
    @apply w-3.5 h-3.5;
  }

  /* ── Sidebar version badge ── */
  .bb-sidebar-version {
    @apply flex items-center gap-1.5 px-3 mt-2 text-[0.6rem] text-base-content/20;
  }

  .bb-sidebar-version svg, .bb-sidebar-version i {
    @apply opacity-50;
  }

  .bb-sidebar-version-link {
    @apply inline-flex items-center gap-1 no-underline;
    color: inherit;
    transition: color 0.15s ease;
  }
  .bb-sidebar-version-link:hover {
    color: var(--color-base-content);
  }

  .bb-sidebar-version .badge {
    @apply no-underline;
    font-size: 0.55rem;
    line-height: 1;
  }

  /* NOTE: the collapsed icon-rail rules live UNLAYERED (search
     "Collapsible sidebar — desktop icon-rail" below the @layer
     components close) — they must beat the `lg:w-64` @apply and the
     `justify-between` utility, which @layer components can't. */

  /* ── Landscape mobile: scroll entire sidebar, keep full touch targets ── */
  @media (orientation: landscape) and (max-height: 500px) {
    .bb-sidebar {
      @apply overflow-y-auto;
      display: block;
    }
    .bb-sidebar-search {
      @apply hidden;
    }
    .bb-sidebar-nav {
      overflow-y: visible;
      flex: none;
    }
    .bb-sidebar-footer {
      margin-top: var(--bb-gap-sm, 0.5rem);
    }
    .bb-sidebar-version {
      @apply hidden;
    }
  }

  /* ─── Global Category Picker Overlay ─── */
  .bb-catpicker-backdrop {
    @apply fixed inset-0 z-50;
    background: oklch(0 0 0 / 0.4);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
    touch-action: none;
    -webkit-overflow-scrolling: auto;
  }

  .bb-catpicker-dialog {
    @apply fixed z-50 bg-base-100 border border-base-300 rounded-xl overflow-hidden;
    top: min(20%, 140px);
    left: 50%;
    transform: translateX(-50%);
    width: min(95vw, 420px);
    max-height: min(70vh, 460px);
    box-shadow: 0 16px 70px oklch(0 0 0 / 0.15), 0 0 0 1px oklch(0 0 0 / 0.04);
  }

  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .bb-catpicker-dialog {
      box-shadow: 0 16px 70px oklch(0 0 0 / 0.5), 0 0 0 1px oklch(from var(--color-base-content) l c h / 0.06);
    }
  }
  [data-theme="dark"] .bb-catpicker-dialog {
    box-shadow: 0 16px 70px oklch(0 0 0 / 0.5), 0 0 0 1px oklch(from var(--color-base-content) l c h / 0.06);
  }

  .bb-catpicker-input-wrap {
    @apply flex items-center gap-3 px-4 border-b border-base-300;
  }

  .bb-catpicker-input-wrap svg,
  .bb-catpicker-input-wrap i {
    @apply w-4 h-4 text-base-content/30 shrink-0;
  }

  .bb-catpicker-input {
    @apply w-full py-3.5 text-sm bg-transparent border-none outline-none placeholder:text-base-content/30;
  }

  .bb-catpicker-input:focus {
    outline: none;
    box-shadow: none;
  }

  /* Mobile: pin to top, prevent Safari zoom on focus */
  @media (max-width: 640px) {
    .bb-catpicker-dialog {
      top: calc(env(safe-area-inset-top, 0px) + 0.5rem);
      border-radius: 0.75rem;
      width: calc(100vw - 1rem);
    }

    .bb-catpicker-input {
      font-size: 16px;
    }
  }

  .bb-catpicker-list {
    @apply overflow-y-auto;
    max-height: min(55vh, 370px);
    scrollbar-width: thin;
    overscroll-behavior: contain;
  }

  .bb-catpicker-item {
    @apply flex items-center gap-3 px-4 py-2 mx-2 rounded-lg text-sm cursor-pointer;
    transition: background-color 0.1s ease, color 0.1s ease;
  }

  .bb-catpicker-item:hover,
  .bb-catpicker-item[data-active="true"] {
    @apply bg-base-200 text-base-content;
  }

  .bb-catpicker-item[data-active="true"] {
    @apply bg-primary/8;
  }

  @media (prefers-color-scheme: dark) {
    .bb-catpicker-item[data-active="true"] {
      @apply bg-primary/12;
    }
  }

  .bb-catpicker-item--indent {
    @apply pl-9;
  }

  .bb-catpicker-item--parent {
    @apply font-semibold;
  }

  .bb-catpicker-dot {
    @apply w-2.5 h-2.5 rounded-full shrink-0;
  }

  .bb-catpicker-icon {
    @apply w-4 h-4 shrink-0 opacity-60;
  }

  .bb-catpicker-empty {
    @apply flex flex-col items-center justify-center py-12 text-base-content/30;
  }

  .bb-catpicker-empty svg,
  .bb-catpicker-empty i {
    @apply w-8 h-8 mb-2 opacity-40;
  }

  .bb-catpicker-footer {
    @apply flex items-center justify-between px-4 py-2.5 border-t border-base-300 text-[0.65rem] text-base-content/30;
  }

  .bb-catpicker-footer kbd {
    @apply inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1
           text-[0.6rem] font-medium text-base-content/40 bg-base-200 border border-base-300 rounded mx-0.5;
  }

  /* Mobile list polish: meet the 44px tap target, tighten the indent gutter,
     and drop the echoed parent label while browsing (the hierarchy is already
     obvious from the indentation). Parent labels still show while searching,
     where results appear out of hierarchy context. */
  @media (max-width: 640px) {
    .bb-catpicker-item {
      @apply py-2.5;
      min-height: 2.75rem;
    }

    .bb-catpicker-item--indent {
      @apply pl-7;
    }

    .bb-catpicker-list[data-searching="false"] .bb-catpicker-item__parent-label {
      display: none;
    }
  }

  /* ─── Global Tag Picker Overlay ─── */
  /*
   * Mirrors the category picker shell (backdrop, dialog, input, footer) but the body is a
   * wrapping hstack of tag pills — tags are flat, not hierarchical. The filter doubles as a
   * quick-create affordance when the query looks like a valid slug and there are no matches.
   */
  .bb-tagpicker-dialog {
    @apply fixed z-50 bg-base-100 border border-base-300 rounded-xl overflow-hidden;
    top: min(20%, 140px);
    left: 50%;
    transform: translateX(-50%);
    width: min(95vw, 520px);
    max-height: min(75vh, 560px);
    display: flex;
    flex-direction: column;
    box-shadow: 0 16px 70px oklch(0 0 0 / 0.15), 0 0 0 1px oklch(0 0 0 / 0.04);
  }

  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .bb-tagpicker-dialog {
      box-shadow: 0 16px 70px oklch(0 0 0 / 0.5), 0 0 0 1px oklch(from var(--color-base-content) l c h / 0.06);
    }
  }
  [data-theme="dark"] .bb-tagpicker-dialog {
    box-shadow: 0 16px 70px oklch(0 0 0 / 0.5), 0 0 0 1px oklch(from var(--color-base-content) l c h / 0.06);
  }

  @media (max-width: 640px) {
    .bb-tagpicker-dialog {
      top: calc(env(safe-area-inset-top, 0px) + 0.5rem);
      border-radius: 0.75rem;
      width: calc(100vw - 1rem);
    }
  }

  .bb-tagpicker-body {
    @apply overflow-y-auto p-4;
    flex: 1 1 auto;
    min-height: 120px;
    scrollbar-width: thin;
    overscroll-behavior: contain;
  }

  /* Wrapping hstack of tag pills inside the picker */
  .bb-tagpicker-grid {
    @apply flex flex-wrap gap-1.5;
  }

  /*
   * Chip states inside the picker. Base .bb-tag styling (background/border via --tag-color)
   * is reused; these modifier classes layer in the interaction state. Five visual states
   * are expressed — absent, present, mixed, pending-add, pending-remove — one modifier at
   * a time. Transitions are kept cheap (box-shadow + opacity only).
   */
  .bb-tagpicker-chip {
    cursor: pointer;
    outline: none;
    transition: box-shadow 0.1s ease, opacity 0.1s ease, background-color 0.1s ease;
  }

  /* Absent: faded, hover restores to read as discoverable. */
  .bb-tagpicker-chip--absent { opacity: 0.35; }
  .bb-tagpicker-chip--absent:hover { opacity: 0.75; }

  /* Present: solid 1.5px ring in the tag's own color. */
  .bb-tagpicker-chip--present {
    box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--tag-color, oklch(from var(--color-base-content) l c h / 0.4)) 55%, transparent);
  }

  /* Mixed: dashed ring signals indeterminate, like Finder/macOS checkboxes. */
  .bb-tagpicker-chip--mixed {
    background-color: color-mix(in srgb, var(--tag-color) 6%, transparent);
    border-style: dashed;
    box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--tag-color, oklch(from var(--color-base-content) l c h / 0.4)) 45%, transparent);
  }

  /* Pending add: green ring + glyph in success. Chip pre-renders as "applied". */
  .bb-tagpicker-chip--pending-add {
    box-shadow: 0 0 0 1.5px var(--color-success);
    background-color: color-mix(in srgb, var(--color-success) 10%, color-mix(in srgb, var(--tag-color) 10%, transparent));
  }
  .bb-tagpicker-chip--pending-add .bb-tagpicker-chip-glyph {
    color: var(--color-success);
  }

  /* Pending remove: red ring, strikethrough label, overall fade. */
  .bb-tagpicker-chip--pending-remove {
    opacity: 0.65;
    box-shadow: 0 0 0 1.5px var(--color-error);
    background-color: color-mix(in srgb, var(--color-error) 8%, color-mix(in srgb, var(--tag-color) 10%, transparent));
  }
  .bb-tagpicker-chip--pending-remove .truncate {
    text-decoration: line-through;
  }
  .bb-tagpicker-chip--pending-remove .bb-tagpicker-chip-glyph {
    color: var(--color-error);
  }

  /* Glyph slot: empty when absent-without-icon, otherwise holds a lucide <svg>.
     Matches the existing .bb-tag inner icon sizing so the chip doesn't shift. */
  .bb-tagpicker-chip-glyph {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
  }
  .bb-tagpicker-chip-glyph:empty {
    display: none;
  }
  .bb-tagpicker-chip-glyph > i,
  .bb-tagpicker-chip-glyph > svg {
    width: 0.75rem;
    height: 0.75rem;
  }

  .bb-tagpicker-empty {
    @apply flex flex-col items-center justify-center py-8 text-base-content/40 gap-1.5;
  }

  .bb-tagpicker-empty svg,
  .bb-tagpicker-empty i {
    @apply w-6 h-6 opacity-40;
  }

  /*
   * Inline create row — appears below the chip grid. Single dense line so the
   * "create new" affordance feels like a natural continuation of the list rather
   * than a separate section.
   */
  .bb-tagpicker-create-inline {
    @apply flex items-center gap-2 w-full px-3 py-2 rounded-xl text-sm
           bg-primary/8 hover:bg-primary/12 text-base-content
           border border-primary/15 hover:border-primary/30
           transition-colors;
  }

  .bb-tagpicker-create-inline > i,
  .bb-tagpicker-create-inline > svg {
    @apply w-3.5 h-3.5 text-primary shrink-0;
  }

  /*
   * Context strip: sits immediately under the search input, visually attached by a
   * shared border. Tells the user how many transactions they're editing so the
   * single-vs-bulk distinction doesn't need to live elsewhere.
   */
  .bb-tagpicker-context {
    @apply flex items-center gap-2 px-4 py-1.5 border-b border-base-300
           text-xs text-base-content/55;
  }
  .bb-tagpicker-context svg,
  .bb-tagpicker-context i {
    @apply w-3.5 h-3.5 opacity-60 shrink-0;
  }
  .bb-tagpicker-context strong {
    @apply font-semibold text-base-content/85;
  }

  /*
   * Diff strip: renders between body and footer when the user has pending changes.
   * Pills mirror the chip states (green add / red remove) so the diff is legible
   * even without looking at the grid.
   */
  .bb-tagpicker-diff {
    @apply flex items-center gap-2 px-4 py-2 border-t border-base-300
           text-[0.65rem] text-base-content/70;
    background-color: color-mix(in srgb, var(--color-base-200) 45%, transparent);
  }
  .bb-tagpicker-diff-pill {
    @apply inline-flex items-center gap-1 px-1.5 py-0.5 rounded-md font-medium;
  }
  .bb-tagpicker-diff-pill svg,
  .bb-tagpicker-diff-pill i {
    @apply w-3 h-3 shrink-0;
  }
  .bb-tagpicker-diff-pill--add {
    @apply text-success;
    background-color: color-mix(in srgb, var(--color-success) 12%, transparent);
  }
  .bb-tagpicker-diff-pill--remove {
    @apply text-error;
    background-color: color-mix(in srgb, var(--color-error) 12%, transparent);
  }
  .bb-tagpicker-diff-revert {
    @apply text-base-content/50 underline-offset-2 cursor-pointer
           hover:text-base-content hover:underline;
  }

  /* ─── Navigation Progress Bar (YouTube/GitHub style) ─── */
  .bb-progress-bar {
    @apply fixed top-0 left-0 right-0 z-[100] h-[2.5px] pointer-events-none;
    opacity: 0;
    transition: opacity 0.15s ease;
  }

  .bb-progress-bar--active {
    opacity: 1;
  }

  .bb-progress-bar__fill {
    @apply h-full;
    width: 0%;
    background: var(--color-base-content);
    border-radius: 0 2px 2px 0;
    transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
    box-shadow: 0 0 8px oklch(from var(--color-base-content) l c h / 0.3);
  }

  .bb-progress-bar__fill--done {
    width: 100% !important;
    transition: width 0.2s ease-out;
  }

  .bb-progress-bar--fade {
    opacity: 0;
    transition: opacity 0.4s ease 0.1s;
  }

  /* ─── Confirmation Modal ─── */
  .bb-confirm-backdrop {
    @apply fixed inset-0 z-50;
    background: oklch(0 0 0 / 0.4);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
  }

  .bb-confirm-dialog {
    @apply fixed z-50 bg-base-100 border border-base-300 rounded-xl overflow-hidden;
    top: min(30%, 200px);
    left: 50%;
    transform: translateX(-50%);
    width: min(92vw, 420px);
    box-shadow: 0 16px 70px oklch(0 0 0 / 0.15), 0 0 0 1px oklch(0 0 0 / 0.04);
  }

  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .bb-confirm-dialog {
      box-shadow: 0 16px 70px oklch(0 0 0 / 0.5), 0 0 0 1px oklch(from var(--color-base-content) l c h / 0.06);
    }
  }
  [data-theme="dark"] .bb-confirm-dialog {
    box-shadow: 0 16px 70px oklch(0 0 0 / 0.5), 0 0 0 1px oklch(from var(--color-base-content) l c h / 0.06);
  }

  .bb-confirm-body {
    @apply flex items-start gap-4 px-5 pt-5 pb-4;
  }

  .bb-confirm-icon {
    @apply w-10 h-10 rounded-xl flex items-center justify-center shrink-0;
  }

  .bb-confirm-actions {
    @apply flex items-center justify-end gap-2 px-5 py-3.5 border-t border-base-300 bg-base-200/30;
  }

  /* Skeleton: daisy 5's `skeleton` primitive is the source of truth.
     Pair with Tailwind sizing utilities — see /design > Loading & skeletons.
     The previous bb-skeleton* family (9 size/shape variants) was deleted in
     favor of daisy's single primitive. */

  /* Inline loading fade */
  .bb-loading-fade {
    opacity: 0.5;
    pointer-events: none;
    transition: opacity 0.2s ease;
  }

  /* ── Keyboard shortcut badges (shadcn Nova .cn-kbd port) ──
     Nova reads kbd hints as small *text* pills, not code-style keycaps:
     `font-sans` (not mono), `text-xs` (12px), `bg-muted` ground, no
     border, no faux-3D shadow. Reads more like a UI affordance and
     less like an inline `<code>`.

     Two contexts where we restyle:
     • Inside a daisy `.tooltip` — the tooltip surface is dark
       (base-content), so the muted bg/fg gets near-invisible. Invert
       to `--color-base-100` at 20% on the dark tip, with the tip's
       own foreground color. Matches Nova's `in-data-[slot=tooltip-content]`
       override.
     • Inside a solid-tone `.btn` (primary/secondary/accent/neutral) —
       the button bg is dark, the surrounding text is white. Same
       inversion trick. */
  .bb-kbd {
    @apply inline-flex items-center justify-center gap-1
           min-w-5 h-5 px-1
           text-xs font-sans font-medium leading-none
           rounded-sm;
    background: var(--color-base-200);
    color: oklch(from var(--color-base-content) l c h / 0.7);
  }

  /* When a kbd lives inside ANY .btn or daisy tooltip, the default
     base-200 ground can collide with the surface. `.btn-soft` sits in
     the base-200 luminance range — a base-200 kbd on a base-200-tinted
     button is invisible. ghost/outline buttons sit on the page base-100
     with a base-200 kbd that reads as a faint smudge. Solid-tone
     surfaces clash too.

     Replace the default ground with `color-mix(currentColor 22%)`
     so the kbd always tints toward whatever foreground colour
     surrounds it. `currentColor` adapts across every button variant +
     tooltips + both themes from a single rule. */
  .btn .bb-kbd,
  .tooltip .bb-kbd,
  [role="tooltip"] .bb-kbd {
    background: color-mix(in oklab, currentColor 22%, transparent);
    color: inherit;
  }

  /* Sidebar command-palette trigger gets a special-case rule because the
     parent `.bb-sidebar-search` deliberately sets a translucent text
     colour (`text-base-content/35`) for the muted "Search..." label.
     Using `currentColor` would inherit that translucency and produce a
     ~5% alpha kbd ground (invisible). Use the opaque `--color-base-content`
     directly so the kbd glyph stays at full saturation and the pill ring
     gets a 14% tint against the bg-base-200/50 trigger surface. */
  .bb-sidebar-search .bb-kbd,
  .bb-sidebar-search .bb-kbd-combo {
    background: color-mix(in oklab, var(--color-base-content) 14%, transparent);
    color: var(--color-base-content);
  }
  .bb-sidebar-search .bb-kbd-combo .bb-kbd-combo__key + .bb-kbd-combo__key {
    border-left-color: color-mix(in oklab, var(--color-base-content) 22%, transparent);
  }

  /* Blended modifier pill: ⌘│K rendered as one tight badge with a hairline
     divider between cells and only the outer corners rounded. Inherits the
     background / fg palette from .bb-kbd so light/dark mode stays in sync —
     only the shape is different (no per-cell rounding). */
  .bb-kbd-combo {
    @apply inline-flex items-center h-5 rounded-sm overflow-hidden
           font-sans font-medium text-xs leading-none;
    background: var(--color-base-200);
    color: oklch(from var(--color-base-content) l c h / 0.7);
  }

  .bb-kbd-combo__key {
    @apply inline-flex items-center justify-center min-w-5 px-1 h-full;
  }

  .bb-kbd-combo__key + .bb-kbd-combo__key {
    border-left: 1px solid oklch(from var(--color-base-content) l c h / 0.10);
  }

  /* Same currentColor-tint treatment as .bb-kbd above for .bb-kbd-combo
     — widened to any .btn / tooltip so soft / ghost / outline surfaces
     all read as a tinted inset of the surrounding fg. Sidebar-search is
     handled by the separate rule above (opaque base-content variant). */
  .btn .bb-kbd-combo,
  .tooltip .bb-kbd-combo,
  [role="tooltip"] .bb-kbd-combo {
    background: color-mix(in oklab, currentColor 22%, transparent);
    color: inherit;
  }
  .btn .bb-kbd-combo .bb-kbd-combo__key + .bb-kbd-combo__key,
  .tooltip .bb-kbd-combo .bb-kbd-combo__key + .bb-kbd-combo__key,
  [role="tooltip"] .bb-kbd-combo .bb-kbd-combo__key + .bb-kbd-combo__key {
    border-left-color: color-mix(in oklab, currentColor 35%, transparent);
  }

  /* ── Triage mode: compact review rows ── */
  .bb-triage-row {
    @apply relative;
    border-bottom: 1px solid var(--color-base-300);
    transition: background-color 0.1s ease;
  }

  .bb-triage-row:last-child {
    border-bottom: none;
  }

  .bb-triage-row:hover {
    background: var(--color-base-200);
  }

  .bb-triage-row--focused {
    background: var(--color-base-200) !important;
  }

  .bb-triage-row__main {
    @apply flex items-center gap-2 sm:gap-3 px-3 sm:px-4 py-2.5;
    min-height: 2.75rem;
  }

  .bb-triage-row__indicator {
    @apply hidden sm:block w-0.5 h-6 rounded-full shrink-0 transition-all duration-150;
    background: transparent;
  }

  .bb-triage-row__indicator--active {
    background: var(--color-base-content);
  }

  .bb-triage-row__type {
    @apply hidden sm:block shrink-0 w-12;
  }

  .bb-triage-row__name {
    @apply flex flex-col min-w-0 flex-1;
  }

  .bb-triage-row__account {
    @apply hidden lg:block min-w-0 shrink-0;
    width: 160px;
  }

  .bb-triage-row__category {
    @apply hidden md:flex md:items-center min-w-0 shrink-0;
    width: 160px;
  }

  .bb-triage-row__category > span {
    @apply block truncate max-w-full;
  }

  .bb-triage-row__date {
    @apply hidden sm:block shrink-0;
    width: 80px;
  }

  .bb-triage-row__amount {
    @apply shrink-0 text-right;
    width: 68px;
  }

  @media (min-width: 640px) {
    .bb-triage-row__amount {
      width: 90px;
    }
  }

  .bb-triage-row__actions {
    @apply flex items-center gap-1 shrink-0 ml-1 sm:ml-2;
    opacity: 1;
    transition: opacity 0.15s ease;
  }

  @media (min-width: 640px) {
    .bb-triage-row__actions {
      opacity: 0;
    }
    .bb-triage-row:hover .bb-triage-row__actions,
    .bb-triage-row--focused .bb-triage-row__actions {
      opacity: 1;
    }
  }

  .bb-triage-row__resolved {
    @apply shrink-0 ml-2;
  }

  .bb-triage-action {
    @apply inline-flex items-center justify-center w-8 h-8 sm:w-7 sm:h-7 rounded-lg
           border border-transparent cursor-pointer;
    transition: all 0.1s ease;
    background: transparent;
    color: oklch(from var(--color-base-content) l c h / 0.6);
  }

  /* On mobile, show colored backgrounds so actions are tappable without hover */
  @media (max-width: 639px) {
    .bb-triage-action--accept {
      color: var(--color-success);
      background: oklch(from var(--color-success) l c h / 0.08);
      border-color: oklch(from var(--color-success) l c h / 0.2);
    }
    .bb-triage-action--skip {
      color: oklch(from var(--color-base-content) l c h / 0.55);
      background: oklch(from var(--color-base-content) l c h / 0.05);
      border-color: oklch(from var(--color-base-content) l c h / 0.12);
    }
  }

  .bb-triage-action:hover {
    border-color: var(--color-base-300);
    background: var(--color-base-200);
  }

  .bb-triage-action--accept:hover {
    color: var(--color-success);
    border-color: oklch(from var(--color-success) l c h / 0.3);
    background: oklch(from var(--color-success) l c h / 0.08);
  }

  .bb-triage-action--skip:hover {
    color: oklch(from var(--color-base-content) l c h / 0.55);
    border-color: var(--color-base-300);
    background: var(--color-base-200);
  }

  .bb-triage-action:disabled {
    opacity: 0.3;
    cursor: not-allowed;
  }

  /* Triage detail panel */
  .bb-triage-detail {
    @apply px-3 sm:px-6 py-2.5 border-t;
    border-color: var(--color-base-300);
    background: var(--color-base-200);
  }

  /* Agent report bodies render via @components.MarkdownLarge into
     .bb-prose.bb-prose-lg — see the unified .bb-prose rules. The old
     .bb-report-body ruleset was retired when markdown moved server-side. */

}

/* ─── Unlayered overrides ───
 * Rules outside any @layer beat anything inside one, regardless of
 * specificity. We need that here because DaisyUI 5's `.tooltip` rule lands
 * in a deeper sub-layer (`daisyui.l1.l2.l3`) than our `@layer components`
 * tag styles (`daisyui.l1.l2`), so a chained `.bb-tag.tooltip` selector
 * inside @layer components still loses on cascade order. Pulling the
 * override out of every layer is the smallest fix that pins icon + label
 * to a single row whenever a chip carries both classes (see #904, where
 * the slug-tooltip class made the chip render two-line). */
.bb-tag.tooltip {
  display: inline-flex;
}

/* ── Collapsible sidebar — desktop icon-rail ──
 * Unlayered (not @layer components) on purpose: the collapsed width must
 * beat the `lg:w-64` @apply'd onto .bb-sidebar, and the inner overrides must
 * beat assorted utilities. Utilities/@apply'd declarations win over @layer
 * components regardless of specificity (see the section note above), so
 * these only land if they sit outside every layer.
 *
 * Toggled by bbToggleSidebar() setting <html data-sidebar="collapsed">
 * (pre-applied before paint by the inline <head> script, so no flash). Only
 * active at lg+, where the sidebar is a persistent rail.
 *
 * Smoothness strategy — the inner pieces animate INTO the collapsed state
 * rather than popping:
 *   • The icons never move. The collapsed width is tuned to 4rem so a nav
 *     icon's UNCHANGED offset (rail px-3 12px + link px-3 12px + half the
 *     16px icon = 32px) lands exactly on the 64px rail's centre line. We
 *     therefore do NOT toggle justify-content/padding on the links, which
 *     can't transition and would snap.
 *   • Labels fade (opacity) and the narrowing rail clips the rest
 *     (overflow-x: clip). They stay in layout so the icons don't reflow;
 *     flex-shrink:0 keeps the icons from being squeezed in the cramped rail.
 *   • The brand mark is a bigger glyph (w-5) sitting flush left, so it slides
 *     onto the icon centre line via an animated margin, in step with width. */
@media (min-width: 1024px) {
  /* --bb-rail-collapsed-w is the single tunable for the collapsed rail. It is
     NOT arbitrary: it must equal 2 × (rail px-3 + link px-3 + half a nav icon)
     = 2 × (0.75rem + 0.75rem + 0.5rem) = 4rem, so a nav icon's unchanged left
     offset lands exactly on the rail's centre line. If you change the sidebar
     padding (.bb-sidebar px-3), the link padding (.bb-sidebar-link px-3), or
     the nav icon size (lg:w-4), recompute this — the brand offset below is
     derived from it, but the nav-icon centring is geometric and breaks
     silently otherwise. */
  .bb-sidebar { --bb-rail-collapsed-w: 4rem; transition: width 0.22s ease; overflow-x: clip; }
  .bb-sidebar-brand { transition: margin-left 0.22s ease; }
  .bb-sidebar-link__label,
  .bb-sidebar-brand-text,
  .bb-sidebar-section { transition: opacity 0.16s ease; }
  /* Icons hold their size + position while the rail squeezes around them. */
  .bb-sidebar-link svg, .bb-sidebar-link i,
  .bb-sidebar-brand svg, .bb-sidebar-brand i { flex-shrink: 0; }

  html[data-sidebar="collapsed"] .bb-sidebar { width: var(--bb-rail-collapsed-w); }

  /* Text fades out; the clipped rail hides whatever's mid-fade. Kept in
     layout so the icons stay put. */
  html[data-sidebar="collapsed"] .bb-sidebar .bb-sidebar-link__label,
  html[data-sidebar="collapsed"] .bb-sidebar .bb-sidebar-brand-text,
  html[data-sidebar="collapsed"] .bb-sidebar .bb-sidebar-section {
    opacity: 0;
  }
  /* Section labels also collapse their height to a slim group spacer. */
  html[data-sidebar="collapsed"] .bb-sidebar .bb-sidebar-section {
    font-size: 0;
    line-height: 0;
  }

  /* Slide the larger brand mark (w-5, flush-left via !px-0) onto the rail
     centre line. Derived from the rail width so it tracks any retune:
     rail-centre (w/2) − rail's left padding (px-3 = 0.75rem) − half the brand
     glyph (w-5 → 0.625rem) = the left margin that centres the glyph. */
  html[data-sidebar="collapsed"] .bb-sidebar-brand {
    margin-left: calc(var(--bb-rail-collapsed-w) / 2 - 0.75rem - 0.625rem);
  }

  /* Badges have nowhere to sit in the rail. */
  html[data-sidebar="collapsed"] .bb-sidebar .bb-nav-badge,
  html[data-sidebar="collapsed"] .bb-sidebar .badge {
    display: none;
  }

  /* The update callout card can't fold into the 4rem icon rail. */
  html[data-sidebar="collapsed"] .bb-sidebar .bb-update-card {
    display: none;
  }
}

/* Honour reduced-motion: collapse snaps instead of animating. */
@media (prefers-reduced-motion: reduce) {
  .bb-sidebar,
  .bb-sidebar-brand,
  .bb-sidebar-link__label,
  .bb-sidebar-brand-text,
  .bb-sidebar-section,
  .bb-settings-rail {
    transition: none;
  }
}

/* ── daisy popover dropdowns: instant close ──
 * A daisy `.dropdown[popover]` anchors next to its trigger while open (via
 * position-area), but on close the anchor reverts and the base `inset:0`
 * snaps it to the viewport top for the duration of the fade — a visible
 * "jump". This affects every popover dropdown (the topbar profile menu, the
 * OverflowMenu kebab on row lists, SidebarUserMenu), so the fix is applied to
 * the mechanism, not one caller: kill the transition while not :popover-open
 * so the menu closes instantly instead of animating from the wrong spot. The
 * open animation (matched by :popover-open) is unaffected. Unlayered to beat
 * daisy's own transition rule. */
.dropdown[popover]:not(:popover-open) {
  transition: none;
}

/* Outline button hover/press fill. Unlayered + currentColor:
 *  - Unlayered so it beats daisy's layered `!important` button background
 *    (a @layer components rule loses to it; even inline styles lose).
 *  - currentColor (the button's own text colour) so the tint tracks the
 *    variant AND stays a valid color on a plain `.btn-outline`, whose
 *    `--btn-color` is unset — a `var(--btn-color)` mix there is invalid and
 *    silently dropped.
 * Press (:active) lands a touch darker than hover. The border + text stay
 * pinned by the @layer rule above, so the outline never disappears. */
.btn-outline:hover {
  background-color: color-mix(in oklch, currentColor 16%, transparent) !important;
}
.btn-outline:active,
.btn-outline.btn-active {
  background-color: color-mix(in oklch, currentColor 24%, transparent) !important;
}

/* ── Command palette: tx-row horizontal alignment ──
 * TxRowCompact ships with `px-2 -mx-1` so its hover paint extends slightly
 * beyond the parent edge on dashboard/recents (where the parent has no
 * horizontal padding of its own). Inside the cmdk, the wrapper has `mx-2`
 * and generic items render at `px-4` (.bb-cmdk-generic), so without an
 * override the tx row's content starts ~12px from the dialog edge while
 * every generic item sits at 24px — the avatar/amount columns visibly
 * drift left of every other row. Pull the inner anchor flush with the
 * wrapper edge and re-pad to `px-4` so all rows share one gutter.
 *
 * Has to live unlayered because `px-2` and `-mx-1` are Tailwind utility
 * classes — @layer components rules lose to utilities regardless of
 * specificity. */
.bb-cmdk-item--tx .bb-tx-row-compact {
  margin-left: 0;
  margin-right: 0;
  padding-left: 1rem;
  padding-right: 1rem;
}

/* ── Activity timeline — prominent overrides ──
 * Has to live unlayered because every rule below targets Tailwind utility
 * classes (w-6, h-6, leading-6, ring-4, py-2.5, …). Inside @layer components
 * those overrides lose to the utilities regardless of specificity, leaving
 * tiles at 24px and the rail offset (engineered for 28px tiles) two pixels
 * to the right of every circle's centre. See input.css line ~660 for the
 * placeholder comment in the @layer components block. */

/* Tile + icon scale-up. Targets the row-tile (`w-6 h-6`) and inner icon
 * (`w-3 h-3`) classes that the card-mode rows render — so we lift the
 * size in CSS without duplicating the templ markup for each variant. */
.bb-timeline-prominent .bb-timeline > li > div:first-child.w-6.h-6,
.bb-timeline-prominent .bb-timeline > li > div:first-child .w-6.h-6 {
  width: 1.75rem;   /* 28px */
  height: 1.75rem;
}
.bb-timeline-prominent .bb-timeline > li > div:first-child .w-3.h-3 {
  width: 0.875rem;  /* 14px */
  height: 0.875rem;
}
/* Lift the text leading on row sentences so the first-line baseline stays
 * vertically centred against the larger 28px tile next to it. */
.bb-timeline-prominent .bb-timeline > li > div.flex-1.text-sm.leading-6 {
  line-height: 1.5rem; /* 24px — tighter row rhythm; the 28px tile and
                        * its 4px ring still anchor the row's vertical
                        * extent, this just brings the body's leading
                        * back in line with the rest of the page. */
  /* The 28px tile is 4px taller than the 24px meta line, so under
   * `items-start` the tile center lands 2px below the text center.
   * Pad the body 2px so every first-line content (sync action, report
   * title, comment meta, bulk_action sentence) re-centers against the
   * tile vertically. */
  padding-top: 0.125rem;
}
/* …EXCEPT comment/report CARDS. A card's body is a bordered, rounded
 * box whose header band must sit flush against the top border. The 2px
 * nudge above shoves the header DOWN off that border, exposing a strip
 * of the darker card body under the rounded top edge — the seam. The
 * card re-centers its own first line via the header band's own padding,
 * so it neither needs nor wants the row nudge. The extra :has() class
 * outweighs the rule above, so this wins. */
.bb-timeline-prominent .bb-timeline > li:has(.bb-comment-bubble) > div.flex-1.text-sm.leading-6 {
  padding-top: 0;
}

/* Rail mask. The 28px tile bg + ring are painted with base-100
 * (card-surface) classes by the shared row markup, and the rail seam is
 * masked by recolouring them to the *page* surface. Since the Mintlify
 * surface flip the prominent timeline's page surface is base-100 (the
 * content column), so the mask now matches the page at base-100 — the
 * seam stays invisible. (These were base-200 while the page was base-200;
 * the flip moved the page to base-100, so the mask follows it.) Keeping
 * the explicit remap — rather than dropping it — preserves the scoping to
 * the rail-mask wrapper (`> li > div:first-child`) so it can't bleed onto
 * other surfaces, and documents the page-surface coupling for the next
 * theme change. */
.bb-timeline-prominent .bb-timeline > li > div:first-child.bg-base-100 {
  background-color: var(--color-base-100);
}
.bb-timeline-prominent .bb-timeline > li > div:first-child .ring-base-100,
.bb-timeline-prominent .bb-timeline > li > div:first-child.ring-base-100 {
  --tw-ring-color: var(--color-base-100);
}
/* Inner tile pills painted `bg-base-200` (sync, deleted-comment trash,
 * neutral review, comment message-circle, unknown-kind fallback) sit only
 * ~3% off the base-100 page surface (and matched the old base-200 page
 * exactly), so they read faintly on the rail. Lift the fill to base-300 so
 * the circle reads as a defined shape on the rail.
 * Targets only direct children of the rail-mask wrapper so the inner
 * lucide icon's color isn't touched. As of #969 comment rows are
 * included — they use the same neutral message-circle tile as every
 * other system row, so the same fill bump applies uniformly. */
.bb-timeline-prominent .bb-timeline > li > div:first-child > .bg-base-200 {
  background-color: var(--color-base-300);
}
/* Slightly thicker ring for the bigger tile so the colour pill reads
 * cleanly against the rail. base-100 to match the flipped page surface so
 * this ring keeps masking the rail seam instead of drawing a gray halo on
 * the white page. */
.bb-timeline-prominent .bb-timeline > li > div:first-child .ring-4 {
  --tw-ring-offset-width: 0;
  box-shadow: 0 0 0 5px var(--color-base-100);
}

/* Day separator. Bigger anchor dot and a less-shouty label — closer to
 * GitHub's "X commented on Aug 12" header than the default uppercase
 * tracking-wider treatment. */
.bb-timeline-prominent .bb-timeline > li[role="separator"] > div:first-child > span {
  width: 0.875rem;   /* 14px dot, up from 12 */
  height: 0.875rem;
}
.bb-timeline-prominent .bb-timeline > li[role="separator"] h3 {
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  color: color-mix(in srgb, var(--color-base-content) 65%, transparent);
}

/* Card-mode comment rows: vertically center the rail tile against the
 * row body's combined meta+bubble height. The default `items-start` on
 * the <li> top-anchors every other row's tile to its first content line
 * — fine for single-line system events. On comment rows the bubble
 * shifts the perceived center down, so let the tile float to that
 * combined-content centre for a chat-style alignment. Overridden in
 * prominent mode below where the row's card chrome anchors to the
 * header bar instead. */
.bb-timeline > li:has(.bb-comment-bubble) > div:first-child {
  align-self: center;
}

/* Comment cards. Each comment row's right-hand column becomes a
 * GitHub-style bordered card: header bar with actor + timestamp, body
 * with the comment text, and a small left-pointing tail anchoring it
 * to the avatar on the rail. Targets only comment rows (those that
 * contain a `.bb-comment-bubble`) so system-event rows and the
 * composer-row stay untouched. */
.bb-timeline-prominent .bb-timeline > li:has(.bb-comment-bubble) {
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}
/* Prominent comment rows keep the rail tile top-anchored to the
 * comment card's header bar — undo the card-mode centering. */
.bb-timeline-prominent .bb-timeline > li:has(.bb-comment-bubble) > div:first-child {
  align-self: flex-start;
}
/* As of #969 comment rows use the same neutral message-circle tile as
 * every other system-event row, so the bespoke 1px outline that used to
 * sit on the avatar is gone — the shared `bg-base-200 + ring-4` chrome
 * already gives every rail tile a defined edge against the rail mask. */
.bb-timeline-prominent .bb-timeline > li:has(.bb-comment-bubble) > div.flex-1 {
  position: relative;
  background: var(--color-base-100);
  border: 1px solid var(--color-base-300);
  border-radius: 0.5rem;
  overflow: visible;
}
/* Header bar (was the "Name commented X ago" meta line).
 * The card carries a 1px border + 0.5rem outer radius, so the header
 * sits one pixel inside the curve. Match the inner curve exactly
 * (outer radius minus border width) — using the same 0.5rem here
 * leaves the gray fill curving inside the border arc, exposing a
 * thin wedge of the card background at the top corners (only visible
 * in dark mode, where the card body and the header are different
 * shades). Originally fixed in #1531; restored here after #1548's
 * conflict resolution silently dropped the calc(). */
.bb-timeline-prominent .bb-timeline > li:has(.bb-comment-bubble) > div.flex-1 > div:first-child {
  background: color-mix(in srgb, var(--color-base-200) 60%, transparent);
  border-bottom: 1px solid var(--color-base-300);
  padding: 0.5rem 0.875rem;
  margin: 0;
  border-top-left-radius: calc(0.5rem - 1px);
  border-top-right-radius: calc(0.5rem - 1px);
}
/* Body (was the chat-bubble) */
.bb-timeline-prominent .bb-timeline > li:has(.bb-comment-bubble) .bb-comment-bubble {
  display: block;
  margin: 0;
  padding: 0.75rem 0.875rem 0.875rem 0.875rem;
  background: transparent;
  border: 0;
  border-radius: 0;
  max-width: none;
}
/* Left-pointing tail. Two stacked triangles: outer (border-color) sits
 * one pixel behind the inner (background) so the card border is hinted
 * along the seam. The triangle is 16px tall; with `top: 6px` its vertical
 * centre lands at y=14 inside the card, which is exactly the centre of
 * the 28px avatar next to it (both flex items share the row's top edge
 * via `items-start`, so card_top == avatar_top). */
.bb-timeline-prominent .bb-timeline > li:has(.bb-comment-bubble) > div.flex-1::before,
.bb-timeline-prominent .bb-timeline > li:has(.bb-comment-bubble) > div.flex-1::after {
  content: '';
  position: absolute;
  top: 6px;
  width: 0;
  height: 0;
  border-style: solid;
}
.bb-timeline-prominent .bb-timeline > li:has(.bb-comment-bubble) > div.flex-1::before {
  left: -8px;
  border-width: 8px 8px 8px 0;
  border-color: transparent var(--color-base-300) transparent transparent;
}
.bb-timeline-prominent .bb-timeline > li:has(.bb-comment-bubble) > div.flex-1::after {
  /* 1.5px horizontal offset (vs ::before at -8) gives a ~1.06px perpendicular
   * diagonal stroke — matches the card's 1px straight border without the
   * heavier read of a 2px shift. */
  left: -6.5px;
  border-width: 8px 8px 8px 0;
  /* Solid mix matching the header bar's composited appearance (60% base-200
   * over base-100). Triggers a visual mismatch if left as the same
   * `transparent`-based color used by the header — the triangle composites
   * over the page (base-200 in prominent mode), the header over the card
   * (base-100), so the two read as different shades. */
  border-color: transparent color-mix(in srgb, var(--color-base-200) 60%, var(--color-base-100)) transparent transparent;
}

/* Composer row gets the same outer card treatment as comment rows so
 * the page reads as one continuous conversation. The composer card
 * carries `border-base-200` from the templ markup (matches the lighter
 * card-mode chrome); against a base-200 page surface that border is
 * invisible from outside the card, so lift it to base-300 here to give
 * the composer a defined edge — same treatment the comment cards above
 * already use. */
.bb-timeline-prominent .bb-timeline > li:has([data-composer-anchor]) > div.flex-1 > [data-composer-anchor] {
  border-radius: 0.5rem;
  border-color: var(--color-base-300);
}
.bb-timeline-prominent .bb-timeline > li:has([data-composer-anchor]) > div.flex-1 {
  position: relative;
}
.bb-timeline-prominent .bb-timeline > li:has([data-composer-anchor]) > div.flex-1::before,
.bb-timeline-prominent .bb-timeline > li:has([data-composer-anchor]) > div.flex-1::after {
  content: '';
  position: absolute;
  top: 6px;
  width: 0;
  height: 0;
  border-style: solid;
}
.bb-timeline-prominent .bb-timeline > li:has([data-composer-anchor]) > div.flex-1::before {
  left: -8px;
  border-width: 8px 8px 8px 0;
  border-color: transparent var(--color-base-300) transparent transparent;
}
.bb-timeline-prominent .bb-timeline > li:has([data-composer-anchor]) > div.flex-1::after {
  /* See the comment-card ::after note — 1.5px offset for matching
   * perpendicular stroke weight on the diagonal. */
  left: -6.5px;
  border-width: 8px 8px 8px 0;
  border-color: transparent var(--color-base-100) transparent transparent;
}

/* Dark-mode contrast lift for the prominent timeline's comment + composer
 * cards. Both rules above paint `var(--color-base-100)`, which in the dark
 * theme is identical to the body background — the cards disappear. Mix in
 * 8% white so they read as elevated surfaces, matching the `.bb-card` lift
 * used elsewhere on /feed. Light-mode is unaffected. */
@media (prefers-color-scheme: dark) {
  .bb-timeline-prominent .bb-timeline > li:has(.bb-comment-bubble) > div.flex-1,
  .bb-timeline-prominent .bb-timeline > li:has([data-composer-anchor]) > div.flex-1 {
    background: color-mix(in oklch, var(--color-base-100) 92%, white 8%);
  }
  /* Composer card has no header — its tail points at the card body, so
   * match the lifted card surface. */
  .bb-timeline-prominent .bb-timeline > li:has([data-composer-anchor]) > div.flex-1::after {
    border-right-color: color-mix(in oklch, var(--color-base-100) 92%, white 8%);
  }
  /* Header bar inside the comment card uses base-200 60% — in dark that
   * is base-200 (oklch 0.145) which is darker than the lifted card. Bump
   * it slightly so the header reads as a defined band rather than a hole. */
  .bb-timeline-prominent .bb-timeline > li:has(.bb-comment-bubble) > div.flex-1 > div:first-child {
    background: color-mix(in oklch, var(--color-base-100) 86%, white 14%);
  }
  /* Comment-card tail points at the header bar (top: 6px lands inside the
   * 0.5rem padding + 1.55 line-height header), so match the header's
   * lifted background — not the card body. In light mode both surfaces
   * composite to nearly the same color so the body-matching value above
   * looked right; in dark mode the header is visibly lighter than the
   * card, and a body-coloured tail reads as a notch hanging below the
   * header. */
  .bb-timeline-prominent .bb-timeline > li:has(.bb-comment-bubble) > div.flex-1::after {
    border-right-color: color-mix(in oklch, var(--color-base-100) 86%, white 14%);
  }
}

/* ── Activity timeline — inline chip fix ──
 * Tag chips (`.bb-tag`) inside timeline sentence bodies carry a `truncate`
 * class on their inner text span. That class truncates chip labels when the
 * parent container is narrow (min-w-0 flex-1). In the prominent timeline the
 * chip is the primary informational element; truncation defeats its purpose.
 * Override: let the chip text fully wrap / display at natural width inside
 * system-event sentence bodies (non-comment, non-composer rows). */
.bb-timeline-prominent .bb-timeline
  > li:not(:has(.bb-comment-bubble)):not(:has([data-composer-anchor]))
  > div.flex-1 .bb-tag span.truncate {
  overflow: visible;
  text-overflow: unset;
  white-space: normal;
}

/* ── Activity timeline — mobile responsive fixes ──
 * Fixes for narrow (≤639px) viewports where system-event sentences with
 * inline chips, long rule names, and tag badges could overflow or look
 * cramped. */

/* System-event sentence bodies: allow inline chips + timestamp suffix to
 * wrap naturally on narrow screens. The leading-6 line-box is preserved
 * for single-line sentences (short actor names, brief verbs); multi-line
 * wrap picks up normal 1.5 leading so content stays readable. */
@media (max-width: 639px) {
  .bb-timeline-prominent .bb-timeline > li > div.flex-1.text-sm {
    line-height: 1.5; /* relax from 1.75rem (28px) to 1.5 on mobile */
  }

  /* Timestamp/origin suffix: break onto its own line after the sentence
   * body on very narrow viewports. The suffix `<span>` has ml-1 from the
   * templ markup; on mobile we push it to a new pseudo-line so it doesn't
   * tuck tightly against the last word of the sentence when the sentence
   * has already wrapped. */
  .bb-timeline-prominent .bb-timeline
    > li:not(:has(.bb-comment-bubble)):not(:has([data-composer-anchor]))
    > div.flex-1 > span:last-child {
    display: block;
    margin-left: 0;
    margin-top: 0.125rem;
  }

  /* Composer card textarea: slightly smaller min-height on mobile so the
   * keyboard doesn't immediately scroll the page when tapping to type. */
  .bb-timeline-prominent .bb-timeline
    > li:has([data-composer-anchor]) textarea {
    min-height: 4rem;
  }
}

/* Comment rows: on mobile the delete button should always be visible (not
 * sm:opacity-0 hidden-until-hover). The templ markup hides it with
 * sm:opacity-0 which means it's visible on mobile by default already —
 * but ensure the tap target is large enough. */
@media (max-width: 639px) {
  .bb-timeline-prominent .bb-timeline
    > li:has(.bb-comment-bubble) > div.flex-1 > div:first-child button {
    min-width: 2rem;
    min-height: 2rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
}

/* Button radius is now vanilla daisy (--radius-field, 4px by default).
 * The previous `rounded-xl on btn-sm` convention was dropped to align
 * with daisyui.com defaults — adopt daisy theme tokens via the daisy
 * theme builder if a different shape is wanted, don't reintroduce
 * per-size CSS overrides here. */

/* ── bb-prose: server-rendered markdown for agent transcripts (and
 * future report bodies). Pairs with components.Markdown. The class
 * sits on a wrapper <div>; descendant selectors style block + inline
 * elements without bleeding into surrounding admin chrome.
 *
 * Tight vertical rhythm — uses --bb-gap-xs/sm spacing tokens so the
 * prose body lines up with chat-bubble padding in the run detail
 * page. */
.bb-prose {
  font-size: 0.875rem;
  line-height: 1.55;
  /* Inherit color from the surrounding surface so a .bb-prose nested
   * inside a daisy chat-bubble-primary picks up primary-content
   * automatically instead of getting locked to base-content (which
   * goes invisible on dark bubbles in light mode + on primary bubbles
   * in dark mode). */
  color: inherit;
  word-wrap: break-word;
}
/* GitHub-like vertical rhythm: each block element carries its own bottom
 * spacing (set per-element below), headings get generous top space but bind
 * tightly to the text beneath, and the first/last child sit flush so the prose
 * block has no stray top/bottom gap. Adjacent margins collapse, so a heading
 * after a paragraph shows the larger heading-top gap, not the sum. */
.bb-prose > :first-child { margin-top: 0; }
.bb-prose > :last-child { margin-bottom: 0; }
/* Tighten nested lists so sub-items don't inherit full block spacing. */
.bb-prose li > ul, .bb-prose li > ol { margin-block: 0.15rem 0; }

/* The renderer (internal/markdown via goldmark) emits semantic, UNclassed
 * HTML — <h2>, <p>, <ul>, <code>, <table>, GFM task-list checkboxes, and
 * chroma-highlighted code blocks. These descendant selectors style those
 * bare tags. Colors inherit (never hard-set base-content) so .bb-prose on a
 * colored chat bubble in the run-detail thread stays legible. */
.bb-prose h1, .bb-prose h2, .bb-prose h3,
.bb-prose h4, .bb-prose h5, .bb-prose h6 {
  font-weight: 600;
  margin: 1.4rem 0 0.5rem;
  line-height: 1.3;
  letter-spacing: -0.005em;
}
.bb-prose h1 { font-size: 1.05rem; }
.bb-prose h2 { font-size: 1rem; }
.bb-prose h3 { font-size: 0.95rem; }
.bb-prose h4, .bb-prose h5, .bb-prose h6 { font-size: 0.875rem; opacity: 0.8; }
.bb-prose p { margin: 0 0 0.875rem; }
.bb-prose strong { font-weight: 600; }
.bb-prose em { font-style: italic; }
.bb-prose del, .bb-prose s { opacity: 0.55; text-decoration: line-through; }
.bb-prose a {
  color: var(--color-primary);
  text-decoration: underline;
  text-decoration-color: color-mix(in oklch, var(--color-primary) 40%, transparent);
  text-underline-offset: 2px;
}
.bb-prose a:hover { text-decoration-color: var(--color-primary); }
/* Transaction cross-reference links render as a subtle chip (carried over
 * from the report/comment styling) so "see <tx>" reads as an affordance. */
.bb-prose a[href^="/transactions/"] {
  border-radius: 0.25rem;
  background: color-mix(in oklch, var(--color-primary) 5%, transparent);
  padding: 0 0.25rem;
  text-decoration: none;
}
.bb-prose a[href^="/transactions/"]:hover {
  background: color-mix(in oklch, var(--color-primary) 10%, transparent);
}
.bb-prose code:not(pre code) {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8125rem;
  padding: 0.0625rem 0.3125rem;
  border-radius: 0.25rem;
  background: color-mix(in oklch, var(--color-base-200) 90%, transparent);
  border: 1px solid color-mix(in oklch, var(--color-base-300) 70%, transparent);
}
.bb-prose pre {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.75rem;
  line-height: 1.5;
  background: color-mix(in oklch, var(--color-base-200) 70%, transparent);
  border: 1px solid color-mix(in oklch, var(--color-base-300) 60%, transparent);
  border-radius: 0.5rem;
  padding: 0.625rem 0.75rem;
  overflow-x: auto;
  white-space: pre;
}
.bb-prose pre code { background: transparent; padding: 0; border: 0; font-size: inherit; }
.bb-prose ul, .bb-prose ol { margin: 0 0 0.875rem; padding-inline-start: 1.25rem; }
.bb-prose ul { list-style-type: disc; }
.bb-prose ol { list-style-type: decimal; }
.bb-prose li { margin: 0.125rem 0; }
.bb-prose li::marker { color: color-mix(in oklch, var(--color-base-content) 45%, transparent); }
/* GFM task lists (extension.TaskList) emit <li><input type=checkbox disabled>.
 * Drop the bullet on those items and align the (read-only) checkbox inline. */
.bb-prose li:has(> input[type="checkbox"]) { list-style: none; margin-inline-start: -1.25rem; }
.bb-prose input[type="checkbox"] {
  margin-inline-end: 0.4rem;
  vertical-align: middle;
  accent-color: var(--color-primary);
  /* Native checkbox UA rendering must track the *app* theme, not the OS.
     The global :root { color-scheme: light dark } (above) lets native
     widgets follow prefers-color-scheme, which paints a dark checkbox on a
     light page when the in-app theme disagrees with the OS. Pin it to the
     active theme via the same dual-guard pattern used elsewhere. */
  color-scheme: light;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .bb-prose input[type="checkbox"] { color-scheme: dark; }
}
[data-theme="dark"] .bb-prose input[type="checkbox"] { color-scheme: dark; }
.bb-prose blockquote {
  margin: 0 0 0.875rem;
  padding: 0.25rem 0 0.25rem 0.75rem;
  border-inline-start: 3px solid color-mix(in oklch, var(--color-primary) 35%, transparent);
  color: color-mix(in oklch, var(--color-base-content) 80%, transparent);
  font-style: italic;
}
.bb-prose hr {
  border: 0;
  border-top: 1px solid color-mix(in oklch, var(--color-base-300) 80%, transparent);
  margin: 1.25rem 0;
}
/* Tables scroll horizontally on narrow viewports rather than overflowing
 * the page. display:block + width:max-content lets the table size to its
 * content and scroll inside the prose container when it exceeds it (goldmark
 * emits a bare <table>, so the table itself is the scroll container). */
.bb-prose table {
  display: block;
  width: max-content;
  max-width: 100%;
  margin: 0 0 0.875rem;
  overflow-x: auto;
  border-collapse: collapse;
  font-size: 0.8125rem;
  border-radius: 0.5rem;
  border: 1px solid color-mix(in oklch, var(--color-base-300) 50%, transparent);
}
.bb-prose th, .bb-prose td {
  padding: 0.375rem 0.625rem;
  border-bottom: 1px solid color-mix(in oklch, var(--color-base-300) 50%, transparent);
  text-align: start;
  vertical-align: top;
}
.bb-prose tbody tr:last-child td { border-bottom: 0; }
.bb-prose tbody tr:nth-child(even) {
  background: color-mix(in oklch, var(--color-base-200) 35%, transparent);
}
.bb-prose thead th {
  font-weight: 600;
  background: color-mix(in oklch, var(--color-base-200) 60%, transparent);
  border-bottom: 1px solid var(--color-base-300);
  white-space: nowrap;
}

/* ── bb-json: server-rendered JSON viewer with collapsible objects +
 * arrays. Pairs with components.JSONViewer. Token classes are coloured
 * via theme tokens so light/dark works without per-mode overrides. */
.bb-json-wrap { position: relative; }
.bb-json-wrap .bb-json-copy {
  position: absolute;
  top: 0.375rem;
  inset-inline-end: 0.375rem;
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.1875rem 0.375rem;
  border-radius: 0.375rem;
  background: color-mix(in oklch, var(--color-base-100) 80%, transparent);
  border: 1px solid color-mix(in oklch, var(--color-base-300) 60%, transparent);
  color: color-mix(in oklch, var(--color-base-content) 70%, transparent);
  font-size: 0.65rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  transition: background 0.15s ease, color 0.15s ease;
  z-index: 2;
}
.bb-json-wrap .bb-json-copy:hover {
  background: var(--color-base-100);
  color: var(--color-base-content);
}
.bb-json-wrap .bb-json-copy.is-copied {
  background: color-mix(in oklch, var(--color-success) 18%, transparent);
  color: var(--color-success);
  border-color: color-mix(in oklch, var(--color-success) 30%, transparent);
}
.bb-json {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.75rem;
  line-height: 1.55;
  padding: 0.625rem 0.75rem;
  border-radius: 0.5rem;
  background: color-mix(in oklch, var(--color-base-200) 60%, transparent);
  border: 1px solid color-mix(in oklch, var(--color-base-300) 50%, transparent);
  overflow-x: auto;
  color: var(--color-base-content);
}
.bb-json--raw { white-space: pre-wrap; }
.bb-json-block { display: block; }
.bb-json-block > .bb-json-summary {
  list-style: none;
  cursor: pointer;
  display: inline;
}
.bb-json-block > .bb-json-summary::-webkit-details-marker { display: none; }
.bb-json-block > .bb-json-summary::marker { content: none; }
.bb-json-block[open] > .bb-json-summary .bb-json-collapsed,
.bb-json-block[open] > .bb-json-summary .bb-json-trailing-punct { display: none; }
.bb-json-block:not([open]) > .bb-json-children,
.bb-json-block:not([open]) > .bb-json-close { display: none; }
.bb-json-block > .bb-json-summary::before {
  content: "▾";
  display: inline-block;
  width: 0.875rem;
  color: color-mix(in oklch, var(--color-base-content) 40%, transparent);
  transition: transform 0.12s ease;
}
.bb-json-block:not([open]) > .bb-json-summary::before { content: "▸"; }
.bb-json-collapsed {
  color: color-mix(in oklch, var(--color-base-content) 45%, transparent);
  font-style: italic;
  margin-inline: 0.25rem;
}
.bb-json-children {
  padding-inline-start: 1.125rem;
}
.bb-json-row { display: block; }
.bb-json-close {
  padding-inline-start: 0;
}
.bb-json-key {
  color: var(--color-primary);
  font-weight: 500;
}
.bb-json-string {
  color: color-mix(in oklch, var(--color-success) 90%, var(--color-base-content));
}
.bb-json-num {
  color: color-mix(in oklch, var(--color-accent) 80%, var(--color-base-content));
}
.bb-json-bool {
  color: color-mix(in oklch, var(--color-warning) 80%, var(--color-base-content));
  font-weight: 500;
}
.bb-json-null {
  color: color-mix(in oklch, var(--color-base-content) 45%, transparent);
  font-style: italic;
}
.bb-json-punct {
  color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
}

/* ── Agent run detail (chat-thread redesign) ─────────────────────────
 * Sticky header + chat-thread transcript + tool-call rows. Pairs with
 * pages.AgentRunDetail + components.Markdown + components.JSONViewer.
 *
 * The chat-thread builds on daisy's `chat` primitive (start/end + image
 * + header + bubble) but tightens the bubble paddings and re-tones the
 * background so a long run feels like a focused transcript rather than
 * a chat app. Tool-call + tool-result rows sit *outside* the daisy
 * chat shapes so they don't get the speech-bubble tail / 70% width cap. */
.bb-run-header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0;
  margin-bottom: 1rem;
  border-bottom: 1px solid color-mix(in oklch, var(--color-base-300) 60%, transparent);
  flex-wrap: wrap;
}
.bb-run-header__lead {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  min-width: 0;
  flex: 1 1 auto;
}
.bb-run-header__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: 9999px;
  background: color-mix(in oklch, var(--color-primary) 15%, transparent);
  color: var(--color-primary);
  flex-shrink: 0;
}
.bb-run-header__id {
  display: inline-flex;
  align-items: center;
  gap: 0.3125rem;
  padding: 0.125rem 0.4375rem;
  border-radius: 0.375rem;
  background: color-mix(in oklch, var(--color-base-200) 80%, transparent);
  color: color-mix(in oklch, var(--color-base-content) 75%, transparent);
  border: 1px solid color-mix(in oklch, var(--color-base-300) 50%, transparent);
  transition: background 0.15s ease, color 0.15s ease;
  cursor: pointer;
}
.bb-run-header__id:hover {
  background: var(--color-base-200);
  color: var(--color-base-content);
}
.bb-run-header__id.is-copied {
  background: color-mix(in oklch, var(--color-success) 18%, transparent);
  color: var(--color-success);
  border-color: color-mix(in oklch, var(--color-success) 30%, transparent);
}
.bb-run-header__actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
}

/* Chat thread layout. The daisy `chat` primitive draws a tail and caps
 * the bubble at 70% width by default; we tighten the spacing rhythm and
 * keep the assistant bubble at full width on small screens so long
 * markdown bodies don't get squeezed. */
.bb-chat-thread {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
/* Let the markdown body inside a daisy chat-bubble flow to a readable
 * width on wide screens — daisy caps chat-bubble at ~70% but the
 * agent's responses are often long-form, so we widen to a max readable
 * column. The padding and tail come from daisy itself. */
.chat .chat-bubble {
  max-width: min(48rem, 100%);
}

/* bb-chat-bubble overrides daisy's default chat-bubble background.
 * Daisy 5 uses --color-neutral for chat-bubble bg, but the breadbox
 * shadcn theme maps neutral to near-black (oklch ~20%), so the
 * default bubble reads as a heavy dark block even in light mode.
 * Pin to base-200 explicitly so both bubbles match the page's
 * surface palette and stay quiet. Borders + tail still come from
 * daisy itself. */
.chat .chat-bubble.bb-chat-bubble {
  background-color: var(--color-base-200);
  color: var(--color-base-content);
}
.chat.chat-start .chat-bubble.bb-chat-bubble::before,
.chat.chat-end .chat-bubble.bb-chat-bubble::before {
  /* daisy paints the speech-tail with the same bg-color as the
   * bubble — pin both so the tail doesn't fall back to neutral. */
  background-color: var(--color-base-200);
}

/* Tool call + tool result rows. Compact, click-to-expand. Aligned with
 * the chat thread's gutter so the visual rhythm matches the bubbles
 * above and below them. */
.bb-tool-row {
  margin: 0;
}
.bb-tool {
  display: block;
  border: 1px solid color-mix(in oklch, var(--color-base-300) 60%, transparent);
  background: color-mix(in oklch, var(--color-base-200) 50%, transparent);
  border-radius: 0.625rem;
  overflow: hidden;
}
.bb-tool__summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4375rem 0.625rem;
  cursor: pointer;
  list-style: none;
  font-size: 0.8125rem;
}
.bb-tool__summary::-webkit-details-marker { display: none; }
.bb-tool__summary::marker { content: none; }
.bb-tool__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.375rem;
  height: 1.375rem;
  border-radius: 0.375rem;
  background: color-mix(in oklch, var(--color-info) 18%, transparent);
  color: var(--color-info);
  flex-shrink: 0;
}
.bb-tool__icon--result {
  background: color-mix(in oklch, var(--color-success) 18%, transparent);
  color: var(--color-success);
}
.bb-tool__icon--raw {
  background: color-mix(in oklch, var(--color-base-300) 60%, transparent);
  color: color-mix(in oklch, var(--color-base-content) 70%, transparent);
}
.bb-tool__label {
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
  font-weight: 600;
}
.bb-tool__name {
  font-size: 0.8125rem;
  color: var(--color-base-content);
  word-break: break-all;
  flex: 1 1 auto;
  min-width: 0;
}
.bb-tool__time {
  font-size: 0.65rem;
  color: color-mix(in oklch, var(--color-base-content) 45%, transparent);
  font-variant-numeric: tabular-nums;
}
.bb-tool__chev {
  color: color-mix(in oklch, var(--color-base-content) 45%, transparent);
  transition: transform 0.15s ease;
  flex-shrink: 0;
}
.bb-tool[open] .bb-tool__chev { transform: rotate(90deg); }
.bb-tool__body {
  padding: 0.5rem 0.625rem 0.625rem 0.625rem;
  border-top: 1px solid color-mix(in oklch, var(--color-base-300) 50%, transparent);
  background: var(--color-base-100);
}

/* Final result + empty-state */
.bb-run-summary {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.75rem 0.875rem;
  border-radius: 0.75rem;
  background: color-mix(in oklch, var(--color-base-200) 60%, transparent);
  border: 1px solid color-mix(in oklch, var(--color-base-300) 50%, transparent);
}
.bb-run-summary__head {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  color: color-mix(in oklch, var(--color-base-content) 60%, transparent);
}
.bb-run-summary__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.625rem 1rem;
}
@media (min-width: 640px) {
  .bb-run-summary__grid {
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
}
.bb-run-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 2rem 1rem;
  border-radius: 0.75rem;
  background: color-mix(in oklch, var(--color-base-200) 40%, transparent);
  border: 1px dashed color-mix(in oklch, var(--color-base-300) 70%, transparent);
}

/* Side-panel prompt collapses */
.bb-run-collapse {
  display: block;
  border-radius: 0.5rem;
  border: 1px solid color-mix(in oklch, var(--color-base-300) 50%, transparent);
  background: color-mix(in oklch, var(--color-base-200) 50%, transparent);
  overflow: hidden;
}
.bb-run-collapse > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.375rem 0.625rem;
  cursor: pointer;
  list-style: none;
}
.bb-run-collapse > summary::-webkit-details-marker { display: none; }
.bb-run-collapse > summary::marker { content: none; }
.bb-run-collapse__chev { transition: transform 0.15s ease; }
.bb-run-collapse[open] .bb-run-collapse__chev { transform: rotate(90deg); }
.bb-run-collapse__pre {
  font-size: 0.7rem;
  line-height: 1.5;
  padding: 0.5rem 0.625rem 0.625rem 0.625rem;
  margin: 0;
  white-space: pre-wrap;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  background: var(--color-base-100);
  border-top: 1px solid color-mix(in oklch, var(--color-base-300) 50%, transparent);
  max-height: 18rem;
  overflow-y: auto;
}

/* Thread within the run page surface: a single shared list rhythm for
 * the inner <ul class="bb-chat-thread">. */
.bb-run-thread { display: block; }

/* Run-detail "system" event row — subtle informational tier that
 * sits below the chat thread visually. Used for SDK init messages,
 * cost_cap_hit notifications, and anything else the operator wants
 * to know happened but didn't author themselves. Pairs with the
 * pages.agentRunTranscriptEvent "system" case. */
.bb-system-row {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.375rem 0.5rem;
  border-radius: 0.5rem;
  background: color-mix(in oklch, var(--color-base-200) 35%, transparent);
  border: 1px dashed color-mix(in oklch, var(--color-base-300) 60%, transparent);
}

/* "Jump to result" target flash — a brief primary-tinted halo when the
 * sticky-header button scrolls the run summary into view, so the eye
 * catches where the page jumped to. */
.bb-run-summary--flash {
  animation: bb-run-summary-flash 1.2s ease-out;
}
@keyframes bb-run-summary-flash {
  0%   { box-shadow: 0 0 0 0 color-mix(in oklch, var(--color-primary) 60%, transparent); }
  20%  { box-shadow: 0 0 0 6px color-mix(in oklch, var(--color-primary) 30%, transparent); }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* Agent run-detail sidebar: stay visible while the operator scrolls a
 * long transcript. We pin to the top, leaving room for the breadcrumb
 * row + page padding above. The aside scrolls internally if it grows
 * taller than the viewport (rare — only happens with a lot of reports
 * + a long tools-used list). */
.bb-run-aside {
  position: sticky;
  top: 1rem;
  align-self: start;
  max-height: calc(100vh - 2rem);
  overflow-y: auto;
  /* Hide the scrollbar gutter to avoid a phantom scroll-track on
   * sidebars that fit in the viewport. */
  scrollbar-gutter: stable;
}

/* Tool call + result combined row. The body now contains two sections
 * (Input / Result) — the section labels carry small directional
 * arrows so the operator can read the call → response flow without
 * reading the JSON. */
.bb-tool__body--paired {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.bb-tool__section {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.bb-tool__section-label {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.65rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
}
.bb-tool__section-label--result {
  color: var(--color-success);
}

/* Prompt intro — system prompt, user prompt, per-run prefix rendered
 * as the first rows of the chat thread instead of a sidebar card.
 * Closer to chat-app conventions: the conversation starts with the
 * setup messages, collapsed so they don't dominate. */
.bb-prompt-intro {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  margin-bottom: 0.75rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px dashed color-mix(in oklch, var(--color-base-300) 70%, transparent);
}
.bb-prompt-collapse {
  border: 1px solid color-mix(in oklch, var(--color-base-300) 60%, transparent);
  border-radius: 0.5rem;
  background: color-mix(in oklch, var(--color-base-200) 35%, transparent);
}
.bb-prompt-collapse > summary {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  padding: 0.4375rem 0.625rem;
  cursor: pointer;
  list-style: none;
  color: color-mix(in oklch, var(--color-base-content) 75%, transparent);
}
.bb-prompt-collapse > summary::-webkit-details-marker { display: none; }
.bb-prompt-collapse > summary::marker { content: none; }
.bb-prompt-collapse__label {
  font-size: 0.75rem;
  font-weight: 500;
}
.bb-prompt-collapse__chev {
  margin-left: auto;
  transition: transform 0.15s ease;
  color: color-mix(in oklch, var(--color-base-content) 45%, transparent);
}
.bb-prompt-collapse[open] .bb-prompt-collapse__chev { transform: rotate(90deg); }
.bb-prompt-collapse__body {
  padding: 0.5rem 0.75rem 0.75rem 0.75rem;
  border-top: 1px solid color-mix(in oklch, var(--color-base-300) 50%, transparent);
  background: var(--color-base-100);
}


/* ── .bb-prose-lg: more generous spacing/size for full-page report bodies ── */
.bb-prose-lg { font-size: 0.95rem; line-height: 1.65; }
.bb-prose-lg h1 { font-size: 1.35rem; margin: 1.8rem 0 0.65rem; }
.bb-prose-lg h2 { font-size: 1.15rem; margin: 1.6rem 0 0.6rem; }
.bb-prose-lg h3 { font-size: 1.02rem; margin: 1.4rem 0 0.5rem; }
.bb-prose-lg p,
.bb-prose-lg ul,
.bb-prose-lg ol,
.bb-prose-lg blockquote,
.bb-prose-lg table,
.bb-prose-lg .bb-code,
.bb-prose-lg .bb-callout { margin-bottom: 1.15rem; }

/* ── chroma syntax highlighting (class-based; generated via internal/markdown
 * ChromaCSS — github + github-dark). Backgrounds are neutralized so the
 * themed .bb-prose pre surface shows through; chroma supplies token colors
 * (and the dark-mode text color). Regenerate by running ChromaCSS and
 * re-applying the .bb-prose .chroma scoping + transparent backgrounds. */
/* PreWrapper */ .bb-prose .chroma { -webkit-text-size-adjust: none; }
/* Error */ .bb-prose .chroma .err { color: #f6f8fa; background-color: #82071e }
/* LineLink */ .bb-prose .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
/* LineTableTD */ .bb-prose .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
/* LineTable */ .bb-prose .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
/* LineHighlight */ .bb-prose .chroma .hl { background-color: #dedede }
/* LineNumbersTable */ .bb-prose .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
/* LineNumbers */ .bb-prose .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
/* Line */ .bb-prose .chroma .line { display: flex; }
/* Keyword */ .bb-prose .chroma .k { color: #cf222e }
/* KeywordConstant */ .bb-prose .chroma .kc { color: #cf222e }
/* KeywordDeclaration */ .bb-prose .chroma .kd { color: #cf222e }
/* KeywordNamespace */ .bb-prose .chroma .kn { color: #cf222e }
/* KeywordPseudo */ .bb-prose .chroma .kp { color: #cf222e }
/* KeywordReserved */ .bb-prose .chroma .kr { color: #cf222e }
/* KeywordType */ .bb-prose .chroma .kt { color: #cf222e }
/* NameAttribute */ .bb-prose .chroma .na { color: #1f2328 }
/* NameClass */ .bb-prose .chroma .nc { color: #1f2328 }
/* NameConstant */ .bb-prose .chroma .no { color: #0550ae }
/* NameDecorator */ .bb-prose .chroma .nd { color: #0550ae }
/* NameEntity */ .bb-prose .chroma .ni { color: #6639ba }
/* NameLabel */ .bb-prose .chroma .nl { color: #990000; font-weight: bold }
/* NameNamespace */ .bb-prose .chroma .nn { color: #24292e }
/* NameOther */ .bb-prose .chroma .nx { color: #1f2328 }
/* NameTag */ .bb-prose .chroma .nt { color: #0550ae }
/* NameBuiltin */ .bb-prose .chroma .nb { color: #6639ba }
/* NameBuiltinPseudo */ .bb-prose .chroma .bp { color: #6a737d }
/* NameVariable */ .bb-prose .chroma .nv { color: #953800 }
/* NameVariableClass */ .bb-prose .chroma .vc { color: #953800 }
/* NameVariableGlobal */ .bb-prose .chroma .vg { color: #953800 }
/* NameVariableInstance */ .bb-prose .chroma .vi { color: #953800 }
/* NameVariableMagic */ .bb-prose .chroma .vm { color: #953800 }
/* NameFunction */ .bb-prose .chroma .nf { color: #6639ba }
/* NameFunctionMagic */ .bb-prose .chroma .fm { color: #6639ba }
/* LiteralString */ .bb-prose .chroma .s { color: #0a3069 }
/* LiteralStringAffix */ .bb-prose .chroma .sa { color: #0a3069 }
/* LiteralStringBacktick */ .bb-prose .chroma .sb { color: #0a3069 }
/* LiteralStringChar */ .bb-prose .chroma .sc { color: #0a3069 }
/* LiteralStringDelimiter */ .bb-prose .chroma .dl { color: #0a3069 }
/* LiteralStringDoc */ .bb-prose .chroma .sd { color: #0a3069 }
/* LiteralStringDouble */ .bb-prose .chroma .s2 { color: #0a3069 }
/* LiteralStringEscape */ .bb-prose .chroma .se { color: #0a3069 }
/* LiteralStringHeredoc */ .bb-prose .chroma .sh { color: #0a3069 }
/* LiteralStringInterpol */ .bb-prose .chroma .si { color: #0a3069 }
/* LiteralStringOther */ .bb-prose .chroma .sx { color: #0a3069 }
/* LiteralStringRegex */ .bb-prose .chroma .sr { color: #0a3069 }
/* LiteralStringSingle */ .bb-prose .chroma .s1 { color: #0a3069 }
/* LiteralStringSymbol */ .bb-prose .chroma .ss { color: #032f62 }
/* LiteralNumber */ .bb-prose .chroma .m { color: #0550ae }
/* LiteralNumberBin */ .bb-prose .chroma .mb { color: #0550ae }
/* LiteralNumberFloat */ .bb-prose .chroma .mf { color: #0550ae }
/* LiteralNumberHex */ .bb-prose .chroma .mh { color: #0550ae }
/* LiteralNumberInteger */ .bb-prose .chroma .mi { color: #0550ae }
/* LiteralNumberIntegerLong */ .bb-prose .chroma .il { color: #0550ae }
/* LiteralNumberOct */ .bb-prose .chroma .mo { color: #0550ae }
/* Operator */ .bb-prose .chroma .o { color: #0550ae }
/* OperatorWord */ .bb-prose .chroma .ow { color: #0550ae }
/* OperatorReserved */ .bb-prose .chroma .or { color: #0550ae }
/* Punctuation */ .bb-prose .chroma .p { color: #1f2328 }
/* Comment */ .bb-prose .chroma .c { color: #57606a }
/* CommentHashbang */ .bb-prose .chroma .ch { color: #57606a }
/* CommentMultiline */ .bb-prose .chroma .cm { color: #57606a }
/* CommentSingle */ .bb-prose .chroma .c1 { color: #57606a }
/* CommentSpecial */ .bb-prose .chroma .cs { color: #57606a }
/* CommentPreproc */ .bb-prose .chroma .cp { color: #57606a }
/* CommentPreprocFile */ .bb-prose .chroma .cpf { color: #57606a }
/* GenericDeleted */ .bb-prose .chroma .gd { color: #82071e; background-color: #ffebe9 }
/* GenericEmph */ .bb-prose .chroma .ge { color: #1f2328 }
/* GenericInserted */ .bb-prose .chroma .gi { color: #116329; background-color: #dafbe1 }
/* GenericOutput */ .bb-prose .chroma .go { color: #1f2328 }
/* GenericUnderline */ .bb-prose .chroma .gl { text-decoration: underline }
/* TextWhitespace */ .bb-prose .chroma .w { color: #ffffff }

@media (prefers-color-scheme: dark) {
  /* PreWrapper */ :root:not([data-theme="light"]) .bb-prose .chroma { color: #e6edf3; -webkit-text-size-adjust: none; }
  /* Error */ :root:not([data-theme="light"]) .bb-prose .chroma .err { color: #f85149 }
  /* LineLink */ :root:not([data-theme="light"]) .bb-prose .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
  /* LineTableTD */ :root:not([data-theme="light"]) .bb-prose .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
  /* LineTable */ :root:not([data-theme="light"]) .bb-prose .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
  /* LineHighlight */ :root:not([data-theme="light"]) .bb-prose .chroma .hl { background-color: #6e7681 }
  /* LineNumbersTable */ :root:not([data-theme="light"]) .bb-prose .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #737679 }
  /* LineNumbers */ :root:not([data-theme="light"]) .bb-prose .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #6e7681 }
  /* Line */ :root:not([data-theme="light"]) .bb-prose .chroma .line { display: flex; }
  /* Keyword */ :root:not([data-theme="light"]) .bb-prose .chroma .k { color: #ff7b72 }
  /* KeywordConstant */ :root:not([data-theme="light"]) .bb-prose .chroma .kc { color: #79c0ff }
  /* KeywordDeclaration */ :root:not([data-theme="light"]) .bb-prose .chroma .kd { color: #ff7b72 }
  /* KeywordNamespace */ :root:not([data-theme="light"]) .bb-prose .chroma .kn { color: #ff7b72 }
  /* KeywordPseudo */ :root:not([data-theme="light"]) .bb-prose .chroma .kp { color: #79c0ff }
  /* KeywordReserved */ :root:not([data-theme="light"]) .bb-prose .chroma .kr { color: #ff7b72 }
  /* KeywordType */ :root:not([data-theme="light"]) .bb-prose .chroma .kt { color: #ff7b72 }
  /* NameClass */ :root:not([data-theme="light"]) .bb-prose .chroma .nc { color: #f0883e; font-weight: bold }
  /* NameConstant */ :root:not([data-theme="light"]) .bb-prose .chroma .no { color: #79c0ff; font-weight: bold }
  /* NameDecorator */ :root:not([data-theme="light"]) .bb-prose .chroma .nd { color: #d2a8ff; font-weight: bold }
  /* NameEntity */ :root:not([data-theme="light"]) .bb-prose .chroma .ni { color: #ffa657 }
  /* NameException */ :root:not([data-theme="light"]) .bb-prose .chroma .ne { color: #f0883e; font-weight: bold }
  /* NameLabel */ :root:not([data-theme="light"]) .bb-prose .chroma .nl { color: #79c0ff; font-weight: bold }
  /* NameNamespace */ :root:not([data-theme="light"]) .bb-prose .chroma .nn { color: #ff7b72 }
  /* NameProperty */ :root:not([data-theme="light"]) .bb-prose .chroma .py { color: #79c0ff }
  /* NameTag */ :root:not([data-theme="light"]) .bb-prose .chroma .nt { color: #7ee787 }
  /* NameVariable */ :root:not([data-theme="light"]) .bb-prose .chroma .nv { color: #79c0ff }
  /* NameVariableClass */ :root:not([data-theme="light"]) .bb-prose .chroma .vc { color: #79c0ff }
  /* NameVariableGlobal */ :root:not([data-theme="light"]) .bb-prose .chroma .vg { color: #79c0ff }
  /* NameVariableInstance */ :root:not([data-theme="light"]) .bb-prose .chroma .vi { color: #79c0ff }
  /* NameVariableMagic */ :root:not([data-theme="light"]) .bb-prose .chroma .vm { color: #79c0ff }
  /* NameFunction */ :root:not([data-theme="light"]) .bb-prose .chroma .nf { color: #d2a8ff; font-weight: bold }
  /* NameFunctionMagic */ :root:not([data-theme="light"]) .bb-prose .chroma .fm { color: #d2a8ff; font-weight: bold }
  /* Literal */ :root:not([data-theme="light"]) .bb-prose .chroma .l { color: #a5d6ff }
  /* LiteralDate */ :root:not([data-theme="light"]) .bb-prose .chroma .ld { color: #79c0ff }
  /* LiteralString */ :root:not([data-theme="light"]) .bb-prose .chroma .s { color: #a5d6ff }
  /* LiteralStringAffix */ :root:not([data-theme="light"]) .bb-prose .chroma .sa { color: #79c0ff }
  /* LiteralStringBacktick */ :root:not([data-theme="light"]) .bb-prose .chroma .sb { color: #a5d6ff }
  /* LiteralStringChar */ :root:not([data-theme="light"]) .bb-prose .chroma .sc { color: #a5d6ff }
  /* LiteralStringDelimiter */ :root:not([data-theme="light"]) .bb-prose .chroma .dl { color: #79c0ff }
  /* LiteralStringDoc */ :root:not([data-theme="light"]) .bb-prose .chroma .sd { color: #a5d6ff }
  /* LiteralStringDouble */ :root:not([data-theme="light"]) .bb-prose .chroma .s2 { color: #a5d6ff }
  /* LiteralStringEscape */ :root:not([data-theme="light"]) .bb-prose .chroma .se { color: #79c0ff }
  /* LiteralStringHeredoc */ :root:not([data-theme="light"]) .bb-prose .chroma .sh { color: #79c0ff }
  /* LiteralStringInterpol */ :root:not([data-theme="light"]) .bb-prose .chroma .si { color: #a5d6ff }
  /* LiteralStringOther */ :root:not([data-theme="light"]) .bb-prose .chroma .sx { color: #a5d6ff }
  /* LiteralStringRegex */ :root:not([data-theme="light"]) .bb-prose .chroma .sr { color: #79c0ff }
  /* LiteralStringSingle */ :root:not([data-theme="light"]) .bb-prose .chroma .s1 { color: #a5d6ff }
  /* LiteralStringSymbol */ :root:not([data-theme="light"]) .bb-prose .chroma .ss { color: #a5d6ff }
  /* LiteralNumber */ :root:not([data-theme="light"]) .bb-prose .chroma .m { color: #a5d6ff }
  /* LiteralNumberBin */ :root:not([data-theme="light"]) .bb-prose .chroma .mb { color: #a5d6ff }
  /* LiteralNumberFloat */ :root:not([data-theme="light"]) .bb-prose .chroma .mf { color: #a5d6ff }
  /* LiteralNumberHex */ :root:not([data-theme="light"]) .bb-prose .chroma .mh { color: #a5d6ff }
  /* LiteralNumberInteger */ :root:not([data-theme="light"]) .bb-prose .chroma .mi { color: #a5d6ff }
  /* LiteralNumberIntegerLong */ :root:not([data-theme="light"]) .bb-prose .chroma .il { color: #a5d6ff }
  /* LiteralNumberOct */ :root:not([data-theme="light"]) .bb-prose .chroma .mo { color: #a5d6ff }
  /* Operator */ :root:not([data-theme="light"]) .bb-prose .chroma .o { color: #ff7b72; font-weight: bold }
  /* OperatorWord */ :root:not([data-theme="light"]) .bb-prose .chroma .ow { color: #ff7b72; font-weight: bold }
  /* OperatorReserved */ :root:not([data-theme="light"]) .bb-prose .chroma .or { color: #ff7b72; font-weight: bold }
  /* Comment */ :root:not([data-theme="light"]) .bb-prose .chroma .c { color: #8b949e; font-style: italic }
  /* CommentHashbang */ :root:not([data-theme="light"]) .bb-prose .chroma .ch { color: #8b949e; font-style: italic }
  /* CommentMultiline */ :root:not([data-theme="light"]) .bb-prose .chroma .cm { color: #8b949e; font-style: italic }
  /* CommentSingle */ :root:not([data-theme="light"]) .bb-prose .chroma .c1 { color: #8b949e; font-style: italic }
  /* CommentSpecial */ :root:not([data-theme="light"]) .bb-prose .chroma .cs { color: #8b949e; font-weight: bold; font-style: italic }
  /* CommentPreproc */ :root:not([data-theme="light"]) .bb-prose .chroma .cp { color: #8b949e; font-weight: bold; font-style: italic }
  /* CommentPreprocFile */ :root:not([data-theme="light"]) .bb-prose .chroma .cpf { color: #8b949e; font-weight: bold; font-style: italic }
  /* GenericDeleted */ :root:not([data-theme="light"]) .bb-prose .chroma .gd { color: #ffa198; background-color: #490202 }
  /* GenericEmph */ :root:not([data-theme="light"]) .bb-prose .chroma .ge { font-style: italic }
  /* GenericError */ :root:not([data-theme="light"]) .bb-prose .chroma .gr { color: #ffa198 }
  /* GenericHeading */ :root:not([data-theme="light"]) .bb-prose .chroma .gh { color: #79c0ff; font-weight: bold }
  /* GenericInserted */ :root:not([data-theme="light"]) .bb-prose .chroma .gi { color: #56d364; background-color: #0f5323 }
  /* GenericOutput */ :root:not([data-theme="light"]) .bb-prose .chroma .go { color: #8b949e }
  /* GenericPrompt */ :root:not([data-theme="light"]) .bb-prose .chroma .gp { color: #8b949e }
  /* GenericStrong */ :root:not([data-theme="light"]) .bb-prose .chroma .gs { font-weight: bold }
  /* GenericSubheading */ :root:not([data-theme="light"]) .bb-prose .chroma .gu { color: #79c0ff }
  /* GenericTraceback */ :root:not([data-theme="light"]) .bb-prose .chroma .gt { color: #ff7b72 }
  /* GenericUnderline */ :root:not([data-theme="light"]) .bb-prose .chroma .gl { text-decoration: underline }
  /* TextWhitespace */ :root:not([data-theme="light"]) .bb-prose .chroma .w { color: #6e7681 }
}

/* Explicit dark-theme toggle (data-theme="dark"), independent of the OS
 * preference — mirrors the dual-guard used across this stylesheet so an
 * in-app theme that disagrees with the OS still colors tokens correctly. */
/* PreWrapper */ [data-theme="dark"] .bb-prose .chroma { color: #e6edf3; -webkit-text-size-adjust: none; }
/* Error */ [data-theme="dark"] .bb-prose .chroma .err { color: #f85149 }
/* LineLink */ [data-theme="dark"] .bb-prose .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
/* LineTableTD */ [data-theme="dark"] .bb-prose .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
/* LineTable */ [data-theme="dark"] .bb-prose .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
/* LineHighlight */ [data-theme="dark"] .bb-prose .chroma .hl { background-color: #6e7681 }
/* LineNumbersTable */ [data-theme="dark"] .bb-prose .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #737679 }
/* LineNumbers */ [data-theme="dark"] .bb-prose .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #6e7681 }
/* Line */ [data-theme="dark"] .bb-prose .chroma .line { display: flex; }
/* Keyword */ [data-theme="dark"] .bb-prose .chroma .k { color: #ff7b72 }
/* KeywordConstant */ [data-theme="dark"] .bb-prose .chroma .kc { color: #79c0ff }
/* KeywordDeclaration */ [data-theme="dark"] .bb-prose .chroma .kd { color: #ff7b72 }
/* KeywordNamespace */ [data-theme="dark"] .bb-prose .chroma .kn { color: #ff7b72 }
/* KeywordPseudo */ [data-theme="dark"] .bb-prose .chroma .kp { color: #79c0ff }
/* KeywordReserved */ [data-theme="dark"] .bb-prose .chroma .kr { color: #ff7b72 }
/* KeywordType */ [data-theme="dark"] .bb-prose .chroma .kt { color: #ff7b72 }
/* NameClass */ [data-theme="dark"] .bb-prose .chroma .nc { color: #f0883e; font-weight: bold }
/* NameConstant */ [data-theme="dark"] .bb-prose .chroma .no { color: #79c0ff; font-weight: bold }
/* NameDecorator */ [data-theme="dark"] .bb-prose .chroma .nd { color: #d2a8ff; font-weight: bold }
/* NameEntity */ [data-theme="dark"] .bb-prose .chroma .ni { color: #ffa657 }
/* NameException */ [data-theme="dark"] .bb-prose .chroma .ne { color: #f0883e; font-weight: bold }
/* NameLabel */ [data-theme="dark"] .bb-prose .chroma .nl { color: #79c0ff; font-weight: bold }
/* NameNamespace */ [data-theme="dark"] .bb-prose .chroma .nn { color: #ff7b72 }
/* NameProperty */ [data-theme="dark"] .bb-prose .chroma .py { color: #79c0ff }
/* NameTag */ [data-theme="dark"] .bb-prose .chroma .nt { color: #7ee787 }
/* NameVariable */ [data-theme="dark"] .bb-prose .chroma .nv { color: #79c0ff }
/* NameVariableClass */ [data-theme="dark"] .bb-prose .chroma .vc { color: #79c0ff }
/* NameVariableGlobal */ [data-theme="dark"] .bb-prose .chroma .vg { color: #79c0ff }
/* NameVariableInstance */ [data-theme="dark"] .bb-prose .chroma .vi { color: #79c0ff }
/* NameVariableMagic */ [data-theme="dark"] .bb-prose .chroma .vm { color: #79c0ff }
/* NameFunction */ [data-theme="dark"] .bb-prose .chroma .nf { color: #d2a8ff; font-weight: bold }
/* NameFunctionMagic */ [data-theme="dark"] .bb-prose .chroma .fm { color: #d2a8ff; font-weight: bold }
/* Literal */ [data-theme="dark"] .bb-prose .chroma .l { color: #a5d6ff }
/* LiteralDate */ [data-theme="dark"] .bb-prose .chroma .ld { color: #79c0ff }
/* LiteralString */ [data-theme="dark"] .bb-prose .chroma .s { color: #a5d6ff }
/* LiteralStringAffix */ [data-theme="dark"] .bb-prose .chroma .sa { color: #79c0ff }
/* LiteralStringBacktick */ [data-theme="dark"] .bb-prose .chroma .sb { color: #a5d6ff }
/* LiteralStringChar */ [data-theme="dark"] .bb-prose .chroma .sc { color: #a5d6ff }
/* LiteralStringDelimiter */ [data-theme="dark"] .bb-prose .chroma .dl { color: #79c0ff }
/* LiteralStringDoc */ [data-theme="dark"] .bb-prose .chroma .sd { color: #a5d6ff }
/* LiteralStringDouble */ [data-theme="dark"] .bb-prose .chroma .s2 { color: #a5d6ff }
/* LiteralStringEscape */ [data-theme="dark"] .bb-prose .chroma .se { color: #79c0ff }
/* LiteralStringHeredoc */ [data-theme="dark"] .bb-prose .chroma .sh { color: #79c0ff }
/* LiteralStringInterpol */ [data-theme="dark"] .bb-prose .chroma .si { color: #a5d6ff }
/* LiteralStringOther */ [data-theme="dark"] .bb-prose .chroma .sx { color: #a5d6ff }
/* LiteralStringRegex */ [data-theme="dark"] .bb-prose .chroma .sr { color: #79c0ff }
/* LiteralStringSingle */ [data-theme="dark"] .bb-prose .chroma .s1 { color: #a5d6ff }
/* LiteralStringSymbol */ [data-theme="dark"] .bb-prose .chroma .ss { color: #a5d6ff }
/* LiteralNumber */ [data-theme="dark"] .bb-prose .chroma .m { color: #a5d6ff }
/* LiteralNumberBin */ [data-theme="dark"] .bb-prose .chroma .mb { color: #a5d6ff }
/* LiteralNumberFloat */ [data-theme="dark"] .bb-prose .chroma .mf { color: #a5d6ff }
/* LiteralNumberHex */ [data-theme="dark"] .bb-prose .chroma .mh { color: #a5d6ff }
/* LiteralNumberInteger */ [data-theme="dark"] .bb-prose .chroma .mi { color: #a5d6ff }
/* LiteralNumberIntegerLong */ [data-theme="dark"] .bb-prose .chroma .il { color: #a5d6ff }
/* LiteralNumberOct */ [data-theme="dark"] .bb-prose .chroma .mo { color: #a5d6ff }
/* Operator */ [data-theme="dark"] .bb-prose .chroma .o { color: #ff7b72; font-weight: bold }
/* OperatorWord */ [data-theme="dark"] .bb-prose .chroma .ow { color: #ff7b72; font-weight: bold }
/* OperatorReserved */ [data-theme="dark"] .bb-prose .chroma .or { color: #ff7b72; font-weight: bold }
/* Comment */ [data-theme="dark"] .bb-prose .chroma .c { color: #8b949e; font-style: italic }
/* CommentHashbang */ [data-theme="dark"] .bb-prose .chroma .ch { color: #8b949e; font-style: italic }
/* CommentMultiline */ [data-theme="dark"] .bb-prose .chroma .cm { color: #8b949e; font-style: italic }
/* CommentSingle */ [data-theme="dark"] .bb-prose .chroma .c1 { color: #8b949e; font-style: italic }
/* CommentSpecial */ [data-theme="dark"] .bb-prose .chroma .cs { color: #8b949e; font-weight: bold; font-style: italic }
/* CommentPreproc */ [data-theme="dark"] .bb-prose .chroma .cp { color: #8b949e; font-weight: bold; font-style: italic }
/* CommentPreprocFile */ [data-theme="dark"] .bb-prose .chroma .cpf { color: #8b949e; font-weight: bold; font-style: italic }
/* GenericDeleted */ [data-theme="dark"] .bb-prose .chroma .gd { color: #ffa198; background-color: #490202 }
/* GenericEmph */ [data-theme="dark"] .bb-prose .chroma .ge { font-style: italic }
/* GenericError */ [data-theme="dark"] .bb-prose .chroma .gr { color: #ffa198 }
/* GenericHeading */ [data-theme="dark"] .bb-prose .chroma .gh { color: #79c0ff; font-weight: bold }
/* GenericInserted */ [data-theme="dark"] .bb-prose .chroma .gi { color: #56d364; background-color: #0f5323 }
/* GenericOutput */ [data-theme="dark"] .bb-prose .chroma .go { color: #8b949e }
/* GenericPrompt */ [data-theme="dark"] .bb-prose .chroma .gp { color: #8b949e }
/* GenericStrong */ [data-theme="dark"] .bb-prose .chroma .gs { font-weight: bold }
/* GenericSubheading */ [data-theme="dark"] .bb-prose .chroma .gu { color: #79c0ff }
/* GenericTraceback */ [data-theme="dark"] .bb-prose .chroma .gt { color: #ff7b72 }
/* GenericUnderline */ [data-theme="dark"] .bb-prose .chroma .gl { text-decoration: underline }
/* TextWhitespace */ [data-theme="dark"] .bb-prose .chroma .w { color: #6e7681 }

/* ── Markdown "fancy" chrome: callouts, code-block bar, heading anchors ── */

/* GitHub-style callouts (> [\!NOTE] etc.) — rendered by internal/markdown.
 * --cl is the accent color per type; box uses soft tints of it. */
.bb-prose .bb-callout {
  --cl: var(--color-info);
  margin: 0 0 0.875rem;
  border: 1px solid color-mix(in oklch, var(--cl) 35%, transparent);
  background: color-mix(in oklch, var(--cl) 8%, transparent);
  border-radius: 0.5rem;
  padding: 0.6rem 0.85rem;
}
.bb-prose .bb-callout-note { --cl: var(--color-info); }
.bb-prose .bb-callout-tip { --cl: var(--color-success); }
.bb-prose .bb-callout-important { --cl: oklch(0.62 0.19 295); }
.bb-prose .bb-callout-warning { --cl: var(--color-warning); }
.bb-prose .bb-callout-caution { --cl: var(--color-error); }
.bb-prose .bb-callout-title {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-weight: 600;
  font-size: 0.82rem;
  color: var(--cl);
  margin-bottom: 0.3rem;
}
.bb-prose .bb-callout-icon { width: 1rem; height: 1rem; }
.bb-prose .bb-callout-body > :first-child { margin-top: 0; }
.bb-prose .bb-callout-body > :last-child { margin-bottom: 0; }

/* Code block chrome — a header bar (language pill + copy) over the <pre>.
 * .bb-code owns the surface + border; the inner <pre> goes flush. */
.bb-prose .bb-code {
  margin: 0 0 1rem;
  border: 1px solid color-mix(in oklch, var(--color-base-300) 50%, transparent);
  border-radius: 0.5rem;
  overflow: hidden;
  background: color-mix(in oklch, var(--color-base-200) 70%, transparent);
}
.bb-prose .bb-code-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.2rem 0.4rem 0.2rem 0.75rem;
  border-bottom: 1px solid color-mix(in oklch, var(--color-base-300) 40%, transparent);
  background: color-mix(in oklch, var(--color-base-300) 22%, transparent);
}
.bb-prose .bb-code-lang {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
}
.bb-prose .bb-code-copy {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.68rem;
  padding: 0.15rem 0.4rem;
  border: 0;
  border-radius: 0.35rem;
  background: transparent;
  color: color-mix(in oklch, var(--color-base-content) 58%, transparent);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.bb-prose .bb-code-copy:hover {
  background: color-mix(in oklch, var(--color-base-content) 10%, transparent);
  color: var(--color-base-content);
}
.bb-prose .bb-code-copy.is-copied { color: var(--color-success); }
.bb-prose .bb-code-copy-icon { width: 0.85rem; height: 0.85rem; }
/* Inner <pre> goes flush — .bb-code provides surface + border. */
.bb-prose .bb-code pre {
  margin: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
}

/* Heading hover-anchor — a link icon trailing the heading text, revealed on
 * hover. Trailing (not gutter) keeps the heading's left edge aligned with
 * paragraphs and never overflows tight containers. */
.bb-prose .bb-heading-anchor {
  display: inline-flex;
  align-items: center;
  margin-inline-start: 0.35em;
  opacity: 0;
  text-decoration: none;
  color: color-mix(in oklch, var(--color-base-content) 40%, transparent);
  transition: opacity 0.12s ease, color 0.12s ease;
}
.bb-prose .bb-heading-anchor:hover { color: var(--color-primary); }
.bb-prose .bb-heading-anchor-icon { width: 0.8em; height: 0.8em; }
.bb-prose :is(h1, h2, h3, h4, h5, h6):hover .bb-heading-anchor,
.bb-prose .bb-heading-anchor:focus-visible { opacity: 1; }

/* Heading deep-link anchors are meaningless inside the prompt modal — the
   slugs don't address anything navigable there — so hide them. */
#bb-prompt-modal .bb-heading-anchor { display: none; }
