/* StackJack Portal — supplemental styles
 *
 * All Tailwind utilities (including arbitrary values like `h-[calc(100vh-7rem)]`
 * and responsive variants like `md:col-span-4`) are now generated by the
 * Tailwind v4 JIT pipeline — see tailwind.css in the project root and the
 * generated wwwroot/css/tailwind.output.css. This file holds ONLY the styles
 * Tailwind can't produce for us:
 *
 *   1. Slash-opacity modifiers on custom theme colors (`muted`, `border`,
 *      `foreground`, `muted-foreground`). Tailwind's default theme doesn't
 *      include these names — they're Blueprint's theme, injected as CSS
 *      custom properties in App.razor. Without registering them via `@theme`
 *      in tailwind.css, the JIT can't emit slash-opacity variants for them,
 *      so we hand-roll the specific stops the razor files actually use.
 *
 *   2. StackJack-specific helpers (`sj-*`). Named utilities for the Agent
 *      Builder / live-test sidecar / accent palette that encapsulate Blueprint
 *      theme-variable consumption (`--background`, `--muted`, etc.) plus the
 *      StackJack accent gradient. These aren't Tailwind utilities — they're
 *      components expressed as classes.
 *
 *   3. The `--sj-accent-*` custom properties that back the helpers above.
 *
 * When you add a new utility class in a .razor file, re-run `npm run build:css`
 * in src/StackJack.Portal and commit the updated tailwind.output.css. You only
 * need to add to THIS file if the utility references one of Blueprint's custom
 * theme colors with slash-opacity, or if it's a StackJack-branded helper.
 */

/* ── Slash-opacity on custom-theme colors ──────────────────────────────────
 * Tailwind v4's JIT only generates slash-opacity variants for colors it knows
 * about. Blueprint's theme colors (`muted`, `border`, `foreground`,
 * `muted-foreground`) live in App.razor's `<style>` block as plain CSS custom
 * properties — Tailwind can't see them without a `@theme` block, so the JIT
 * silently drops things like `bg-muted/40` and `hover:border-border/60`. The
 * rules below back the specific stops each razor file uses; add a new rule
 * only when a real consumer appears.
 * ──────────────────────────────────────────────────────────────────────── */

/* Model-picker unselected card hover (Step1Basics). */
.hover\:border-border\/60:hover {
  border-color: color-mix(in oklch, var(--border) 60%, transparent);
}

/* Inactive step-row hover fill (BuilderStepRail) + multiple list/card hover
 * surfaces. Also used as a non-hover background on WizardCharCounter and
 * bg-muted/40 callout blocks. */
.bg-muted\/40,
.hover\:bg-muted\/40:hover {
  background-color: color-mix(in oklch, var(--muted) 40%, transparent);
}

/* ModeCard secondary CTA hover border. */
.hover\:border-foreground\/30:hover {
  border-color: color-mix(in oklch, var(--foreground) 30%, transparent);
}

/* SupportDrawer helper copy (softer than plain text-muted-foreground). */
.text-muted-foreground\/60 {
  color: color-mix(in oklch, var(--muted-foreground) 60%, transparent);
}

/* BulkMigrationDialog row divider. */
.border-border\/30 {
  border-color: color-mix(in oklch, var(--border) 30%, transparent);
}

/* Connector tile hover border (ConnectorTile). `primary` is Blueprint's theme
 * color, so the JIT drops this the same way it drops bg-muted/40 — the tile
 * would keep its resting border under the pointer and the only hover signal
 * left would be the shadow.
 *
 * `--primary` is declared by App.razor's <style> block (both the light and the
 * `.dark` stop), same as the four tokens above. If it ever stops being, the
 * color-mix() is invalid at computed-value time and this declaration is
 * dropped in silence — which is the no-op it exists to fix — so the test pin
 * checks the variable is declared, not just that this rule is present. */
.hover\:border-primary\/50:hover {
  border-color: color-mix(in oklch, var(--primary) 50%, transparent);
}

/* The "Not configured" dot in the drawer's per-surface blocks
 * (ConnectorSurfacePresentation.DotClass). `muted-foreground` is Blueprint's
 * theme color, so the JIT drops the slash-opacity variant exactly as it drops
 * bg-muted/40 — and the failure is silent AND total here: `bg-` with no color
 * leaves a fully transparent 6px circle, so the row loses its dot entirely
 * while the Configured and Partial rows keep their green and amber. An
 * unconfigured ScalePad drawer is five such rows.
 *
 * Deliberately NOT swapped for Blueprint's shipped `/30`: that would make the
 * dot's existence depend on which stops a third-party stylesheet happens to
 * carry, with the same invisible no-op waiting on the next upgrade. */
.bg-muted-foreground\/40 {
  background-color: color-mix(in oklch, var(--muted-foreground) 40%, transparent);
}

/* The facet-tile highlight ring on a per-surface block
 * (ConnectorSurfacePresentation.BlockClass). `ring-1` alone resolves
 * `var(--tw-ring-color, currentcolor)`, so without this rule the ring still
 * DRAWS — in the block's text color, a heavy foreground outline where a soft
 * accent was intended. That is worse than the missing dot above: a pin that
 * only checked "something renders" would pass.
 *
 * `primary` rather than the tile's `amber-500/60`: amber already means "needs
 * attention" everywhere else on this page, and this ring means "here is the
 * sub-module you clicked". */
.ring-primary\/60 {
  --tw-ring-color: color-mix(in oklch, var(--primary) 60%, transparent);
}

/* SupportDrawer overlay scrim. Tailwind v4 here imports only
 * `tailwindcss/utilities.css` (see tailwind.css) which does NOT register
 * `--color-black` in the theme, so `bg-black/50` cannot be JIT-generated.
 * Plain rgba — black dim is theme-agnostic by design. */
.bg-black\/50 {
  background-color: rgba(0, 0, 0, 0.5);
}

/* ── StackJack builder shell helpers ───────────────────────────────────────
 * Named utilities for the Agent Builder two-column layout. These wrap
 * Blueprint theme tokens and encapsulate the media-query breakpoints so the
 * razor files stay clean — they're components, not utilities.
 * ──────────────────────────────────────────────────────────────────────── */

/* Fills the inset column vertically so the sticky footer has a long enough
 * flex parent to anchor against when content is short (viewport minus the
 * 64px MainLayout header minus padding ≈ 7rem). */
.sj-builder-page {
  min-height: calc(100vh - 7rem);
}

/* Two-column builder shell: 14rem rail + 1fr content at ≥lg, stacked below. */
.sj-builder-shell {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1.5rem;
  flex: 1 1 0%;
  min-width: 0;
}
@media (min-width: 64rem) {
  .sj-builder-shell {
    grid-template-columns: 14rem minmax(0, 1fr);
  }
}

/* One row in the custom step rail. Fixed 2rem indicator column + 1fr text
 * column so the four rows share a single left edge regardless of title or
 * description length. `min-height` keeps row heights uniform. */
.sj-builder-rail-row {
  display: grid;
  grid-template-columns: 2rem minmax(0, 1fr);
  align-items: flex-start;
  gap: 0.75rem;
  min-height: 3.5rem;
}

/* Scroll-spy active row (P3-3). The `data-active` attribute is toggled by the
 * IntersectionObserver in Builder.razor.js — NOT by Blazor — so a rail re-render
 * cannot wipe it (the row `class` is held constant in scroll-spy mode). Reuses the
 * same accent values as the integer-path active row (sj-accent-bg-soft 10% /
 * sj-accent-border-soft 40%) so both nav modes look identical. */
.sj-builder-rail-row[data-active] {
  background-color: color-mix(in oklch, var(--sj-accent-1) 10%, transparent);
  border-color: color-mix(in oklch, var(--sj-accent-1) 40%, transparent);
}

/* Review summary <dl>s. 160px label column + 1fr value column, with min-width:0
 * on the grid so long tool-id / URL strings in the values cannot push the page
 * wider than its inset (the pre-refactor `grid-cols-[160px_1fr]` let them). */
.sj-summary-dl {
  display: grid;
  grid-template-columns: 160px minmax(0, 1fr);
  column-gap: 1rem;
  row-gap: 0.5rem;
  min-width: 0;
}

/* Definite-height container for the Step 4 live-test card. WizardLiveTestPanel
 * renders a `flex flex-col h-full` root and a BbScrollArea FillContainer for
 * the transcript — both require a definite parent height to produce a real
 * scroll contract. Using `max-h-` here let the card grow with chat content and
 * paint past the sticky footer. The xl+ height tracks the viewport so the
 * sticky sidecar fits between the 64px header and the sticky footer. */
.sj-livetest-card {
  height: 600px;
}
@media (min-width: 80rem) {
  .sj-livetest-card {
    height: calc(100vh - 9rem);
  }
}

/* Step 4 review/deploy live-test sidecar column. `w-full` below xl so the
 * panel stacks full-width under summary cards, switches to a 400px fixed
 * column at xl+ to preserve the two-column layout intent. */
.sj-livetest-sidecar {
  width: 100%;
}
@media (min-width: 80rem) {
  .sj-livetest-sidecar {
    width: 400px;
    flex-shrink: 0;
  }
}

/* NOTE (P3-3): the old `.sj-builder-step` unified step layout (1fr main + fixed
 * 24rem sidecar) was RETIRED when the four gated wizard steps decomposed into the
 * continuous section surface — every Sections/*.razor is sidecar-less main-column
 * content, so the per-step split no longer exists. The dissolved sidecars relocated
 * to the persistent preview rail (.sj-builder-preview) and the live-test sheet. Do
 * not reintroduce a per-section sidecar grid. */

/* ── 3-zone builder shell (P3-3 IA redesign) ───────────────────────────────
 * The continuous-surface builder is laid out as: section-nav rail | capped
 * main reading column | persistent preview rail. `BuilderShell` applies this
 * variant ONLY when a Preview slot is supplied; when it is not, the 2-column
 * `.sj-builder-shell` above stays the default (so the pre-decomposition step
 * surface renders unchanged). Named helper, not `grid-cols-[…]`: the Portal has
 * no Tailwind JIT, so an arbitrary track template would silently drop.
 *   base : single column (rail + main stack; preview opens as a sheet <xl)
 *   ≥lg  : 12rem rail | 1fr main
 *   ≥xl  : 12rem rail | 1fr main | 24rem preview
 * ──────────────────────────────────────────────────────────────────────── */
.sj-builder-shell-3zone {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1.5rem;
  flex: 1 1 0%;
  min-width: 0;
}
@media (min-width: 64rem) {
  .sj-builder-shell-3zone {
    grid-template-columns: 12rem minmax(0, 1fr);
  }
}
@media (min-width: 80rem) {
  .sj-builder-shell-3zone {
    grid-template-columns: 12rem minmax(0, 1fr) 24rem;
  }
}

/* Capped reading column for the continuous section surface. ~48rem keeps line
 * length comfortable instead of stretching the form across the full inset (the
 * pre-redesign builder squeezed to ~30rem at 1440px for the opposite reason).
 * min-width:0 keeps long values from forcing horizontal scroll. */
.sj-builder-main {
  max-width: 48rem;
  min-width: 0;
}

/* Persistent preview rail. A real 24rem grid track at ≥xl (where it sticks within
 * the inset between the header and the action bar). Below xl the grid has no third
 * column, so the preview MUST span the full row — `grid-column: 1 / -1` — otherwise
 * grid auto-placement drops it into the 12rem rail track at lg (a cramped column
 * with dead space beside it). A dedicated collapsible-sheet affordance below xl is a
 * later refinement; today it stacks full-width under the main column. */
.sj-builder-preview {
  min-width: 0;
  grid-column: 1 / -1;   /* full-width row when it is not its own xl column */
}
@media (min-width: 80rem) {
  .sj-builder-preview {
    grid-column: auto;   /* the 24rem third column at xl */
    position: sticky;
    top: 5rem;
    align-self: start;
  }
}

/* Anchored section on the continuous surface. `scroll-margin-top` clears the
 * 64px MainLayout header + gap so a `scrollIntoView` / `#anchor` deep-link lands
 * the section heading below the sticky header instead of behind it. */
.sj-builder-section {
  scroll-margin-top: 5rem;
}

/* ── StackJack accent palette ──────────────────────────────────────────────
 * Shared indigo→violet accent used across the Agent Builder, Wizard AI assist,
 * and live-test surfaces. `--sj-accent-1` matches `--sidebar-primary` in
 * App.razor's dark block — visual parity with the logo/login surfaces. Add a
 * new opacity stop only with a real razor consumer.
 * ──────────────────────────────────────────────────────────────────────── */
:root,
.dark {
  --sj-accent-1: oklch(0.488 0.243 264.376);
  --sj-accent-2: oklch(0.55 0.22 280);
  --sj-accent-1-hover: oklch(0.42 0.22 264.376);
}

.sj-accent-text {
  color: var(--sj-accent-1);
}

/* Pairs with Ghost-variant buttons where hover transitions the text to the
 * secondary accent (WizardAiAssistButton trigger). */
.sj-accent-text-hover {
  color: var(--sj-accent-1);
  transition: color 150ms ease;
}
.sj-accent-text-hover:hover {
  color: var(--sj-accent-2);
}

.sj-accent-bg {
  background-color: var(--sj-accent-1);
}
.sj-accent-bg-soft {
  background-color: color-mix(in oklch, var(--sj-accent-1) 10%, transparent);
}
.sj-accent-bg-softer {
  background-color: color-mix(in oklch, var(--sj-accent-1) 15%, transparent);
}
/* 5% tint used by "Pro Tip" / "How this is injected" informational callouts
 * (Step1Basics, Step2Knowledge). */
.sj-accent-bg-faint {
  background-color: color-mix(in oklch, var(--sj-accent-1) 5%, transparent);
}

.sj-accent-border {
  border-color: var(--sj-accent-1);
}
.sj-accent-border-soft {
  border-color: color-mix(in oklch, var(--sj-accent-1) 40%, transparent);
}
.sj-accent-border-medium {
  border-color: color-mix(in oklch, var(--sj-accent-1) 60%, transparent);
}
/* 20% border tint — pairs with `sj-accent-bg-faint` on informational callouts. */
.sj-accent-border-faint {
  border-color: color-mix(in oklch, var(--sj-accent-1) 20%, transparent);
}

/* Primary-CTA gradient used on all builder/live-test/AI-assist CTAs. */
.sj-accent-gradient {
  background-image: linear-gradient(to right, var(--sj-accent-1), var(--sj-accent-2));
}

/* Solid accent button for automation ModeCard's primary CTA where the design
 * intent is a single accent color (not the indigo→violet gradient). Pairs
 * with `sj-focus-accent` below for the focus-visible outline. */
.sj-accent-solid-hover {
  background-color: var(--sj-accent-1);
  border-color: var(--sj-accent-1);
  color: white;
  transition: background-color 150ms ease, border-color 150ms ease;
}
.sj-accent-solid-hover:hover {
  background-color: var(--sj-accent-1-hover);
  border-color: var(--sj-accent-1-hover);
}

/* Accent focus-visible outline. */
.sj-focus-accent:focus-visible {
  outline: 2px solid var(--sj-accent-1);
  outline-offset: 2px;
}

/* Compact native-tool ROW (FIX 5 — was a square tile). Owner: the square tiles were
 * ~150px of near-empty box for an icon + four words + a toggle. Native tools are now a
 * full-width vertical list of compact rows — icon, name, inline muted billing hint, and a
 * right-aligned toggle — so this helper just floors the row height (~52px) for a consistent,
 * comfortable tap target. Named helper (not a Tailwind min-h-* arbitrary value) because the
 * Portal has no JIT and an arbitrary min-height class would drop from the committed bundle. */
.sj-native-tile {
  min-height: 3.25rem;
}

/* Textarea min-heights used by the Agent Builder. Three tiers cover current
 * uses; add more only with a real consumer. */
.sj-textarea-lg {
  min-height: 420px;   /* Step 2 core knowledge textarea */
}
.sj-textarea-md {
  min-height: 280px;   /* Step 1 system prompt textarea */
}
.sj-textarea-sm {
  min-height: 180px;   /* Step 2 "paste text block" dialog textarea */
}

/* ── Wizard console two-zone layout (FIX 2) ────────────────────────────────
 * The guided wizard (/automations/wizard) is a conversation console (left,
 * flex-1) beside a sticky requirements rail (right, 24rem). It MUST be a
 * single column on narrow screens and a two-column row on wide ones.
 *
 * THE CROSS-BUNDLE CASCADE TRAP (verified live in prod, 1440px):
 *   blazorblueprint.css ships UN-LAYERED base utilities (e.g. `.flex-col`),
 *   while the Portal's tailwind.output.css emits its rules INSIDE `@layer`.
 *   An un-layered rule beats a layered one regardless of source order, so a
 *   responsive variant like `lg:flex-row` (layered) whose BASE utility
 *   `flex-col` (un-layered) is applied on the same element SILENTLY LOSES —
 *   the console computed `flex-direction: column` at 1440px and the rail fell
 *   below the transcript. Never rely on a cross-bundle responsive override for
 *   load-bearing layout. These UN-LAYERED helpers (app.css loads last, no
 *   @layer) own the whole flex chain with their own media query, so nothing in
 *   either bundle can outrank them. Same institutional pattern as
 *   `.sj-stat-grid` and the admin-tabs helpers above. */
.sj-wizard-console {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}
@media (min-width: 1024px) {
  .sj-wizard-console {
    flex-direction: row;
    align-items: stretch;
  }
}
/* Right rail: full width stacked; a fixed 24rem sticky column at ≥1024px.
 * `align-self: flex-start` overrides the console's `align-items: stretch` so
 * the rail shrink-wraps its content and `position: sticky` actually pins. */
.sj-wizard-rail {
  width: 100%;
}
@media (min-width: 1024px) {
  .sj-wizard-rail {
    width: 24rem;
    flex: none;
    align-self: flex-start;
    position: sticky;
    top: 1.5rem;
  }
}

/* ── ToolSelector coarse-tree + fine-picker layout (FIX 4) ─────────────────
 * ToolSelector embeds in BOTH a wide surface (Endpoints / Team full page) and
 * a NARROW one (the automation builder's center column, the wizard build
 * panel). The old `grid grid-cols-1 lg:grid-cols-3` keyed off the VIEWPORT
 * (`lg:` = 1024px), so in the narrow builder center at a 1440px viewport it
 * still split three columns and the connector tree collapsed to a ~4rem sliver
 * that truncated labels to single letters ("H… 0/594"). This is a CONTAINER
 * problem, not a viewport one — so we drive it with a container query.
 *
 * `.sj-toolselector-region` establishes the query container (its own inline
 * size); `.sj-toolselector-grid` is a single stacked column by default and a
 * two-column [tree | picker] row once the CONTAINER (not the window) is wide
 * enough. The tree gets an explicit `minmax(14rem, 18rem)` track so it can
 * never truncate, and the picker takes the rest. Un-layered (app.css loads
 * last) so no bundle can outrank it; raw `@container` (not a Tailwind
 * container utility, which isn't in the no-JIT committed bundle). The picker
 * is FIRST in the DOM so it (and its prominent search input) leads when the
 * two stack on a narrow container; explicit `grid-column`/`grid-row` place the
 * tree left and the picker right when they sit side by side. */
.sj-toolselector-region {
  container-type: inline-size;
}
.sj-toolselector-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: minmax(0, 1fr);
}
@container (min-width: 640px) {
  .sj-toolselector-grid {
    grid-template-columns: minmax(14rem, 18rem) minmax(0, 1fr);
  }
  .sj-toolselector-tree {
    grid-column: 1;
    grid-row: 1;
  }
  .sj-toolselector-picker {
    grid-column: 2;
    grid-row: 1;
  }
}

/* Frosted sticky surface. Used by the Advanced Agent Builder's footer bars
 * (BuilderNavBar, BuilderActionBar) and by the connectors browse header, which
 * sticks to the TOP — hence the second selector rather than a second rule. One
 * declaration keeps a page whose header and footer both float over scrolling
 * content from drifting into two different translucencies.
 *
 * The reason this is a named helper at all, and the trap to remember before
 * writing `bg-background/95 backdrop-blur` in markup again: a slash-opacity
 * modifier on one of Blueprint's custom theme colors cannot be synthesized
 * without @theme registration, and the plain `backdrop-blur` utility is not in
 * Blueprint's build either. Both compile, render, and silently style nothing —
 * which is exactly how the builder footer lost its surface once already
 * (BuilderFooterSurfacePlumbingTests).
 *
 * Base tier: 95%-opacity background. Enhanced tier: when backdrop-filter is
 * supported, drop to 75% and blur for the frosted effect. `-webkit-` kept
 * for Safari ≤17. */
.sj-sticky-footer-surface,
.sj-sticky-header-surface {
  background-color: color-mix(in oklch, var(--background) 95%, transparent);
}
@supports ((backdrop-filter: blur(8px)) or (-webkit-backdrop-filter: blur(8px))) {
  .sj-sticky-footer-surface,
  .sj-sticky-header-surface {
    background-color: color-mix(in oklch, var(--background) 75%, transparent);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
  }
}

/* ── Chart series palette ──────────────────────────────────────────────────
 * BlazorBlueprint's BbBarChart / BbLineChart / BbPieChart read their series
 * colors from `--chart-1`..`--chart-5` (shadcn convention). The bundled
 * blazorblueprint.css (3.12.x) does NOT ship these tokens, so any chart using
 * the default `var(--chart-N)` fill rendered as invalid → SVG default black →
 * invisible on the dark card surface. Define the official shadcn categorical
 * palette here (light in :root, brighter set in .dark) so every Portal chart
 * has legible, on-brand series colors. Consumed directly as `var(--chart-N)`
 * (these are full color values, never hsl()-wrapped — see the theme-token note
 * in App.razor's :root block).
 * ──────────────────────────────────────────────────────────────────────── */
:root {
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);
}
.dark {
  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);
}

/* ── Stat-tile / control grid helper ───────────────────────────────────────
 * Admin KPI rows historically used `grid grid-cols-1 md:grid-cols-N` from
 * Tailwind utilities. In this build Blueprint's base `.grid-cols-1` lives in a
 * higher `@layer utilities` than the JIT's responsive `md:/lg:grid-cols-N`
 * (which land in the nested `utilities.app` sublayer), so the responsive
 * override is silently outranked for several values (md:grid-cols-3,
 * md:grid-cols-4, lg:grid-cols-2 all lose to base grid-cols-1) and the row
 * collapses to a single full-width column of stacked bands. This helper is an
 * UNLAYERED rule (app.css loads last, no @layer) so it always wins, and uses
 * auto-fit/minmax for a deterministic responsive tile row: many-up on desktop,
 * gracefully wrapping to 1-up on mobile. Override the per-tile minimum with an
 * inline `style="--sj-stat-min: 22rem"` (e.g. for a two-up chart pair). */
.sj-stat-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(var(--sj-stat-min, 220px), 1fr));
}

/* ── Product-card grid helper (the auto-FILL sibling of .sj-stat-grid) ──────
 * Same unlayered-rule rationale as .sj-stat-grid above — read that block first;
 * the only difference is the track-repeat keyword, and it is a deliberate one.
 *
 * `auto-fit` COLLAPSES the tracks no item lands in and hands their width to the
 * items that remain, which is right for a KPI row: three tiles should span the
 * row whether the page ships three or four of them. It is wrong for a row of
 * PRODUCT cards, where the card is a fixed piece of furniture and its width must
 * not testify to how many of them a deploy happens to have configured — a single
 * credit pack rendered as one card stretched across the entire page. `auto-fill`
 * keeps the empty tracks, so the card holds its width at every count. Same
 * choice, same reason, as .sj-template-grid and .sj-connector-grid below.
 *
 * The 15rem default is the value the credit-pack row already carried inline, so
 * adopting this helper changes the collapse behaviour and nothing else. Override
 * per-row with `style="--sj-pack-min: 18rem"`. */
.sj-pack-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(var(--sj-pack-min, 15rem), 1fr));
}

/* ── Connector Configure dialog: vertical (left-rail) tabs ─────────────────
 * Used by the multi-surface connector forms (Ingram Micro, Cisco Duo, Keeper,
 * ScalePad, D&H), one tab per independently-configurable credential surface.
 *
 * WHY THIS IS CSS AND NOT TAILWIND UTILITIES. Two separate mechanisms decide
 * which class wins on a Blueprint component, and only one of them is reliable:
 *
 *   1. Conflicting BASE utilities are resolved by `ClassNames.cn` (TwMerge) at
 *      RENDER time — the loser never reaches the DOM. That is why the trigger
 *      and panel classes can stay plain utilities: `justify-start` genuinely
 *      deletes Blueprint's `justify-center`, `h-auto` deletes `h-10`, and so on.
 *   2. A RESPONSIVE variant is a different conflict group to TwMerge, so
 *      `w-full` and `sm:w-48` BOTH survive onto the element — and then the
 *      cascade picks the wrong one, because Blueprint's base utilities sit in a
 *      parent `@layer utilities` while the JIT's `sm:` variants land in the
 *      nested `utilities.app` sublayer, and a parent layer outranks a sublayer
 *      regardless of media query. (Same trap as `.sj-stat-grid` above.)
 *
 * Measured before this rule existed, at a 1440px viewport: `flex flex-col
 * sm:flex-row` computed to `flex-direction: column` and `w-full sm:w-48` gave a
 * 703px-wide rail — i.e. the left rail silently rendered as a full-width stack.
 * These rules are UNLAYERED (app.css loads last, no @layer) so they always win.
 * Do NOT "simplify" them back into `sm:`-prefixed utilities. */
.sj-cfg-tabs {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.sj-cfg-tablist {
  /* The whole point of the rail. Blueprint's tablist is a horizontal bar and the
   * primitive supplies no direction for Orientation="Vertical" — without this the
   * triggers sit side by side inside a 192px box (measured: flex-direction row,
   * 44px tall). It lives here rather than as a `flex-col` utility so the rail's
   * geometry is in one place and cannot be half-removed by a class edit. */
  flex-direction: column;
  width: 100%;
}

.sj-cfg-panel {
  /* min-width:0 is load-bearing once the rail row exists: the panel is a flex
   * ITEM, and a flex item's default min-width:auto lets a wide input or an
   * unbreakable token push the whole dialog sideways instead of wrapping. */
  min-width: 0;
  flex: 1 1 0%;
}

@media (min-width: 640px) {
  .sj-cfg-tabs {
    flex-direction: row;
    align-items: flex-start;
  }

  .sj-cfg-tablist {
    width: 12rem;
    flex-shrink: 0;
  }
}

/* ── Configure-dialog paired-field row ─────────────────────────────────────
 * Same layer-precedence problem, same unlayered fix, different sizing: this
 * pairs two SHORT credential fields (Ingram Micro's customer number + country
 * code, sender id + application id) on one row inside a tabbed connector form,
 * so it drops to one column much earlier than a stat tile would. Do NOT replace
 * it with `grid-cols-1 sm:grid-cols-2` — that is the exact pattern whose
 * responsive half is outranked here. */
.sj-cfg-fieldgrid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}

/* ── Admin support shell helpers ───────────────────────────────────────────
 * Verified DOM contract (BlazorBlueprint.Components — confirmed live by
 * Chrome DevTools probing on 2026-05-04). What `<BbTabs Class="sj-admin-tabs">`
 * actually renders in the currently-installed version:
 *
 *   <div class="sj-admin-tabs">          ← styled wrapper, gets `Class`
 *     <div role="tablist">…</div>        ← BbTabsList — direct child!
 *     <div role="tabpanel"                ← BbTabsContent (when active)
 *          class="mt-2 ring-offset-background … {your Class}">
 *       {ChildContent}
 *     </div>
 *   </div>
 *
 * REGRESSION HISTORY (read this BEFORE touching the rules below):
 *
 * Round 1 — primitive wrapper era. An earlier Blueprint version inserted an
 *   unclassed primitive `<div>` between the styled `.sj-admin-tabs` root
 *   and the tablist/tabpanel siblings. We had a rule
 *   `.sj-admin-tabs > div { flex: 1 1 0; ... overflow: hidden }` to flex
 *   that wrapper into a column so the height chain reached the children.
 *   When Blueprint inlined the wrapper, that rule started matching BOTH
 *   the tablist AND the tabpanel — stretching the tablist to ~250px and
 *   stretching the tabpanel into a flex-overflow:hidden container too.
 *
 * Round 2 — `> [role="tabpanel"]` era. We scoped the rule to
 *   `.sj-admin-tabs > [role="tabpanel"]` to leave the tablist alone. That
 *   fixed the tablist height bug, but introduced a NEW bug: it had higher
 *   specificity (0,2,0 — class + attribute) than the tab-content helpers
 *   `.sj-admin-panel-scroll` and `.sj-admin-panel-flex` (0,1,0 — class).
 *   Because `Class=` on `<BbTabsContent>` is merged onto the SAME
 *   `<div role="tabpanel">` element this rule targeted, the
 *   `> [role="tabpanel"]` declaration of `overflow: hidden` clobbered
 *   `.sj-admin-panel-scroll`'s `overflow-y: auto` — making every Diagnostics
 *   / Tenant / Subscriptions / Usage / Connectors / MCP Clients / Team
 *   Members / Agents tab unable to scroll its content.
 *
 * Round 3 — current. Removed the `> [role="tabpanel"]` rule entirely. The
 *   helpers (`sj-admin-panel-scroll`, `sj-admin-panel-flex`) already supply
 *   the full height chain (`flex: 1 1 0; min-height: 0`) plus the right
 *   `display` + `overflow` for their use case. The
 *   `AdminSupport_EveryBbTabsContent_UsesPanelFlexOrPanelScroll` test pins
 *   that every BbTabsContent on this page carries one of the helpers, so
 *   no naked tabpanel can slip through.
 *
 *   Apply `.sj-admin-panel-scroll` / `.sj-admin-panel-flex` directly to
 *   `<BbTabsContent>` via `Class=`. They turn the tabpanel itself into
 *   the scroll/flex container — no extra wrapper indirection.
 *
 * Layered chain (production):
 *   sj-admin-shell           viewport-anchored row container
 *   ├── (left pane)          flex column, w-[340px], shrink-0
 *   │    └── sj-admin-list-scroll  ticket list, simple flex scroll
 *   └── sj-admin-pane        right column, flex column, definite height
 *        ├── (header)        shrink-0
 *        └── sj-admin-tab-host  flex column (px-6 wrapper for tabs)
 *             └── sj-admin-tabs (BbTabs styled wrapper, flex column)
 *                  ├── BbTabsList                  shrink-0, h-10 strip
 *                  └── BbTabsContent (tabpanel)    one of:
 *                       • sj-admin-panel-scroll   scrollable panel
 *                       • sj-admin-panel-flex     internal layout
 *                            └── sj-admin-conversation-scroll
 *                                                              messages
 *
 * `100dvh` keeps viewport math correct when mobile address bars or
 * desktop browser zoom changes the visible area; the `7rem` offset
 * matches the MainLayout header + admin chrome.
 *
 * DO NOT add a rule that targets `.sj-admin-tabs > div` or
 * `.sj-admin-tabs > [role="tabpanel"]` directly. Both forms have
 * historically caused a regression — the first by matching the inlined
 * tablist, the second by clobbering the panel-scroll/panel-flex helpers
 * via specificity. The `AdminSupport_NoLegacyTabBodyOrTabScrollClasses`
 * and `AdminSupport_TabsHeightChainHasNoChildSelectorOnTabs` tests pin
 * both bans.
 * ──────────────────────────────────────────────────────────────────────── */
.sj-admin-shell {
  display: flex;
  height: calc(100dvh - 7rem);
  min-height: 0;
  overflow: hidden;
}
/* ── Support-console page column (SupportWorkspaceNav + shell) ───────────────
 * /admin/support renders the shared SupportWorkspaceNav strip ABOVE the shell so
 * the console isn't a navigation dead-end. Adding a natural-height strip above a
 * `height: calc(100dvh - 7rem)` shell would overflow the viewport. Instead, this
 * page is a fixed-height flex COLUMN: the nav takes its natural height and the
 * shell takes the remainder. `.sj-support-console-shell` overrides the shell's
 * FIXED height to `flex: 1 1 0` — it MUST stay source-ordered AFTER `.sj-admin-shell`
 * (equal specificity, so later wins) or the shell reverts to full height and clips.
 * The base `.sj-admin-shell` rule is intentionally left untouched (the partner
 * console + AdminSupport_FullPaneHeight both depend on it). */
.sj-support-console-page {
  display: flex;
  flex-direction: column;
  height: calc(100dvh - 7rem);
  min-height: 0;
  gap: 0.75rem;
}
.sj-support-console-shell {
  height: auto;
  flex: 1 1 0;
  min-height: 0;
}
.sj-admin-pane {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
}
.sj-admin-tab-host {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}
.sj-admin-tabs {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}
/* Left ticket list — simple flex-based scroll. No Blueprint indirection
   here, so `flex: 1 1 0; overflow-y: auto` works reliably.

   `overflow-x: hidden` is load-bearing: `overflow-y: auto` alone makes the
   browser compute `overflow-x` to `auto` too (per the spec, a non-`visible`
   value on one axis promotes `visible` on the other to `auto`), so any row
   whose inner content is wider than the fixed 340px column — a long ticket
   subject in the `truncate` BbItemTitle, an unbreakable email in the
   description — produced a horizontal scrollbar on the whole list. Pinning
   `overflow-x: hidden` clips at the column edge instead; rows truncate inside
   their own cells (BbItemTitle carries `truncate min-w-0`, BbItemDescription
   `line-clamp-2`) rather than forcing the list to scroll sideways. */
.sj-admin-list-scroll {
  flex: 1 1 0;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
}
/* Scrollable tab panel — applied DIRECTLY to <BbTabsContent> via Class=.
   The tabpanel `<div>` itself becomes the scroll container. flex: 1 1 0 +
   min-height: 0 fills the remaining height of `.sj-admin-tabs > div`. */
.sj-admin-panel-scroll {
  flex: 1 1 0;
  min-height: 0;
  overflow-y: auto;
  padding-top: 1rem;
  padding-bottom: 1rem;
}
/* Flex-column tab panel — for tabs whose ChildContent has its own internal
   layout (Conversation: thread + reply; Live Testing: log feed + sidebar).
   Applied DIRECTLY to <BbTabsContent>; ChildContent provides the inner
   flex children (one of which is typically `.sj-admin-conversation-scroll`). */
.sj-admin-panel-flex {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding-top: 1rem;
  padding-bottom: 1rem;
}
/* Conversation thread inside `.sj-admin-panel-flex` — flex item that takes
   the remaining space and scrolls independently. min-height: 12rem
   guarantees the messages can never collapse below 12rem regardless of
   how tall the reply textarea grows. */
.sj-admin-conversation-scroll {
  flex: 1 1 auto;
  min-height: 12rem;
  overflow-y: auto;
}
/* Private partner↔admin thread scroller (SupportPrivateThreadPane). Distinct from
   `.sj-admin-conversation-scroll` because on the admin console the private thread now
   shares the Conversation tab's `.sj-admin-panel-flex` column WITH the client thread —
   two 12rem-floor scrollers plus their composers would overflow `overflow: hidden` on a
   short viewport and clip the "Post internal note" button unreachably. The smaller 6rem
   floor lets both threads coexist; the pane still scrolls its own overflow. The partner
   console (roomier host, single thread) tolerates the smaller floor fine. */
.sj-admin-private-thread-scroll {
  flex: 1 1 auto;
  min-height: 6rem;
  overflow-y: auto;
}

/* ── Admin tablist horizontal-scroll guard ──────────────────────────────────
 * Applied DIRECTLY to <BbTabsList> via Class= on every admin surface that
 * has more tabs than will fit in its host pane.
 *
 * The bug this prevents: BlazorBlueprint.Components.BbTabsList hard-codes
 * `inline-flex h-10` on the tablist. `inline-flex` shrink-wraps to its
 * children's natural width — when the row of tabs is wider than the pane,
 * the tablist either:
 *   (a) overflows its parent (pushes the pane wider, body gets a horizontal
 *       scrollbar), if there's no `flex-wrap`, or
 *   (b) wraps to multiple rows that DRAW OVER the tabpanel below, if
 *       `flex-wrap` is added — because the styled BbTabsList still applies
 *       `h-10`, clipping the box height to 40px while wrapped rows visually
 *       overflow downward.
 *
 * The durable fix: keep the single 40px row, never wrap, scroll-x. The
 * tablist becomes a fixed-height horizontal scroll-strip that NEVER
 * encroaches on the tabpanel and NEVER pushes the pane wider than its
 * column. Page-level scroll behavior is preserved.
 *
 * - `max-width: 100%` constrains the inline-flex root to the host pane.
 * - `flex-wrap: nowrap !important` overrides any `flex-wrap` utility we
 *   pass via `Class=` (defends against the wrap-overlap regression).
 * - `overflow-x: auto` + `overflow-y: hidden` keeps scrolling on the
 *   horizontal axis only, so vertical body/page scroll stays untouched.
 * - `scrollbar-width: thin` matches the rest of the admin chrome.
 * - `scroll-snap-type: x proximity` + `scroll-snap-align: start` on each
 *   trigger snaps each tab into a clean left edge so partial slivers never
 *   end up at the start of the scrollport when the user uses arrow keys
 *   or wheel.
 * - `flex-shrink: 0` on triggers prevents the inline-flex from shrinking
 *   each trigger to fit, which would defeat the horizontal scroll and
 *   re-introduce the wrap problem.
 *
 * BLUEPRINT-VERSION DEPENDENCY: same as `.sj-admin-tabs > div` — this
 * targets `[role="tab"]` directly, which the BlazorBlueprint.Primitives
 * BbTabsTrigger always renders as a `<button role="tab">`. Stable as of
 * 3.10.0.
 * ──────────────────────────────────────────────────────────────────────── */
.sj-admin-tablist-scroll {
  /* Pin display to `inline-flex` (matching Blueprint's BbTabsList default) and
     direction to `row` so a parent flex-column can never stretch the tablist
     into a vertical stack.

     Why `inline-flex` and not `flex`:
     - When the tablist's parent is a flex container (e.g. AdminSupport's
       `.sj-admin-tabs`, which is flex-column), the CSS spec blockifies
       `inline-flex` children to `flex` automatically. So the OUTER 12-tab
       tablist still spans the parent's full width and scrolls horizontally
       when its tabs overflow — exactly what we want.
     - When the parent is a normal block container (e.g. a `BbCard /
       BbCardContent` wrapper around a nested `BbTabs`, like
       TenantAgentsPanel's 4-tab strip), `inline-flex` shrink-wraps to the
       tab content width. Without this — i.e. with `display: flex !important`
       — the tablist's `bg-muted` strip stretches the full card width,
       leaving a huge empty rounded box to the left of the tabs. That was
       the symptom the user saw inside a real support ticket's Agents tab:
       a 1073px-wide muted-bg strip with the four ~360px-wide tabs clumped
       at the right side because Blueprint's `justify-center` plus the
       block-level stretching produced an off-balance visual.
     `flex-direction: row !important` and `flex-wrap: nowrap !important`
     defend against any utility class accidentally applied via `Class=` that
     would otherwise wrap rows over the tabpanel. */
  display: inline-flex !important;
  flex-direction: row !important;
  max-width: 100%;
  flex-wrap: nowrap !important;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
  scroll-snap-type: x proximity;
}
.sj-admin-tablist-scroll > [role="tab"] {
  scroll-snap-align: start;
  flex-shrink: 0;
}

/* =====================================================================================================
   Phase 3 live docs — prose styling for injected, build-time-sanitized docs HTML (plan §3.5).
   Rendered via MarkupString on the /docs page AND in the contextual-help dialog, so this is a single
   GLOBAL class both surfaces share (no CSS isolation / ::deep — the injected HTML is not attribute-scoped).
   Loaded after tailwind.output.css, so these rules override Tailwind preflight. Colors use the design-system
   variables (full-color values, consumed directly as var(--x)) so docs read correctly in light + dark.
   Unrelated to the .sj-admin-* tab contract above — safe to edit independently.
   ===================================================================================================== */
.sj-docs-content { color: var(--foreground); line-height: 1.7; font-size: 0.9375rem; }
.sj-docs-content > :first-child { margin-top: 0 !important; }
.sj-docs-content > :last-child { margin-bottom: 0; }
.sj-docs-content h1 { font-size: 2rem; font-weight: 700; letter-spacing: -0.025em; line-height: 1.2; margin: 0 0 1.25rem; scroll-margin-top: 5rem; }
.sj-docs-content h2 { font-size: 1.5rem; font-weight: 650; letter-spacing: -0.015em; line-height: 1.3; margin: 2.75rem 0 1rem; padding-bottom: 0.4rem; border-bottom: 1px solid var(--border); scroll-margin-top: 5rem; }
.sj-docs-content h3 { font-size: 1.2rem; font-weight: 600; letter-spacing: -0.01em; margin: 2rem 0 0.6rem; scroll-margin-top: 5rem; }
.sj-docs-content h4 { font-size: 1rem; font-weight: 600; margin: 1.5rem 0 0.5rem; scroll-margin-top: 5rem; }
.sj-docs-content h5, .sj-docs-content h6 { font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted-foreground); margin: 1.25rem 0 0.5rem; scroll-margin-top: 5rem; }
.sj-docs-content p { margin: 0 0 1.1rem; text-wrap: pretty; }
.sj-docs-content strong, .sj-docs-content b { font-weight: 650; color: var(--foreground); }
.sj-docs-content a { color: var(--primary); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; font-weight: 500; }
.sj-docs-content a:hover { text-decoration: none; }
.sj-docs-content ul, .sj-docs-content ol { margin: 0 0 1.1rem; padding-left: 1.5rem; }
.sj-docs-content ul { list-style: disc; }
.sj-docs-content ol { list-style: decimal; }
.sj-docs-content li { margin: 0.35rem 0; padding-left: 0.25rem; }
.sj-docs-content li::marker { color: var(--muted-foreground); }
.sj-docs-content li > ul, .sj-docs-content li > ol { margin: 0.35rem 0 0.35rem; }
.sj-docs-content code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.8125em; background: var(--muted); color: var(--foreground); padding: 0.15em 0.4em; border-radius: 0.3rem; border: 1px solid color-mix(in srgb, var(--border) 60%, transparent); white-space: nowrap; }
/* There is deliberately NO wrap override for code inside table cells, and adding one back is what
   broke the connector tool-catalog pages twice. Tool names are single unbreakable tokens
   (`liongard_get_environment_related_entities`, and 64-character Meraki names). The previous rule here
   was `white-space: normal; overflow-wrap: anywhere` — and `anywhere` (unlike `break-word`) DOES feed a
   box's min-content intrinsic size, so the automatic table-layout algorithm sized the Tool column to a
   SINGLE CHARACTER and handed all the slack to the Description column. Measured on /docs/liongard-tools
   at 1440px: Tool column 59px wide, each name rendered 25px wide and up to 256px TALL, i.e. shredded one
   or two characters per line; on /docs/cisco-meraki-tools all 226 rows were shredded at every width down
   to 390px. Cells now inherit `white-space: nowrap` from the .sj-docs-content code rule above, and the
   table is its own `overflow-x: auto` container (below), so a wide catalog scrolls sideways instead —
   measured Tool column 439px with every name on one line. If a future cell really is too wide, widen the
   reading column or let it scroll; do not reintroduce a character-level break here. */
.sj-docs-content pre { background: var(--muted); border: 1px solid var(--border); border-radius: 0.6rem; padding: 1rem 1.15rem; overflow-x: auto; margin: 0 0 1.25rem; line-height: 1.55; }
.sj-docs-content pre code { background: transparent; padding: 0; border: 0; font-size: 0.8125rem; white-space: pre; }
.sj-docs-content blockquote { border-left: 3px solid var(--primary); background: color-mix(in srgb, var(--muted) 55%, transparent); border-radius: 0 0.4rem 0.4rem 0; padding: 0.6rem 1rem; margin: 0 0 1.25rem; color: var(--muted-foreground); }
.sj-docs-content blockquote > :last-child { margin-bottom: 0; }
.sj-docs-content img { max-width: 100%; height: auto; display: block; border: 1px solid var(--border); border-radius: 0.6rem; margin: 1.25rem 0; box-shadow: 0 1px 2px color-mix(in srgb, var(--foreground) 8%, transparent); }
.sj-docs-content table { width: 100%; border-collapse: collapse; margin: 0 0 1.25rem; font-size: 0.875rem; display: block; overflow-x: auto; border: 1px solid var(--border); border-radius: 0.5rem; }
.sj-docs-content th, .sj-docs-content td { border-bottom: 1px solid var(--border); border-right: 1px solid var(--border); padding: 0.55rem 0.85rem; text-align: left; vertical-align: top; }
.sj-docs-content th:last-child, .sj-docs-content td:last-child { border-right: 0; }
.sj-docs-content tr:last-child td { border-bottom: 0; }
.sj-docs-content th { background: var(--muted); font-weight: 600; white-space: nowrap; }
.sj-docs-content tbody tr:nth-child(even) { background: color-mix(in srgb, var(--muted) 35%, transparent); }
.sj-docs-content hr { border: 0; border-top: 1px solid var(--border); margin: 2.5rem 0; }

/* Reading-column width contract for doc pages. The article fills the reading
 * column (`.sj-docs-main`, up to 88rem so it stays sane on ultra-wide) instead
 * of the old fixed `max-w-3xl` — that constraint squished the connector
 * tool-catalog pages (wide 4-column Tool/Plan/Access/Description tables) into a
 * narrow strip with the rest of the screen empty. Prose flow text keeps a
 * readable ~48rem measure via the child caps below; tables, <details> param
 * blocks, and code blocks span the full column so they get real room. Applies
 * only to the /docs page article (NOT the help-modal reuse of .sj-docs-content,
 * which has no .sj-docs-article and stays modal-width). */
.sj-docs-article { max-width: 88rem; }
.sj-docs-article > p,
.sj-docs-article > ul,
.sj-docs-article > ol,
.sj-docs-article > blockquote { max-width: 48rem; }

/* -----------------------------------------------------------------------------------------------------
   Docs browser layout chrome (Phase 3 UX overhaul) — the section-tree sidebar + "On this page" TOC rail.
   Namespaced .sj-docs-* on the Portal's OWN razor markup (NOT injected HTML), so these are plain global
   helpers, wholly independent of the .sj-admin-* BbTabs contract above and of the .sj-docs-content prose.
   Active states that are toggled by the scroll-spy JS live here as [data-active] rules (the component
   cannot know the scroll position); the nav-link active state is also mirrored here for a crisp left rail.
   ----------------------------------------------------------------------------------------------------- */
.sj-docs-nav-link { display: block; position: relative; border-radius: 0.375rem; padding: 0.3rem 0.6rem; font-size: 0.85rem; line-height: 1.35; color: color-mix(in srgb, var(--foreground) 78%, transparent); text-decoration: none; transition: background-color 0.12s ease, color 0.12s ease; }
.sj-docs-nav-link:hover { background: var(--accent); color: var(--accent-foreground); }
.sj-docs-nav-link[data-active] { background: color-mix(in srgb, var(--primary) 12%, transparent); color: var(--primary); font-weight: 600; }
.sj-docs-nav-link[data-active]::before { content: ""; position: absolute; left: -0.75rem; top: 0.25rem; bottom: 0.25rem; width: 2px; border-radius: 999px; background: var(--primary); }
.sj-docs-nav-branch { margin-left: 0.55rem; padding-left: 0.55rem; border-left: 1px solid var(--border); }
.sj-docs-section-toggle { display: flex; align-items: center; gap: 0.4rem; width: 100%; border-radius: 0.375rem; padding: 0.35rem 0.4rem; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted-foreground); background: transparent; border: 0; cursor: pointer; text-align: left; }
.sj-docs-section-toggle:hover { color: var(--foreground); background: var(--accent); }
.sj-docs-section-chevron { transition: transform 0.15s ease; flex-shrink: 0; }
.sj-docs-section-toggle[aria-expanded="true"] .sj-docs-section-chevron { transform: rotate(90deg); }

.sj-docs-toc-link { display: block; border-left: 2px solid var(--border); padding: 0.2rem 0 0.2rem 0.75rem; font-size: 0.8rem; line-height: 1.35; color: var(--muted-foreground); text-decoration: none; transition: color 0.12s ease, border-color 0.12s ease; }
.sj-docs-toc-link:hover { color: var(--foreground); border-color: color-mix(in srgb, var(--foreground) 45%, transparent); }
.sj-docs-toc-link[data-level="3"] { padding-left: 1.5rem; }
.sj-docs-toc-link[data-active] { color: var(--primary); border-color: var(--primary); font-weight: 500; }

/* -----------------------------------------------------------------------------------------------------
   Docs 3-pane RESPONSIVE shell. This lives here — NOT in Tailwind md:/xl: utilities — for the same
   reason .sj-builder-shell does: the Portal's Tailwind sits in a weak `utilities.app` sublayer that
   loses to Blueprint's prebuilt bundle for any utility Blueprint also emits, so `md:flex-row` /
   `md:w-64` / `xl:block` silently never win over Blueprint's base `flex-col` / `w-full` / `hidden`
   (verified: the layout stayed a single stacked column at 1440px). app.css is unlayered and loads
   last, so these media-query rules always win. Breakpoints mirror Tailwind's md (48rem) / xl (80rem).
   ----------------------------------------------------------------------------------------------------- */
.sj-docs-layout { display: flex; flex-direction: column; align-items: stretch; gap: 2rem; }
.sj-docs-nav { width: 100%; flex-shrink: 0; }
.sj-docs-main { min-width: 0; width: 100%; flex: 1 1 0%; }
.sj-docs-toc { display: none; }
.sj-docs-nav-toggle { display: flex; }
.sj-docs-nav-panel { display: none; }
.sj-docs-nav-panel[data-open] { display: block; }

.sj-docs-cards { display: grid; gap: 1rem; grid-template-columns: minmax(0, 1fr); }
.sj-docs-pager { display: grid; gap: 1rem; grid-template-columns: minmax(0, 1fr); max-width: 48rem; margin-top: 3rem; }
.sj-docs-pager-next { text-align: right; }

@media (min-width: 40rem) {
  .sj-docs-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .sj-docs-pager { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .sj-docs-pager-prev { grid-column: 1; }
  .sj-docs-pager-next { grid-column: 2; }
}

@media (min-width: 48rem) {
  .sj-docs-layout { flex-direction: row; align-items: flex-start; }
  .sj-docs-nav { width: 16rem; position: sticky; top: 1.5rem; align-self: flex-start; }
  .sj-docs-nav-toggle { display: none; }
  .sj-docs-nav-panel { display: block; }
}

@media (min-width: 80rem) {
  .sj-docs-cards { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .sj-docs-toc { display: block; width: 14rem; flex-shrink: 0; position: sticky; top: 1.5rem; align-self: flex-start; }
}

/* =====================================================================================================
   Run transcript conversation timeline — used ONLY by RunTranscriptView.razor (the run-detail
   transcript + live stream) and its preview harness. Turns the flat Anthropic session-event list
   into a readable agent↔stack conversation: a left spine with role-colored nodes, tool-call cards
   with an accent rail, and a wrapping monospace surface for args/results. Token-based colors so it
   reads in light + dark. Completely independent of the .sj-admin-* tab contract above — safe to edit.
   ===================================================================================================== */

/* Timeline row: a node in a fixed left gutter + a connecting spine drawn between successive nodes. */
.sj-tl-item { position: relative; padding-left: 2.25rem; }
.sj-tl-item::before {
  content: "";
  position: absolute;
  left: 0.6875rem;                 /* centered under the 1.375rem node */
  top: 1.625rem;
  bottom: -0.375rem;
  width: 2px;
  background: color-mix(in oklch, var(--border) 85%, transparent);
}
.sj-tl-item:last-child::before { display: none; }   /* no dangling line after the final node */

.sj-tl-node {
  position: absolute;
  left: 0;
  top: 0.125rem;
  width: 1.375rem;
  height: 1.375rem;
  border-radius: 9999px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--background);
  border: 2px solid var(--border);
  color: var(--muted-foreground);
}
.sj-tl-node-assistant { border-color: var(--primary); color: var(--primary); }
.sj-tl-node-tool { border-color: var(--sj-accent-1); color: var(--sj-accent-1); }
.sj-tl-node-thinking {
  border-color: color-mix(in oklch, var(--sj-accent-1) 55%, transparent);
  color: var(--sj-accent-1);
  border-style: dashed;
}
.sj-tl-node-user { border-color: var(--foreground); color: var(--foreground); }
.sj-tl-node-danger {
  border-color: var(--destructive);
  color: var(--destructive);
  background: color-mix(in oklch, var(--destructive) 14%, var(--background));
}
.sj-tl-node-success { border-color: oklch(0.7 0.15 162); color: oklch(0.7 0.15 162); }
.sj-tl-node-warning { border-color: oklch(0.76 0.16 70); color: oklch(0.76 0.16 70); }

/* Tool-call card: subtle body with an indigo accent rail (destructive rail when the call errored). */
.sj-tool-card { border: 1px solid var(--border); border-left: 3px solid var(--sj-accent-1); border-radius: 0.5rem; }
.sj-tool-card-error { border-left-color: var(--destructive); }

/* Monospace surface for tool args / results / raw JSON. Wraps hard so a long token can never push the
   page body wider than its column (responsive contract), while still allowing horizontal scroll for
   pre-formatted rows that genuinely need it. */
.sj-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.78rem;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-wrap: anywhere;
  background: var(--muted);
  border-radius: 0.375rem;
  padding: 0.625rem 0.75rem;
  max-width: 100%;
  overflow-x: auto;
}
.sj-code-scroll { max-height: 22rem; overflow-y: auto; }

/* Faint tint behind an assistant prose block so the agent's "voice" reads as a card without a heavy border. */
.sj-assistant-surface { background: color-mix(in oklch, var(--muted) 45%, transparent); border-radius: 0.5rem; }

/* ── S4 support-console shared surfaces ────────────────────────────────────
 * Used by BOTH /admin/support and /partner/support via the shared components
 * in Components/Pages/Admin/Support/. Deliberately OUTSIDE the .sj-admin-*
 * tab cascade — the queue segmented control is a BbToggleGroup, not a BbTabs,
 * so it dodges the tablist/tabpanel traps entirely (spec § CSS-contract #4).
 * ──────────────────────────────────────────────────────────────────────── */
.sj-support-queue-tabs {
  display: flex;
  width: 100%;
}
.sj-support-queue-tabs > * {
  flex: 1 1 0;
}

/* Hand-rolled chat bubbles (BbMessage/BbBubble are absent from the pinned
 * Blueprint 3.12.1 — see plan M5). Rendered INSIDE .sj-admin-conversation-scroll,
 * so they change bubble markup only, never the height chain. */
.sj-support-bubble {
  border-radius: 0.6rem;
  padding: 0.6rem 0.8rem;
  max-width: 82%;
  border: 1px solid var(--border);
}
.sj-support-bubble-inbound {
  align-self: flex-start;
  background: var(--card);
}
.sj-support-bubble-outbound {
  align-self: flex-end;
  background: color-mix(in srgb, var(--primary) 14%, var(--card));
}
.sj-support-bubble-meta {
  font-size: 0.6875rem;
  color: var(--muted-foreground);
}

/* Private partner↔admin thread banner — unmissable "the client can't see this". */
.sj-support-private-banner {
  border: 1px solid color-mix(in srgb, var(--primary) 45%, var(--border));
  background: color-mix(in srgb, var(--primary) 10%, transparent);
  border-radius: 0.5rem;
  padding: 0.5rem 0.75rem;
  font-size: 0.8125rem;
  font-weight: 600;
}

/* Demotes an evidence BbTabs BELOW the always-mounted Conversation pane. Caps the
 * strip's share of the tab-host column and lets its inner BbTabsContent scroll.
 * PARTNER-CONSOLE ONLY: /partner/support (PartnerSupport.razor) still uses this
 * always-mounted-conversation + demoted-evidence-strip layout. The admin console
 * (/admin/support) NO LONGER uses it — it was restored to ONE first-class full-height
 * BbTabs, so this class must not reappear in AdminSupport.razor. It wraps the tabs from
 * OUTSIDE, so it never uses a banned `.sj-admin-tabs > X` form. Do NOT delete this rule
 * while PartnerSupport.razor still references it (deleting it silently flattens the
 * partner evidence strip — the child `.sj-admin-tabs`'s `flex:1 1 0` goes inert without
 * a flex parent). Pinned by SupportConsoleStylePlumbingTests. */
.sj-support-evidence-strip {
  flex: 0 1 auto;
  min-height: 0;
  max-height: 42%;
  display: flex;
  flex-direction: column;
}

/* ── /signup — static-SSR public signup page (design 2026-07-16) ─────────────
 * CSS isolation (.razor.css) is NOT wired in this Portal (App.razor links no
 * per-component bundle), so the signup page's bespoke styling lives here as
 * global .sj-signup-* helpers. All colors consume Blueprint theme tokens
 * directly (var(--x) — full oklch values, never hsl()-wrapped). The Portal is
 * dark-only. Blueprint 3.9.6 ships no semantic-color VARIANT utilities
 * (hover:/focus-visible:/placeholder: on bg-primary/ring-ring), and the JIT
 * can't synthesize them, so the inputs + primary button are hand-rolled here
 * from tokens rather than Tailwind utilities. Signature: a restrained indigo
 * "jack-in" glow behind the card + a luminous hairline on the card's top edge. */
.sj-signup-shell {
  --sj-signup-accent: oklch(0.488 0.243 264.376); /* the dark theme's one chromatic token (264° indigo) */
  position: relative;
  min-height: 100vh;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: clamp(1.5rem, 6vh, 5rem) 1rem;
  overflow: hidden;
  background:
    radial-gradient(120% 70% at 50% -10%,
      color-mix(in oklch, var(--sj-signup-accent) 22%, transparent) 0%,
      transparent 60%),
    var(--background);
}
.sj-signup-shell::before { /* faint off-center bloom for depth */
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(38% 30% at 88% 102%,
    color-mix(in oklch, var(--sj-signup-accent) 10%, transparent) 0%, transparent 70%);
}

.sj-signup-stage {
  position: relative;
  width: 100%;
  max-width: 26rem;
  display: flex;
  flex-direction: column;
  gap: 1.75rem;
}

.sj-signup-brand { display: flex; justify-content: center; }
/* The full brand lockup as the SVG composes it — mascot + STACKJACK.IO wordmark + the baked
 * "Jack in to your MSP Stack" tagline, integrated alignment intact. Text recolored for dark
 * (wordmark #ffffff, tagline #d4d4d8 muted); mascot keeps its original opaque colors. At this
 * size (~400px) the baked tagline renders ~13px — legible. NO border-radius / glow on the logo
 * itself: the old square PNG's rounded corners + accent shadow read as an "invisible box"
 * hugging the mascot. The card keeps its glow; the logo sits directly on the shell's bloom. */
.sj-signup-logo {
  width: min(96%, 26rem);
  height: auto;
}

/* Card — luminous top edge reads as a powered connection (the signature) */
.sj-signup-card {
  position: relative;
  overflow: hidden;
  box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.7);
}
.sj-signup-card::before {
  content: "";
  position: absolute; top: 0; left: 0; right: 0; height: 1px;
  background: linear-gradient(90deg, transparent,
    color-mix(in oklch, var(--sj-signup-accent) 90%, transparent), transparent);
}

.sj-signup-body { font-size: 0.9rem; line-height: 1.55; color: var(--muted-foreground); }
.sj-signup-body strong { color: var(--foreground); font-weight: 600; }
.sj-signup-alert { margin-bottom: 1.1rem; }

/* Instance-select step (plan §7, Milestone EU-2). Signup-scoped on purpose — nothing here is shared
   with, or cascades into, the .sj-admin-* tab rules whose contract is documented at :261-335. */
.sj-signup-instances {
  display: flex; flex-direction: column; gap: 0.5rem;
  margin-bottom: 1.1rem; padding-bottom: 1.1rem;
  border-bottom: 1px solid var(--border);
}
.sj-signup-instance-list {
  display: flex; flex-wrap: wrap; gap: 0.5rem;
  margin: 0; padding: 0; list-style: none;
}
.sj-signup-instance {
  border: 1px solid var(--input); border-radius: var(--radius);
  padding: 0.4rem 0.7rem; font-size: 0.85rem;
}
.sj-signup-instance.is-selected {
  border-color: color-mix(in oklch, var(--sj-signup-accent) 70%, var(--input));
  background: color-mix(in oklch, var(--sj-signup-accent) 10%, transparent);
}
.sj-signup-instance-current { color: var(--foreground); font-weight: 600; }
.sj-signup-instance-tag {
  margin-left: 0.35rem; font-size: 0.68rem; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-foreground);
}

.sj-signup-fields { display: flex; flex-direction: column; gap: 1.1rem; }
.sj-signup-field { display: flex; flex-direction: column; gap: 0.4rem; }
.sj-signup-label {
  font-size: 0.7rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--muted-foreground);
}
.sj-signup-input {
  width: 100%;
  height: 2.6rem;
  border-radius: var(--radius);
  border: 1px solid var(--input);
  background: var(--background);
  padding: 0 0.85rem;
  font-size: 0.9rem;
  color: var(--foreground);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.sj-signup-input::placeholder { color: var(--muted-foreground); opacity: 0.7; }
.sj-signup-input:focus,
.sj-signup-input:focus-visible {
  outline: none;
  border-color: color-mix(in oklch, var(--sj-signup-accent) 70%, var(--input));
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--sj-signup-accent) 30%, transparent);
}
.sj-signup-input.invalid { border-color: var(--destructive); }
.sj-signup-hint { font-size: 0.75rem; color: var(--muted-foreground); }
.sj-signup .validation-message {
  color: var(--destructive); font-size: 0.78rem; margin-top: 0.15rem;
}

/* Center the Turnstile widget. The container stacks three children (.cf-turnstile
 * ~300px iframe, the <noscript> block, and the error div), so centering is on the
 * cross axis of a column — align-items:center, not justify-content. The guidance
 * blocks stretch full-width (align-self below) so only the fixed-width widget centers. */
.sj-signup-turnstile {
  margin-top: 1.35rem; min-height: 65px;
  display: flex; flex-direction: column; align-items: center;
}
.sj-signup-proxy-error {
  align-self: stretch;
  margin-top: 0.6rem;
  font-size: 0.8rem; line-height: 1.45; color: var(--foreground);
  border: 1px solid var(--border);
  border-left: 3px solid var(--destructive);
  border-radius: 0.5rem;
  padding: 0.6rem 0.75rem;
  background: color-mix(in oklch, var(--destructive) 8%, transparent);
}
.sj-signup-proxy-error strong { font-weight: 600; }

/* Primary action — one class for both <button> and <a>; matches the app's
 * shadcn primary (light on dark). */
.sj-signup-submit {
  display: inline-flex; align-items: center; justify-content: center;
  width: 100%;
  height: 2.6rem;
  margin-top: 1.5rem;
  border: none;
  border-radius: var(--radius);
  background: var(--primary);
  color: var(--primary-foreground);
  font-size: 0.9rem; font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition: opacity 0.15s ease, transform 0.05s ease;
}
.sj-signup-submit:hover { opacity: 0.9; }
.sj-signup-submit:active { transform: translateY(1px); }
.sj-signup-submit:focus-visible {
  outline: 2px solid color-mix(in oklch, var(--sj-signup-accent) 80%, transparent);
  outline-offset: 2px;
}
.sj-signup-submit[disabled],
.sj-signup-submit[aria-busy="true"] { opacity: 0.6; cursor: default; }

.sj-signup-link { color: var(--foreground); font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }
.sj-signup-link:hover { opacity: 0.85; }
.sj-signup-foot { text-align: center; font-size: 0.85rem; color: var(--muted-foreground); }

.sj-signup-success-badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: 3rem; height: 3rem; margin-bottom: 0.25rem;
  border-radius: 999px;
  color: var(--foreground);
  background: color-mix(in oklch, var(--sj-signup-accent) 24%, transparent);
}

/* Honeypot — offscreen; never shown to humans (D3) */
.sj-signup-honeypot {
  position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden;
}

/* Quiet entrance; disabled under reduced-motion */
@media (prefers-reduced-motion: no-preference) {
  .sj-signup-stage { animation: sj-signup-rise 0.5s ease both; }
}
@keyframes sj-signup-rise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ── Automation flow diagram (.sj-flow-*) — AutomationFlowDiagram.razor ───────────────────────
   Server-composed run diagram, no mermaid.js. Read the component's .razor.cs first: it records
   why the 2026-08-07 rework replaced the left-to-right stage-column strip this block used to
   style. Two things here are load-bearing rather than decorative.

   1. TWO EDGE KINDS, and they must never converge. `.sj-flow-rail::after` is a SOLID vertical
      rule joining consecutive medallions — the sequence edge, read top to bottom.
      `.sj-flow-branch` is a DASHED left rule captioned "Can call" — reach, not sequence. The old
      strip drew one chevron for both relationships, which is exactly what made "Agent › Tools"
      read as "the tools run after the model", and that was the reported complaint.

   2. VERTICAL, and deliberately so. This component is mounted into the builder preview rail and
      the wizard rail, whose budget is ~24rem wide; a horizontal pipeline wrapped there into an
      unreadable pile. A column reads at any width and at any chain length.

   Step types are distinguished on THREE independent channels — a plain-English caption in the
   markup, medallion SHAPE (circle for the run's bookends, rounded square for the model's own
   turn, pill for capabilities), and a tint. No single channel is load-bearing, so the diagram
   survives a colour-blind reader or an icon that fails to resolve.

   Every colour is a theme variable or a color-mix over one, so this renders in BOTH themes. Three
   things would break that and none of them fails a build: a literal colour value, a dark-only
   variant prefix on a utility, and the chart palette tokens (app.css declares those twice and
   ships only the .dark copy). A source pin scans this block for all three — which is also why
   this comment names none of them literally, since prose is scanned too. Every flex/grid child
   carries min-width: 0: a long tool name in this rail has pushed the window sideways before.
   NOT under .sj-admin-*. */
.sj-flow { display: flex; flex-direction: column; min-width: 0; }

/* --- the spine: an ordered list whose rows are consecutive in time -------------------------- */
.sj-flow-spine { list-style: none; margin: 0; padding: 0; min-width: 0; }
.sj-flow-step { display: flex; gap: 0.625rem; min-width: 0; }

/* THE ROW GAP LIVES ON THE BODY, NOT THE ROW — do not "tidy" it onto .sj-flow-step. The rail is a
   stretched flex ITEM, so it stretches to the row's CONTENT box, which excludes the row's own
   padding. Put the gap on .sj-flow-step and the rule below stops ~12px short of the next
   medallion: the sequence edge renders as disconnected segments, which is precisely the reading
   ("these rows are consecutive") the edge exists to carry. On the body it is inside the content
   box, so the rail stretches through it and the rule is continuous. */
.sj-flow-step .sj-flow-body { padding-bottom: 0.75rem; }
.sj-flow-step:last-child .sj-flow-body { padding-bottom: 0; }

.sj-flow-rail { display: flex; flex-direction: column; align-items: center; flex: 0 0 auto; width: 1.75rem; }
/* The sequence edge. Suppressed on the last row so the rule never dangles past the final step. */
.sj-flow-rail::after { content: ""; flex: 1 1 auto; width: 2px; min-height: 0.5rem;
    margin-top: 0.25rem; border-radius: 1px; background: var(--border); }
.sj-flow-step:last-child .sj-flow-rail::after { display: none; }

.sj-flow-medallion { display: inline-flex; align-items: center; justify-content: center;
    flex: 0 0 auto; height: 1.75rem; width: 1.75rem;
    border: 1px solid var(--border); background: var(--card); color: var(--muted-foreground); }
/* Shape channel. */
.sj-flow-medallion-trigger, .sj-flow-medallion-result,
.sj-flow-medallion-prep, .sj-flow-medallion-verify { border-radius: 9999px; }
.sj-flow-medallion-turn { border-radius: 0.5rem; }
/* A fixed bookend step is a smaller circle than a trigger or a result: same family, lesser weight. */
.sj-flow-medallion-prep, .sj-flow-medallion-verify { height: 1.5rem; width: 1.5rem; background: var(--muted); }
/* Tint channel. */
.sj-flow-medallion-trigger { color: var(--primary);
    border-color: color-mix(in oklch, var(--primary) 45%, var(--border));
    background: color-mix(in oklch, var(--primary) 12%, var(--card)); }
.sj-flow-medallion-turn { color: var(--primary-foreground); border-color: var(--primary); background: var(--primary); }
.sj-flow-medallion-result { color: var(--foreground); }

.sj-flow-body { flex: 1 1 auto; min-width: 0; padding-top: 0.1rem; }
.sj-flow-kind { font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.06em;
    font-weight: 600; line-height: 1.4; color: var(--muted-foreground); }
.sj-flow-title { font-size: 0.8125rem; font-weight: 600; line-height: 1.3;
    color: var(--foreground); min-width: 0; overflow-wrap: break-word; }
.sj-flow-sub { font-size: 0.6875rem; line-height: 1.35; color: var(--muted-foreground);
    min-width: 0; overflow-wrap: break-word; }

/* --- the branch: what the AI turn MAY call, hanging off the model turn ---------------------- */
.sj-flow-branch { margin-top: 0.5rem; padding-left: 0.625rem; min-width: 0;
    border-left: 2px dashed color-mix(in oklch, var(--muted-foreground) 45%, transparent); }
.sj-flow-branch-label, .sj-flow-capgroup-label { font-size: 0.625rem; text-transform: uppercase;
    letter-spacing: 0.06em; font-weight: 600; color: var(--muted-foreground); }
.sj-flow-capgroup { margin-top: 0.375rem; min-width: 0; }
.sj-flow-caps { list-style: none; margin: 0.1875rem 0 0; padding: 0; min-width: 0;
    display: flex; flex-wrap: wrap; gap: 0.3125rem; }
.sj-flow-cap { display: inline-flex; align-items: center; gap: 0.375rem;
    min-width: 0; max-width: 100%; padding: 0.1875rem 0.5rem;
    border: 1px solid var(--border); border-radius: 9999px; background: var(--card); }
.sj-flow-cap-empty { border-style: dashed; margin-top: 0.1875rem; font-size: 0.6875rem;
    color: var(--muted-foreground); }
.sj-flow-cap-icon { flex: 0 0 auto; color: var(--muted-foreground); }
.sj-flow-cap-title { font-size: 0.6875rem; font-weight: 600; color: var(--foreground);
    min-width: 0; overflow-wrap: break-word; }
.sj-flow-cap-sub { font-size: 0.625rem; color: var(--muted-foreground);
    min-width: 0; overflow-wrap: break-word; }

/* A chain is an ordered mini-sequence, so it takes a full line rather than a pill in the wrap
   row, and its tools become NUMBERED steps. The strip this replaced rendered them as one
   " → "-joined string, which is what made a real nine-step chain unreadable in a 24rem rail. */
.sj-flow-chain { margin-top: 0.1875rem; padding: 0.375rem 0.5rem; min-width: 0;
    border: 1px solid var(--border); border-radius: 0.5rem; background: var(--card); }
.sj-flow-chain-head { display: flex; align-items: center; gap: 0.375rem; min-width: 0; }
.sj-flow-chain-steps { list-style: none; margin: 0.25rem 0 0; padding: 0; min-width: 0;
    display: flex; flex-wrap: wrap; gap: 0.25rem; }
.sj-flow-chain-step { display: inline-flex; align-items: center; gap: 0.3125rem;
    min-width: 0; max-width: 100%; padding: 0.0625rem 0.375rem;
    border: 1px solid var(--border); border-radius: 0.375rem; background: var(--background); }
.sj-flow-chain-n { flex: 0 0 auto; font-size: 0.625rem; font-variant-numeric: tabular-nums;
    color: var(--muted-foreground); }
.sj-flow-chain-tool { font-family: ui-monospace, monospace; font-size: 0.6875rem;
    color: var(--foreground); min-width: 0; overflow-wrap: break-word; }

/* --- empty state and the copy affordance --------------------------------------------------- */
.sj-flow-empty { border: 1px dashed var(--border); border-radius: 0.5rem; padding: 1rem;
    text-align: center; font-size: 0.8125rem; color: var(--muted-foreground); }
.sj-flow-copyrow { display: flex; align-items: center; justify-content: flex-end;
    gap: 0.375rem; margin-top: 0.5rem; }
.sj-flow-copynote { margin-top: 0.125rem; text-align: right; font-size: 0.625rem;
    line-height: 1.35; color: var(--muted-foreground); }

/* ── Chat bubble markdown (SjChatBubble) — P1-2 ──────────────────────────────
   Tailwind Preflight strips list markers/margins; restore just enough so an
   assistant turn's markdown reads as real lists/bold/code, not a wall of text.
   NOT under .sj-admin-* — this is a standalone chat surface, unrelated to the
   admin tabs CSS contract. */
.sj-chat-prose > :first-child { margin-top: 0; }
.sj-chat-prose > :last-child { margin-bottom: 0; }
.sj-chat-prose p { margin: 0 0 0.5rem; }
.sj-chat-prose ul { list-style: disc; padding-left: 1.25rem; margin: 0 0 0.5rem; }
.sj-chat-prose ol { list-style: decimal; padding-left: 1.25rem; margin: 0 0 0.5rem; }
.sj-chat-prose li { margin: 0.125rem 0; }
.sj-chat-prose strong { font-weight: 600; }
.sj-chat-prose code { font-family: ui-monospace, monospace; font-size: 0.85em;
    background: var(--muted); padding: 0.05rem 0.3rem; border-radius: 0.25rem; }
.sj-chat-prose a { color: var(--primary); text-decoration: underline; }
.sj-chat-prose pre { background: var(--background); padding: 0.6rem; border-radius: 0.375rem;
    overflow-x: auto; margin: 0 0 0.5rem; }
.sj-chat-prose pre code { background: transparent; padding: 0; }

/* Typing indicator dots (SjChatBubble IsTyping) */
.sj-chat-typing-dot { width: 0.4rem; height: 0.4rem; border-radius: 9999px;
    background: currentColor; opacity: 0.4; display: inline-block;
    animation: sj-chat-typing 1s ease-in-out infinite; }
@keyframes sj-chat-typing {
    0%, 60%, 100% { opacity: 0.25; transform: translateY(0); }
    30% { opacity: 0.9; transform: translateY(-2px); }
}

/* Native compose / freeform inputs — a guaranteed-visible keyboard-focus ring off the theme --ring
   token. The shadcn focus-ring utilities (focus-visible:ring-ring) are NOT in the committed JIT
   bundle, so relying on them with outline:none would leave these native <textarea>/<input> with no
   focus indicator (keyboard-a11y regression). Hand-authored here so it works regardless of the JIT. */
.sj-chat-input:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }


/* ---------------------------------------------------------------------------
   Agent tool picker (opt-in "advanced" mode on ToolSelector / AgentToolPicker).

   Every rule here is UNLAYERED and hand-authored rather than composed from
   Tailwind utilities, for two reasons this project has hit before:
     * variant+semantic utilities (hover:bg-muted/50, hover:text-primary, ...)
       compile to NOTHING in this bundle, so an interactive chip built from them
       would have no hover or active state at all;
     * unlayered rules beat the Tailwind layer cascade, so a base rule here is
       not silently outranked by a utility on the same element.
   Colors come from the theme CSS variables (App.razor), so these follow the
   Portal dark-only palette without hard-coding a single hex value.
   --------------------------------------------------------------------------- */

/* --- A2: facet bar ------------------------------------------------------- */
.sj-facet-bar { display: flex; flex-direction: column; gap: 0.5rem;
    padding: 0.5rem 0.625rem; border: 1px solid var(--border);
    border-radius: 0.5rem; background: var(--card); }
.sj-facet-label { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.05em; color: var(--muted-foreground); padding-top: 0.3rem; }

.sj-facet-chip { display: inline-flex; align-items: center; gap: 0.375rem;
    padding: 0.15rem 0.5rem; font-size: 0.75rem; line-height: 1.25rem;
    border: 1px solid var(--border); border-radius: 9999px;
    background: var(--background); color: var(--foreground);
    cursor: pointer; transition: background-color 120ms, border-color 120ms; }
.sj-facet-chip:hover { background: var(--muted); }
.sj-facet-chip:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
.sj-facet-chip.is-on { background: var(--primary); color: var(--primary-foreground);
    border-color: var(--primary); }
/* The destructive facet is tinted only when ON: an always-red chip in a filter row
   reads as a warning about the page rather than as a filter the user can pick. */
.sj-facet-chip-destructive.is-on { background: var(--destructive);
    color: var(--destructive-foreground); border-color: var(--destructive); }
.sj-facet-count { font-size: 0.6875rem; opacity: 0.7; font-variant-numeric: tabular-nums; }
/* Hairline between the access facets and the plan-tier facets (1b.1). They share one
   wrap row to keep the bar short inside the Edit Tools dialogs, so the two groups need
   a cheap visual boundary; a 1px rule costs no row height, unlike a heading would. */
.sj-facet-divider { display: inline-block; width: 1px; align-self: stretch;
    min-height: 1.25rem; background: var(--border); margin: 0 0.125rem; }

/* 1b.5: per-row "this connector's credential is unhealthy" marker. Warning-toned but NOT
   destructive-red — the row is still perfectly selectable and the grant is still valid, so
   the colour must not read as "you cannot have this". Unlayered for the usual reason: a
   variant/semantic Tailwind pair (text-warning, text-destructive/70) compiles to nothing in
   this bundle, so a utility-built version would render with no colour at all. */
.sj-picker-attention { display: inline-flex; align-items: center; gap: 0.25rem;
    color: color-mix(in oklab, var(--destructive) 70%, var(--muted-foreground)); }
.sj-facet-more { color: var(--muted-foreground); border-style: dashed; }
.sj-facet-scope { background: var(--muted); }
.sj-facet-scope-clear { color: var(--muted-foreground); line-height: 1; cursor: pointer;
    background: none; border: 0; padding: 0 0 0 0.125rem; font-size: 0.9rem; }
.sj-facet-scope-clear:hover { color: var(--foreground); }

/* --- A3: row "only this connector" chevron -------------------------------- */
/* Hidden until the row is hovered or the button itself is keyboard-focused, so it
   never competes with the badges for attention. focus-visible must win, or the
   control would look unreachable for keyboard users even while focused. */
.sj-picker-only-button { display: inline-flex; align-items: center; justify-content: center;
    height: 1.25rem; width: 1.25rem; border-radius: 0.25rem; border: 0;
    background: none; color: var(--muted-foreground); cursor: pointer; opacity: 0; }
[role="option"]:hover .sj-picker-only-button { opacity: 1; }
.sj-picker-only-button:focus-visible { opacity: 1; outline: 2px solid var(--ring); outline-offset: 1px; }
.sj-picker-only-button:hover { background: var(--muted); color: var(--foreground); }

/* --- B1: selected-tool groups -------------------------------------------- */
.sj-selected-group { border: 1px solid var(--border); border-radius: 0.5rem;
    padding: 0.375rem 0.5rem; background: var(--card); }
.sj-selected-group-header { display: flex; align-items: center;
    justify-content: space-between; gap: 0.5rem; flex-wrap: wrap; }
.sj-selected-group-toggle { display: inline-flex; align-items: center; gap: 0.375rem;
    font-size: 0.75rem; background: none; border: 0; padding: 0.125rem 0;
    color: var(--foreground); cursor: pointer; min-width: 0; }
.sj-selected-group-toggle:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }

/* --- B2: per-connector colour swatches ----------------------------------- */
/* Assigned over the connectors PRESENT in a selection (8 or fewer), never globally
   — 44 connectors exceed any accessible palette. Colour is always paired with the
   connector NAME, so it is a scanning aid and never the only carrier of meaning. */
.sj-connector-swatch { display: inline-block; width: 0.5rem; height: 0.5rem;
    border-radius: 9999px; flex: 0 0 auto; }
.sj-connector-swatch-0 { background: #60a5fa; }
.sj-connector-swatch-1 { background: #34d399; }
.sj-connector-swatch-2 { background: #f472b6; }
.sj-connector-swatch-3 { background: #fbbf24; }
.sj-connector-swatch-4 { background: #a78bfa; }
.sj-connector-swatch-5 { background: #22d3ee; }
.sj-connector-swatch-6 { background: #fb923c; }
.sj-connector-swatch-7 { background: #a3e635; }

/* --- B3: least-privilege review table ------------------------------------ */
/* A plain semantic table, NOT BbDataTable — its InitialPageSize is read once in
   OnInitialized, so a live-changing row count (every click here changes one) goes
   stale until a page refresh. The wrapper owns the horizontal scroll. */
.sj-review-table { width: 100%; border-collapse: collapse; font-size: 0.75rem; }
.sj-review-table th, .sj-review-table td { padding: 0.375rem 0.625rem; white-space: nowrap; }
.sj-review-table thead th { font-weight: 600; color: var(--muted-foreground);
    text-align: left; border-bottom: 1px solid var(--border); }
.sj-review-table tbody th { font-weight: 500; text-align: left; }
.sj-review-table tbody tr + tr { border-top: 1px solid var(--border); }
.sj-review-table tfoot { border-top: 2px solid var(--border); }
.sj-review-table tfoot th, .sj-review-table tfoot td { font-weight: 600; }
.sj-review-table td { font-variant-numeric: tabular-nums; }
.sj-review-destructive { color: var(--destructive); font-weight: 600; }

/* --- B3: Chips / Review segmented toggle --------------------------------- */
/* Deliberately NOT BbTabs: three hosts of this picker render inside the
   /admin/support tab chain, and a nested BbTabs there is a documented regression. */
.sj-segmented { display: inline-flex; border: 1px solid var(--border);
    border-radius: 0.375rem; overflow: hidden; }
.sj-segment { font-size: 0.6875rem; padding: 0.15rem 0.5rem; background: var(--background);
    color: var(--muted-foreground); border: 0; cursor: pointer; }
.sj-segment + .sj-segment { border-left: 1px solid var(--border); }
.sj-segment:hover { background: var(--muted); }
.sj-segment:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
.sj-segment.is-on { background: var(--primary); color: var(--primary-foreground); }

/* --- C1: suggestion panel ------------------------------------------------ */
.sj-suggestion-list { display: flex; flex-direction: column; gap: 0.125rem;
    max-height: 22rem; overflow-y: auto; }
.sj-suggestion-row { display: flex; align-items: flex-start; gap: 0.625rem;
    padding: 0.25rem 0.375rem; border-radius: 0.375rem; }
.sj-suggestion-row:hover { background: var(--muted); }
.sj-suggestion-check { background: none; border: 0; padding: 0; margin-top: 0.125rem;
    color: var(--muted-foreground); cursor: pointer; flex: 0 0 auto; }
.sj-suggestion-check[aria-checked="true"] { color: var(--primary); }
.sj-suggestion-check:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px;
    border-radius: 0.25rem; }
/* Monospace so the padded star/dot string stays a fixed-width relevance bar rather
   than a ragged one whose length varies with the proportional font glyph widths. */
.sj-suggestion-stars { font-family: ui-monospace, monospace; font-size: 0.6875rem;
    letter-spacing: 0.05em; color: var(--muted-foreground); }

/* ── Connector logo plate (ConnectorLogo.razor) ────────────────────────────
 * Two things the Tailwind JIT cannot express for this component.
 *
 * 1) The dark-variant swap keys on the `.dark` CLASS that App.razor puts on
 *    <html>. It deliberately does NOT use Tailwind's `dark:` variant, because
 *    this build compiles that to `@media (prefers-color-scheme: dark)` (grep
 *    tailwind.output.css) — the OS preference, which is NOT what drives the
 *    Portal's theme. A `dark:hidden` / `dark:block` pair would swap the vendor
 *    mark on the wrong signal, showing the light mark on a dark page for every
 *    user whose OS is set to light.
 *
 * 2) The monogram tint. Spec §4 asks for the letters "on the vendor's brand
 *    color"; no brand colors are recorded anywhere, so each connector gets a
 *    stable stop derived from its logo slug (ConnectorMonogram.TintIndex).
 *    One hue channel drives both the wash and the letters, so a stop is a
 *    single number rather than a hand-tuned pair, and both themes stay legible
 *    by construction. Kept low-chroma on purpose — §5 rejects accent-colored
 *    plates, and this must read as a palette, not as decoration.
 * ──────────────────────────────────────────────────────────────────────── */

/* CORRECTED 2026-08-05: this rule is a NO-OP today, and is kept deliberately.
 * Its original comment claimed no preflight border-color reset exists; that is
 * false — Blueprint's stylesheet carries `* { border-color: var(--border) }`
 * in its @layer base, so a bare `border` utility already resolves to the theme
 * border rather than to Tailwind's `currentColor` default.
 *
 * What the rule buys is independence from a global in a stylesheet this repo
 * does not control. If a Blueprint upgrade drops or re-scopes that preflight,
 * `currentColor` takes over — and on the monogram plate that is the TINT, so
 * the grid would fill with bright ringed plates next to neutral ones, in one
 * theme more than the other, with nothing failing to say so. Being explicit is
 * one line; diagnosing that later is not. */
.sj-connector-plate { border-color: var(--border); }

.sj-connector-mark-dark { display: none; }
.dark .sj-connector-mark-light { display: none; }
.dark .sj-connector-mark-dark { display: block; }

.sj-connector-monogram {
  background-color: oklch(0.62 0.14 var(--sj-mono-hue) / 0.14);
  color: oklch(0.45 0.13 var(--sj-mono-hue));
}
.dark .sj-connector-monogram {
  background-color: oklch(0.70 0.13 var(--sj-mono-hue) / 0.16);
  color: oklch(0.80 0.11 var(--sj-mono-hue));
}

/* The monogram letters WITHOUT the tinted plate under them, for the eight
 * connectors whose mark is legible in one theme only (ConnectorMarkThemes):
 * the vendor mark renders in its theme and the monogram stands in for the
 * other, both inside one plate.
 *
 * That plate stays NEUTRAL in both themes rather than switching to the tinted
 * monogram fill, for two reasons. A background that changed on theme toggle
 * would single those eight tiles out of a grid where every other plate holds
 * still. And winning against `.bg-muted\/40` in one theme while losing to it in
 * the other comes down to source order between two equal-specificity
 * single-class rules in this same file — the cascade trap this stylesheet has
 * been bitten by before.
 *
 * Colours are the two stops `.sj-connector-monogram` already uses, so a
 * stand-in monogram and a plain one read identically. --sj-mono-hue is
 * inherited from the tint class on the plate, which is why that class is
 * applied there in both branches even though it paints nothing on its own. */
.sj-connector-monogram-letters { color: oklch(0.45 0.13 var(--sj-mono-hue)); }
.dark .sj-connector-monogram-letters { color: oklch(0.80 0.11 var(--sj-mono-hue)); }

/* Ten stops around the wheel. The count is mirrored by
 * ConnectorMonogram.TintCount. A mismatch leaves --sj-mono-hue unset, which
 * makes both declarations invalid at computed-value time: the plate falls back
 * to a transparent fill and inherited letters. It still reads, so nothing
 * fails and nobody notices the tint went missing — hence the test pin. */
.sj-connector-monogram-tint-0 { --sj-mono-hue: 264; }
.sj-connector-monogram-tint-1 { --sj-mono-hue: 226; }
.sj-connector-monogram-tint-2 { --sj-mono-hue: 196; }
.sj-connector-monogram-tint-3 { --sj-mono-hue: 168; }
.sj-connector-monogram-tint-4 { --sj-mono-hue: 142; }
.sj-connector-monogram-tint-5 { --sj-mono-hue: 96; }
.sj-connector-monogram-tint-6 { --sj-mono-hue: 62; }
.sj-connector-monogram-tint-7 { --sj-mono-hue: 28; }
.sj-connector-monogram-tint-8 { --sj-mono-hue: 350; }
.sj-connector-monogram-tint-9 { --sj-mono-hue: 306; }

/* The "needs attention" line on a connector tile. Keyed on the `.dark` CLASS
 * for the same reason the mark swap above is: Tailwind's `dark:` variant
 * compiles to @media (prefers-color-scheme: dark) here, which is the OS
 * setting rather than the Portal's theme.
 *
 * The two stops are not the same amber. amber-500 on a light card is roughly
 * 2:1 against the background — decorative-only contrast on text that is the
 * whole point of the state — so light mode drops to amber-700, and dark mode
 * rises to amber-400 rather than staying dark on a dark card. The DOT beside
 * it keeps bg-amber-500 in both themes: it is decoration next to the text, so
 * it answers to the non-text contrast rule. */
.sj-connector-attention { color: #b45309; }
.dark .sj-connector-attention { color: #fbbf24; }

/* ── The connector CARD (spec §3, amended by owner ruling 2026-08-05) ──────
 *
 * "the cards are silly small right now and them being taller when configured to
 * show the number looks stupid and uneven […] the connectors names are cut off
 * in some cases too."
 *
 * WHY THIS IS A NAMED RULE AND NOT UTILITIES. Two independent reasons, either
 * sufficient. First, none of these values exist in the committed
 * tailwind.output.css, so each would depend on a JIT pass having seen its exact
 * string in a scanned file — the silent-no-op failure that cost this very grid
 * its columns (see .sj-connector-grid below) and the tile its hover border.
 * Second, and the reason it would still be a named rule if they all existed:
 * the card's height is DERIVED from the boxes stacked inside it, and three
 * unrelated utility strings cannot state that they add up. They are:
 *
 *     padding-block  0.875rem × 2   = 1.75rem
 *     logo plate                      3.5rem
 *     name, 0.5rem margin + 1–2 lines @ 1.25rem
 *     status zone (meter + count)     1.75rem
 *
 * At 10.25rem that leaves ~24px of breathing room under a one-line name and
 * ~4px under a two-line one, which is the right way round: measured at the
 * 12rem track below, every one of the 45 display names fits on one line —
 * including "Blackpoint CompassOne" and "CyberCNS / ConnectSecure", the two the
 * owner reported as cut off — so the common case is the one that should look
 * composed, and the second line is headroom rather than the norm.
 *
 * WHAT THIS RULE MUST NEVER SET. app.css is unlayered, so it outranks every
 * Tailwind utility on the same element — and the card's STATE is expressed in
 * utilities (ConnectorTilePresentation: `cursor-default opacity-60` for coming
 * soon, `cursor-pointer hover:border-primary/50 hover:shadow-md
 * motion-safe:hover:-translate-y-0.5` for interactive, `ring-1
 * ring-amber-500/60` for needs-attention). Adding `cursor`, `opacity`,
 * `border-color`, `box-shadow`, `transform` or a ring property here would
 * flatten those states while leaving their classes sitting in the DOM, which is
 * as close to invisible as a regression gets. Box only. */
.sj-connector-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  position: relative;
  width: 100%;
  height: 10.25rem;
  padding: 0.875rem 0.75rem;
  text-align: center;
}

/* The "Free" corner ribbon on a not-configured card (owner ruling, 2026-08-06).
 *
 * `position: absolute` against the card is the whole reason `.sj-connector-tile`
 * above declares `position: relative`, and it is also what keeps the card 164px
 * in this state like every other: an out-of-flow box contributes nothing to the
 * flex column's height, so the ribbon cannot make an unconfigured card taller
 * than the configured one beside it — which is the property the entire reserved
 * status zone exists to protect.
 *
 * A TAB, not a rotated sash. A 45-degree ribbon has to have its ends clipped by
 * an `overflow: hidden` on the card, and the card deliberately declares no
 * overflow — adding one would clip anything that ever legitimately sits proud of
 * the card, and there is no way to clip only the ribbon. Nesting the tab into
 * the existing corner instead gets the same "folded corner" read with no
 * clipping, no transform, and nothing that can round differently on one engine:
 * its top-right radius matches the card's own `rounded-xl` (0.75rem) so the two
 * curves are one curve, and only the inner corner is rounded away from it.
 *
 * THE PLATE IS THE SAME color-mix() IDIOM AS THE METER TRACK, and for the same
 * measured reason. This rule first shipped `background: var(--secondary)`, with
 * a comment claiming that pairing came from BbBadge's secondary variant. Both
 * halves were wrong. Measured on canvas from the real tokens: `--secondary` is
 * rgb(38,38,38) on a `--card` of rgb(23,23,23) — a ratio of **1.18**, which is
 * the exact number the meter-track comment 150 lines below calls invisible. The
 * LABEL was fine (14.5 against that plate), so what shipped was the word FREE
 * floating in the corner with no visible tab behind it at all. And BbBadge pairs
 * `--secondary` with `--secondary-foreground` on the PAGE background, not with
 * `--muted-foreground` on a card — it was never this pairing.
 *
 * 40% measures **3.73** against the card, clearing the 3:1 non-text threshold,
 * while `--foreground` text on it measures **4.61**, clearing 4.5:1 for text
 * this small. Those two move in opposite directions — 50% lifts the plate to
 * 5.06 but drops the label to 3.40, below the text floor — so 40% is where both
 * clear, which is the claim the meter track's own comment should have made and
 * did not (its 28% leaves the track at 2.44; see the correction there).
 *
 * The text is `--foreground`, NOT `--muted-foreground`: muted on a 40% plate
 * measures 1.86 and would be less legible than the invisible-plate version this
 * replaces. A quiet ribbon is achieved by the SIZE and weight, not by dimming
 * the one thing on it that has to be read.
 *
 * `var(--secondary)` above the mix is a parse-time fallback for an engine with
 * no color-mix() — the same two-declaration idiom the meter track uses, and it
 * covers the same narrow case: it does NOT rescue an undeclared `--foreground`,
 * which is invalid at computed-value time and yields no background at all. That
 * token is pinned in App.razor by a test for exactly this reason. */
.sj-connector-tile-ribbon {
  position: absolute;
  top: 0;
  right: 0;
  padding: 0.125rem 0.4375rem;
  border-top-right-radius: 0.75rem;
  border-bottom-left-radius: 0.5rem;
  background: var(--secondary);
  background: color-mix(in oklch, var(--foreground) 40%, transparent);
  color: var(--foreground);
  font-size: 0.625rem;
  font-weight: 600;
  line-height: 1rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  pointer-events: none;
}

/* The configure affordance, in the reserved status zone of a not-configured
 * card. Muted and unadorned: the CARD is the button, so anything that read as a
 * control here would advertise a second target that does not exist.
 *
 * `max-width: 100%` with the `truncate` utility beside it, because the zone is a
 * column flex container with `align-items: center` — a flex item there is sized
 * to its content and would otherwise push past the card's padding on a narrow
 * track rather than ellipsing. */
.sj-connector-tile-cta {
  max-width: 100%;
  color: var(--muted-foreground);
  font-size: 0.75rem;
  line-height: 1rem;
}

/* The logo plate. ConnectorLogo requires a DEFINITE height in SizeClass — the
 * mark inside is sized as a percentage of it, so an indefinite value collapses
 * the mark to nothing while the plate still draws its border. `h-14 w-14` would
 * have said this, but `h-14` is in NEITHER committed bundle (`w-14` is in
 * Blueprint's, which is exactly the half-present trap that makes utilities the
 * wrong tool here). At 3.5rem the mark box measures ~36px against ~25px before,
 * which is what makes a wide wordmark legible rather than a hairline. */
.sj-connector-tile-logo { height: 3.5rem; width: 3.5rem; }

/* The name, clamped to two lines — the ruling's "the connectors names are cut
 * off in some cases too". The clamp is a CAP and not a target: it is what stops
 * a third line, which the card has no room for.
 *
 * There is deliberately NO reserved two-line min-height here, and that is worth
 * stating because reserving one is the obvious-looking way to keep the grid
 * even. It buys nothing: uniformity comes from the card's fixed height plus the
 * bottom-anchored status zone below, and with those two in place every card
 * already aligns on all three of the things a reader can see — logos at the top,
 * names starting directly under them, status text on one baseline. A reserved
 * second line would only add 20px of blank under the ~45 names that fit on one,
 * which is exactly the airless look the ruling complained about.
 *
 * `overflow-wrap: break-word` and NOT `anywhere`/`break-all`: those two feed
 * min-content width, which inside a grid track is how a column of unbreakable
 * tokens collapses to one character per line (the twice-reported /docs bug).
 * `break-word` does not, and it only engages for a single word too long for the
 * card — "ConnectWise Automate" simply wraps at its space. */
.sj-connector-tile-name {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  overflow-wrap: break-word;
  width: 100%;
  margin-top: 0.5rem;
}

/* The reserved status zone, at the foot of every card in every state.
 *
 * `margin-top: auto` bottom-anchors it, and `min-height` reserves it even when
 * the card has nothing to say. Between them they are the whole answer to "them
 * being taller when configured looks stupid and uneven": a not-configured card
 * is the same height as a metered one, and a metered card's count sits on the
 * same baseline as its unmetered neighbour's rather than floating above it.
 *
 * The min-height is the tallest occupant — meter 0.375 + gap 0.375 + a 1rem
 * text line = 1.75rem — so nothing in it can push the card taller. */
.sj-connector-tile-status {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: 0.375rem;
  width: 100%;
  min-height: 1.75rem;
  margin-top: auto;
}

/* The quota meter — a track and a fill, both plain spans inside the card's
 * <button>. It replaced a BbProgress that the SECTION LIST rendered as a
 * sibling of the card; that is what made the grid uneven, since the list item
 * then measured card-plus-bar while the card itself stayed fixed.
 *
 * Nesting it costs no accessibility, because what could not live in a button
 * was the ROLE rather than the picture: `role="progressbar"` is a widget, and a
 * widget inside a button is neither valid nor reachable. These carry no role,
 * the pair is aria-hidden, and the number is already in the button's accessible
 * name — so a screen reader hears exactly what it heard before.
 *
 * The FILL sets no background: its colour arrives as bg-green-500 /
 * bg-amber-500 / bg-red-500 from ConnectorTilePresentation.DotClass, the same
 * mapping the dot beside it uses, and an unlayered `background` here would
 * outrank all three and paint every meter one colour. The TRACK sets one, and
 * that is fine — it is a different element.
 *
 * THE TRACK IS NOT `var(--muted)`, and that is measured rather than taste. In
 * the dark theme — the only one that ships — `--muted` is oklch(0.269 0 0) on a
 * `--card` of oklch(0.205 0 0): rgb(38,38,38) on rgb(23,23,23), a contrast
 * ratio of **1.18**. The unfilled remainder is invisible, so the meter reads as
 * a floating stub rather than as a fraction — which loses exactly what the
 * owner asked for, "where they are in their quota". A quota bar without a
 * visible denominator is a coloured dash.
 *
 * 28% of `--foreground` measures 2.44 against the card while leaving the FILL
 * at 3.31 against the track. Both numbers matter and they trade against each
 * other: brightening the track past ~30% pushes the fill under the 3:1 non-text
 * threshold, and the fill is the actual datum. 28% is where both clear.
 * `--foreground` rather than a literal so the track inverts with the theme;
 * the light theme lands at 1.92, which is weaker but unshipped (App.razor
 * hard-codes `<html class="dark">` with no toggle).
 *
 * The `var(--muted)` line above it is a deliberate fallback, not leftovers —
 * but it covers a NARROWER case than the obvious reading, and the difference
 * matters because it decides what else has to be guarded:
 *
 *  - It covers an engine that cannot PARSE `color-mix()`. That is invalid at
 *    parse time, so the declaration never enters the cascade and the
 *    `var(--muted)` above it stands. This is the case the two-declaration
 *    idiom exists for, and the only one it handles.
 *  - It does NOT cover an undeclared `--foreground`. A failed `var()`
 *    substitution is invalid at COMPUTED-VALUE time, which is a different and
 *    later stage: the property is set to `unset` — for a non-inherited
 *    property like `background`, its initial value, i.e. transparent — and
 *    earlier declarations in the same rule are NOT resurrected. So that
 *    failure yields NO track at all, which is worse than the 1.18 contrast
 *    this rule replaced.
 *
 * That is why the token is pinned in App.razor by a test rather than left to
 * the fallback, and it is the same reason the hand-rolled
 * `hover:border-primary/50` rule pins `--primary`. */
.sj-connector-meter {
  display: block;
  width: 100%;
  height: 0.375rem;
  overflow: hidden;
  border-radius: 9999px;
  background: var(--muted);
  background: color-mix(in oklch, var(--foreground) 28%, transparent);
}

.sj-connector-meter-fill {
  display: block;
  height: 100%;
  border-radius: 9999px;
}

/* Spec §5: all motion respects prefers-reduced-motion. Stated as an opt-IN
 * rather than as a transition plus a reduced-motion override, so a user who
 * asked for no movement gets none rather than getting it briefly and then
 * having it cancelled. */
@media (prefers-reduced-motion: no-preference) {
  .sj-connector-meter-fill { transition: width 150ms ease-out; }
}

/* ── The browse tile grid (spec §3, "Tiles — fixed ~192–210×164px", as
 *    amended by owner ruling A2 on 2026-08-05; the line this used to quote,
 *    "fixed ~160×140px", is the pre-amendment text) ─────────────────────────
 *
 * Rendered by ConnectorSectionList's Grid fragment and, identically, by the
 * loading skeleton in Connectors.razor. It replaces
 * `grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6`, which
 * rendered every tile ~700px wide at 1440px — TWO columns, at every width.
 *
 * The utilities are all emitted; the cascade is what loses. Blueprint's base
 * `.grid-cols-2` sits in blazorblueprint.css's top-level `@layer utilities`
 * while the JIT's `sm:/md:/lg:grid-cols-N` land in the nested `utilities.app`
 * sublayer, and a parent layer outranks its own sublayer regardless of media
 * query — so the base class beat all three overrides. Identical trap to
 * `.sj-stat-grid` and `.sj-cfg-tabs` above; the fix is the same UNLAYERED
 * rule, which wins because app.css loads last and declares no @layer.
 *
 * `auto-fill`, NOT `auto-fit`, and that is load-bearing rather than stylistic:
 * auto-fit COLLAPSES the empty tracks, so a single-member category — there are
 * five of them — would stretch its one tile across the whole row while the
 * tile beside it in the next section stayed at track width. auto-fill keeps the
 * tracks, so every card in every section is the same size, which is the entire
 * point of a fixed card (see `.sj-connector-tile` above).
 *
 * The track MINIMUM was 9.5rem and is 12rem (owner ruling, 2026-08-05: "the
 * cards are silly small"). It is a minimum and not a width — the `1fr` still
 * lets a row's cards share the leftover space — so the effect is fewer, wider
 * columns rather than a fixed jump: about five at a 1440px window instead of
 * seven, and the two-line name the card now allows gets ~11rem of inner width
 * to use, which is what stops "ConnectWise Automate" ellipsising.
 *
 * The gap matches the `gap-3` the markup used to carry, so card rhythm is
 * unchanged; it is stated here rather than left as a utility so the grid's
 * geometry cannot be half-removed by a class edit. */
.sj-connector-grid {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
}

/* ── The DASHBOARD's connector grid (owner ruling, 2026-08-06) ──────────────
 *
 * "use the same card convention on the dashboard for the client to replace the
 *  one giant column with one status per row of connector health. we could
 *  combine the connector subscriptions and connector health stuff."
 *
 * Same convention, different card, and the difference is the content rather
 * than taste. The browse tile is a fixed-height SQUARE holding a logo, a name
 * and one status line, because 45 of them have to be scannable at a glance.
 * A dashboard row carries four facts (name, plan, health-plus-freshness, quota),
 * so it lays the plate beside the text instead of above it — which is also what
 * lets the card size to its content and keeps a tenant with three connectors
 * from getting three squares of mostly empty space.
 *
 * `auto-fill` and a 17rem minimum, not `auto-fit`: auto-fit collapses empty
 * tracks, so a tenant with one connector would get one card stretched across
 * the whole dashboard. Same reasoning, same choice, as .sj-connector-grid.
 *
 * A NAMED rule rather than responsive `grid-cols-*` utilities for the reason
 * recorded on that rule: Blueprint's BASE column utility sits in a parent
 * utilities layer while the JIT's `sm:/md:/lg:` variants land in the nested
 * utilities.app sublayer, and a parent layer outranks its own sublayer
 * regardless of media query — so the base class beats every breakpoint
 * override. app.css is unlayered and loads last, so this wins outright. */
.sj-dash-connector-grid {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
}

/* The card. `align-items: flex-start` so the logo plate stays pinned to the top
 * of a card whose text block grew a second line — centring it would make the
 * plate drift down as the meter appears, which is exactly the "uneven" read the
 * browse grid was reworked to remove.
 *
 * `height: 100%` makes every card in a row the same height, because the GRID
 * ITEM is what has to be uniform: a card that sized to its own content would
 * leave a shorter neighbour floating against the top of a taller row.
 *
 * UNLIKE the browse tile, this rule DOES own its interactive chrome — the hover
 * border, the shadow and the focus ring are all below, in `:hover` and
 * `:focus-visible` rules on this same class. That is the opposite of
 * `.sj-connector-tile`, which must leave those properties alone, and the
 * difference is real rather than an inconsistency: the browse tile's state
 * (coming-soon dimming, needs-attention ring) arrives as per-status Tailwind
 * utilities that an unlayered app.css rule would silently outrank, so its box
 * rule has to stay out of their way. A dashboard card has no per-status chrome —
 * every card here is an ordinary link — so there is nothing to lose the cascade
 * to, and keeping the states in the same named rule is what stops them being
 * half-removed by an edit to a utility string.
 *
 * The hover colour is hand-rolled with color-mix() rather than written as
 * `hover:border-primary/50`: that is a slash-opacity modifier on a Blueprint
 * theme colour, which this JIT cannot synthesize and which would emit nothing. */
.sj-dash-connector-card {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  height: 100%;
  padding: 0.75rem;
  border-radius: 0.75rem;
  border: 1px solid var(--border);
  background: var(--card);
  text-align: left;
  text-decoration: none;
  color: inherit;
  transition: border-color 150ms ease-out, box-shadow 150ms ease-out;
}

.sj-dash-connector-card:hover {
  border-color: color-mix(in oklch, var(--primary) 50%, transparent);
  box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}

.sj-dash-connector-card:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

/* The plan tier, as a quiet caption rather than a BbBadge. A badge here would
 * compete with the health dot for the eye on a card whose whole point is the
 * health, and the tier is the slowest-changing fact on it. */
.sj-dash-connector-plan {
  flex-shrink: 0;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted-foreground);
}

/* ── The browse header's scroll-jump chip strip (spec §3) ──────────────────
 *
 * Thirteen chips — Your Stack plus twelve categories — do not fit one laptop
 * row. Left to wrap they add two or three rows to a STICKY header, which eats
 * the viewport and, worse here, moves the landing point the anchor rule below
 * is sized against. Same failure and same fix as `.sj-admin-tablist-scroll`
 * further up this file, which exists because a wrapped tab strip overlapped
 * the panel beneath it.
 *
 * `flex-wrap` is deliberately absent rather than set to `nowrap`: nowrap is
 * the initial value, and Tailwind's `flex-nowrap` utility is NOT in this
 * build's output, so writing it in markup would style nothing (the same class
 * of silent no-op as `backdrop-blur`, which cost the builder footer its
 * surface once — see the sticky-surface note above).
 *
 * overflow-y is pinned to `hidden` because a non-visible overflow-x computes
 * the other axis to `auto`, which would hang a phantom vertical scrollbar off
 * the strip. The block padding is what then keeps the chips' focus ring from
 * being clipped by that same hidden axis — the strip is keyboard-reachable,
 * so the ring has to survive.
 *
 * THE SCROLLBAR ITSELF IS HIDDEN (owner-reported, 2026-08-05: a thin bar ran
 * the full width of the header and read as page chrome). `scrollbar-width:
 * thin` was worse than no rule at all on Windows, where overlay scrollbars are
 * off by default and the track is therefore painted permanently, overflow or
 * not. Hiding it removes the artifact without moving the header by a pixel,
 * which is what keeps the anchor rule below correct.
 *
 * Hiding the bar does NOT strand a chip. Focus scrolls its own chip into view,
 * so the whole strip stays keyboard-reachable; a trackpad swipes it; and
 * Chromium maps a vertical wheel onto the horizontal axis for an element that
 * can only scroll horizontally. WRAPPING the strip instead — the other way to
 * kill the bar — is an owner ruling, not a CSS choice: it would add two rows
 * to a sticky header at 1024px and about seven at 375px, and the anchor rule
 * below would have to be re-derived from a real measurement rather than an
 * estimate. Recorded, not taken. */
.sj-connector-chip-strip {
  overflow-x: auto;
  overflow-y: hidden;
  padding-block: 0.25rem;
  scrollbar-width: none;
}

.sj-connector-chip-strip::-webkit-scrollbar {
  display: none;
}

/* Where a chip's jump lands.
 *
 * The header is `sticky top-0`, so a scroll that puts a section's heading at
 * the top of the viewport puts it UNDERNEATH that header. `scroll-margin-top`
 * on the target is what scrollIntoView honours natively, which is why the JS
 * module measures nothing and listens for no resize.
 *
 * Tailwind's `scroll-mt-*` utilities cannot do this job here: `scroll-margin`
 * appears NOWHERE in tailwind.output.css, so a `scroll-mt-36` in markup would
 * compile, render, and silently leave every jump hidden under the header.
 *
 * Two tiers because the header itself wraps. Below `lg` the search box, the
 * filter group and the coming-soon switch stack onto separate rows and the
 * header roughly doubles. Both values err GENEROUS on purpose: overshooting
 * leaves a gap above the heading, which is cosmetic, while undershooting hides
 * it under the header, which is the one failure this rule exists to prevent.
 * (The exact alternative — measuring the header at click time in JS — was
 * rejected as more moving parts for a cosmetic gain.) */
.sj-connector-section-anchor { scroll-margin-top: 15rem; }

@media (min-width: 1024px) {
  .sj-connector-section-anchor { scroll-margin-top: 9.5rem; }
}

/* Where a ScalePad facet tile's jump lands INSIDE the drawer.
 *
 * A separate class from the section anchor above, not a shared one, because the
 * two scroll inside different boxes. The section anchor clears the page's
 * sticky header, which is why it is measured in whole rems and has a wrapping
 * tier; this one scrolls the sheet's own `overflow-y-auto` body, which has no
 * sticky chrome above it at all — it only wants the block clear of the sheet's
 * top edge rather than flush against it. Reusing the 15rem value here would
 * push a short surface list mostly out of view.
 *
 * Same reason as above for it living here rather than as `scroll-mt-4` in
 * markup: `scroll-margin` appears NOWHERE in tailwind.output.css, so the
 * utility would compile, render and silently do nothing. */
.sj-connector-surface-anchor { scroll-margin-top: 1rem; }
/* Same job for the drawer-hosted config form's heading, which is the arrival target when Configure
   reveals the form: keeps it off the sheet's top edge instead of flush against it. Its own rule
   rather than a shared class because the two anchors sit in different components and a shared name
   would tie their spacing together for no reason. */
.sj-connector-form-anchor { scroll-margin-top: 1rem; }

/* Sidebar badge for connectors needing attention (credential-visibility F3).
 *
 * Themed through Blueprint's OWN documented custom properties rather than utility classes.
 * BbSidebarMenuBadge paints `background-color: var(--sidebar-badge-bg)` from a stylesheet rule,
 * and its defaults are `transparent` / `--sidebar-foreground` — i.e. plain text, which is not a
 * warning. A `bg-destructive` utility on its Class would have to WIN that rule on cascade order,
 * and Blueprint runs Class through TwMerge, which can only drop conflicting utility classes and
 * has nothing to drop here. Setting the variables on the element itself sidesteps the question
 * entirely: `var()` resolves from the element's own scope, so there is no specificity fight and
 * no !important. This is exactly the theming path the component documents.
 *
 * Unlayered on purpose (see the axis-gap and grid-cols notes above): rules in this file must be
 * able to answer Blueprint's own layered rules. */
.sj-attention-badge {
    --sidebar-badge-bg: var(--destructive);
    --sidebar-badge-color: var(--destructive-foreground);
}

/* ── /billing "Usage this month" — per-connector rows (billing-local) ──────────
 *
 * Geometry for BillingConnectorUsageCard. NAMED rules rather than utility strings,
 * for the same two reasons the connector tile's geometry is:
 *
 *  1. The values are not reliably in the committed bundles. `h-14` is in NEITHER
 *     bundle while `w-14` IS in Blueprint's — the half-present case that makes
 *     utilities the wrong tool for anything load-bearing. ConnectorLogo REQUIRES a
 *     definite height in SizeClass (the mark inside is sized as a percentage of it,
 *     so an indefinite value collapses the mark to nothing while the plate still
 *     draws its border), which is exactly the property that must not silently fail
 *     to compile.
 *  2. A row's height is DERIVED from what is stacked inside it; three unrelated
 *     utility strings cannot state that they add up.
 *
 * These rules set LAYOUT ONLY. app.css is unlayered, so it outranks every Tailwind
 * utility on the same element — nothing here may set a property a state utility
 * needs to win. The meter fill's colour arrives as bg-green-500 / bg-amber-500 /
 * bg-red-500 from BillingUsagePresentation.MeterFillClass, so `background` is banned
 * here exactly as it is on .sj-connector-meter-fill.
 *
 * The meter itself is deliberately NOT redefined: these rows reuse
 * .sj-connector-meter and .sj-connector-meter-fill verbatim, so the measured
 * track-contrast decision (color-mix(--foreground 28%) rather than var(--muted),
 * which measures 1.18 against the card and shows no denominator at all) is made
 * once for the whole product instead of being re-derived per page. */

.sj-billing-usage-list {
  display: flex;
  flex-direction: column;
}

/* A divider between rows rather than a card each: the connectors are one list, and
 * boxing each one costs a border plus padding per connector for no separation a
 * rule does not already provide. The last row drops its border so the list does not
 * end on a line. */
.sj-billing-usage-row {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--border);
}

.sj-billing-usage-row:first-child { padding-top: 0; }
.sj-billing-usage-row:last-child { padding-bottom: 0; border-bottom: 0; }

/* The logo plate. 2.5rem is ConnectorLogo's own h-10/w-10 default, restated as a
 * definite length so the row cannot depend on those two utilities being emitted. */
.sj-billing-usage-logo { height: 2.5rem; width: 2.5rem; }

/* Two rules were removed here on 2026-08-06 by the /billing combine, and are recorded
 * rather than silently dropped because both read as things a future edit might want back:
 *   .sj-billing-row-logo  — the 2rem plate in the Active Subscriptions table's Connector
 *                           cell. That table folded into the rows above, which carry the
 *                           2.5rem plate; there is no second, smaller mark on the page.
 *   .sj-billing-quiet     — the collapsed "N connectors made no calls" footnote. The quiet
 *                           and no-cycle groups render as FULL rows now: once a row carries
 *                           a price, a tier and a cancel link, collapsing it to a name in a
 *                           sentence would delete billing detail, not save space. */

/* ── Connector logo STRIP (ConnectorLogoStrip.razor) ────────────────────────
 *
 * The run of small plates on a template card and an automation card. Shared by
 * both, so a change here moves both surfaces together — which is the point: the
 * two card types were drawing connector identity in two different vocabularies
 * (text badges on templates, nothing at all on automations) before this.
 *
 * 2rem, not the 2.5rem the billing row and ConnectorLogo's own default use. A
 * card footer carries up to five of these beside a badge row, where 2.5rem
 * starts competing with the card title for weight; 2rem still reads as a logo
 * rather than a dot. It is a NAMED definite length for the reason every other
 * plate size in this file is one: the mark inside is sized as a fraction of the
 * plate, so an indefinite or half-edited height collapses the mark while the
 * plate keeps drawing its border.
 *
 * Known limitation, inherited rather than introduced here: the plate's 1px
 * border shrinks the content box before the mark's 2/3 fraction applies, so an
 * extremely wide wordmark (Umbrella is ~9.4:1) renders as a hairline at any
 * card-appropriate size. Square-ish app-icon marks — the large majority — read
 * fine. The fix, if it is ever wanted, belongs in ConnectorLogo's mark sizing
 * for wide aspect ratios, not in a per-caller size bump here. */
.sj-connector-strip-logo { height: 2rem; width: 2rem; }

/* The "+N" overflow chip. Sized to sit on the same optical line as the plates
 * beside it rather than to match their box, because it is a count and not a
 * mark — matching the plate exactly would read as a sixth connector. */
.sj-connector-strip-more {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  min-width: 2rem;
  padding: 0 0.375rem;
  border-radius: 0.5rem;
  border: 1px solid var(--border);
  background: color-mix(in oklch, var(--muted) 40%, transparent);
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--muted-foreground);
}

/* ── The TEMPLATE gallery grid + card ───────────────────────────────────────
 *
 * A NAMED unlayered rule rather than responsive `grid-cols-*` utilities, for the
 * cascade reason recorded on .sj-connector-grid and .sj-stat-grid above:
 * Blueprint's BASE column utility sits in a parent utilities layer while the
 * JIT's sm:/md:/lg: variants land in the nested utilities.app sublayer, and a
 * parent layer outranks its own sublayer regardless of media query — so the base
 * class beats every breakpoint override and the grid silently renders at one
 * width. app.css is unlayered and loads last, so this wins outright.
 *
 * `auto-fill`, NOT `auto-fit`, matching .sj-connector-grid: auto-fit collapses
 * empty tracks, so a filter that narrows the gallery to a single template would
 * stretch that one card across the whole page. The 20rem minimum is wider than
 * the connector browse grid's 12rem because these cards carry a summary
 * paragraph, two badge rows and a logo strip rather than a name and one status
 * line. */
.sj-template-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
}

/* `height: 100%` is what makes a row of cards uniform, and it has to sit on the
 * GRID ITEM: CSS grid rows size to their tallest item, so a card that sized to
 * its own content would leave every shorter card in that row floating against
 * the top of a taller neighbour. This is the same trap the connector browse grid
 * hit from the other direction, where a sibling element inside the list item
 * made the ITEM taller than the card it held.
 *
 * Unlike .sj-connector-tile, this rule DOES own its hover and focus chrome — the
 * .sj-dash-connector-card precedent. That distinction is real rather than an
 * inconsistency: the browse tile's per-status states arrive as Tailwind
 * utilities that an unlayered app.css rule would silently outrank, so its box
 * rule must leave those properties alone. A template card has no per-status
 * chrome, so there is nothing to lose the cascade to, and keeping the states in
 * the named rule is what stops them being half-removed by a utility-string edit.
 *
 * The hover colour is hand-rolled with color-mix() rather than written as
 * `hover:border-primary/50`: that is a slash-opacity modifier on a Blueprint
 * theme colour, which this JIT cannot synthesize and which would emit nothing. */
.sj-template-card {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 1rem;
  border-radius: 0.75rem;
  border: 1px solid var(--border);
  background: var(--card);
  text-align: left;
  cursor: pointer;
  transition: border-color 150ms ease-out, box-shadow 150ms ease-out;
}

.sj-template-card:hover {
  border-color: color-mix(in oklch, var(--primary) 50%, transparent);
  box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}

.sj-template-card:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

/* The summary. Clamped to three lines so a card's height is driven by its
 * furniture rather than by how much prose an author wrote — without this a
 * single verbose template sets the height of every card in its row. */
.sj-template-summary {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}

/* Pushes the card's footer to the bottom edge regardless of how short the
 * summary is, so the logo strips across a row line up with each other. */
.sj-template-footer { margin-top: auto; }
