{"version":3,"file":"styloviz-stat-card.mjs","names":[],"sources":["../../../../projects/stat-card-free/src/lib/stat-card.component.ts","../../../../projects/stat-card-free/src/lib/stat-card.component.html"],"sourcesContent":["import {\n  Component,\n  input,\n  output,\n  computed,\n  ChangeDetectionStrategy,\n} from '@angular/core';\nimport { NgClass, DecimalPipe, NgTemplateOutlet } from '@angular/common';\nimport { SvErrorShellComponent, type SvDataError } from '@styloviz/core';\nimport { SvIconComponent } from '@styloviz/icons';\nimport { SvSparklineComponent } from '@styloviz/sparkline';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n/**\n * @deprecated The free card has one colour treatment, so this type has one\n * value and changes nothing. Kept so 1.x code compiles; removed in 2.0. The six\n * semantic colour variants are Pro: `sv-stat-card-pro [variant]`.\n */\nexport type StatCardVariant = 'default';\nexport type StatCardTrend   = 'up' | 'down' | 'neutral';\nexport type StatCardLayout  = 'default' | 'compact' | 'horizontal' | 'minimal';\n\n/** A single point in the inline sparkline (value 0–100 scale, or raw). */\nexport interface SparkPoint {\n  value: number;\n  label?: string;\n}\n\n/**\n * All data needed to render a StatCard.\n * Pass these individually as inputs or compose them into a stats object.\n */\nexport interface StatCardData {\n  title:        string;\n  value:        string | number;\n  previousValue?: string | number;\n  prefix?:      string;\n  suffix?:      string;\n  trend?:       StatCardTrend;\n  trendValue?:  number;        // absolute delta (e.g. 1234)\n  trendPercent?: number;       // % change (e.g. 12.4)\n  trendLabel?:  string;        // e.g. \"vs last month\"\n  /** Name of an icon in `@styloviz/icons` (was a raw SVG path + viewBox). */\n  icon?:        string;\n  /** @deprecated Has no effect on the free card; removed in 2.0. See {@link StatCardVariant}. */\n  variant?:     StatCardVariant;\n  sparkline?:   SparkPoint[];\n  loading?:     boolean;\n  description?: string;\n  linkLabel?:   string;\n}\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\n/**\n * StatCard — A metric summary card for dashboards.\n *\n * Features:\n * - 4 layout modes: default · compact · horizontal · minimal\n * - Neutral colour treatment (the six semantic colour variants are Pro: `sv-stat-card-pro [variant]`)\n * - Trend indicator (up / down / neutral) with % and absolute delta\n * - Inline SVG sparkline chart (no external lib)\n * - Skeleton loading state\n * - Accessible markup (aria-label, role=\"status\")\n * - Click-through CTA link slot\n */\n@Component({\n  selector: 'sv-stat-card',\n  standalone: true,\n  imports: [SvSparklineComponent, NgClass, NgTemplateOutlet, DecimalPipe, SvIconComponent, SvErrorShellComponent],\n  templateUrl: './stat-card.component.html',\n  changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class SvStatCardComponent {\n  // ── Core content ─────────────────────────────────────────────────────────\n\n  /** Card title / metric label. */\n  title       = input<string>('');\n\n  /** Primary metric value — string or number, displayed as-is or formatted. */\n  value       = input<string | number>('');\n\n  /** Optional prefix rendered before the value (e.g. \"$\", \"€\"). */\n  prefix      = input<string>('');\n\n  /** Optional suffix rendered after the value (e.g. \"K\", \"%\", \"ms\"). */\n  suffix      = input<string>('');\n\n  /** Short helper text beneath the title (e.g. \"Last 30 days\"). */\n  description = input<string>('');\n\n  // ── Trend ─────────────────────────────────────────────────────────────────\n\n  /** Direction of change. @default 'neutral' */\n  trend        = input<StatCardTrend>('neutral');\n\n  /** Absolute delta value shown alongside the trend icon. */\n  trendValue   = input<number | null>(null);\n\n  /** Percentage change shown in the trend chip. */\n  trendPercent = input<number | null>(null);\n\n  /** Comparison context label (e.g. \"vs last month\"). @default '' */\n  trendLabel   = input<string>('');\n\n  /**\n   * When this figure was last refreshed, as `YYYY-MM-DD` or an ISO timestamp.\n   *\n   * ★★★ A CACHED KPI LOOKS IDENTICAL TO A LIVE ONE. A number is a number, and\n   *     nothing about a stale one announces itself — so the reader assumes the\n   *     dashboard is current, which is exactly the assumption a broken pipeline\n   *     exploits.\n   *\n   * ★ This is in the FREE tier deliberately. Telling a reader their figure may\n   *   be out of date is an honesty feature, not a premium one, and a free card\n   *   that hides staleness is the defect rather than a smaller version of it.\n   * @default ''\n   */\n  updatedAt = input<string>('');\n\n  /** How many minutes before the figure is called out as possibly stale. @default 60 */\n  staleAfterMinutes = input<number>(60);\n\n  /** Where the figure came from — a warehouse, a job, a service. @default '' */\n  source = input<string>('');\n\n  // ── Appearance ────────────────────────────────────────────────────────────\n\n  /**\n   * Deprecated, and has no effect: the free card has one colour treatment.\n   *\n   * @deprecated Kept so 1.x templates compile; removed in 2.0. For the six\n   * semantic colour variants use `sv-stat-card-pro [variant]`.\n   * @default 'default'\n   */\n  variant = input<StatCardVariant>('default');\n\n  /** Card layout mode. @default 'default' */\n  layout  = input<StatCardLayout>('default');\n\n  /**\n   * Name of an icon in `@styloviz/icons`, e.g. `'trending-up'`.\n   *\n   * This was `iconPath` — a raw `<path d=\"…\">` — with a companion\n   * `iconViewBox` because a pasted path might be 20-box or 24-box. Both are\n   * gone: a library name is always 24-box, and the viewBox binding is what hid\n   * these glyphs from `check:ui` in the first place (a bound `viewBox` does not\n   * match a literal `viewBox=\"…\"` test).\n   */\n  icon        = input<string>('');\n\n  /** SVG viewBox for the icon. @default '0 0 24 24' */\n  // ── Sparkline ─────────────────────────────────────────────────────────────\n\n  /**\n   * Array of data points for the inline sparkline.\n   * Values should be on a consistent numeric scale; the component\n   * normalises them internally. Empty array hides the sparkline.\n   */\n  sparkline = input<SparkPoint[]>([]);\n\n  // ── State ─────────────────────────────────────────────────────────────────\n\n  /** Show skeleton loading shimmer instead of content. @default false */\n  loading = input<boolean>(false);\n\n  /**\n   * The figure could not be loaded. Rendered in place of the metric, because a\n   * card that keeps showing a stale number after a failed refresh is worse than\n   * one that admits it: the reader has no way to tell the two apart.\n   *\n   * Use `toSvDataError()` from `@styloviz/core` to normalize an `HttpErrorResponse`.\n   * @default null\n   */\n  error = input<SvDataError | string | null>(null);\n\n  // ── Interaction ───────────────────────────────────────────────────────────\n\n  /** Label for the bottom \"view more\" link. Empty string hides the link. */\n  linkLabel = input<string>('');\n\n  /** Additional CSS classes merged on the root card element. */\n  customClass = input<string>('');\n\n  // ── Outputs ───────────────────────────────────────────────────────────────\n\n  /**\n   * Deprecated, and never emitted: the free card is not interactive.\n   *\n   * @deprecated Kept so 1.x templates compile; removed in 2.0. For a card that\n   * activates on click, Enter or Space use `sv-stat-card-pro [clickable]`\n   * with its `(cardClick)`.\n   */\n  cardClick  = output<void>();\n\n  /** Emitted when the \"view more\" link is clicked. */\n  linkClick  = output<void>();\n\n  /** The host should re-run the failed load. */\n  retry      = output<void>();\n\n  // ── Computed ──────────────────────────────────────────────────────────────\n\n  /**\n   * How old the figure is, in whole minutes, or `null` when it does not say.\n   *\n   * ★ A bare `YYYY-MM-DD` is read as a LOCAL calendar day. `new Date()` on that\n   *   form is UTC midnight, which is the previous day west of Greenwich —\n   *   enough to report this morning's refresh as a day old.\n   */\n  readonly ageInMinutes = computed<number | null>(() => {\n    const raw = this.updatedAt().trim();\n    if (!raw) return null;\n    const dayOnly = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(raw);\n    const when = dayOnly\n      ? new Date(Number(dayOnly[1]), Number(dayOnly[2]) - 1, Number(dayOnly[3]))\n      : new Date(raw);\n    if (Number.isNaN(when.getTime())) return null;\n    const ms = Date.now() - when.getTime();\n    return ms < 0 ? 0 : Math.floor(ms / 60_000);\n  });\n\n  /** True when the figure is old enough that it should not be read as current. */\n  readonly isStale = computed(() => {\n    const age = this.ageInMinutes();\n    const limit = this.staleAfterMinutes();\n    return age !== null && Number.isFinite(limit) && limit > 0 && age > limit;\n  });\n\n  /** The freshness line, in words. */\n  readonly freshnessLabel = computed<string>(() => {\n    const age = this.ageInMinutes();\n    if (age === null) return '';\n    const when =\n      age < 1 ? 'just now'\n      : age < 60 ? `${age} min ago`\n      : age < 1440 ? `${Math.floor(age / 60)}h ago`\n      : `${Math.floor(age / 1440)}d ago`;\n    const src = this.source() ? ` · ${this.source()}` : '';\n    return this.isStale() ? `Updated ${when} — may be stale${src}` : `Updated ${when}${src}`;\n  });\n\n  /** True when a trend figure is shown with nothing to compare it against. */\n  readonly trendPeriodMissing = computed(\n    () => (this.trendPercent() !== null || this.trendValue() !== null) && !this.trendLabel(),\n  );\n\n  /**\n   * The trend, as a screen reader should hear it.\n   *\n   * ★★★ \"+12%\" AGAINST LAST WEEK, LAST MONTH AND LAST YEAR ARE THREE DIFFERENT\n   *     CLAIMS. A sighted reader sees the figure in the context of the dashboard\n   *     around it; a screen-reader user gets the number alone.\n   */\n  readonly trendAriaLabel = computed<string>(() => {\n    const pct = this.trendPercent();\n    const val = this.trendValue();\n    if (pct === null && val === null) return '';\n    const figure = pct !== null ? `${pct}%` : String(val);\n    const direction =\n      this.trend() === 'up' ? 'up' : this.trend() === 'down' ? 'down' : 'change of';\n    const against = this.trendLabel() || 'comparison period not stated';\n    return `Trend ${direction} ${figure}, ${against}`;\n  });\n\n  /** The metric itself is only shown once there is one: not loading, not failed. */\n  showBody     = computed(() => !this.loading() && !this.error());\n\n  hasIcon      = computed(() => !!this.icon());\n  hasSparkline = computed(() => this.sparkline().length >= 2);\n  hasLink      = computed(() => !!this.linkLabel());\n  hasTrend     = computed(() =>\n    this.trend() !== 'neutral' || this.trendPercent() !== null || this.trendValue() !== null\n  );\n\n  /** Formatted value — passes numbers through DecimalPipe logic, strings untouched. */\n  displayValue = computed<string>(() => {\n    const v = this.value();\n    if (typeof v === 'number') {\n      if (v >= 1_000_000) return (v / 1_000_000).toFixed(1) + 'M';\n      if (v >= 1_000)     return (v / 1_000).toFixed(1)     + 'K';\n      return v.toLocaleString();\n    }\n    return v;\n  });\n\n  /** Formatted trend percent string, e.g. \"12.4%\". */\n  displayTrendPercent = computed<string>(() => {\n    const p = this.trendPercent();\n    if (p === null) return '';\n    return `${Math.abs(p).toFixed(1)}%`;\n  });\n\n  // ── Color maps ────────────────────────────────────────────────────────────\n\n  /** Icon wrapper background + text. */\n  iconClasses = computed<string>(() => {\n    return 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400';\n  });\n\n  /** Border color for the card — subtle gray, no colored accent. */\n  accentClass = computed<string>(() => {\n    return 'border-gray-200/80 dark:border-gray-800';\n  });\n\n  /** Trend chip color classes. */\n  trendClasses = computed<string>(() => {\n    switch (this.trend()) {\n      case 'up':   return 'bg-accent-50 text-accent-700 ring-1 ring-accent-100 dark:bg-accent-950/50 dark:text-accent-400 dark:ring-accent-900/50';\n      case 'down':  return 'bg-danger-50 text-danger-700 ring-1 ring-danger-100 dark:bg-danger-950/50 dark:text-danger-400 dark:ring-danger-900/50';\n      default:      return 'bg-gray-100 text-gray-600 ring-1 ring-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:ring-gray-700';\n    }\n  });\n\n  /** Left accent bar + background tint for the minimal layout. */\n  minimalAccentClass = computed<{ bar: string; bg: string }>(() => {\n    return { bar: 'bg-gray-400 dark:bg-gray-500', bg: '' };\n  });\n\n  /** Plain trend text color used by the minimal layout (no pill). */\n  minimalTrendColor = computed<string>(() => {\n    switch (this.trend()) {\n      case 'up':   return 'text-accent-700 dark:text-accent-400';\n      case 'down': return 'text-danger-600 dark:text-danger-400';\n      default:     return 'text-gray-500 dark:text-gray-400';\n    }\n  });\n}\n","<!-- ── Root card ─────────────────────────────────────────────────────────── -->\n<article\n  class=\"group relative flex flex-col overflow-hidden rounded-xl border bg-white\n         shadow-sm transition-all duration-200 hover:shadow-md\n         dark:bg-gray-900 dark:shadow-gray-950/60\"\n  [ngClass]=\"[accentClass(), customClass()]\"\n  [attr.aria-label]=\"title()\"\n  role=\"region\"\n>\n\n  <!-- ════════════════════════ LOAD FAILURE ════════════════════════════════ -->\n  <!-- Ahead of the skeleton, and in place of the metric rather than beneath it.\n       The card had a loading state but no way to say the load failed, so a failed\n       refresh left the previous figure on screen looking current — a dashboard\n       reporting yesterday's revenue as today's, with nothing on the card to\n       suggest otherwise. Compact, because the whole shell has to fit where a\n       single number used to sit. -->\n  @if (error()) {\n    <div class=\"p-4\">\n      <sv-error-shell [error]=\"error()\" [compact]=\"true\" (retry)=\"retry.emit()\" data-sv-state=\"error\" />\n    </div>\n  }\n\n  <!-- ════════════════════════ LOADING SKELETON ════════════════════════════ -->\n  @if (loading() && !error()) {\n    <div class=\"flex flex-col gap-4 p-5 animate-pulse\">\n      <div class=\"flex items-center justify-between\">\n        <div class=\"h-3 w-20 rounded-full bg-gray-200 dark:bg-gray-700/70\"></div>\n        <div class=\"h-8 w-8 rounded-xl bg-gray-200 dark:bg-gray-700/70\"></div>\n      </div>\n      <div class=\"h-7 w-28 rounded-md bg-gray-200 dark:bg-gray-700/70\"></div>\n      <div class=\"flex items-center gap-2\">\n        <div class=\"h-5 w-12 rounded-full bg-gray-200 dark:bg-gray-700/70\"></div>\n        <div class=\"h-3 w-16 rounded-full bg-gray-100 dark:bg-gray-800\"></div>\n      </div>\n    </div>\n  }\n\n  <!-- ════════════════════════ DEFAULT LAYOUT ══════════════════════════════ -->\n  @if (showBody() && layout() === 'default') {\n    <div class=\"flex flex-1 flex-col px-5 pt-5 pb-4 gap-3.5\">\n\n      <!-- Row 1 — label + icon -->\n      <div class=\"flex items-start justify-between gap-3\">\n        <div class=\"flex-1 min-w-0\">\n          <p class=\"text-[13px] font-medium tracking-tight text-gray-500 dark:text-gray-400 truncate\">\n            {{ title() }}\n          </p>\n          @if (description()) {\n            <p class=\"mt-0.5 text-xs text-gray-500 dark:text-gray-400 truncate\">\n              {{ description() }}\n            </p>\n          }\n        </div>\n        @if (hasIcon()) {\n          <span\n            class=\"inline-flex shrink-0 items-center justify-center h-9 w-9 rounded-xl\n                   ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n            [ngClass]=\"iconClasses()\"\n            aria-hidden=\"true\"\n          >\n            <sv-icon [name]=\"icon()\" cssClass=\"h-4 w-4\" [strokeWidth]=\"1.75\" />\n          </span>\n        }\n      </div>\n\n      <!-- Row 2 — primary value -->\n      <div class=\"flex items-end justify-between gap-2\">\n        <p\n          class=\"text-[28px] font-bold tracking-tight text-gray-900 dark:text-white tabular-nums leading-none\"\n          aria-live=\"polite\"\n        >\n          @if (prefix()) {\n            <span class=\"text-lg font-semibold text-gray-500 dark:text-gray-400 mr-0.5\">{{ prefix() }}</span>\n          }\n          {{ displayValue() }}\n          @if (suffix()) {\n            <span class=\"text-lg font-semibold text-gray-500 dark:text-gray-400 ml-0.5\">{{ suffix() }}</span>\n          }\n        </p>\n\n        <!-- Sparkline (right-aligned) -->\n        @if (hasSparkline()) {\n          <sv-sparkline\n            class=\"mb-0.5 shrink-0\"\n            [data]=\"sparkline()\"\n            [width]=\"120\"\n            [height]=\"32\"\n            [strokeWidth]=\"1.75\"\n            tone=\"neutral\"\n            [ariaHidden]=\"true\" />\n        }\n      </div>\n\n      <!-- Row 3 — trend + label -->\n      @if (hasTrend()) {\n        <div class=\"flex items-center gap-2\">\n          <ng-container *ngTemplateOutlet=\"trendChip; context: {\n            chipClass: 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-semibold ' + trendClasses(),\n            iconClass: 'h-3 w-3'\n          }\" />\n          <ng-container *ngTemplateOutlet=\"trendPeriod; context: { periodClass: 'text-xs text-gray-500 dark:text-gray-400' }\" />\n        </div>\n      }\n    </div>\n\n    <ng-container *ngTemplateOutlet=\"freshnessLine; context: { lineClass: 'px-5 pb-3' }\" />\n\n    <!-- View more link -->\n    @if (hasLink()) {\n      <button\n        type=\"button\"\n        class=\"flex items-center justify-between border-t border-gray-100 px-5 py-2.5\n               text-xs font-medium text-gray-500 transition-colors duration-150\n               hover:bg-gray-50/80 hover:text-primary-600\n               dark:border-gray-800 dark:text-gray-400 dark:hover:bg-gray-800/60\n               dark:hover:text-primary-400\"\n        (click)=\"linkClick.emit()\"\n      >\n        <span>{{ linkLabel() }}</span>\n        <sv-icon name=\"arrow-right\" cssClass=\"h-3.5 w-3.5\" [strokeWidth]=\"2\" />\n      </button>\n    }\n  }\n\n  <!-- ════════════════════════ COMPACT LAYOUT ══════════════════════════════ -->\n  @if (showBody() && layout() === 'compact') {\n    <div class=\"flex items-center gap-3.5 px-4 py-3.5\">\n      @if (hasIcon()) {\n        <span\n          class=\"inline-flex shrink-0 items-center justify-center h-9 w-9 rounded-xl\n                 ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n          [ngClass]=\"iconClasses()\"\n          aria-hidden=\"true\"\n        >\n          <sv-icon [name]=\"icon()\" cssClass=\"h-4 w-4\" [strokeWidth]=\"1.75\" />\n        </span>\n      }\n      <div class=\"flex-1 min-w-0\">\n        <p class=\"text-[12px] font-medium text-gray-500 dark:text-gray-400 truncate leading-tight\">{{ title() }}</p>\n        <p class=\"text-xl font-bold text-gray-900 dark:text-white tabular-nums leading-snug\">\n          @if (prefix()) { <span class=\"text-sm text-gray-500 dark:text-gray-400\">{{ prefix() }}</span> }\n          {{ displayValue() }}\n          @if (suffix()) { <span class=\"text-sm text-gray-500 dark:text-gray-400\">{{ suffix() }}</span> }\n        </p>\n      </div>\n      @if (hasTrend()) {\n        <div class=\"flex shrink-0 flex-col items-end gap-0.5\">\n          <ng-container *ngTemplateOutlet=\"trendChip; context: {\n            chipClass: 'inline-flex shrink-0 items-center gap-0.5 rounded-full px-2 py-0.5 text-[11px] font-semibold ' + trendClasses(),\n            iconClass: 'h-3 w-3'\n          }\" />\n          <ng-container *ngTemplateOutlet=\"trendPeriod; context: { periodClass: 'text-[10px] leading-tight text-gray-500 dark:text-gray-400 text-right' }\" />\n        </div>\n      }\n    </div>\n    <ng-container *ngTemplateOutlet=\"freshnessLine; context: { lineClass: 'px-4 pb-3 -mt-1.5' }\" />\n  }\n\n  <!-- ════════════════════════ HORIZONTAL LAYOUT ═══════════════════════════ -->\n  <!-- Two-column split: left = icon + label + context | right = value + trend -->\n  @if (showBody() && layout() === 'horizontal') {\n    <div class=\"flex items-stretch min-h-[88px]\">\n\n      <!-- Left column — identity / context -->\n      <div class=\"flex flex-1 flex-col justify-center gap-1.5 px-5 py-4\n                  border-r border-gray-100 dark:border-gray-800\">\n        @if (hasIcon()) {\n          <span\n            class=\"inline-flex items-center justify-center h-9 w-9 rounded-xl mb-1\n                   ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n            [ngClass]=\"iconClasses()\"\n            aria-hidden=\"true\"\n          >\n            <sv-icon [name]=\"icon()\" cssClass=\"h-4 w-4\" [strokeWidth]=\"1.75\" />\n          </span>\n        }\n        <p class=\"text-[13px] font-semibold text-gray-700 dark:text-gray-200 tracking-tight leading-tight truncate\">\n          {{ title() }}\n        </p>\n        @if (description()) {\n          <p class=\"text-[11px] text-gray-500 dark:text-gray-400 truncate\">{{ description() }}</p>\n        }\n        <ng-container *ngTemplateOutlet=\"trendPeriod; context: { periodClass: 'block text-[11px] text-gray-500 dark:text-gray-400' }\" />\n        <ng-container *ngTemplateOutlet=\"freshnessLine; context: { lineClass: '' }\" />\n      </div>\n\n      <!-- Right column — metric + trend -->\n      <div class=\"flex flex-col items-end justify-center gap-2 px-5 py-4 shrink-0\">\n        <p class=\"text-[26px] font-bold tracking-tight text-gray-900 dark:text-white tabular-nums leading-none\">\n          @if (prefix()) {\n            <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 mr-0.5\">{{ prefix() }}</span>\n          }\n          {{ displayValue() }}\n          @if (suffix()) {\n            <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 ml-0.5\">{{ suffix() }}</span>\n          }\n        </p>\n\n        @if (hasTrend()) {\n          <ng-container *ngTemplateOutlet=\"trendChip; context: {\n            chipClass: 'inline-flex items-center gap-0.5 rounded-full px-2 py-0.5 text-[11px] font-semibold ' + trendClasses(),\n            iconClass: 'h-3 w-3'\n          }\" />\n        }\n\n        @if (hasSparkline()) {\n          <sv-sparkline\n            class=\"mt-1\"\n            [data]=\"sparkline()\"\n            [width]=\"120\"\n            [height]=\"28\"\n            [strokeWidth]=\"1.75\"\n            tone=\"neutral\"\n            [ariaHidden]=\"true\" />\n        }\n      </div>\n    </div>\n  }\n\n  <!-- ════════════════════════ MINIMAL LAYOUT ══════════════════════════════ -->\n  <!-- Accent bar + tinted background + large value + plain trend text (no pill) -->\n  @if (showBody() && layout() === 'minimal') {\n    <div class=\"flex items-stretch\" [ngClass]=\"minimalAccentClass().bg\">\n\n      <!-- Colored left accent bar —— the defining visual signature of this layout -->\n      <div class=\"w-1 shrink-0\" [ngClass]=\"minimalAccentClass().bar\" aria-hidden=\"true\"></div>\n\n      <!-- Content -->\n      <div class=\"flex flex-1 items-center justify-between gap-4 px-4 py-4\">\n\n        <!-- Left: optional icon + label + value -->\n        <div class=\"flex items-center gap-3 min-w-0\">\n          @if (hasIcon()) {\n            <span\n              class=\"inline-flex shrink-0 items-center justify-center h-10 w-10 rounded-xl\n                     ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n              [ngClass]=\"iconClasses()\"\n              aria-hidden=\"true\"\n            >\n              <sv-icon [name]=\"icon()\" cssClass=\"h-5 w-5\" [strokeWidth]=\"1.75\" />\n            </span>\n          }\n          <div class=\"min-w-0\">\n            <p class=\"text-[10px] font-semibold tracking-widest uppercase text-gray-500 dark:text-gray-400 truncate mb-1\">\n              {{ title() }}\n            </p>\n            <p class=\"text-[30px] font-extrabold text-gray-900 dark:text-white tabular-nums leading-none\">\n              @if (prefix()) {\n                <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 mr-0.5\">{{ prefix() }}</span>\n              }\n              {{ displayValue() }}\n              @if (suffix()) {\n                <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 ml-0.5\">{{ suffix() }}</span>\n              }\n            </p>\n            @if (description()) {\n              <p class=\"mt-1 text-[11px] text-gray-500 dark:text-gray-400 truncate\">{{ description() }}</p>\n            }\n          </div>\n        </div>\n\n        <!-- Right: plain colored trend (no pill/chip) -->\n        @if (hasTrend()) {\n          <div class=\"flex flex-col items-end shrink-0 gap-0.5\">\n            <ng-container *ngTemplateOutlet=\"trendChip; context: {\n              chipClass: 'inline-flex items-center gap-1 text-sm font-bold ' + minimalTrendColor(),\n              iconClass: 'h-4 w-4'\n            }\" />\n            <ng-container *ngTemplateOutlet=\"trendPeriod; context: { periodClass: 'text-[10px] text-gray-500 dark:text-gray-400 text-right leading-tight' }\" />\n          </div>\n        }\n      </div>\n    </div>\n    <ng-container *ngTemplateOutlet=\"freshnessLine; context: { lineClass: 'px-5 py-2 border-t border-gray-100 dark:border-gray-800' }\" />\n  }\n\n  <!-- ════════════════════════ SHARED PIECES ═══════════════════════════════ -->\n  <!--\n    ★★ ONE TREND CHIP, ONE PERIOD, ONE FRESHNESS LINE — FOR EVERY LAYOUT. They\n       used to be written out per layout, and only `default` got the accessible\n       name with its comparison period and the freshness line: the same figure\n       in `compact`, `horizontal` or `minimal` read as a bare \"12.4%\" and a\n       cached KPI looked live. Each layout now only chooses the chip's CLASSES.\n  -->\n  <ng-template #trendChip let-chipClass=\"chipClass\" let-iconClass=\"iconClass\">\n    <span\n      data-sv-trend\n      [ngClass]=\"chipClass\"\n      [attr.role]=\"trendAriaLabel() ? 'img' : null\"\n      [attr.aria-label]=\"trendAriaLabel() || null\"\n    >\n      @if (trend() === 'up') {\n        <sv-icon name=\"arrow-up\" [cssClass]=\"iconClass\" [strokeWidth]=\"2.5\" />\n      }\n      @if (trend() === 'down') {\n        <sv-icon name=\"arrow-down\" [cssClass]=\"iconClass\" [strokeWidth]=\"2.5\" />\n      }\n      @if (displayTrendPercent()) {\n        {{ displayTrendPercent() }}\n      }\n      @if (trendValue() !== null && displayTrendPercent() === '') {\n        {{ trendValue()! | number }}\n      }\n    </span>\n  </ng-template>\n\n  <ng-template #trendPeriod let-periodClass=\"periodClass\">\n    @if (trendLabel()) {\n      <span data-sv-trend-period [ngClass]=\"periodClass\">{{ trendLabel() }}</span>\n    }\n  </ng-template>\n\n  <!--\n    ★★★ A CACHED KPI LOOKS IDENTICAL TO A LIVE ONE. A number is a number, and\n        nothing about a stale one announces itself — so the reader assumes the\n        dashboard is current, which is exactly the assumption a broken\n        pipeline exploits.\n  -->\n  <ng-template #freshnessLine let-lineClass=\"lineClass\">\n    @if (freshnessLabel(); as freshness) {\n      <p data-sv-freshness\n         class=\"text-[11px]\"\n         [ngClass]=\"lineClass\"\n         [class.text-gray-500]=\"!isStale()\"\n         [class.dark:text-gray-400]=\"!isStale()\"\n         [class.text-warning-800]=\"isStale()\"\n         [class.dark:text-warning-200]=\"isStale()\">\n        {{ freshness }}\n      </p>\n    }\n  </ng-template>\n\n</article>\n"],"mappings":"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"}