@font-face {
  font-family: 'Poppins';
  src: url('/assets/fonts/Poppins-Light.woff2?v=38c63e72') format('woff2');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Poppins';
  src: url('/assets/fonts/Poppins-Regular.woff2?v=396bba55') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Poppins';
  src: url('/assets/fonts/Poppins-Medium.woff2?v=29a1d8a5') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Poppins';
  src: url('/assets/fonts/Poppins-SemiBold.woff2?v=d730ee62') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Poppins';
  src: url('/assets/fonts/Poppins-Bold.woff2?v=b997a2bf') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* === tokens.css === */
/* ═══════════════════════════════════════════════════════════════════════
   PlantHint Greenhouse — Design Tokens
   Ground truth: ios/PlantHint/Resources/Assets.xcassets + AppColors.swift
   + AppTypography.swift + AppSpacing.swift + AppRadius.swift.
   Web port pattern: server/sitter/web/public/css/sitter.css.
   Hex values match iOS resolved-light values exactly so a designer can
   diff the two without surprise.
   ═══════════════════════════════════════════════════════════════════════ */

:root {
  /* ── Primary green family (iOS named-asset hex) ──────────────────── */
  --primary: #498B52;                         /* PrimaryGreen, light */
  --primary-shade: #2D8038;                   /* PrimaryShade, light */
  --primary-bg: rgba(73, 139, 82, 0.08);      /* tinted card / hover */
  --primary-bg-hover: rgba(73, 139, 82, 0.14);
  --primary-border: rgba(73, 139, 82, 0.18);
  --leaf-green: #395C2D;                      /* deeper success / health */
  --botanical-dark: #2D3423;                  /* sidebar + onboarding bg */

  /* Login screen full-viewport backdrop. Light mode keeps the
     iOS onboarding-screen-1 botanical green — the cream card on
     deep green is the brand frame and we want it to stay even when
     the OS is light. Dark mode shifts to a near-black warm so the
     dark card (#28231E) actually pops; same green radial accents
     stay so the look isn't theme-blind. */
  --login-bg-start: #2D3423;
  --login-bg-end:   #1F2417;
  --login-glow:     rgba(73, 139, 82, 0.18);
  --login-glow-soft:rgba(73, 139, 82, 0.10);

  /* ── Backgrounds (warm cream / paper, NOT pure white) ────────────── */
  --bg-page: #F5EFE8;                         /* AppGroupedBackground — main page */
  --bg-card: #FFFCF8;                         /* AppCardBackground — cream card */
  --bg-card-hover: #FAF6F0;                   /* slight depress */
  --bg-secondary: #E8E1D5;                    /* AppSecondaryBackground */
  --bg-input: #F8F5EF;                        /* InputBackground — slightly warmer than card */
  --bg-input-focus: #FFFFFF;                  /* lifts on focus */
  --bg-pill: #EBE4D7;                         /* SurfacePill — neutral chip */

  /* Sidebar stays dark — botanical-dark from iOS palette */
  --bg-sidebar: #1F2417;                      /* slightly deeper than BotanicalDark for contrast */
  --bg-sidebar-hover: rgba(255, 255, 255, 0.06);
  --bg-sidebar-active: rgba(73, 139, 82, 0.22);
  --bg-topbar: rgba(245, 239, 232, 0.92);     /* matches bg-page with translucency */
  --bg-overlay: rgba(45, 52, 35, 0.55);

  /* ── Text (warm, NOT cool gray) ──────────────────────────────────── */
  --text-primary: #1E1A13;                    /* PrimaryText — near-black warm */
  --text-secondary: #706558;                  /* SecondaryText — WCAG ~5.2:1 on white */
  --text-tertiary: #85796B;                   /* TertiaryText — WCAG ~4.6:1 (AA) */
  --text-inverse: #FFFCF8;                    /* on dark surfaces — match card cream */
  --text-on-primary: #FFFFFF;                 /* on solid primary green */
  --text-sidebar: rgba(255, 252, 248, 0.72);  /* idle nav item */
  --text-sidebar-active: #FFFCF8;             /* active nav item */
  --text-link: #2D8038;                       /* primary-shade for higher contrast on bg */

  /* ── Borders & separators (warm brown, low alpha) ────────────────── */
  --border: rgba(90, 74, 53, 0.10);           /* iOS separator on light */
  --border-strong: rgba(90, 74, 53, 0.18);    /* explicit dividers */
  --border-input: rgba(90, 74, 53, 0.22);     /* input idle outline */
  --border-focus: var(--primary);
  --border-emphasis: rgba(90, 74, 53, 0.28);  /* one tick stronger than border-strong, used on hover/active outlines */

  /* ── Status (semantic, iOS AppColors hex) ────────────────────────── */
  --status-positive: #3A9C52;                 /* statusPositive */
  --status-warning: #A27B1A;                  /* statusWarning */
  --status-warning-text: #8C6B14;             /* darkened for AA on white */
  --status-caution: #D27832;                  /* statusCaution */
  --status-critical: #C45A4A;                 /* statusCritical */
  --status-critical-text: #A64D38;            /* darkened for AA on white */
  --status-neutral: #7D7264;                  /* statusNeutral */
  --status-positive-bg: rgba(58, 156, 82, 0.10);
  --status-warning-bg: rgba(162, 123, 26, 0.10);
  --status-caution-bg: rgba(210, 120, 50, 0.10);
  --status-critical-bg: rgba(196, 90, 74, 0.10);
  --overdue-coral: #C5573D;                   /* OverdueCoral */

  /* ── Quality tiers (gold, silver-neutral, bronze — iOS premium) ──── */
  --tier-gold: #BE963C;                       /* slightly muted from premiumGold */
  --tier-gold-bg: rgba(190, 150, 60, 0.12);
  --tier-silver: var(--status-neutral);       /* warm neutral, not cool gray */
  --tier-silver-bg: rgba(125, 114, 100, 0.10);
  --tier-bronze: #A16207;                     /* iOS taskRepotting tone */
  --tier-bronze-bg: rgba(161, 98, 7, 0.10);

  /* ── Task type tints (iOS AppColors exact, used for badges) ──────── */
  --task-watering: #427096;
  --task-watering-bg: rgba(66, 112, 150, 0.12);
  --task-watering-text: #2F557B;              /* darkened for AA on light bg */
  --task-fertilizing: #38B44E;
  --task-fertilizing-bg: rgba(56, 180, 78, 0.12);
  --task-cleaning: #9464C8;
  --task-cleaning-bg: rgba(148, 100, 200, 0.12);
  --task-cleaning-text: #5C3A8A;              /* darkened for AA on light bg */
  --task-misting: #50A0B4;
  --task-misting-bg: rgba(80, 160, 180, 0.12);
  --task-rotating: #A86C23;
  --task-rotating-bg: rgba(168, 108, 35, 0.12);
  --task-pruning: #C8648C;
  --task-pruning-bg: rgba(200, 100, 140, 0.12);
  --task-pest_check: #A07814;
  --task-pest_check-bg: rgba(160, 120, 20, 0.12);
  --task-repotting: #A16207;
  --task-repotting-bg: rgba(161, 98, 7, 0.12);

  /* Tier text tones — darkened tier hues for badge labels that need AA
     contrast on cream / white backgrounds. Mirrors the status-X-text
     pattern; tier-gold at #BE963C is too soft for label text. */
  --tier-gold-text: #8C6B14;
  --tier-bronze-text: #7A4A06;

  /* ── Taxonomy palette (sunburst — iOS doesn't define these, kept) ── */
  --tax-monocots: #38B44E;                    /* aligned with fertilizing */
  --tax-eudicots: #427096;                    /* aligned with watering */
  --tax-magnoliids: #9464C8;
  --tax-basal: #C8648C;
  --tax-conifers: #50A0B4;
  --tax-ferns: #3A9C52;
  --tax-lycophytes: #6366F1;
  --tax-mosses: #84CC16;
  --tax-gymnosperms: #D27832;

  /* ── Channel tokens (RGB-only triplets) ──────────────────────────────
     Space-separated R G B values for the brand + status hues, so any
     consumer can compose `rgb(var(--<x>-rgb) / α)` at an arbitrary alpha
     without re-stating the hex. Used by insights.css for borders /
     box-shadows that need a softer or stronger tint than the prebaked
     `--*-bg` (0.10) / `--primary-border` (0.18) tokens. Match the
     light-theme hex above; the dark-theme block redefines `--primary-rgb`
     because `--primary` shifts to #5CB868 there. */
  --primary-rgb: 73 139 82;
  --status-positive-rgb: 58 156 82;
  --status-warning-rgb: 162 123 26;
  --status-caution-rgb: 210 120 50;
  --status-critical-rgb: 196 90 74;
  --status-neutral-rgb: 125 114 100;
  --tier-gold-rgb: 190 150 60;
  --task-watering-rgb: 66 112 150;

  /* ── Spacing (iOS AppSpacing) ────────────────────────────────────── */
  --sp-xxs: 2px;
  --sp-xs: 4px;
  --sp-sm: 8px;
  --sp-md: 12px;
  --sp-lg: 16px;
  --sp-xl: 20px;
  --sp-2xl: 24px;
  --sp-3xl: 32px;
  --sp-card: 12px;                            /* cardPadding */
  --sp-section: 24px;                         /* sectionSpacing */

  /* ── Radius (iOS AppRadius — 14 is the "softer botanical" card) ──── */
  --radius-xs: 2px;                           /* progress segments */
  --radius-sm: 4px;                           /* gallery thumbs */
  --radius-md: 8px;                           /* small cards, pills, inputs */
  --radius-lg: 14px;                          /* standard cards, sheets */
  --radius-xl: 18px;                          /* pill buttons */
  --radius-xxl: 22px;                         /* modals, large sheets */
  --radius-pill: 999px;                       /* circular / capsule */

  /* ── Shadows (warm brown, NOT cool gray) ─────────────────────────── */
  --shadow-sm: 0 1px 2px rgba(90, 65, 30, 0.06);
  --shadow-card: 0 2px 8px rgba(90, 65, 30, 0.08), 0 1px 2px rgba(90, 65, 30, 0.04);
  --shadow-medium: 0 4px 12px rgba(90, 65, 30, 0.10);
  --shadow-elevated: 0 8px 24px rgba(90, 65, 30, 0.12);
  --shadow-dropdown: 0 12px 32px rgba(90, 65, 30, 0.14);

  /* ── Fonts ───────────────────────────────────────────────────────────
     Body type is Poppins (loaded in shell.css); --font-mono is reserved
     for tabular figures, eyebrows, and developer-tone labels. Defined
     once here so consumers can stop carrying the inline fallback chain. */
  --font-mono: ui-monospace, SFMono-Regular, 'JetBrains Mono', Menlo, Consolas, monospace;

  /* ── Layout ──────────────────────────────────────────────────────── */
  --sidebar-width: 240px;
  --sidebar-collapsed: 64px;
  --topbar-height: 56px;

  /* Single page-content max-width. Was previously 1200/1280/1400 spread
     across analytics/diseases/pending/users/pipeline; pinning to one
     value keeps the visual rhythm consistent when the sidebar narrows
     or expands and lets a future redesign pivot via one token. */
  --page-max-width: 1400px;

  /* ── Transitions ─────────────────────────────────────────────────── */
  --transition-fast: 150ms ease;
  --transition-normal: 250ms ease;
  --transition-slow: 400ms ease;
}

/* ── Dark theme ────────────────────────────────────────────────────── */
/* Hex resolved from iOS dark-mode named assets where available; rest derived
   to keep AA contrast against text-primary on dark surfaces. */
[data-theme="dark"] {
  --primary: #5CB868;                         /* PrimaryGreen, dark */
  --primary-shade: #44994E;                   /* PrimaryShade, dark */
  --primary-bg: rgba(92, 184, 104, 0.12);
  --primary-bg-hover: rgba(92, 184, 104, 0.20);
  --primary-border: rgba(92, 184, 104, 0.28);
  --leaf-green: #5BA849;
  --botanical-dark: #A8B897;                  /* iOS BotanicalDark dark resolve — used as accent on dark */

  /* Dark-mode login backdrop — near-black warm with subtle green
     halo so the dark card (#28231E) reads as elevated. Same gradient
     direction + same accent hue as light, just deeper. */
  --login-bg-start: #14110D;
  --login-bg-end:   #08070A;
  --login-glow:     rgba(73, 139, 82, 0.14);
  --login-glow-soft:rgba(73, 139, 82, 0.06);

  --bg-page: #1A1613;                         /* AppGroupedBackground dark */
  --bg-card: #28231E;                         /* AppCardBackground dark */
  --bg-card-hover: #2F2A24;
  --bg-secondary: #211E17;
  --bg-input: #1E1A15;
  --bg-input-focus: #28231E;
  --bg-pill: #2D2821;                         /* SurfacePill dark */
  --bg-sidebar: #141210;
  --bg-sidebar-hover: rgba(255, 252, 248, 0.06);
  --bg-sidebar-active: rgba(92, 184, 104, 0.20);
  --bg-topbar: rgba(26, 22, 19, 0.92);
  --bg-overlay: rgba(0, 0, 0, 0.6);

  --text-primary: #ECE8DF;                    /* PrimaryText dark */
  --text-secondary: rgba(158, 148, 135, 1);   /* approximate of iOS dark secondary */
  --text-tertiary: rgba(112, 102, 89, 1);
  --text-inverse: #1E1A13;
  --text-sidebar: rgba(236, 232, 223, 0.72);
  --text-sidebar-active: #ECE8DF;
  --text-link: #5CB868;

  --border: rgba(255, 252, 248, 0.08);
  --border-strong: rgba(255, 252, 248, 0.14);
  --border-input: rgba(255, 252, 248, 0.16);

  --status-positive-bg: rgba(58, 156, 82, 0.18);
  --status-warning-bg: rgba(162, 123, 26, 0.20);
  --status-caution-bg: rgba(210, 120, 50, 0.20);
  --status-critical-bg: rgba(196, 90, 74, 0.20);

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.30);
  --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.35), 0 1px 2px rgba(0, 0, 0, 0.20);
  --shadow-medium: 0 4px 12px rgba(0, 0, 0, 0.40);
  --shadow-elevated: 0 8px 24px rgba(0, 0, 0, 0.45);
  --shadow-dropdown: 0 12px 32px rgba(0, 0, 0, 0.55);

  /* Channel token redef — only --primary shifts in dark mode (#5CB868).
     Other status / tier / task hues stay the same in both themes. */
  --primary-rgb: 92 184 104;
}

/* ═══════════════════════════════════════════════════════════════════════
   Reset & Base
   ═══════════════════════════════════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html {
  /* iOS Poppins type scale uses absolute pt values via font tokens below.
     Setting root to 14px keeps any rem-based legacy CSS proportional but
     new code should reference --font-size-* directly. */
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: var(--bg-page);
  color: var(--text-primary);
  line-height: 1.5;
  overflow: hidden;
  height: 100vh;
  display: flex;
}

a { color: var(--text-link); text-decoration: none; }
a:hover { text-decoration: underline; }

button {
  font-family: inherit;
  cursor: pointer;
  border: none;
  background: none;
  color: inherit;
}

/* Form control density — desktop admin reads better at 14 than the
   iOS body 17 / subheadline 15 default. The token lets a future
   redesign push it back up without hunting through every CSS file.

   Three field-metric tiers are layered on top so a page CSS file stops
   inventing its own height/padding/font-size combo per dropdown. Pick
   one tier per surface and stop:

     sm — dense filter density (5+ filters in one row → smaller font
          buys horizontal space without dropping the click target).
          Catalog + editor filter rows.
     md — standalone toolbar dropdown — the default for "one or two
          dropdowns sitting in a toolbar". Pending feed, audit log,
          contributions detail status, user-row role picker.
     lg — modal form input. Mirrors .modal-form's 40px pin so a Create
          User / New Species / New FAQ modal renders one tier wider
          than the toolbars below it.

   Heights: sm and md share 36px so all toolbar/filter dropdowns line
   up across surfaces; only the font-size + padding differ. lg jumps
   to 40 to give modal forms breathing room.

   Padding stays on raw px on the vertical axis because the iOS
   spacing scale jumps from 4 to 8 with no 6 in between, and md
   visually needs that 6 to stay aligned with the surrounding 13px
   text. Horizontal axis uses spacing tokens. */
:root {
  --input-font-size: 14px;

  --field-h-sm: 36px;
  --field-h-md: 36px;
  --field-h-lg: 40px;

  --field-fs-sm: var(--font-size-caption2);    /* 11 */
  --field-fs-md: var(--font-size-footnote);    /* 13 */
  --field-fs-lg: var(--input-font-size);       /* 14 */

  --field-pad-sm: var(--sp-xs) var(--sp-sm);   /* 4 8  */
  --field-pad-md: 6px var(--sp-md);            /* 6 12 */
  --field-pad-lg: var(--sp-sm) var(--sp-md);   /* 8 12 */
}

input, select, textarea {
  font-family: inherit;
  font-size: var(--input-font-size);
  color: var(--text-primary);
  background: var(--bg-input);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-md);
  padding: var(--sp-sm) var(--sp-md);
  transition: border-color var(--transition-fast), background var(--transition-fast);
}

input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--border-focus);
  background: var(--bg-input-focus);
  box-shadow: 0 0 0 3px var(--primary-bg);
}

/* Placeholder — tertiary tone + normal weight so the helper text never
   visually outweighs the typed value or the small-caps label above. The
   default browser styling is too prominent (often body weight at full
   ink) and made screens like the user/species creation modals feel
   shouty. Match opacity to 1 because we're already supplying a soft
   color via the token; double-dimming makes WCAG fail in dark theme. */
::placeholder {
  color: var(--text-tertiary);
  font-weight: 400;
  opacity: 1;
}

/* Scrollbar — warm tertiary, not cool gray */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--text-tertiary);
  border-radius: var(--radius-xs);
  opacity: 0.5;
}
::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }

.hidden { display: none !important; }

/* ═══════════════════════════════════════════════════════════════════════
   Typography utility classes
   Sizes match iOS Poppins type scale. Use these instead of arbitrary px.
   ═══════════════════════════════════════════════════════════════════════ */
:root {
  /* iOS Poppins absolute sizes (the relativeTo: scaling lives in Dynamic
     Type on iOS; on web we hold a single point size and lean on font-
     size-adjust for fallback metric calibration). */
  --font-size-caption2: 11px;
  --font-size-caption: 12px;
  --font-size-footnote: 13px;
  --font-size-subheadline: 15px;
  --font-size-callout: 16px;
  --font-size-body: 17px;
  --font-size-title3: 20px;
  --font-size-title2: 22px;
  --font-size-title1: 28px;
  --font-size-largetitle: 34px;
}

/* Weight helpers */
.font-light { font-weight: 300; }
.font-regular { font-weight: 400; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
.italic { font-style: italic; }

/* Size helpers (iOS-named) */
.text-caption2 { font-size: var(--font-size-caption2); line-height: 1.35; }
.text-caption { font-size: var(--font-size-caption); line-height: 1.4; }
.text-footnote { font-size: var(--font-size-footnote); line-height: 1.4; }
.text-subheadline { font-size: var(--font-size-subheadline); line-height: 1.45; }
.text-callout { font-size: var(--font-size-callout); line-height: 1.5; }
.text-body { font-size: var(--font-size-body); line-height: 1.5; }
.text-title3 { font-size: var(--font-size-title3); line-height: 1.3; font-weight: 600; }
.text-title2 { font-size: var(--font-size-title2); line-height: 1.25; font-weight: 700; }
.text-title1 { font-size: var(--font-size-title1); line-height: 1.2; font-weight: 700; letter-spacing: -0.01em; }
.text-largetitle { font-size: var(--font-size-largetitle); line-height: 1.15; font-weight: 700; letter-spacing: -0.015em; }

/* Color helpers */
.text-primary-color { color: var(--text-primary); }
.text-secondary { color: var(--text-secondary); }
.text-tertiary { color: var(--text-tertiary); }
.text-on-primary { color: var(--text-on-primary); }

/* Small-cap label, e.g. "BOTANICAL NAME" above an input — mirrors iOS form pattern */
.label-caps {
  font-size: var(--font-size-caption);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-tertiary);
}

/* Legacy size aliases — keep page CSS files compiling until they migrate. */
.text-xs { font-size: var(--font-size-caption); }
.text-sm { font-size: var(--font-size-footnote); }
.text-lg { font-size: var(--font-size-callout); }
.text-xl { font-size: var(--font-size-title3); }
.text-2xl { font-size: var(--font-size-title2); }

/* ═══════════════════════════════════════════════════════════════════════
   Typography role tokens — SSOT for "what size goes with what role".
   Pages reach for these semantic names instead of the raw scale tokens
   so a designer can re-tune the role from this file alone, without
   walking 12 page CSS files. Two-layer system:

     Primitives        — Apple HIG scale (--font-size-caption2 … title1)
     Semantic roles    — what the admin uses each scale step for

   Roles + their canonical mapping:

     --t-page-title       page-level surface title (e.g. "Database
                          Analytics", "Pending changes")             title3 (20)
     --t-modal-title      modal heading (Add species, Create user,
                          editor validation)                         title3 (20)
     --t-card-title       title of a card row (.card h3, dashboard
                          panel, disease card)                       subheadline (15)
     --t-sub-section      heading inside an expanded card detail
                          (Description / Symptoms / Treatment)       footnote (13)
     --t-field-label      uppercase form label / section caps        caption (12)
     --t-hint             tertiary metadata, helper line             caption2 (11)
     --t-hero-number      big stat-card / ops-card primary number    title2 (22)

   Body copy stays on `--font-size-callout` (16). `--font-size-body`
   (17) is reserved for long-form prose (description paragraphs);
   `--font-size-title1` (28) is reserved for marketing surfaces and
   not used in the admin today. Anything below caption2 (11) is
   sub-readable — don't introduce `0.65rem` style values anywhere.
   ═══════════════════════════════════════════════════════════════════════ */
:root {
  --t-page-title:  var(--font-size-title3);
  --t-modal-title: var(--font-size-title3);
  --t-card-title:  var(--font-size-subheadline);
  --t-sub-section: var(--font-size-footnote);
  --t-field-label: var(--font-size-caption);
  --t-hint:        var(--font-size-caption2);
  --t-hero-number: var(--font-size-title2);
}


/* === archived.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Soft-delete UI affordance — shared across every catalog surface.

   Two pieces:

     .catalog-show-archived
         Toolbar checkbox the operator flips to pull soft-deleted rows
         back into the list for an audit. Pages own the boolean state
         and the count; this stylesheet owns the visual treatment so
         every tab (tips / faqs / diseases / species / cultivars / …)
         renders the toggle the same way.

     .is-archived
         Modifier applied to any row / card whose `status` ≠ 'active'
         OR whose `isActive` === false. Lowers opacity so the operator
         can spot which rows in a "Show archived" view are tombstones
         versus the live set, without interfering with the row's own
         badge / name / category stack.

   Both rules use existing tokens — no new design language. The dim
   factor (0.55) matches `.applied` rows in pending.css so the visual
   vocabulary for "this is hidden from end users / kept for archival"
   is consistent across the admin.
   ═══════════════════════════════════════════════════════════════════════ */

.catalog-show-archived {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  font-size: var(--font-size-caption);
  color: var(--text-secondary);
  cursor: pointer;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-strong);
  background: var(--bg-card);
  user-select: none;
  white-space: nowrap;
  transition: background var(--transition-fast),
              border-color var(--transition-fast),
              color var(--transition-fast);
}
.catalog-show-archived:hover {
  background: var(--bg-card-hover);
  color: var(--text-primary);
}
.catalog-show-archived input[type="checkbox"] {
  width: 13px;
  height: 13px;
  margin: 0;
  cursor: pointer;
  accent-color: var(--primary);
}
.catalog-show-archived:has(input:checked) {
  background: var(--primary-bg);
  border-color: var(--primary-border);
  color: var(--primary-shade);
  font-weight: 600;
}

/* Row / card dimmer. Applied to any element the page renderer marks
   as soft-deleted — works on .tip-card, .faq-item, .disease-card,
   .ed-row, .cv-row, .species-card. The opacity transition gives the
   toggle a soft fade so flipping the checkbox doesn't read as a
   layout shock. */
.is-archived {
  opacity: 0.55;
  transition: opacity 0.18s ease;
}
.is-archived:hover {
  /* Hover regains full clarity so the operator can read the soft-
     deleted content without disabling the toggle. */
  opacity: 0.92;
}


/* === badge.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Badge family — small inline labels.

   Three roles:

     .badge          — semantic-tinted small pill (status / tier / count).
                       Compose with a colour modifier:
                         .badge-primary  .badge-gold  .badge-silver
                         .badge-bronze   .badge-success .badge-warning
                         .badge-danger
                       Use for any short label that takes its meaning
                       from a single semantic colour.

     .badge-type     — uppercase categorical label (record type, role).
                       Compose with a category modifier:
                         .badge-type.species   .badge-type.cultivar
                         .badge-type.disease   .badge-type.tip
                         .badge-type.faq
                         .badge-type.role-admin .badge-type.role-editor
                         .badge-type.role-viewer
                       Use anywhere a row, card, or table cell needs to
                       say "this is a SPECIES record" or "this user is
                       an EDITOR" in caps.

   Both base rules give themselves the same pill radius + caption2 font
   so a row of mixed badges aligns cleanly.

   Markup contract:
     <span class="badge badge-primary">100 tips</span>
     <span class="badge-type species">Species</span>
     <span class="badge-type role-admin">Admin</span>
   ═══════════════════════════════════════════════════════════════════════ */

.badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  font-size: var(--font-size-caption2);
  font-weight: 600;
  line-height: 1.4;
}

.badge-primary { background: var(--primary-bg);          color: var(--primary); }
.badge-gold    { background: var(--tier-gold-bg);        color: var(--tier-gold); }
.badge-silver  { background: var(--tier-silver-bg);      color: var(--tier-silver); }
.badge-bronze  { background: var(--tier-bronze-bg);      color: var(--tier-bronze); }
.badge-success { background: var(--status-positive-bg);  color: var(--status-positive); }
.badge-warning { background: var(--status-warning-bg);   color: var(--status-warning-text); }
.badge-danger  { background: var(--status-critical-bg);  color: var(--status-critical-text); }
/* Info — watering-blue tint, used for in-progress / reviewing states.
   Picks up the same hue as the iOS Watering task badge so a "review
   in progress" pill in moderation reads with the same family colour
   as Watering reminders elsewhere in the product. */
.badge-info    { background: var(--task-watering-bg);    color: var(--task-watering-text); }
/* Neutral — flat label with no semantic colour. Used when an item is
   skipped / deduplicated / archived and the row no longer wants to
   draw attention. */
.badge-neutral { background: var(--bg-pill);             color: var(--text-secondary); }

/* ── .badge-type — uppercase categorical label ─────────────────────── */
.badge-type {
  display: inline-block;
  padding: 3px 8px;
  border-radius: var(--radius-pill);
  font-size: var(--font-size-caption2);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  text-align: center;
  width: fit-content;
}

/* Record-type tints — drawn from the iOS task palette so a Species pill
   in the admin shares its hue family with the Watering / Fertilizing
   chips on the iPhone. The species hue is primary-tinted at 18% alpha
   to read a touch louder than the badge-primary above (this row needs
   to call out type, not just be a soft label). */
.badge-type.species   { background: rgba(73, 139, 82, 0.18);    color: var(--primary-shade); }
.badge-type.cultivar  { background: var(--tier-gold-bg);        color: var(--tier-gold-text); }
.badge-type.disease   { background: var(--status-critical-bg);  color: var(--status-critical-text); }
.badge-type.tip       { background: var(--task-watering-bg);    color: var(--task-watering-text); }
.badge-type.faq       { background: var(--task-cleaning-bg);    color: var(--task-cleaning-text); }

/* User-role tints — owner is the top of the ladder and reads loudest
   in the gold tint reserved for first-class record types. Admin keeps
   the primary-tinted hue. Editor sits between admin and viewer in
   seniority and gets a softer primary tint. Viewer falls to neutral
   so it never accidentally reads as an action. Analyst mirrors viewer
   semantics (read-mostly) but uses the info-tinted task-watering
   palette to distinguish "data-only" from "no-write". */
.badge-type.role-owner   { background: var(--tier-gold-bg);     color: var(--tier-gold-text); }
.badge-type.role-admin   { background: rgba(73, 139, 82, 0.18); color: var(--primary-shade); }
.badge-type.role-editor  { background: rgba(73, 139, 82, 0.10); color: var(--text-secondary); }
.badge-type.role-viewer  { background: var(--bg-pill);          color: var(--text-tertiary); }
.badge-type.role-analyst { background: var(--task-watering-bg); color: var(--task-watering-text); }


/* === catalog-editor.css === */
/* ═══════════════════════════════════════════════════════════════════════
   CatalogEditor — component CSS extracted from runtime injection
   ═══════════════════════════════════════════════════════════════════════
   Pre-2026-05-07 this lived inside catalog-editor.js as a runtime
   `document.createElement('style')` injection. CSP `style-src 'self'`
   (no `'unsafe-inline'`) blocks dynamically-injected <style> blocks the
   same way it blocks inline style attributes — verified live when 22
   playwright specs failed once the test server started serving the
   strict CSP. Moving the CSS into a real stylesheet that build.py
   concatenates into /assets/styles.css means it loads under the
   `'self'` allowlist with no exception needed.

   Token references intentionally include hex fallbacks
   (`var(--primary, #498B52)`) so a token rename doesn't blank-out
   destructive-action affordances or empty-state visuals.
   ─────────────────────────────────────────────────────────────────── */

/* Discard draft / Delete buttons — distinct danger styling so a
   destructive action never sits on a transparent pill that could
   be mistaken for a passive toggle. */
.cat-discard-btn,
.cat-delete-btn {
    background: var(--status-critical, #C45A4A);
    color: #fff;
    border: 1px solid var(--status-critical-text, #A64D38);
}
.cat-discard-btn:hover,
.cat-delete-btn:hover { filter: brightness(0.92); }
.cat-discard-btn:active,
.cat-delete-btn:active { filter: brightness(0.85); }

/* Soft-deleted rows — strikethrough on the name + dim the whole
   row so the operator can scan the deletion queue and tell
   what's pending vs. what's a normal record. */
.cv-row.deleted, .ed-row.deleted { opacity: 0.55; }
.cv-row.deleted .cv-row-name,
.ed-row.deleted .ed-row-sci { text-decoration: line-through; }

/* Soft-delete banner — sits above the action bar in the detail
   panel. Uses the critical-bg token so it reads as a warning
   without yelling. */
.cat-soft-delete-banner {
    display: flex; align-items: center; gap: 8px;
    padding: 10px 14px;
    margin: 12px 0;
    background: var(--status-critical-bg, rgba(196, 90, 74, 0.10));
    border: 1px solid var(--status-critical, #C45A4A);
    border-radius: 8px;
    color: var(--status-critical-text, #A64D38);
    font-size: 13px;
}
.cat-soft-delete-banner > span { flex: 1; }

/* Thumbnail loading spinner — shown while the row's <img> is still
   in flight. Disappears via fade once the load (or fallback) event
   fires. Mirrors the iOS plant-card loading affordance: small,
   primary-tinted spinner with a soft cream scrim so the placeholder
   behind it stays partially visible. Honours prefers-reduced-motion:
   under that preference the spinner stops rotating and just sits as
   a static ring. */
.cat-thumb-wrap {
    position: relative;
    display: inline-block;
    vertical-align: top;
}
.cat-thumb-wrap .cat-thumb-spinner {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    background: rgba(250, 246, 238, 0.55);
    pointer-events: none;
    transition: opacity 200ms ease-out;
    border-radius: 6px;
}
.cat-thumb-wrap .cat-thumb-spinner::after {
    content: '';
    width: 16px; height: 16px;
    border: 2px solid var(--primary, #498B52);
    border-right-color: transparent;
    border-radius: 50%;
    animation: cat-thumb-spin 0.7s linear infinite;
}
.cat-thumb-wrap.thumb-loaded .cat-thumb-spinner { opacity: 0; }
@keyframes cat-thumb-spin {
    to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
    .cat-thumb-wrap .cat-thumb-spinner::after { animation: none; }
}

/* Empty-state illustration — appears when filters/search yield
   zero rows. Replaces a previously-blank list panel that gave the
   operator no clue why the catalog "disappeared". */
.cat-empty-state {
    display: flex; flex-direction: column; align-items: center;
    justify-content: center;
    padding: 56px 24px; gap: 10px;
    text-align: center;
    color: var(--text-secondary, #706558);
}
.cat-empty-state-icon { color: var(--primary, #498B52); opacity: 0.45; }
.cat-empty-state-title {
    font-size: 15px; font-weight: 600;
    color: var(--text-primary, #2A2418);
}
.cat-empty-state-sub {
    font-size: 13px;
    color: var(--text-tertiary, #85796B);
}
.cat-empty-state-action { margin-top: 8px; }


/* === chip.css === */
/* ═══════════════════════════════════════════════════════════════════════
   .chip-filter — clickable rounded toggle.

   Used wherever the user picks one option out of a row (status filter,
   type filter, env filter, breadcrumb segment, taxonomy tier toggle).
   For READ-ONLY metadata pills (FAQ tags, enum values), use .pill-tag
   from components/pill.css instead — chips are interactive, pills
   aren't.

   Two sizes:

     .chip-filter           — default toolbar tab. Padding 6px 14px,
                              footnote font, soft border. The width
                              accommodates an embedded count.

     .chip-filter--sm       — dense filter (explorer cosmos panel,
                              breadcrumb crumbs). Padding 4px 10px,
                              caption2 font, no border.

   Active state contract — set ONE of:

     .chip-filter[aria-pressed="true"]   ← preferred (ARIA-correct)
     .chip-filter.is-active              ← legacy, still honoured
     .chip-filter.active                 ← legacy, still honoured

   Markup contract:

     <button class="chip-filter" data-filter="status" data-value="new">
       <span>New</span>
       <span class="pill-count pill-count--sm">3</span>
     </button>

     <button class="chip-filter chip-filter--sm" data-depth="1">
       Senecio
     </button>

   The embedded count uses the `.pill-count.pill-count--sm` from
   components/pill.css — keeps the contrast story consistent across
   the entire admin (active chip → primary tint applies to BOTH the
   chip and the count inside it via the cascade rule below).
   ═══════════════════════════════════════════════════════════════════════ */

.chip-filter {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: 6px 14px;
  border-radius: var(--radius-pill);
  /* `--border` (8% alpha in dark) made trigger-style chips like the
     Explorer "Filters" button melt into the page background. Use the
     stronger separator weight so the chip outline is always visible
     in both themes. */
  border: 1px solid var(--border-strong);
  background: var(--bg-card);
  font-family: inherit;
  font-size: var(--font-size-footnote);
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--transition-fast),
              border-color var(--transition-fast),
              color var(--transition-fast);
}

/* "This trigger has work behind it" state — used by trigger-style
   chips that open a side panel (Explorer Filters button). Page JS
   toggles `chip-filter--has-value` on/off as the underlying state
   changes; the dot is a passive read-only indicator, NOT a button. */
.chip-filter--has-value {
  border-color: var(--primary-border);
  color: var(--primary-shade);
}
.chip-filter--has-value::after {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--primary);
  margin-left: var(--sp-xs);
}

.chip-filter:hover:not(.active):not(.is-active):not([aria-pressed="true"]) {
  background: var(--bg-card-hover);
  color: var(--text-primary);
}

.chip-filter.active,
.chip-filter.is-active,
.chip-filter[aria-pressed="true"] {
  background: var(--primary-bg);
  border-color: var(--primary-border);
  color: var(--primary-shade);
  font-weight: 600;
  cursor: default;
}

/* Embedded count tints with the chip's active state — when the chip
   goes primary, the count goes primary too. Without this rule the
   count stays neutral grey on a green chip and reads as a separate
   element rather than a child of the active state. */
.chip-filter.active .pill-count,
.chip-filter.is-active .pill-count,
.chip-filter[aria-pressed="true"] .pill-count {
  background: rgba(73, 139, 82, 0.18);
  color: var(--primary-shade);
}

/* ── .chip-filter--sm — dense variant ─────────────────────────────── */
.chip-filter--sm {
  gap: 4px;
  padding: 4px 10px;
  border: 1px solid transparent;
  background: transparent;
  font-size: var(--font-size-caption2);
}

.chip-filter--sm:hover:not(.active):not(.is-active):not([aria-pressed="true"]) {
  background: var(--bg-secondary);
  color: var(--text-primary);
}

/* The dense variant uses a heavier hover/active background because the
   small-size chip risks being lost against the page when only the
   text colour shifts. */
.chip-filter--sm.active,
.chip-filter--sm.is-active,
.chip-filter--sm[aria-pressed="true"] {
  background: var(--primary-bg);
  color: var(--primary-shade);
  border-color: transparent;
}


/* === combobox.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Combobox — component CSS extracted from runtime injection
   ═══════════════════════════════════════════════════════════════════════
   Pre-2026-05-07 this lived inside combobox.js as a runtime
   `document.createElement('style')` injection. CSP `style-src 'self'`
   (no `'unsafe-inline'`) blocks dynamically-injected <style> blocks
   the same way it blocks inline style attributes. Moving the CSS into
   a real stylesheet that build.py concatenates into /assets/styles.css
   means it loads under the `'self'` allowlist with no exception.
   ─────────────────────────────────────────────────────────────────── */

.cb-popup {
    position: absolute;
    z-index: 1100;
    /* Use shared surface tokens so the popup tracks the active
       theme. Earlier cream fallbacks painted the popup light-on-light
       in dark mode. */
    background: var(--bg-card);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-card);
    padding: 6px;
    max-height: 320px;
    overflow-y: auto;
    box-sizing: border-box;
}
.cb-popup.cb-flip-up {
    box-shadow: var(--shadow-card-up, 0 -4px 12px rgba(0,0,0,0.10));
}
.cb-option {
    padding: 8px 10px;
    border-radius: 5px;
    cursor: pointer;
    font-size: var(--font-size-footnote);
    color: var(--text-primary);
}
.cb-option:hover {
    background: var(--bg-card-hover);
}
.cb-option.cb-active {
    background: var(--primary-bg);
    color: var(--primary-shade);
}
.cb-empty {
    padding: 10px;
    color: var(--text-tertiary);
    font-size: var(--font-size-caption);
}


/* === modal.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Modal — overlay, card, header, form metrics.

   Replaces three pre-existing dialect families (.aem-* / .users-modal* /
   .ed-modal*) that all rendered the same visual idea with subtle
   per-page divergence: 480 vs 600 max-width, different padding
   scales, two scrim z-indices, three footer-button heights.

   Markup contract:

     <div class="modal-overlay" id="some-overlay">
       <div class="modal-card modal-card--sm">          <!-- size variant -->
         <div class="modal-header">
           <h3>Create user</h3>
           <p class="modal-sub">A random temporary password …</p>
         </div>
         <form class="modal-form">
           <div class="modal-field">
             <label class="label-caps">USERNAME</label>
             <input type="text" placeholder="2-32 chars" />
             <div class="modal-help">firstname.lastname OK</div>
           </div>
           ...
           <div class="modal-actions">
             <button class="btn btn-ghost">Cancel</button>
             <button class="btn btn-primary">Create</button>
           </div>
         </form>
       </div>
     </div>

   Size variants:

     .modal-card--sm   ← 480px, fits create-user / new-FAQ / Add Species.
     .modal-card--md   ← 600px, max-height 80vh + body scroll. Editor
                         validation modal sits here because the issue
                         list can be long.
     .modal-card--lg   ← 760px. Reserved for future side-by-side
                         compare / merge UIs.

   The form metric (40 px input height, label-caps spacing, footer
   button alignment) is enforced by .modal-form so any modal that uses
   it gets the same typing tempo, not just `aem-form`.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Overlay scrim ────────────────────────────────────────────────── */
.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 600;
  background: var(--bg-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-lg);
  animation: modal-fade-in 160ms ease;
}

@keyframes modal-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ── Card surface ─────────────────────────────────────────────────── */
.modal-card {
  width: 100%;
  background: var(--bg-card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-elevated);
  overflow: hidden;
}

.modal-card--sm { max-width: 480px; }
.modal-card--md {
  max-width: 600px;
  max-height: 80vh;
  overflow-y: auto;
}
.modal-card--lg {
  max-width: 760px;
  max-height: 80vh;
  overflow-y: auto;
}
/* --xl — reference / encyclopedia surfaces (insights glossary, future
   schema reference). Wider band so multi-column dl + index lists
   don't reflow into 1-col stacks at every line. */
.modal-card--xl {
  max-width: 960px;
  max-height: 86vh;
  overflow-y: auto;
}

/* ── Header ───────────────────────────────────────────────────────── */
.modal-header {
  padding: var(--sp-xl) var(--sp-xl) var(--sp-md);
}
.modal-header h3 {
  font-size: var(--t-modal-title);
  font-weight: 600;
  color: var(--text-primary);
  margin: 0;
}
/* Optional close-button row variant — used by the editor validation
   modal where the operator needs an explicit close affordance. */
.modal-header--with-close {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-md);
  border-bottom: 1px solid var(--border);
  padding: var(--sp-lg) var(--sp-xl);
}

/* When the title needs a secondary id chip below it (Pending diff
   modal: "cultivar · Test Cv" + the opaque cv_07f4cd... underneath),
   stack them in a flex column. min-width:0 lets the long monospace
   id ellipsis instead of pushing the close button off-canvas. */
.modal-header-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}
.modal-header-id {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Square close affordance with a real hit-area. The previous "✕"
   inside btn-ghost btn-sm read as a tiny ghost character against
   the modal corner; bumping to 32×32 + a subtle hover tint matches
   the other chrome buttons (sidebar collapse, photo zoom close). */
.modal-close-btn {
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-md);
  background: transparent;
  border: none;
  color: var(--text-secondary);
  cursor: pointer;
  flex-shrink: 0;
  transition: background var(--transition-fast), color var(--transition-fast);
}
.modal-close-btn:hover {
  background: var(--bg-card-hover);
  color: var(--text-primary);
}
.modal-close-btn:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}

.modal-sub {
  font-size: var(--font-size-caption);
  color: var(--text-secondary);
  margin: var(--sp-xs) 0 var(--sp-md);   /* bottom space so the form below doesn't butt into the description line */
  line-height: 1.4;
}

/* Fallback chrome for modals that use raw <h3> + .modal-sub directly
   under .modal-card (instead of the .modal-header wrapper). Without
   this the title sits flush against the card edge — proper structure
   still uses .modal-header, this just keeps the legacy pattern from
   looking broken. */
.modal-card > h3:first-child {
  margin: 0;
  padding: var(--sp-xl) var(--sp-xl) 0;
  font-size: var(--t-modal-title);
  font-weight: 600;
  color: var(--text-primary);
}
.modal-card > h3:first-child + .modal-sub {
  padding: 0 var(--sp-xl);
}
/* ── Body (scrollable, used with --md / --lg) ─────────────────────── */
.modal-body {
  padding: var(--sp-xl);
  /* Modal body content reads at footnote size — same as pending row
     text + audit log + disease excerpts. Without this, body inherits
     the html-default 14px which feels chunky against the admin's
     12-13px content density. Per-page rules can override for emphasis
     (review-text, glossary paragraphs, etc.). */
  font-size: var(--font-size-footnote);
  line-height: 1.55;
  color: var(--text-secondary);
}

/* ── Form metric carrier ──────────────────────────────────────────── */
.modal-form {
  padding: var(--sp-md) var(--sp-xl) var(--sp-xl);
}

.modal-field { margin-bottom: var(--sp-lg); }
.modal-field:last-of-type { margin-bottom: var(--sp-sm); }

/* Pin <input>/<select>/<textarea> heights so the picker chrome doesn't
   render at a different vertical extent than a plain text field. */
.modal-form input,
.modal-form select,
.modal-form textarea {
  width: 100%;
  height: 40px;
  padding: var(--sp-sm) var(--sp-md);
  box-sizing: border-box;
}

.modal-form textarea {
  height: auto;
  min-height: 80px;
  padding-top: var(--sp-sm);
}

.modal-required { color: var(--status-critical); }

.modal-help {
  margin-top: var(--sp-xs);
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  line-height: 1.4;
}

/* Live-match indicator for datalist fields. Renders empty (just
   reserves a visual slot) until the operator types something; then
   colour + text reflect whether the value is in the option set. */
.modal-live-status {
  margin-top: var(--sp-xs);
  font-size: var(--font-size-caption2);
  line-height: 1.4;
  min-height: 1em;
  font-weight: 500;
}
.modal-live-match   { color: var(--status-positive); }
.modal-live-nomatch { color: var(--status-warning); }

.modal-error {
  margin-top: var(--sp-md);
  padding: var(--sp-sm) var(--sp-md);
  background: var(--status-critical-bg);
  border-left: 3px solid var(--status-critical);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-caption);
  color: var(--status-critical-text);
}

/* ── Footer actions ──────────────────────────────────────────────── */
.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-sm);
  margin-top: var(--sp-xl);
  padding-top: var(--sp-md);
  border-top: 1px solid var(--border);
}

/* Standalone actions row (direct child of modal-card, not nested
   inside modal-form) needs its own horizontal + bottom padding —
   modal-card has overflow:hidden + no padding, so without this the
   Close button sat flush against the card edge in the pending diff
   modal. modal-form / modal-body already provide their own padding
   so descendants there are fine. */
.modal-card > .modal-actions {
  margin-top: 0;
  padding: var(--sp-md) var(--sp-xl) var(--sp-xl);
}

/* Pin button heights so a primary + ghost combo doesn't visibly
   disagree at the modal footer. */
.modal-actions .btn {
  min-height: 40px;
  padding-left: var(--sp-lg);
  padding-right: var(--sp-lg);
}

/* Inline checkbox + label pair for modal forms. Used by Create User's
   "Email a setup link" toggle and by any future modal field that wants
   an inline checkbox after the labelled fields. Mirrors the iOS
   pattern of a faint secondary line that sits below a primary input
   and reads as a single horizontal row. Replaces .login-checkbox
   (which lived in login.css solely because the Create User modal was
   originally rendered with login-form markup). */
.modal-checkbox {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  font-size: var(--font-size-caption);
  color: var(--text-secondary);
  margin-bottom: var(--sp-md);
  line-height: 1.4;
  cursor: pointer;
  user-select: none;
}

/* Override .modal-form's `input { height:40; padding:8 12; width:100% }`
   pin which would otherwise stretch a checkbox into a tall blue square
   with weird padding inside. Native checkbox metric. */
.modal-checkbox input[type="checkbox"] {
  width: 16px;
  height: 16px;
  margin: 0;
  padding: 0;
  cursor: pointer;
  flex-shrink: 0;
}

.modal-checkbox input[type="checkbox"]:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* Fade the entire row when the checkbox is disabled — opacity on the
   checkbox alone left the label at full contrast, which read as an
   active control while the input was actually inert (Create User
   modal: "Email a setup link" while no email is typed). The :has()
   parent-selector means the same rule covers any future modal-checkbox
   instance without a per-call class flag. */
.modal-checkbox:has(input[type="checkbox"]:disabled) {
  opacity: 0.55;
  cursor: not-allowed;
}

/* Optional small-caps eyebrow line that sits ABOVE the modal h3 — used
   when the modal needs to surface its parent context ("PULSE · COMPOSITE
   HEALTH", "WATCHTOWER · TRIGGERED"). Insights surfaces them; other
   pages can opt in by rendering a `<p class="modal-eyebrow">` inside
   `.modal-header-text` before the h3. Same caps convention as
   `.label-caps` but tuned for modal headers (slightly tighter spacing
   so the title underneath dominates). */
.modal-eyebrow {
  font-size: var(--font-size-caption2);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin: 0 0 var(--sp-xs);
}


/* === pill.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Pill family — passive (non-clickable) rounded labels.

   Two roles:

     .pill-count     — heading-adjacent or row-adjacent count display.
                       Default size sits next to a section title
                       (e.g. "Daily Tips · 100 tips"); the --sm modifier
                       is for inline group counters that hug a line of
                       text (e.g. "Senecio (57)").
                       Compose with --accent for a primary-tinted look
                       when the count needs visual weight.

                         <span class="pill-count">
                           <strong>1</strong> open
                         </span>

                         <span class="pill-count pill-count--accent">
                           100 tips
                         </span>

                         <span class="pill-count pill-count--sm">57</span>

     .pill-tag       — readonly metadata label inside a card (FAQ tags,
                       enum values, photo overlay credits). Not
                       clickable — for a clickable filter use
                       .chip-filter from components/chip.css.
                       The --overlay variant is for media overlays
                       (photo credit, "From parent" markers) where the
                       label sits on top of an image and needs a dark
                       scrim background.

                         <span class="pill-tag">exposure</span>
                         <span class="pill-tag pill-tag--overlay">© Wikimedia</span>

   Both bases share radius and base font so a row mixing pills aligns
   cleanly. Visual weight is communicated via colour (default → bg-pill
   neutral; accent → primary tint), not via size.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── .pill-count ──────────────────────────────────────────────────── */
.pill-count {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  font-size: var(--font-size-footnote);
  font-weight: 500;
  background: var(--bg-pill);
  color: var(--text-secondary);
  white-space: nowrap;
}

.pill-count strong {
  color: var(--text-primary);
  font-weight: 700;
}

/* Accent variant — primary-tinted. Used when the count itself is the
   page-level statistic (e.g. "100 tips" beside a section title); the
   default neutral tone is reserved for filter rows and group counters
   where the count is supporting information. */
.pill-count--accent {
  background: var(--primary-bg);
  color: var(--primary-shade);
}
.pill-count--accent strong {
  color: var(--primary-shade);
}

/* Small variant — inline group counter (genus header, list-row badge).
   Tighter padding, caption2 font so it sits inside a single line of
   text without throwing the line-height. */
.pill-count--sm {
  padding: 1px 8px;
  font-size: var(--font-size-caption2);
  font-weight: 600;
}

/* ── .pill-tag ────────────────────────────────────────────────────── */
.pill-tag {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  font-size: var(--font-size-caption2);
  font-weight: 500;
  background: var(--bg-secondary);
  color: var(--text-secondary);
  white-space: nowrap;
}

/* Accent — primary-tinted readonly tag (FAQ shade, half-sun, plant —
   the in-card semantic chips that read at a glance). Reaches for the
   same primary-bg as .badge-primary so a card row mixing pill-tag and
   badge-primary stays colour-coordinated. */
.pill-tag--accent {
  background: var(--primary-bg);
  color: var(--primary-shade);
}

/* Overlay — media overlay credit / "from parent" marker. Sits on top
   of a photo so the contrast comes from the dark scrim, not the
   surrounding card surface. Backdrop blur softens busy photos. */
.pill-tag--overlay {
  background: rgba(20, 18, 14, 0.78);
  color: var(--text-inverse);
  -webkit-backdrop-filter: blur(6px);
          backdrop-filter: blur(6px);
}


/* === search.css === */
/* ═══════════════════════════════════════════════════════════════════════
   .toolbar-search — single source of truth for any search input that
   sits inside a page toolbar (catalog, editor, tips, FAQ, audit log,
   pending feed). Page CSS files MUST NOT redeclare these properties;
   width / margin / max-width can be overridden via the parent context
   (e.g. `.cat-toolbar .toolbar-search { flex: 1 1 220px; }`).

   Markup contract:
     <div class="toolbar-search">
       <svg class="toolbar-search-icon">…</svg>
       <input class="toolbar-search-input" type="search" placeholder="…">
     </div>

   The pill variant is reserved for the global topbar — opt in with
   `.toolbar-search--pill`. Anything else stays on the default
   medium-radius shape so the visual rhythm across pages is uniform.
   ═══════════════════════════════════════════════════════════════════════ */

.toolbar-search {
  position: relative;
  display: block;
  width: 100%;
}

.toolbar-search-icon {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  width: 16px;
  height: 16px;
  fill: none;
  stroke: var(--text-tertiary);
  pointer-events: none;
}

.toolbar-search-input {
  width: 100%;
  height: 36px;
  padding: 0 var(--sp-md) 0 32px;
  font-size: var(--input-font-size);
  /* All other properties (background, border, radius, focus ring) come
     from the global input rule in tokens.css — keeping them here would
     duplicate the source of truth and let one drift behind the other. */
}

/* Topbar pill variant. Only `.topbar > .toolbar-search--pill` should
   reach for this; the modifier exists so a future shell.css change
   can adjust the global header without touching components/search.css. */
.toolbar-search--pill .toolbar-search-input {
  border-radius: var(--radius-pill);
  border-color: var(--border);
}


/* === select.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Native <select> — global styling.

   tokens.css already styles the bare element (background, border,
   radius, focus ring) so an unstyled <select> renders correctly.

   Why no `appearance: none` + custom chevron SVG: Chromium dark mode
   tiles the parent's background-image into every <option> row of an
   appearance-stripped <select>, because option styling is OS-rendered
   and CSS rules on `select option` don't reach the popup. We saw this
   on macOS Chromium with the "Create user" role dropdown and the
   editor's group-by select — chevrons painted across each option,
   text unreadable. Dropping appearance: none + background-image
   surrenders the custom polish but lets the OS render a clean,
   readable picker in both themes. The native chevron is fine; visual
   consistency across page CSS files is preserved by the shared
   border / bg / focus ring tokens applied via tokens.css.

   Page CSS files MAY adjust min-width, max-width, padding, or
   font-size on contextual selectors (`.cat-filter-select`,
   `.editor-filters select`); they MUST NOT redeclare appearance or
   background-image — that would re-introduce the dark-mode bug.
   ═══════════════════════════════════════════════════════════════════════ */

select {
  /* Explicitly leave appearance at the default so the OS draws the
     native chevron + option popup. No background-image. */
  appearance: auto;
  -webkit-appearance: auto;
}

/* ─── Field-metric utility classes ──────────────────────────────────────
   Pick one per <select>/<input>/<textarea> surface; never invent a 4th
   size. Tokens live in tokens.css under the "Form control density"
   block. Page CSS files that previously hand-rolled height + font-size +
   padding compose these instead so a redesign tunes one tier at a time
   instead of grepping six files.

   sm — dense filter row (catalog / editor / explorer)
   md — toolbar dropdown (pending / audit / contributions / role picker)
   lg — modal form field (mirrors .modal-form's 40px pin)
   ────────────────────────────────────────────────────────────────────── */
.field-sm {
  height: var(--field-h-sm);
  font-size: var(--field-fs-sm);
  padding: var(--field-pad-sm);
}
.field-md {
  height: var(--field-h-md);
  font-size: var(--field-fs-md);
  padding: var(--field-pad-md);
}
.field-lg {
  height: var(--field-h-lg);
  font-size: var(--field-fs-lg);
  padding: var(--field-pad-lg);
}


/* === utilities.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Greenhouse — utility classes
   ═══════════════════════════════════════════════════════════════════════
   Small set of one-property classes that replace `style="…"` attribute
   strings in template literals. CSP `style-src 'self'` rejects inline
   style attributes, so any place that previously wrote
       <div style="margin-top: var(--sp-sm)">
   is now
       <div class="u-mt-sm">
   instead. Names mirror tokens.css (spacing scale, font-size scale) so
   adding a new step is "find the token, write the rule".

   Atomic policy:
   - one CSS property per class. Compose by stacking class names.
   - never tied to a layout context — `.u-mt-sm` works the same in any
     parent.
   - prefixed `u-` so `grep '^\.u-'` finds the whole utility surface.

   For DYNAMIC values (per-row bar widths, per-event colours) we use
   CSS custom properties + the data-style helper in util/dom-styles.js.
   See that file for the pattern.
   ─────────────────────────────────────────────────────────────────── */

/* ── Margin (top) ──────────────────────────────────────────────────── */
.u-mt-xs   { margin-top: var(--sp-xs); }
.u-mt-sm   { margin-top: var(--sp-sm); }
.u-mt-md   { margin-top: var(--sp-md); }
.u-mt-lg   { margin-top: var(--sp-lg); }

/* ── Margin (bottom) ───────────────────────────────────────────────── */
.u-mb-sm   { margin-bottom: var(--sp-sm); }
.u-mb-md   { margin-bottom: var(--sp-md); }

/* ── Margin (left) ─────────────────────────────────────────────────── */
.u-ml-xxs   { margin-left: var(--sp-xxs); }   /* 2px */
.u-ml-xs    { margin-left: var(--sp-xs); }    /* 4px */
.u-ml-sm    { margin-left: var(--sp-sm); }    /* 8px */
.u-ml-auto  { margin-left: auto; }

/* ── Padding ──────────────────────────────────────────────────────── */
.u-p-md    { padding: var(--sp-md); }
.u-p-lg    { padding: var(--sp-lg); }
.u-p-2xl   { padding: var(--sp-2xl); }
.u-py-xs   { padding-top: var(--sp-xs); padding-bottom: var(--sp-xs); }

/* ── Layout ───────────────────────────────────────────────────────── */
.u-flex-1            { flex: 1; }
.u-row-center        { display: flex; align-items: center; gap: var(--sp-xs); }
.u-row-between       { display: flex; justify-content: space-between; align-items: center; gap: var(--sp-sm); }
.u-row-sm            { display: flex; gap: var(--sp-sm); }
.u-col-xl            { display: flex; flex-direction: column; gap: var(--sp-xl); }
.u-col-md            { display: flex; flex-direction: column; gap: var(--sp-md); }

/* ── Grid column span (DYNAMIC via --span; 1 fallback so layout never collapses) ── */
.u-grid-span {
  grid-column: span var(--span, 1);
}

/* ── Text ─────────────────────────────────────────────────────────── */
.u-text-center       { text-align: center; }
.u-text-right        { text-align: right; }
.u-mono              { font-family: monospace; }
.u-leading-relaxed   { line-height: 1.6; }

/* ── Border (single-edge top divider with margin — mid-card splitter) ─ */
.u-divider-top {
  margin-top: var(--sp-md);
  padding-top: var(--sp-md);
  border-top: 1px solid var(--border);
}

/* ── Opacity ──────────────────────────────────────────────────────── */
.u-opacity-40 { opacity: 0.4; }

/* ── SVG inline alignment (for icons rendered inside text runs) ────── */
.u-icon-inline {
  vertical-align: -0.125em;
  flex-shrink: 0;
}

/* ── Bar fill (DYNAMIC width via --bar-w; populated by data-style) ─── */
/*
   Pattern:
   <span class="u-bar-fill" data-style="--bar-w: 67%"></span>
   util/dom-styles.js applies the custom property on next paint.

   The fallback value (0%) keeps the bar visible-but-empty if the
   helper hasn't run yet, instead of breaking layout.
*/
.u-bar-fill {
  display: inline-block;
  width: var(--bar-w, 0%);
  height: 100%;
  background: var(--bar-bg, currentColor);
  transition: width 240ms ease;
}

/* ── Min-width (small fixed utility for label columns) ─────────────── */
.u-min-w-48 { min-width: 48px; }

/* ── Width — fixed utility steps used in tables/columns ────────────── */
.u-w-50   { width: 50%; }
.u-w-120  { width: 120px; }


/* === shell.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Shell Layout — Sidebar + Topbar + Main Content
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Sidebar ───────────────────────────────────────────────────────── */
.sidebar {
  width: var(--sidebar-width);
  height: 100vh;
  background: var(--bg-sidebar);
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  transition: width var(--transition-normal);
  z-index: 100;
  overflow: hidden;
}

.sidebar-header {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-xl) var(--sp-lg);
  border-bottom: 1px solid rgba(255,255,255,0.08);
}

.sidebar-logo {
  width: 36px;
  height: 36px;
  flex-shrink: 0;
  object-fit: contain;
}

.sidebar-title {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
  min-width: 0;
}

.sidebar-app-name {
  font-size: var(--font-size-subheadline);
  font-weight: 700;
  color: var(--text-sidebar-active);
  letter-spacing: -0.01em;
}

.sidebar-app-sub {
  font-size: var(--font-size-caption2);
  font-weight: 500;
  color: var(--text-sidebar);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* ── Navigation ────────────────────────────────────────────────────── */
.sidebar-nav {
  flex: 1;
  padding: var(--sp-lg) var(--sp-sm);
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow-y: auto;
}

.nav-item {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: 10px var(--sp-md);
  border-radius: var(--radius-sm);
  color: var(--text-sidebar);
  font-size: var(--font-size-caption);
  font-weight: 500;
  transition: all var(--transition-fast);
  text-decoration: none;
  white-space: nowrap;
}

.nav-item:hover {
  background: var(--bg-sidebar-hover);
  color: var(--text-sidebar-active);
  text-decoration: none;
}

.nav-item.active {
  background: var(--bg-sidebar-active);
  color: var(--text-sidebar-active);
}

.nav-icon {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ── Sidebar Footer ───────────────────────────────────────────────── */
/* Vertical stack: session pill (nickname + logout) on top, then a slim
   theme-toggle + build-info row at the bottom. Earlier this was a single
   horizontal flex row that crammed all three pieces into one line —
   readable on a 24-inch monitor, broken in the 240 px sidebar. */
.sidebar-footer {
  padding: var(--sp-md) var(--sp-md) var(--sp-lg);
  border-top: 1px solid rgba(255, 252, 248, 0.08);
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
}

.sidebar-footer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-sm);
}

.theme-toggle {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-sidebar);
  opacity: 0.65;
  transition: all var(--transition-fast);
}

.theme-toggle:hover {
  background: var(--bg-sidebar-hover);
  color: var(--text-sidebar-active);
  opacity: 1;
}

.theme-toggle svg {
  width: 16px;
  height: 16px;
  /* Stroke is set inline on the sun/moon SVGs (Lucide-style); keeping
     fill:none here defends against any rogue user-agent default. */
  fill: none;
}

.sidebar-build-info {
  font-size: var(--font-size-caption2);
  color: rgba(255, 252, 248, 0.32);
  letter-spacing: 0.04em;
  white-space: nowrap;
  flex-shrink: 1;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Main Content ──────────────────────────────────────────────────── */
.main-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
  height: 100vh;
  overflow: hidden;
}

/* ── Topbar ─────────────────────────────────────────────────────────── */
.topbar {
  height: var(--topbar-height);
  background: var(--bg-topbar);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: var(--sp-lg);
  padding: 0 var(--sp-xl);
  flex-shrink: 0;
  z-index: 50;
}

.sidebar-toggle {
  display: none;
  width: 32px;
  height: 32px;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
}

.sidebar-toggle:hover { background: var(--primary-bg); }

.topbar-title {
  font-size: var(--font-size-callout);
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
}

/* The visual chrome (icon position, input padding, height, pill
   radius, focus ring) lives in components/search.css under the
   shared .toolbar-search / .toolbar-search-input / --pill rules.
   The topbar only owns the flex slot it occupies in the header. */
.topbar-search {
  flex: 1;
  max-width: 420px;
  margin-left: auto;
}

.search-results {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-dropdown);
  max-height: 400px;
  overflow-y: auto;
  display: none;
  z-index: 200;
}

.search-results.visible { display: block; }

.search-result-item {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-sm) var(--sp-lg);
  cursor: pointer;
  transition: background var(--transition-fast);
}

.search-result-item:hover { background: var(--primary-bg); }

.search-result-item .result-name {
  font-weight: 500;
  font-size: var(--font-size-caption);
}

.search-result-item .result-name em {
  font-style: italic;
  color: var(--text-secondary);
}

.search-result-item .result-meta {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  margin-left: auto;
}

/* Search result type indicator is rendered with the shared
   .badge-type component (see components/badge.css). The categorical
   tints (species / cultivar / disease) are colour-coordinated with
   the pending feed and the role badge so a search hit and a pending
   row read with the same hue family. */

/* ── Page Container ────────────────────────────────────────────────── */
/* The page-container is THE single owner of page-level horizontal +
   vertical padding. Per-page wrappers (.contrib-page, .diseases-page,
   .users-page, …) used to redeclare their own padding which doubled
   the inset on Contributions and made every page start at a slightly
   different x-coordinate. Pages now lean on this padding and only
   declare a max-width via the shared `.page-shell` modifier or the
   `--page-max-width` token. */
.page-container {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--sp-xl);
}

/* Optional opt-in: pages that want their content centered in a max
   width band (most of them) layer this class over their existing
   wrapper. Pages that need full-bleed (Explorer, Editor master-detail)
   skip it. */
.page-shell {
  max-width: var(--page-max-width);
  margin-left: auto;
  margin-right: auto;
}

.page {
  display: none;
  animation: pageIn 0.25s ease;
}

.page.active { display: block; }

@keyframes pageIn {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ── Common Components ─────────────────────────────────────────────── */
.card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  transition: box-shadow var(--transition-fast);
}

.card:hover { box-shadow: var(--shadow-elevated); }

.stat-card {
  padding: var(--sp-xl);
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
}

.stat-value {
  font-size: var(--font-size-title1);
  font-weight: 700;
  line-height: 1;
  color: var(--text-primary);
}

.stat-label {
  font-size: var(--font-size-caption2);
  font-weight: 500;
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.stat-sub {
  font-size: var(--font-size-caption2);
  color: var(--text-secondary);
  margin-top: var(--sp-xs);
}

/* The .badge family lives in components/badge.css — semantic colour
   variants (.badge-primary/.badge-gold/.badge-silver/.badge-bronze/
   .badge-success/.badge-warning/.badge-danger) and the categorical
   .badge-type companion. */

/* Button */
.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-sm) var(--sp-lg);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-caption);
  font-weight: 500;
  transition: all var(--transition-fast);
  white-space: nowrap;
}

.btn-primary {
  background: var(--primary);
  color: var(--text-inverse);
}
.btn-primary:hover { background: var(--primary-shade); }

.btn-secondary {
  background: var(--bg-secondary);
  color: var(--text-primary);
  border: 1px solid var(--border);
}
.btn-secondary:hover { background: var(--border); }

.btn-ghost {
  color: var(--text-secondary);
}
.btn-ghost:hover { background: var(--primary-bg); color: var(--primary); }

.btn svg {
  width: 16px;
  height: 16px;
  /* Lucide stroke icons (util/icons.js + shell-inline) carry their own
     fill="none" stroke="currentColor" inline. Keeping the parent rule
     stroke-friendly defends any future SVG that lands here without an
     inline fill/stroke pair. */
  fill: none;
  stroke: currentColor;
}

/* Section header.
   `flex-wrap` + `gap` together keep the trailing button from clipping
   when the title is long (Disease tab, Pending Changes), and the
   left-side title stays readable down to ~360 px. The previous
   `space-between` only layout collapsed the +New button under the
   right edge on the Users page. */
.section-header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-sm) var(--sp-md);
  margin-bottom: var(--sp-lg);
}

.section-title {
  font-size: var(--t-page-title);
  font-weight: 600;
  /* Removed `margin-right: auto` — that flag pushed the title hard to
     the left and threw any sibling badge / count pill all the way to
     the opposite edge of the viewport. The user-facing pattern is
     "Daily Tips · 100 tips" reading as one phrase, not the title and
     a counter on opposite ends of the row. Action buttons that need
     right-edge positioning (e.g. "+ New") already carry their own
     inline `style="margin-left:auto"` so they keep flowing to the
     right; badges without that marker now sit next to the title
     separated by the .section-header `gap`. */
}

/* Grid helpers */
.grid { display: grid; gap: var(--sp-lg); }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
.grid-5 { grid-template-columns: repeat(5, 1fr); }

/* Toast */
.toast-container {
  position: fixed;
  bottom: var(--sp-xl);
  right: var(--sp-xl);
  z-index: 1000;
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
}

.toast {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-elevated);
  padding: var(--sp-md) var(--sp-lg);
  font-size: var(--font-size-caption);
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  animation: toastIn 0.3s ease;
  max-width: 360px;
}

.toast.success { border-left: 3px solid var(--status-positive); }
.toast.error { border-left: 3px solid var(--status-critical); }
.toast.info { border-left: 3px solid var(--task-watering); }

@keyframes toastIn {
  from { opacity: 0; transform: translateY(16px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Empty state */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--sp-3xl);
  color: var(--text-tertiary);
  text-align: center;
}

.empty-state svg {
  width: 48px;
  height: 48px;
  fill: none;
  stroke: var(--text-tertiary);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.5;
  margin-bottom: var(--sp-lg);
}

/* Breadcrumb */
.breadcrumb {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  font-size: var(--font-size-caption);
  color: var(--text-tertiary);
  margin-bottom: var(--sp-lg);
  flex-wrap: wrap;
}

.breadcrumb-item {
  cursor: pointer;
  transition: color var(--transition-fast);
  padding: 2px 4px;
  border-radius: var(--radius-sm);
}

.breadcrumb-item:hover { color: var(--primary); background: var(--primary-bg); }
.breadcrumb-item.active { color: var(--text-primary); font-weight: 500; }
.breadcrumb-sep { color: var(--text-tertiary); opacity: 0.5; }

/* Progress bar */
.progress-bar {
  height: 6px;
  background: var(--bg-secondary);
  border-radius: var(--radius-xs);
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  width: var(--bar-w, 0%);
  border-radius: var(--radius-xs);
  transition: width var(--transition-slow);
}

.progress-fill.green { background: var(--status-positive); }
.progress-fill.blue { background: var(--task-watering); }
.progress-fill.gold { background: var(--tier-gold); }

/* Tabs */
.tabs {
  display: flex;
  gap: 0;
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--sp-xl);
}

.tab {
  padding: var(--sp-sm) var(--sp-lg);
  font-size: var(--font-size-caption);
  font-weight: 500;
  color: var(--text-tertiary);
  border-bottom: 2px solid transparent;
  transition: all var(--transition-fast);
  cursor: pointer;
}

.tab:hover { color: var(--text-primary); }
.tab.active {
  color: var(--primary);
  border-bottom-color: var(--primary);
}

/* ── Button sizes ──────────────────────────────────────────────────── */
.btn-sm {
  padding: var(--sp-xs) var(--sp-md);
  font-size: var(--font-size-caption2);
}

/* ── Responsive ────────────────────────────────────────────────────── */
@media (max-width: 1024px) {
  .sidebar {
    position: fixed;
    left: 0;
    top: 0;
    transform: translateX(-100%);
    z-index: 200;
  }
  .sidebar.open { transform: translateX(0); }
  .sidebar-toggle { display: flex; }
  .grid-4 { grid-template-columns: repeat(2, 1fr); }
  .grid-5 { grid-template-columns: repeat(3, 1fr); }
  .page-container { padding: var(--sp-lg); }
}

@media (max-width: 768px) {
  .grid-3 { grid-template-columns: 1fr; }
  .grid-4 { grid-template-columns: 1fr; }
  .topbar-search { display: none; }
  .session-stats { display: none !important; }
  .page-container { padding: var(--sp-md); }
}

@media (max-width: 480px) {
  .topbar { padding: 0 var(--sp-md); gap: var(--sp-sm); }
  .topbar-title { font-size: var(--font-size-subheadline); }
}

/* ═══════════════════════════════════════════════════════════════════════
   Striped Progress — port of iOS Shared/Components/Badges/StripedProgressView
   ═══════════════════════════════════════════════════════════════════════
   Determinate: set inline style="--progress: 0.42" on the root element.
   Indeterminate: omit --progress (defaults to 1, fills track), keep the
   slide animation running so the user sees motion while async work runs.

   Stripe geometry mirrors iOS: 8 px stripe width, ~22.6 px (8√2) cycle on
   a 45° pattern, 0.64 s per cycle. Reduce-motion users get a flat fill.
   ─────────────────────────────────────────────────────────────────── */
.striped-progress {
  width: 100%;
  height: 6px;
}
.striped-progress.tall { height: 10px; }

.striped-progress-track {
  position: relative;
  width: 100%;
  height: 100%;
  background: rgba(45, 52, 35, 0.12);
  border-radius: var(--radius-pill);
  overflow: hidden;
}

.striped-progress-fill {
  height: 100%;
  width: calc(var(--progress, 1) * 100%);
  background-color: var(--primary);
  background-image: repeating-linear-gradient(
    -45deg,
    rgba(255, 255, 255, 0.30) 0,
    rgba(255, 255, 255, 0.30) 8px,
    transparent 8px,
    transparent 16px
  );
  background-size: 22.627px 22.627px;
  animation: striped-progress-slide 0.64s linear infinite;
  border-radius: var(--radius-pill);
  transition: width 0.18s ease;
}

@keyframes striped-progress-slide {
  from { background-position-x: 0; }
  to   { background-position-x: 22.627px; }
}

@media (prefers-reduced-motion: reduce) {
  .striped-progress-fill { animation: none; }
}

[data-theme="dark"] .striped-progress-track {
  background: rgba(255, 255, 255, 0.10);
}

/* ═══════════════════════════════════════════════════════════════════════
   Boot Splash — covers the viewport until App.boot() finishes its first
   network round-trip. Without this, the user stares at a white page for
   the 5-7 seconds it takes to fetch the catalog on a cold cache.
   ─────────────────────────────────────────────────────────────────── */
.boot-splash {
  position: fixed;
  inset: 0;
  /* Higher than .login-screen (1000) and the toast container so the
     splash always wins until App.boot() explicitly hides it. */
  z-index: 1100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-page);
  transition: opacity 0.25s ease;
}
.boot-splash.fading {
  opacity: 0;
  pointer-events: none;
}

.boot-splash-stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-lg);
  width: 280px;
  text-align: center;
}

.boot-splash-logo {
  width: 56px;
  height: 56px;
  object-fit: contain;
}
.boot-splash-logo.hidden { display: none; }

.boot-splash-brand {
  font-family: 'Poppins', sans-serif;
  font-weight: 600;
  font-size: var(--font-size-title3);
  color: var(--text-primary);
  margin: 0;
  letter-spacing: -0.2px;
}
.boot-splash-brand span { color: var(--primary); }

.boot-splash-status {
  font-family: 'Poppins', sans-serif;
  font-weight: 500;
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  letter-spacing: 0.4px;
  text-transform: uppercase;
  height: 1em;
  min-height: 1em;
}

/* ═══════════════════════════════════════════════════════════════════════
   AddEntityModal — generic create-new dialog used by Species / Cultivar
   / Tip / FAQ "+ New" flows. Same scrim + card pattern as the editor's
   validation modal so the visual treatment of "small focused dialog"
   stays consistent across surfaces.
   ═══════════════════════════════════════════════════════════════════════ */
/* The Add-Entity-Modal chrome lives in components/modal.css —
   .modal-overlay, .modal-card (--sm/--md/--lg), .modal-header,
   .modal-form, .modal-field, .modal-actions, etc. */


/* === login.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Login screen — full-viewport overlay shown when /api/admin/ping → 401.
   Aesthetic mirrors iOS onboarding screen 1: dark botanical background,
   centered cream card, Poppins type, primary green CTA.
   ═══════════════════════════════════════════════════════════════════════ */

.login-screen {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-2xl);
  /* Backdrop endpoints + green halo are theme-tokenised (tokens.css)
     so the screen pivots when the user toggles theme — light keeps
     the botanical onboarding green, dark goes near-black with the
     same accent. Hard-coded hex was theme-blind and made the dark
     card (#28231E) sit on near-identical #2D3423, hiding the edge. */
  background: var(--login-bg-start);
  background-image:
    radial-gradient(ellipse at 20% 0%,  var(--login-glow)      0%, transparent 50%),
    radial-gradient(ellipse at 90% 100%, var(--login-glow-soft) 0%, transparent 60%),
    linear-gradient(160deg, var(--login-bg-start) 0%, var(--login-bg-end) 70%);
  overflow-y: auto;
}

/* Belt-and-braces hide overrides — `display: flex/grid` on the shell &
   login wrappers wins over the browser's default [hidden] {display:none},
   so without these rules an element marked hidden via the attribute would
   still render. Lesson learned: never trust [hidden] alongside a
   container with explicit display set. */
.login-screen[hidden],
.app-shell[hidden],
.sidebar-session[hidden],
.nav-item[hidden],
.nav-badge[hidden],
#change-password-screen[hidden],
#boot-splash[hidden] {
  display: none !important;
}

.nav-badge {
  margin-left: auto;
  font-size: var(--font-size-caption2);
  font-weight: 600;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  background: var(--primary);
  color: var(--text-on-primary);
  min-width: 18px;
  text-align: center;
}

/* App shell wrapper needs to act as a flex container itself — the
   sidebar (240px) + main-content (flex 1) layout was originally
   directly under <body> (which sets display:flex). Wrapping them in
   .app-shell to gate visibility on session means the wrapper has to
   carry the same flex contract. */
.app-shell {
  display: flex;
  flex: 1;
  min-height: 100vh;
  width: 100%;
}

.login-card {
  width: 100%;
  max-width: 420px;
  background: var(--bg-card);
  border-radius: var(--radius-lg);
  padding: var(--sp-3xl) var(--sp-2xl) var(--sp-2xl);
  box-shadow: var(--shadow-elevated);
  text-align: center;
}

/* ── Bookplate masthead ───────────────────────────────────────────────
   Mirrors the layout in mailer.js renderInvite():
       [ leaf logo ]
         PlantHint            ← "Hint" in primary
       Pro Care, simplified   ← italic on "simplified"
       ──── ❦ ────            ← hairlines + fleuron
         GREENHOUSE           ← uppercase, tracked, tertiary
   Used by all three login-card screens (sign-in, force-change,
   invite-setup). The per-screen heading + sub follows below. */
.login-mast {
  margin-bottom: var(--sp-2xl);
}

.login-mast-logo {
  width: 56px;
  height: 52px;
  display: block;
  margin: 0 auto var(--sp-md);
  opacity: 0.92;
}

.login-mast-brand {
  font-size: var(--font-size-title1);
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.02em;
  line-height: 1;
  margin: 0 0 var(--sp-xs) 0;
}

.login-mast-brand span {
  color: var(--primary);
}

.login-mast-tagline {
  font-size: var(--font-size-footnote);
  color: var(--text-secondary);
  letter-spacing: 0.01em;
  line-height: 1.4;
  margin: 0;
}

.login-mast-tagline em {
  font-style: italic;
  color: var(--text-primary);
}

.login-mast-rule {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-md);
  margin: var(--sp-lg) 0 var(--sp-sm) 0;
}

.login-mast-rule-line {
  flex: 0 0 60px;
  height: 1px;
  background: var(--border-strong);
}

.login-mast-rule-fleuron {
  font-size: 14px;
  color: var(--text-tertiary);
  line-height: 1;
}

.login-mast-surface {
  font-size: var(--font-size-caption2);
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin: 0;
}

/* Per-screen heading sits between the masthead and the form. The
   <span> inside it picks up the primary green so each screen can
   highlight a key phrase ("new password", "aboard", …). */
.login-heading {
  font-size: var(--font-size-title3);
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.01em;
  line-height: 1.25;
  margin: 0 0 var(--sp-xs) 0;
}

.login-heading span {
  color: var(--primary);
}

.login-sub {
  font-size: var(--font-size-footnote);
  color: var(--text-tertiary);
  letter-spacing: 0.02em;
  line-height: 1.5;
  margin: 0;
}

/* Tight gap when two .login-sub elements stack (setup screen has an
   optional username line right after the main subtitle). The big
   breathing-room before the form is owned by .login-form below. */
.login-sub + .login-sub { margin-top: var(--sp-xs); }

/* Honour the `hidden` attribute even when an explicit display rule
   below would otherwise win. Without this, `<form class="login-form"
   hidden>` stays visible because `display: flex` on .login-form has
   higher specificity than the UA stylesheet's `[hidden] { display: none }`.
   The setup-screen's expired-link state toggles `form.hidden` to swap
   the active password form for a static recovery message; that swap
   silently no-op'd until this rule landed. */
[hidden] { display: none !important; }

.login-form {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--sp-sm);
  text-align: left;
  margin-top: var(--sp-2xl);
}

.login-label {
  font-size: var(--font-size-caption);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-tertiary);
}

.login-input {
  font-size: var(--font-size-body);
  font-weight: 500;
  padding: 14px var(--sp-lg);
  background: var(--bg-input);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-lg);
  color: var(--text-primary);
  transition: border-color var(--transition-fast), background var(--transition-fast),
              box-shadow var(--transition-fast);
  width: 100%;
}

.login-input::placeholder {
  color: var(--text-tertiary);
  font-weight: 400;
}

.login-input:focus {
  outline: none;
  border-color: var(--primary);
  background: var(--bg-input-focus);
  box-shadow: 0 0 0 4px var(--primary-bg);
}

.login-hint {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  margin-top: var(--sp-xxs);
  margin-bottom: var(--sp-md);
  line-height: 1.45;
}

.login-submit {
  font-family: inherit;
  font-size: var(--font-size-body);
  font-weight: 600;
  color: var(--text-on-primary);
  background: var(--primary);
  padding: 14px var(--sp-lg);
  border-radius: var(--radius-lg);
  border: none;
  cursor: pointer;
  transition: background var(--transition-fast), transform var(--transition-fast);
  width: 100%;
  margin-top: var(--sp-sm);
}

.login-submit:hover { background: var(--primary-shade); }
.login-submit:active { transform: scale(0.985); }
.login-submit:disabled {
  background: var(--primary);
  opacity: 0.55;
  cursor: wait;
}

.login-error {
  font-size: var(--font-size-footnote);
  color: var(--status-critical-text);
  min-height: 18px;
  margin-top: var(--sp-sm);
  text-align: center;
}
.login-error:empty { display: none; }

.login-footer {
  margin-top: var(--sp-2xl);
  padding-top: var(--sp-lg);
  border-top: 1px solid var(--border);
  font-size: var(--font-size-caption);
  color: var(--text-tertiary);
}

/* In-card text-only buttons — used for "Sign out instead" on the
   force-change screen and the "logout-everywhere" affordance later.
   Same warm tone as .login-footer text but underlined on hover so the
   affordance is obvious without competing with the primary CTA. */
.login-text-link {
  background: transparent;
  border: none;
  padding: 0;
  font: inherit;
  font-size: var(--font-size-caption);
  color: var(--text-tertiary);
  cursor: pointer;
  text-decoration: none;
  transition: color var(--transition-fast);
}
.login-text-link:hover {
  color: var(--primary-shade);
  text-decoration: underline;
}

/* The label-input pairs sit close together; give input a tiny vertical
   nudge below its label and a slightly bigger gap before the next
   label so the form scans as label/input/label/input rather than as a
   uniformly-spaced wall. */
.login-form .login-label + .login-input { margin-bottom: var(--sp-sm); }
.login-form .login-input + .login-label { margin-top: var(--sp-md); }

/* ── Sidebar session pill (footer) ────────────────────────────────── */
.sidebar-session {
  margin-bottom: var(--sp-md);
  padding: var(--sp-sm) var(--sp-md);
  background: var(--bg-sidebar-hover);
  border-radius: var(--radius-md);
}

.sidebar-session-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-xs);
}

.sidebar-session-label {
  font-size: var(--font-size-caption2);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-sidebar);
  opacity: 0.7;
}

.sidebar-logout {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-sm);
  color: var(--text-sidebar);
  opacity: 0.7;
  transition: opacity var(--transition-fast), background var(--transition-fast),
              color var(--transition-fast);
}
.sidebar-logout:hover {
  opacity: 1;
  background: rgba(255, 252, 248, 0.10);
  color: var(--text-sidebar-active);
}

.sidebar-session-nick {
  margin-top: var(--sp-xxs);
  font-size: var(--font-size-footnote);
  font-weight: 600;
  color: var(--text-sidebar-active);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Placeholder card (used by Contributions page until Phase 5) ──── */
.placeholder-card {
  background: var(--bg-card);
  border-radius: var(--radius-lg);
  padding: var(--sp-3xl) var(--sp-2xl);
  box-shadow: var(--shadow-card);
  text-align: center;
  max-width: 560px;
  margin: var(--sp-2xl) auto 0;
}

.placeholder-icon {
  width: 48px;
  height: 48px;
  margin: 0 auto var(--sp-lg);
  display: block;
  fill: none;
  stroke: var(--primary);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.55;
}

.placeholder-title {
  font-size: var(--font-size-title3);
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: var(--sp-sm);
}

.placeholder-body {
  font-size: var(--font-size-callout);
  color: var(--text-secondary);
  line-height: 1.55;
  margin-bottom: var(--sp-xl);
}

.placeholder-body a {
  color: var(--primary-shade);
  font-weight: 600;
}

.placeholder-list {
  text-align: left;
  background: var(--bg-input);
  border-radius: var(--radius-md);
  padding: var(--sp-lg) var(--sp-xl);
  list-style: none;
  font-size: var(--font-size-footnote);
  color: var(--text-secondary);
  line-height: 1.7;
}

.placeholder-list li {
  position: relative;
  padding-left: var(--sp-md);
}

.placeholder-list li::before {
  content: "·";
  position: absolute;
  left: 0;
  color: var(--primary);
  font-weight: 700;
}

/* ── Floating theme toggle (pre-shell screens only) ────────────────
   The sidebar's #theme-toggle lives inside .app-shell, so users on
   the login / change-password / setup screens had no way to flip
   theme — the auth surface was effectively dark-mode-blind. This
   button mirrors the same sun/moon SVGs the sidebar uses (so we
   don't introduce a second design token), but lives at the body
   root and is shown only while one of the pre-shell screens is
   visible. `:has()` baseline: Safari 15.4+ / Chrome 105+ / Firefox
   121+ — well above Greenhouse's modern-browser-only contract.

   Position: bottom-left, 16px from each edge — far from the form
   so it never competes with the primary CTA, but visually
   counter-balances the masthead which lives top-center of the card. */
.theme-toggle--floating {
  position: fixed;
  left: var(--sp-lg);
  bottom: var(--sp-lg);
  z-index: 1100;                        /* above .login-screen (1000) */
  width: 36px;
  height: 36px;
  border-radius: var(--radius-md);
  display: none;                        /* shown via :has() rules below */
  align-items: center;
  justify-content: center;
  /* Glassy chip — sits readably on both light-green and near-black
     gradients without an extra theme override. */
  background: rgba(255, 252, 248, 0.06);
  border: 1px solid rgba(255, 252, 248, 0.10);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: rgba(255, 252, 248, 0.72);
  cursor: pointer;
  transition: background var(--transition-fast),
              border-color var(--transition-fast),
              color var(--transition-fast),
              transform var(--transition-fast);
}

.theme-toggle--floating:hover {
  background: rgba(255, 252, 248, 0.12);
  border-color: rgba(255, 252, 248, 0.18);
  color: rgba(255, 252, 248, 0.95);
}

.theme-toggle--floating:active {
  transform: scale(0.96);
}

.theme-toggle--floating:focus-visible {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-bg);
}

.theme-toggle--floating svg {
  width: 16px;
  height: 16px;
  fill: none;
}

/* Reveal only while a pre-shell auth screen is on-stage. The boot
   splash + app-shell have no business with this control — the
   sidebar toggle takes over inside the shell. */
body:has(#login-screen:not([hidden])) .theme-toggle--floating,
body:has(#change-password-screen:not([hidden])) .theme-toggle--floating,
body:has(#setup-screen:not([hidden])) .theme-toggle--floating {
  display: flex;
}

/* ── Topbar snapshot pill ─────────────────────────────────────────── */
.topbar-snapshot {
  font-size: var(--font-size-caption2);
  font-weight: 500;
  color: var(--text-tertiary);
  letter-spacing: 0.04em;
  padding: 4px var(--sp-sm);
  background: var(--bg-pill);
  border-radius: var(--radius-pill);
  white-space: nowrap;
  margin-left: var(--sp-md);
  cursor: help;
}


/* === dashboard.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Dashboard Page
   ═══════════════════════════════════════════════════════════════════════ */

.dashboard-stats {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: var(--sp-lg);
  margin-bottom: var(--sp-2xl);
}

.stat-card .stat-icon {
  width: 36px;
  height: 36px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: var(--sp-sm);
}

.stat-card .stat-icon svg {
  width: 20px;
  height: 20px;
  fill: white;
}

.stat-icon.green { background: var(--primary); }
.stat-icon.gold { background: var(--tier-gold); }
.stat-icon.red { background: var(--status-critical); }
.stat-icon.blue { background: var(--task-watering); }
.stat-icon.purple { background: var(--task-cleaning); }

.stat-value {
  /* Hero stat number — pinned via the role token so this lines up
     with .ops-value below (and any other hero number we add). */
  font-size: var(--t-hero-number);
  font-weight: 700;
  color: var(--text-primary);
  line-height: 1.1;
  margin-top: var(--sp-xs);
}
.stat-label {
  font-size: var(--font-size-caption);
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-top: var(--sp-xs);
}
.stat-sub {
  font-size: var(--font-size-caption);
  color: var(--text-tertiary);
  margin-top: 2px;
}

/* ── Operations strip (Phase 6.1: live counts) ───────────────────────
   Sits between the catalog stats (passive snapshot) and the coverage
   bars. Each card is a link to the surface that owns the work; the
   colour ramp differentiates "review queue" / "draft pipeline" /
   "compliance gap" without relying on icons alone (accessibility). */
.dashboard-ops {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--sp-lg);
  margin-bottom: var(--sp-2xl);
}

.ops-card {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-lg);
  text-decoration: none;
  color: inherit;
  border: 1px solid transparent;
  transition: border-color var(--transition-fast), background var(--transition-fast), transform var(--transition-fast);
}

.ops-card:hover {
  text-decoration: none;
  border-color: var(--primary-border);
  background: var(--bg-card-hover);
}

.ops-card:active { transform: scale(0.99); }

.ops-icon {
  width: 36px;
  height: 36px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  flex-shrink: 0;
}

.ops-icon svg { width: 18px; height: 18px; }

.ops-amber   .ops-icon { background: var(--status-caution); }
.ops-primary .ops-icon { background: var(--primary); }
.ops-warning .ops-icon { background: var(--status-warning); }

.ops-body { flex: 1; min-width: 0; }

.ops-value {
  font-size: var(--t-hero-number);
  font-weight: 700;
  line-height: 1;
  color: var(--text-primary);
}

.ops-label {
  font-size: var(--font-size-caption);
  font-weight: 600;
  color: var(--text-secondary);
  margin-top: 2px;
}

.ops-hint {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  margin-top: 2px;
}

.ops-arrow {
  color: var(--text-tertiary);
  flex-shrink: 0;
  transition: transform var(--transition-fast);
}

.ops-card:hover .ops-arrow {
  transform: translateX(2px);
  color: var(--primary);
}

/* ── Coverage cards ─────────────────────────────────────────────── */
.dashboard-coverage {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: var(--sp-lg);
  margin-bottom: var(--sp-2xl);
}

.coverage-card {
  padding: var(--sp-lg);
}

.coverage-card .coverage-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: var(--sp-sm);
}

.coverage-card .coverage-label {
  font-size: var(--font-size-caption);
  font-weight: 600;
  color: var(--text-secondary);
}

.coverage-card .coverage-value {
  font-size: var(--font-size-callout);
  font-weight: 700;
  color: var(--text-primary);
}

/* Sub-line under the bar — used when raw counts are more meaningful
   than the percentage alone (e.g. photo credit: "8042 of 9214 photos"). */
.coverage-sub {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  margin-top: var(--sp-xs);
}

/* Dashboard sections */
.dashboard-grid {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: var(--sp-xl);
}

.dashboard-section {
  padding: var(--sp-xl);
}

.dashboard-section h3 {
  font-size: var(--t-card-title);
  font-weight: 600;
  margin-bottom: var(--sp-lg);
  color: var(--text-primary);
}

/* Tier distribution */
.tier-row {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-sm) 0;
}

.tier-row .tier-label {
  width: 60px;
  font-size: var(--font-size-caption);
  font-weight: 600;
}

.tier-row .tier-bar {
  flex: 1;
  height: 24px;
  background: var(--bg-secondary);
  border-radius: var(--radius-sm);
  overflow: hidden;
  position: relative;
}

.tier-row .tier-fill {
  height: 100%;
  width: var(--bar-w, 0%);
  border-radius: var(--radius-sm);
  transition: width 0.8s ease;
  display: flex;
  align-items: center;
  padding-left: var(--sp-sm);
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: white;
}

.tier-fill.gold { background: var(--tier-gold); }
.tier-fill.silver { background: var(--tier-silver); }
.tier-fill.bronze { background: var(--tier-bronze); }

.tier-row .tier-count {
  width: 60px;
  text-align: right;
  font-size: var(--font-size-caption);
  font-weight: 500;
  color: var(--text-secondary);
}

/* Quick actions */
.quick-actions {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
}

.quick-action {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-sm) var(--sp-md);
  border-radius: var(--radius-md);
  transition: background var(--transition-fast);
  cursor: pointer;
  color: var(--text-primary);
  text-decoration: none;
}

.quick-action:hover {
  background: var(--primary-bg);
  text-decoration: none;
}

.quick-action svg {
  width: 18px;
  height: 18px;
  /* Lucide stroke icons (helper sets stroke=currentColor inline). The
     parent rule overrides the Icon helper so the quick-action chiclet
     keeps its sage tint regardless of surrounding text colour. */
  fill: none;
  stroke: var(--primary);
  flex-shrink: 0;
}

.quick-action .qa-label {
  font-size: var(--font-size-caption);
  font-weight: 600;
  color: var(--text-primary);
}

.quick-action .qa-desc {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  margin-top: 1px;
}

/* Taxonomy mini preview — canvas-based mini Living tree (replaces the
   D3 sunburst as of F11.2). The container hosts a <canvas> sibling
   plus an absolute-positioned center label overlay. CSP forbids
   inline `style=""` attributes (`style-src 'self'`), hence the named
   subclasses below for the canvas + label slots. */
.taxonomy-preview {
  width: 100%;
  aspect-ratio: 1;
  max-height: 280px;
  margin: 0 auto;
  position: relative;
  cursor: pointer;
}

.taxonomy-preview canvas {
  display: block;
  width: 100%;
  height: 100%;
}

.taxonomy-preview .mini-living-center {
  position: absolute;
  /* Anchored to the bottom band so the canvas's tree centre is free
     for branch joins. Previously sat dead-centre and overlapped the
     trunk node + descending Eudicot branches, which the user reported
     as "Plantae yazısı node'lara binmiş." */
  left: 0;
  right: 0;
  bottom: var(--sp-md);
  display: flex;
  flex-direction: column;
  align-items: center;
  pointer-events: none;
  text-align: center;
}

.taxonomy-preview .mini-living-name {
  font-weight: 700;
  font-size: var(--font-size-subheadline);
  color: var(--text-primary);
}

.taxonomy-preview .mini-living-count {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  margin-top: 2px;
}

/* Toxicity summary */
.tox-row {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-xs) 0;
  font-size: var(--font-size-caption);
}

.tox-icon {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-secondary);
}
.tox-icon svg { width: 16px; height: 16px; }
.tox-label { flex: 1; color: var(--text-secondary); }
.tox-count { font-weight: 600; color: var(--text-primary); }

@media (max-width: 1280px) {
  .dashboard-coverage { grid-template-columns: repeat(3, 1fr); }
}

@media (max-width: 1024px) {
  .dashboard-stats { grid-template-columns: repeat(3, 1fr); }
  .dashboard-ops { grid-template-columns: 1fr; }
  .dashboard-coverage { grid-template-columns: repeat(2, 1fr); }
  .dashboard-grid { grid-template-columns: 1fr; }
}


/* === explorer.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Taxonomic Explorer — Cosmic Canvas
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Root ────────────────────────────────────────────────────────────── */
.explorer-cosmos {
  display: flex;
  flex-direction: column;
  height: calc(100vh - var(--topbar-height) - 2 * var(--sp-xl));
  min-height: 500px;
  position: relative;
}

/* ═══ NAV BAR ════════════════════════════════════════════════════════ */
.cosmos-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--sp-xs) 0 var(--sp-sm);
  gap: var(--sp-lg);
  flex-shrink: 0;
}

.cosmos-breadcrumb {
  display: flex;
  align-items: center;
  gap: 2px;
  flex-wrap: wrap;
  min-width: 0;
}

/* Breadcrumb segments render via the shared .chip-filter component
   (see components/chip.css) with the --sm dense variant. Only the
   icon-slot rule stays here since the breadcrumb is the only place
   that nests an emoji-icon inside the chip. */
.cosmos-crumb-icon { font-size: var(--font-size-caption2); line-height: 1; }

.cosmos-crumb-sep {
  color: var(--text-tertiary);
  opacity: 0.4;
  font-size: var(--font-size-caption2);
  margin: 0 1px;
}

.cosmos-nav-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-lg);
  flex-shrink: 0;
}

/* Cosmos toolbar buttons render via the shared .chip-filter component
   (see components/chip.css). No explorer-specific override. */

/* ── View-mode segmented control ────────────────────────────────────
   Two-button pill that lets the user toggle between Pack and Radial
   topologies. Sits in the cosmos-nav alongside the Filters chip and
   borrows the surrounding chip metrics so it doesn't visually outweigh
   them. The active state lifts the button with a subtle primary-bg
   tint + cream surface — same tactile pattern as iOS segmented
   controls, no shadow-heavy "raised button" look. */
.cosmos-view-seg {
  display: inline-flex;
  align-items: center;
  background: var(--bg-pill);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 2px;
  gap: 2px;
}

.cosmos-view-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  font-size: var(--font-size-caption2);
  font-weight: 500;
  color: var(--text-tertiary);
  background: transparent;
  transition: color var(--transition-fast), background var(--transition-fast);
}

.cosmos-view-btn:hover {
  color: var(--text-secondary);
}

.cosmos-view-btn.active {
  color: var(--primary);
  background: var(--bg-card);
  font-weight: 600;
  box-shadow: var(--shadow-sm);
}

.cosmos-view-btn svg {
  flex-shrink: 0;
  opacity: 0.85;
}

.cosmos-view-btn.active svg { opacity: 1; }

@media (max-width: 768px) {
  .cosmos-view-btn span { display: none; }
  .cosmos-view-btn { padding: 5px 8px; }
}

.cosmos-stats {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
}

.cosmos-stat-sep { opacity: 0.35; }

/* ═══ LAYOUT ═════════════════════════════════════════════════════════ */
.cosmos-layout {
  flex: 1;
  display: flex;
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-lg);
  border: 1px solid var(--border);
}

/* ═══ VIEWPORT (canvas container) ════════════════════════════════════ */
.cosmos-viewport {
  flex: 1;
  position: relative;
  overflow: hidden;
}

/* Bell-jar / vitrine ring — both modes now (pack at half-strength).
   A soft inset edge glow that reads as curved glass at the canvas
   perimeter, paired with the canvas-painted sage halo + paper grain
   to sell the "diorama under glass" frame. The double inset (warm-
   light highlight on top + warm-shadow on bottom-right) gives the
   curvature a directional cue without using per-corner gradients.
   Pack inherits the same glass framing because the user shouldn't
   feel a sudden "now we're in a diorama, now we're not" shift when
   toggling views — the diorama is the room; the view mode picks how
   the specimens are arranged in it. */
.explorer-cosmos .cosmos-viewport::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: var(--radius-lg);
  /* Pack baseline — softer rim. */
  box-shadow:
    inset 0 1px 0 rgba(255, 252, 248, 0.30),
    inset 0 -1px 0 rgba(90, 65, 30, 0.06),
    inset 10px 14px 30px rgba(255, 252, 248, 0.10),
    inset -10px -14px 36px rgba(90, 65, 30, 0.06);
}

/* Living — full-strength rim (the original "vitrine glass" feel). */
.explorer-cosmos.cosmos-living .cosmos-viewport::after {
  box-shadow:
    inset 0 1px 0 rgba(255, 252, 248, 0.45),
    inset 0 -1px 0 rgba(90, 65, 30, 0.10),
    inset 14px 18px 40px rgba(255, 252, 248, 0.18),
    inset -14px -18px 50px rgba(90, 65, 30, 0.10);
}

[data-theme="dark"] .explorer-cosmos .cosmos-viewport::after {
  box-shadow:
    inset 0 1px 0 rgba(255, 252, 248, 0.06),
    inset 0 -1px 0 rgba(0, 0, 0, 0.28),
    inset 10px 14px 30px rgba(255, 252, 248, 0.02),
    inset -10px -14px 36px rgba(0, 0, 0, 0.20);
}

[data-theme="dark"] .explorer-cosmos.cosmos-living .cosmos-viewport::after {
  box-shadow:
    inset 0 1px 0 rgba(255, 252, 248, 0.10),
    inset 0 -1px 0 rgba(0, 0, 0, 0.40),
    inset 14px 18px 40px rgba(255, 252, 248, 0.04),
    inset -14px -18px 50px rgba(0, 0, 0, 0.30);
}

.cosmos-viewport canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* Center info overlay */
.cosmos-center-info {
  position: absolute;
  bottom: var(--sp-xl);
  left: 50%;
  transform: translateX(-50%) translateY(8px);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--sp-md) var(--sp-xl);
  text-align: center;
  box-shadow: var(--shadow-elevated);
  opacity: 0;
  transition: all 0.3s ease;
  pointer-events: none;
  z-index: 10;
  max-width: 320px;
  cursor: pointer;
}

.cosmos-center-info.visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
  pointer-events: auto;
}

.center-info-level {
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.center-info-name {
  font-size: var(--font-size-callout);
  font-weight: 700;
  margin: 2px 0;
}

.center-info-count {
  font-size: var(--font-size-caption);
  color: var(--text-secondary);
}

.center-info-hint {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  margin-top: 6px;
}

/* ═══ FILTER PANEL ═══════════════════════════════════════════════════ */
.cosmos-filters {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 230px;
  background: var(--bg-card);
  border-right: 1px solid var(--border);
  padding: var(--sp-lg);
  overflow-y: auto;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
  transform: translateX(-100%);
  transition: transform var(--transition-normal);
  box-shadow: var(--shadow-elevated);
}

.cosmos-filters.open { transform: translateX(0); }

.cosmos-filters-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-weight: 600;
  font-size: var(--font-size-footnote);
  padding-bottom: var(--sp-sm);
  border-bottom: 1px solid var(--border);
}

.cosmos-filters-close {
  width: 26px;
  height: 26px;
  border-radius: var(--radius-pill);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
}
.cosmos-filters-close:hover { background: var(--bg-secondary); }

.cosmos-filter-reset {
  padding: 6px;
  border-radius: var(--radius-pill);
  font-size: var(--font-size-caption2);
  font-weight: 500;
  color: var(--text-tertiary);
  background: var(--bg-secondary);
  text-align: center;
  cursor: pointer;
  margin-top: auto;
}
.cosmos-filter-reset:hover { color: var(--primary); background: var(--primary-bg); }

.filter-group label {
  display: block;
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: var(--sp-xs);
}

/* Vertical filter sidebar density — narrower than the .field-sm tier
   (which is for horizontal filter rows). Kept bespoke because the
   sidebar packs many controls vertically and benefits from a slightly
   shorter row + slightly bigger font (12 vs 11) for legibility. */
.filter-group select {
  width: 100%;
  padding: 5px var(--sp-sm);
  font-size: var(--font-size-caption);
}

.filter-chips { display: flex; flex-wrap: wrap; gap: 4px; }

/* Filter-panel chips render via the shared .chip-filter.chip-filter--sm
   component (see components/chip.css). */

/* ═══ TOOLTIP ════════════════════════════════════════════════════════ */
.cosmos-tooltip {
  position: fixed;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-elevated);
  padding: var(--sp-md) var(--sp-lg);
  pointer-events: none;
  z-index: 300;
  font-size: var(--font-size-caption);
  max-width: 260px;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.12s ease, transform 0.12s ease;
}
.cosmos-tooltip.visible { opacity: 1; transform: translateY(0); }

.cosmos-tt-level {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 2px;
}
.cosmos-tt-name { font-weight: 600; font-size: var(--font-size-footnote); margin-bottom: 4px; }
.cosmos-tt-count { color: var(--text-secondary); font-size: var(--font-size-caption2); }
.cosmos-tt-children { color: var(--text-tertiary); font-size: var(--font-size-caption2); }
.cosmos-tt-pct { color: var(--primary); font-weight: 600; font-size: var(--font-size-caption2); margin-top: 3px; }
.cosmos-tt-hint {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  margin-top: 6px;
  padding-top: 5px;
  border-top: 1px solid var(--border);
  font-style: italic;
}

/* ═══ ALBUM MODE ═════════════════════════════════════════════════════ */
.cosmos-album {
  display: none;
  flex-direction: column;
  flex: 1;
  overflow: hidden;
  padding: var(--sp-lg);
}
.cosmos-album.active { display: flex; }

.album-header {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding-bottom: var(--sp-md);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--sp-md);
  flex-shrink: 0;
}

.album-back {
  width: 34px;
  height: 34px;
  border-radius: var(--radius-pill);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-secondary);
  color: var(--text-primary);
  flex-shrink: 0;
  transition: all var(--transition-fast);
}
.album-back:hover { background: var(--primary-bg); color: var(--primary); }

.album-header-info { display: flex; flex-direction: column; min-width: 0; }

.album-title {
  font-size: var(--font-size-callout);
  font-weight: 600;
  font-style: italic;
  line-height: 1.2;
}

.album-subtitle {
  font-size: var(--font-size-caption2);
  color: var(--text-secondary);
}

.album-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  /* Without `grid-auto-rows`, the implicit rows tried to fill the flex
     parent's height and stretched cards (or compressed them when there
     were many rows). Pinning each row to content-height keeps every
     card at its photo + body intrinsic size — Carex (326 species, 41
     rows) was the failure mode where rows collapsed to ~30px strips
     and the photo + body overflowed out of an `overflow:hidden` card.
     `align-content: start` keeps any leftover vertical space below the
     last row instead of dividing it across tracks. */
  grid-auto-rows: auto;
  align-content: start;
  gap: var(--sp-md);
  overflow-y: auto;
  flex: 1;
  padding-right: var(--sp-xs);
}

/* Species card */
.species-card {
  background: var(--bg-card);
  border-radius: var(--radius-md);
  overflow: hidden;
  cursor: pointer;
  transition: all 0.2s ease;
  border: 1px solid var(--border);
  /* Lock the card's intrinsic block size so the grid can size each
     row deterministically. Without this, the card's height resolves
     to ~2px (border + collapsed content) because `aspect-ratio` on
     `.species-card-photo` introduces a chicken-and-egg with grid
     row sizing — the photo's intrinsic height depends on width,
     which depends on grid layout, which depends on row height.
     Chromium short-circuits the cycle by treating intrinsic block
     size as 0 in the first pass and never reflowing. The result was
     the "Carex 326 species → 30px strips" bug; min-height pins the
     row to the expected photo + body height so the grid commits a
     correct track size before the aspect-ratio calc lands. */
  min-height: 220px;
}
.species-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-card);
  border-color: var(--border);
}

.species-card-photo {
  width: 100%;
  aspect-ratio: 4/3;
  background: var(--bg-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  position: relative;
}

.species-card-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Default to opacity:0 so the warm `--bg-secondary` swatch on the
     parent shows through while the photo decodes. The image flips to
     `.loaded` (set by bindImageLoadFade in explorer.js) on the load
     event, fading in over 240ms — replaces the prior pop-in flash that
     read as "image keeps re-loading on scroll". Cached images become
     complete before the listener attaches, in which case bindImageLoadFade
     applies `.loaded` synchronously and no animation plays. */
  opacity: 0;
  transition: opacity 0.24s ease, transform 0.3s ease;
}
.species-card-photo img.loaded { opacity: 1; }
.species-card:hover .species-card-photo img.loaded { transform: scale(1.04); }
@media (prefers-reduced-motion: reduce) {
  .species-card-photo img { transition: none; }
  .species-card-photo img.loaded { opacity: 1; }
  .species-card:hover .species-card-photo img { transform: none; }
}

.species-card-photo .no-photo { font-size: var(--font-size-title1); opacity: 0.15; }

.species-card-badges {
  position: absolute;
  top: var(--sp-xs);
  right: var(--sp-xs);
  display: flex;
  gap: 3px;
}

.species-card-body { padding: var(--sp-sm) var(--sp-md) var(--sp-md); }

.species-card-name {
  font-size: var(--font-size-caption);
  font-weight: 600;
  font-style: italic;
  color: var(--text-primary);
  line-height: 1.3;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.species-card-common {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  margin-top: 1px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.species-card-meta {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-top: var(--sp-xs);
  flex-wrap: wrap;
}

/* Cosmos meta tags render via the shared .pill-tag component (see
   components/pill.css). The cultivar-tinted variant uses
   .pill-tag.pill-tag--accent. No explorer-specific override. */

/* ═══ DETAIL PANEL ═══════════════════════════════════════════════════ */
.cosmos-detail {
  width: 0;
  flex-shrink: 0;
  overflow: hidden;
  transition: width 0.3s ease;
}
.cosmos-detail.visible {
  width: 340px;
  border-left: 1px solid var(--border);
  overflow-y: auto;
  padding: var(--sp-lg);
}

.detail-header {
  display: flex;
  justify-content: flex-end;
  margin-bottom: var(--sp-xs);
}

.detail-close {
  width: 28px;
  height: 28px;
  border-radius: var(--radius-pill);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-caption);
  color: var(--text-tertiary);
}
.detail-close:hover { background: var(--bg-secondary); }

.detail-photo {
  width: 100%;
  aspect-ratio: 4/3;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--bg-secondary);
  margin-bottom: var(--sp-lg);
}
.detail-photo img { width: 100%; height: 100%; object-fit: cover; }

.detail-identity { margin-bottom: var(--sp-lg); }
.detail-name { font-size: var(--font-size-callout); font-weight: 700; font-style: italic; line-height: 1.3; }
.detail-common { font-size: var(--font-size-caption); color: var(--text-secondary); margin-top: 2px; }

.detail-section { margin-bottom: var(--sp-lg); }
.detail-section-title {
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: var(--sp-sm);
  padding-bottom: var(--sp-xs);
  border-bottom: 1px solid var(--border);
}

.detail-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 2px 0;
  font-size: var(--font-size-caption);
}
.detail-row-label { color: var(--text-secondary); }
.detail-row-value { font-weight: 500; text-align: right; max-width: 55%; }

/* Detail-panel readonly tags render via the shared .pill-tag
   component (see components/pill.css). */
.detail-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: var(--sp-xs); }

.detail-cultivar-item {
  padding: var(--sp-xs) 0;
  border-bottom: 1px solid var(--border);
  font-size: var(--font-size-caption);
  font-weight: 500;
}
.detail-cultivar-item:last-child { border-bottom: none; }

.detail-more { font-size: var(--font-size-caption2); color: var(--text-tertiary); padding: var(--sp-xs) 0; }

.detail-actions {
  margin-top: var(--sp-lg);
  padding-top: var(--sp-lg);
  border-top: 1px solid var(--border);
}
.detail-action-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 7px 14px;
  border-radius: var(--radius-pill);
  font-size: var(--font-size-caption2);
  font-weight: 500;
  color: var(--primary);
  background: var(--primary-bg);
  transition: all var(--transition-fast);
}
.detail-action-btn:hover { background: var(--primary-bg-hover); }

/* ═══ RESPONSIVE ═════════════════════════════════════════════════════ */
@media (max-width: 1200px) {
  .cosmos-detail.visible { width: 290px; }
}

@media (max-width: 1024px) {
  .cosmos-detail.visible {
    position: fixed;
    right: 0; top: 0; bottom: 0;
    width: 340px;
    z-index: 150;
    background: var(--bg-card);
    box-shadow: var(--shadow-elevated);
    border-left: 1px solid var(--border);
  }
  .cosmos-stats { display: none; }
}

@media (max-width: 768px) {
  .cosmos-breadcrumb { overflow-x: auto; flex-wrap: nowrap; }
  .album-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
}


/* === catalog.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Catalog Editor — shared chrome for the species + cultivar list pages.

   Owned classes (`.cat-*`) live here once. Page-specific row visuals
   (`.ed-row` for species, `.cv-row` for cultivars) stay in their
   own stylesheets — they share the .cat-row anchor for delegation
   but their internal layout differs.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Stats bar ─────────────────────────────────────────────────────── */
.cat-stats-bar {
  display: flex;
  gap: var(--sp-md);
  margin-bottom: var(--sp-lg);
  flex-wrap: wrap;
}

.cat-stat-pill {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-sm);
  padding: var(--sp-sm) var(--sp-lg);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
}

.cat-stat-value {
  font-size: var(--font-size-callout);
  font-weight: 700;
  color: var(--text-primary);
  line-height: 1;
}

.cat-stat-label {
  font-size: var(--font-size-caption);
  color: var(--text-secondary);
}

.cat-stat-pill.cat-stat-success { border-color: var(--status-positive); }
.cat-stat-pill.cat-stat-success .cat-stat-value { color: var(--status-positive); }

.cat-stat-pill.cat-stat-warning { border-color: var(--status-warning); }
.cat-stat-pill.cat-stat-warning .cat-stat-value { color: var(--status-warning); }

.cat-stat-pill.cat-stat-danger { border-color: var(--status-critical); }
.cat-stat-pill.cat-stat-danger .cat-stat-value { color: var(--status-critical); }

.cat-stat-pill.cat-stat-primary { border-color: var(--primary); }
.cat-stat-pill.cat-stat-primary .cat-stat-value { color: var(--primary); }

/* ── Toolbar ──────────────────────────────────────────────────────── */
.cat-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-sm);
  margin-bottom: var(--sp-sm);
  align-items: center;
}

/* Search input is rendered via the shared .toolbar-search component
   (see components/search.css). Catalog toolbars give it a generous
   flex slot since the species/cultivar editors are wide pages. */
.cat-toolbar .toolbar-search {
  flex: 1 1 220px;
  min-width: 200px;
}

/* Size via .field-sm on the markup. Page rule only enforces the
   minimum width so 5 dropdowns side-by-side don't collapse below
   readable label-truncation width. */
.cat-filter-select {
  min-width: 110px;
}

.cat-group-select {
  /* Visually distinguish "Group by …" from content filters. */
  background: var(--primary-bg);
  color: var(--primary);
  font-weight: 600;
  border-color: var(--primary-border, var(--border));
}

/* ── Mode bar ─────────────────────────────────────────────────────── */
.cat-mode-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-sm) 0;
  margin-bottom: var(--sp-sm);
  border-bottom: 1px solid var(--border);
}

.cat-actions {
  display: flex;
  gap: var(--sp-xs);
  margin-left: auto;
  position: relative;
  align-items: center;
  flex-wrap: wrap;
}

/* ── Indeterminate progress strip ──────────────────────────────────
   Sits flush above the list when GroupedListView reports busy
   (mounting a big group, expand-all over many buckets). Classic
   "sliding thumb" pattern: a track + a translating segment that
   sweeps left → right indefinitely.

   The earlier striped-march design rendered as dashes at 2 px
   height (the gradient stops were too dense for the available
   pixel runway). This version uses a single moving gradient
   segment which reads as motion at any height ≥ 3 px and is
   robust across the Chromium versions we care about (animation
   never silently no-ops). */
.cat-progress {
  position: relative;
  height: 3px;
  margin-bottom: var(--sp-xs);
  background: var(--bg-secondary);
  border-radius: var(--radius-pill);
  overflow: hidden;
}
.cat-progress[hidden] { display: none; }

.cat-progress::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 35%;
  border-radius: var(--radius-pill);
  /* Horizontal gradient gives the leading + trailing edges a soft
     fall-off so the segment reads as a swept highlight rather than
     a hard block. The middle stop holds at the primary tint. */
  background: linear-gradient(
    90deg,
    transparent 0%,
    var(--primary) 30%,
    var(--primary) 70%,
    transparent 100%
  );
  animation: cat-progress-slide 1.1s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

@keyframes cat-progress-slide {
  /* Off-screen left → off-screen right. Use translateX(%)
     relative to the ::after width; -100% = fully past the left
     edge, 286% = (100/35)% past the right edge of the track. */
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(286%); }
}

@media (prefers-reduced-motion: reduce) {
  /* Stop the sweep but keep the track visible so the operator
     still sees work is in flight. A tiny pulse on the track
     itself is the only motion cue. */
  .cat-progress::after { animation: none; left: 0; width: 100%; opacity: 0.6; }
  .cat-progress { animation: cat-progress-pulse 1.5s ease-in-out infinite; }
}
@keyframes cat-progress-pulse {
  0%, 100% { opacity: 0.5; }
  50%      { opacity: 1; }
}

/* ── Pagination footer ────────────────────────────────────────────── */
.cat-pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-sm) 0;
}

/* ── Group header (grouped mode, unified virtualization) ──────────────
   Headers are emitted as rows in the same VirtualListView stream as
   data rows: position + height come from .vl-row (absolute, top
   stamped per slot). We do NOT layer sticky on top — sticky among
   absolute siblings has no flow position to anchor against, and the
   prior cross-layer composite was the source of the mid-scroll
   compression artefact this refactor was aimed at. The header
   simply scrolls with the content; collapse re-flows the flat list
   so the toggled group's rows disappear in place. */
.cat-group-header {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-sm) var(--sp-md);
  background: var(--bg-secondary);
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  user-select: none;
  transition: background var(--transition-fast);
  font-size: var(--font-size-caption);
}

.cat-group-header:hover { background: var(--bg-pill, var(--border)); }

.cat-group-header:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: -2px;
}

.cat-group-chevron {
  display: inline-flex;
  align-items: center;
  color: var(--text-tertiary);
  flex-shrink: 0;
  transition: transform 150ms ease-out;
}

/* When the header carries .collapsed (set on toggle), rotate the
   chevron to point right. The chevron icon itself is "down" by
   default, so the transform needs to take it 90° counter-clockwise. */
.cat-group-header.collapsed .cat-group-chevron {
  transform: rotate(-90deg);
}

@media (prefers-reduced-motion: reduce) {
  .cat-group-chevron { transition: none; }
}

.cat-group-label {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Group-header counter renders via .pill-count.pill-count--sm
   (see components/pill.css). Layout-only override. */
.cat-group-count { flex-shrink: 0; }

/* ── Row anchor (shared between flat virtual + grouped paths) ─────── */
/* The catalog-editor delegated listener walks .closest('.cat-row').
   Page-specific row classes (.ed-row, .cv-row) carry the visuals.
   When a row is selected, both classes get .selected so the
   page-specific selectors keep working. */
.cat-row { /* presentational anchor only */ }


/* === editor.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Species Editor — Master-Detail with Virtual Scroll + Field Renderers
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Layout ───────────────────────────────────────────────────────── */
.editor-layout {
  display: flex;
  gap: var(--sp-lg);
  height: calc(100vh - var(--topbar-height) - 2 * var(--sp-xl));
  min-height: 500px;
}

.editor-list-panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
  max-width: 560px;
}

.editor-detail-panel {
  flex: 1;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-card);
  min-width: 380px;
}

/* ── Toolbar ──────────────────────────────────────────────────────── */
.editor-toolbar {
  display: flex;
  gap: var(--sp-sm);
  margin-bottom: var(--sp-sm);
  flex-wrap: wrap;
}

/* Search input is rendered via the shared .toolbar-search component
   (see components/search.css). The editor toolbar only owns the
   layout slot — flex sizing so the search column stretches between
   the page mode bar and the filter group. */
.editor-toolbar .toolbar-search {
  flex: 1;
  min-width: 200px;
}

.editor-filters {
  display: flex;
  gap: var(--sp-xs);
  flex-wrap: wrap;
}

/* Size via .field-sm on the markup (rendered by catalog-editor.js).
   Page rule only enforces a minimum width for short enum labels. */
.editor-filters select {
  min-width: 90px;
}

.editor-toggle-inline {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  font-size: var(--font-size-caption2);
  color: var(--text-secondary);
  cursor: pointer;
  padding: 0 var(--sp-sm);
  white-space: nowrap;
}

.editor-toggle-inline input { width: 14px; height: 14px; accent-color: var(--primary); }

/* ── Mode bar ─────────────────────────────────────────────────────── */
.editor-mode-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-sm) 0;
  margin-bottom: var(--sp-sm);
  border-bottom: 1px solid var(--border);
}

.editor-mode-toggle {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  cursor: pointer;
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--text-secondary);
}

.editor-mode-toggle input {
  width: 14px;
  height: 14px;
  accent-color: var(--primary);
}

.mode-label {
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--text-secondary);
}

.editor-stats-bar {
  display: flex;
  gap: var(--sp-xs);
  flex: 1;
}

.editor-actions-bar {
  display: flex;
  gap: var(--sp-xs);
  margin-left: auto;
  position: relative;
}

/* ── Virtual scroll list ──────────────────────────────────────────── */
.editor-vlist-wrap {
  flex: 1;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-card);
}

.editor-vlist-content {
  position: relative;
}

/* Slot anchor for VirtualListView. Every row the primitive mounts
   gets `class="vl-row"` (in addition to its page-level class like
   .cv-row / .ed-row) and `data-vl-slot="<idx>"`. These few rules are
   the same for every list page, so they live here once instead of
   being duplicated as inline `style="position:absolute;..."` on every
   row template. The primitive stamps `top` + `height` per row. */
.vl-row {
  position: absolute;
  left: 0;
  right: 0;
}

/* Two-line row: thumb + (sci-name + tier + flag line) + (common · family
   · cv-count line). The previous one-line layout (sci | family | tier |
   cv) clipped family text on every other row and made the tier badge
   collide with cv-count on dense lists. Stacking gives each piece room
   and lets the typography scale match the iOS card pattern (large
   primary, smaller secondary). */
.ed-row {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-xs) var(--sp-md);
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background var(--transition-fast);
  overflow: hidden;
}

.ed-row:hover { background: var(--primary-bg); }
.ed-row.selected { background: var(--primary-bg-hover); }
.ed-row.modified { border-left: 3px solid var(--status-warning); }
.ed-row.flagged { border-left: 3px solid var(--status-critical); }
.ed-row.modified.flagged { border-left: 3px solid var(--status-critical); }

.ed-row-thumb {
  width: 40px;
  height: 40px;
  border-radius: var(--radius-md);
  object-fit: cover;
  flex-shrink: 0;
  background: var(--primary-bg);
}
.ed-row-thumb-placeholder {
  object-fit: contain;
  padding: 6px;
  background: var(--primary-bg);
  border: 1px dashed var(--primary-border);
}

.ed-row-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}

.ed-row-line1, .ed-row-line2 {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  min-width: 0;
}

.ed-row-sci {
  font-size: var(--font-size-footnote);
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
}

.ed-row-common {
  font-size: var(--font-size-caption);
  color: var(--text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ed-row-family {
  font-size: var(--font-size-caption);
  color: var(--text-tertiary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex-shrink: 0;
}

.ed-row-sep {
  color: var(--text-tertiary);
  opacity: 0.6;
}

.ed-status-icon {
  font-size: var(--font-size-caption2);
  flex-shrink: 0;
  width: 16px;
  text-align: center;
}

.ed-status-icon.reviewed { color: var(--status-positive); }
.ed-status-icon.modified { color: var(--status-warning); }

.ed-flag-icon {
  color: var(--status-critical);
  font-size: var(--font-size-caption2);
}

/* Cultivar-count pill in the species list renders via
   .pill-count.pill-count--sm.pill-count--accent (see
   components/pill.css). No editor-specific override needed. */

/* ── Pagination ───────────────────────────────────────────────────── */
.editor-pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-sm) 0;
}

/* ── Detail panel ─────────────────────────────────────────────────── */
.ed-detail-scroll {
  padding: var(--sp-xl);
}

.ed-detail-header {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-lg);
  margin-bottom: var(--sp-lg);
}

/* Photo + credit-overlay wrapper. The overlay floats over the bottom
   of the image and mirrors the iOS PhotoCredit pill (PhotoCredit.swift)
   so the editor sees what the user will see in the app. */
.ed-detail-photo-wrap {
  position: relative;
  flex-shrink: 0;
}

.ed-detail-photo {
  width: 200px;
  height: 200px;
  border-radius: var(--radius-lg);
  object-fit: cover;
  display: block;
  background: var(--bg-secondary);
}

/* Photo credit overlay — the visual pill chrome (scrim, blur, radius,
   caption font, white text) lives in .pill-tag.pill-tag--overlay
   (see components/pill.css). This rule owns the absolute position
   slot at the bottom of the photo + the ellipsis behaviour for long
   credits. Both classes are applied together in the markup. */
.ed-photo-credit {
  position: absolute;
  left: var(--sp-xs);
  right: var(--sp-xs);
  bottom: var(--sp-xs);
  gap: var(--sp-xs);
  line-height: 1.2;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Compact "Source" pill — replaces the wide author+license overlay
   that used to clip and make the source link unreachable. Author /
   license / license-URL now live exclusively in the Photo &
   Attribution section below the hero. */
.ed-photo-credit-source-only {
  text-decoration: none;
  cursor: pointer;
}
.ed-photo-credit-source-only:hover {
  background: rgba(20, 18, 14, 0.92);
}

/* Top-left "From parent" badge: surfaces when the cultivar's hero
   is borrowed from the parent species (no own photoUrl). Same
   visual language as the credit pill but pinned to the opposite
   corner so the two coexist without overlap. */
/* "From parent" badge — visual chrome lives in
   .pill-tag.pill-tag--overlay; this rule owns the absolute position
   slot at the top-left corner of the photo. */
.ed-photo-badge {
  position: absolute;
  left: var(--sp-xs);
  top: var(--sp-xs);
  gap: var(--sp-xs);
  line-height: 1.2;
}
.ed-photo-badge svg { flex-shrink: 0; opacity: 0.85; color: var(--text-inverse); }
/* Parent-source badge tints the dark scrim with primary-green so the
   "borrowed from parent species" status is colour-coded distinctly
   from a generic credit pill. Higher alpha (82%) keeps the green
   readable against light photos. */
.ed-photo-badge-parent {
  background: rgba(72, 139, 82, 0.82);
}

.ed-photo-credit svg { flex-shrink: 0; opacity: 0.85; color: var(--text-inverse); }
.ed-photo-credit a {
  color: var(--text-inverse);
  text-decoration: underline;
  text-decoration-color: rgba(255, 252, 248, 0.4);
  text-underline-offset: 2px;
}
.ed-photo-credit a:hover { text-decoration-color: var(--text-inverse); }
.ed-photo-credit-sep { opacity: 0.55; }
.ed-photo-credit-source { margin-left: auto; padding-left: var(--sp-xs); }

.ed-photo-credit-warn {
  background: rgba(196, 90, 74, 0.92);
  color: var(--text-inverse);
}

.ed-detail-photo.photo-error {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--primary-bg);
}

.ed-photo-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--primary-bg);
  border: 1px dashed var(--border);
  padding: 0;
}

.ed-photo-placeholder,
.ed-detail-photo.photo-fallback {
  object-fit: contain;
  padding: 18px;
  background: var(--primary-bg);
}

.ed-detail-info { min-width: 0; }

.ed-detail-name {
  font-size: var(--font-size-callout);
  font-weight: 700;
  line-height: 1.3;
}

.ed-detail-taxonomy {
  margin-top: 2px;
}

.ed-detail-badges {
  display: flex;
  gap: var(--sp-xs);
  margin-top: var(--sp-xs);
  flex-wrap: wrap;
}

.ed-detail-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  margin-bottom: var(--sp-lg);
  flex-wrap: wrap;
}

.btn-flag-active {
  background: var(--status-critical-bg);
  color: var(--status-critical);
  border: 1px solid var(--status-critical);
}

/* ── Column headers ───────────────────────────────────────────────── */
.ed-col-headers {
  display: flex;
  gap: var(--sp-lg);
  padding: var(--sp-xs) 0;
  margin-bottom: var(--sp-sm);
  border-bottom: 1px solid var(--border);
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.ed-col-label { flex: 1; }
.ed-col-editor { flex: 1; }

/* ── Sections ─────────────────────────────────────────────────────── */
.ed-section {
  margin-bottom: var(--sp-lg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.ed-section.collapsed .ed-section-body { display: none; }

.ed-section-toggle {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-sm) var(--sp-md);
  background: var(--bg-secondary);
  cursor: pointer;
  user-select: none;
  transition: background var(--transition-fast);
}

.ed-section-toggle:hover { background: var(--border); }

.ed-section-arrow {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  width: 12px;
}

.ed-section-title {
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.ed-section-body {
  padding: var(--sp-md);
}

/* ── Notes ─────────────────────────────────────────────────────────── */
.ed-notes {
  margin-top: var(--sp-lg);
  padding-top: var(--sp-lg);
  border-top: 1px solid var(--border);
}

/* ── Cultivar sub-list ─────────────────────────────────────────────── */
.ed-cv-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
}

.ed-cv-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--sp-xs) var(--sp-sm);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-caption);
}

.ed-cv-item:hover { background: var(--primary-bg); }

/* ── Export menu ───────────────────────────────────────────────────── */
.ed-export-menu {
  position: absolute;
  top: 100%;
  right: 0;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-dropdown);
  min-width: 180px;
  z-index: 100;
  overflow: hidden;
}

.ed-export-item {
  padding: var(--sp-sm) var(--sp-lg);
  font-size: var(--font-size-caption);
  cursor: pointer;
  transition: background var(--transition-fast);
}

.ed-export-item:hover { background: var(--primary-bg); }

.ed-export-sep {
  height: 1px;
  background: var(--border);
  margin: var(--sp-xs) 0;
}

/* ── Validation modal ─────────────────────────────────────────────── */
/* Renders via the shared .modal-overlay + .modal-card.modal-card--md
   component (see components/modal.css). The validation list can be
   long, so the --md variant gives a scrollable body up to 80vh. */

.ed-val-group { margin-bottom: var(--sp-xl); }
.ed-val-group h4 { margin-bottom: var(--sp-sm); }
/* Dynamic group title colour driven by --val-group-color (catalog-editor.js
   sets it via data-style on the heading). Falls back to body text colour. */
.ed-val-group-title { color: var(--val-group-color, var(--text-primary)); }

.ed-val-item {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-xs) var(--sp-sm);
  cursor: pointer;
  border-radius: var(--radius-sm);
  font-size: var(--font-size-caption);
}

.ed-val-item:hover { background: var(--primary-bg); }

/* ── Shortcut display ─────────────────────────────────────────────── */
/* ── Guide button + table ──────────────────────────────────────────── */
.ed-guide-btn {
  font-size: var(--font-size-caption2);
  font-weight: 400;
  color: var(--text-tertiary);
  cursor: pointer;
  margin-left: auto;
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  transition: all var(--transition-fast);
}

.ed-guide-btn:hover {
  background: var(--primary-bg);
  color: var(--primary);
}

.ed-guide-table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: var(--sp-lg);
  font-size: var(--font-size-caption);
}

.ed-guide-table thead th {
  text-align: left;
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: var(--sp-sm) var(--sp-md);
  border-bottom: 2px solid var(--border);
}

.ed-guide-table td {
  padding: var(--sp-sm) var(--sp-md);
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}

.ed-guide-table code {
  background: var(--bg-secondary);
  padding: 1px 6px;
  border-radius: var(--radius-sm);
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--primary);
}

.ed-shortcut {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-xs) 0;
  font-size: var(--font-size-caption);
  color: var(--text-secondary);
}

.ed-shortcut kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  height: 24px;
  padding: 0 var(--sp-xs);
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-caption2);
  font-family: inherit;
  font-weight: 600;
  color: var(--text-primary);
}

/* ═══ FIELD RENDERER STYLES ═══════════════════════════════════════ */

.fr-field {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: var(--sp-xs) 0;
  border-bottom: 1px solid var(--border);
  position: relative;
}

.fr-field:last-child { border-bottom: none; }

.fr-field.field-modified {
  background: var(--status-warning-bg);
  padding-left: var(--sp-sm);
  margin-left: calc(-1 * var(--sp-sm));
  border-left: 2px solid var(--status-warning);
  border-radius: 0;
}

.fr-field.field-error {
  border-left-color: var(--status-critical);
  background: var(--status-critical-bg);
}

.fr-label {
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
}

.fr-value {
  font-size: var(--font-size-caption);
  color: var(--text-primary);
  line-height: 1.4;
}

.fr-original {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  text-decoration: line-through;
}

.fr-original-col {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  text-decoration: line-through;
  padding: var(--sp-xs) 0;
}

.fr-editor-col {
  flex: 1;
}

.fr-empty {
  color: var(--text-tertiary);
  font-style: italic;
}

.fr-error {
  font-size: var(--font-size-caption2);
  color: var(--status-critical);
  margin-top: 2px;
}

/* Tertiary helper line under a field — schema authors can attach a
   `help` string to a FIELD_GROUPS entry and it surfaces here without
   stealing layout from the value or input. */
.fr-help {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  margin-top: 2px;
  line-height: 1.4;
}

.fr-url-link {
  color: var(--text-link);
  word-break: break-all;
}
.fr-url-link:hover { text-decoration: underline; }

/* Attribution warning banner shown above the cultivar / species
   Photo & Attribution section when a photoUrl is set but credit is
   blank. Same intent as the iOS overlay being suppressed when
   PhotoCredit.hasAttribution returns false but a photo is present. */
.fr-attribution-warning {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-sm);
  padding: var(--sp-sm) var(--sp-md);
  margin-bottom: var(--sp-md);
  background: var(--status-caution-bg);
  border-left: 3px solid var(--status-caution);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-caption);
  color: var(--text-secondary);
  line-height: 1.4;
}
.fr-attribution-warning svg {
  flex-shrink: 0;
  margin-top: 1px;
  color: var(--status-caution);
}

.fr-revert-btn {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  cursor: pointer;
  background: none;
  border: none;
  padding: 0 2px;
  font-family: inherit;
}

.fr-revert-btn:hover { color: var(--primary); }

/* ── Input styles ─────────────────────────────────────────────────── */
/* Field-renderer inputs sit inside table cells, so they need a denser
   pad than the .field-sm filter tier and a font that stays legible at
   table density (12, not 11). Kept as a custom rule rather than a 4th
   tier — there's only one consumer. */
.fr-input {
  width: 100%;
  font-size: var(--font-size-caption);
  padding: var(--sp-xs) var(--sp-sm);
  font-family: inherit;
}

.fr-textarea {
  min-height: 60px;
  resize: vertical;
  line-height: 1.5;
}

.fr-invalid { border-color: var(--status-critical) !important; }
.fr-invalid-opt { color: var(--status-critical); }

/* ── Bool ─────────────────────────────────────────────────────────── */
.fr-bool { font-weight: 500; }
.fr-bool-yes { color: var(--status-positive); }
.fr-bool-no { color: var(--status-critical); }
.fr-bool-unknown { color: var(--text-tertiary); }
.fr-bool-select { max-width: 150px; }

/* ── Enum value ───────────────────────────────────────────────────── */
.fr-enum-val {
  font-size: var(--font-size-caption);
}

/* ── Names ─────────────────────────────────────────────────────────── */
.fr-name-item {
  display: inline-block;
  padding: 1px var(--sp-sm);
  background: var(--bg-secondary);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-caption);
  margin: 1px 2px;
}

.fr-name-primary {
  background: var(--primary-bg);
  color: var(--primary);
  font-weight: 500;
}

/* Names editor */
.fr-names-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.fr-name-row {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: 2px 0;
}

.fr-name-row.dragging { opacity: 0.4; }
.fr-name-row.drag-over { border-top: 2px solid var(--primary); }

.fr-drag-handle {
  cursor: grab;
  color: var(--text-tertiary);
  font-size: var(--font-size-caption);
  user-select: none;
  width: 16px;
  text-align: center;
}

.fr-star {
  color: var(--text-tertiary);
  cursor: pointer;
  font-size: var(--font-size-caption);
  background: none;
  border: none;
  padding: 0;
  line-height: 1;
}

.fr-star.active { color: var(--tier-gold); }
.fr-star:hover { color: var(--tier-gold); }

.fr-name-input {
  flex: 1;
  font-size: var(--font-size-caption);
  padding: 2px var(--sp-xs);
  min-width: 0;
}

.fr-name-remove, .fr-array-remove {
  color: var(--text-tertiary);
  cursor: pointer;
  font-size: var(--font-size-subheadline);
  background: none;
  border: none;
  padding: 0 2px;
  line-height: 1;
}

.fr-name-remove:hover, .fr-array-remove:hover { color: var(--status-critical); }

.fr-name-add {
  display: flex;
  gap: var(--sp-xs);
  margin-top: var(--sp-xs);
}

.fr-name-new, .fr-array-new {
  flex: 1;
  font-size: var(--font-size-caption2);
  padding: 2px var(--sp-xs);
}

.fr-name-add-btn {
  font-size: var(--font-size-subheadline);
  color: var(--primary);
  cursor: pointer;
  background: none;
  border: none;
  padding: 0 var(--sp-xs);
  font-weight: 700;
}

/* ── Tags (array) ─────────────────────────────────────────────────── */
.fr-tag {
  display: inline-block;
  padding: 1px var(--sp-sm);
  background: var(--bg-secondary);
  border-radius: var(--radius-pill);
  font-size: var(--font-size-caption2);
  margin: 1px 2px;
}

.fr-array-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
}

.fr-array-tag {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px var(--sp-sm);
  background: var(--bg-secondary);
  border-radius: var(--radius-pill);
  font-size: var(--font-size-caption2);
}

.fr-array-add {
  margin-top: var(--sp-xs);
}

/* ── Chips ─────────────────────────────────────────────────────────── */
.fr-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
}

.fr-chip {
  display: inline-flex;
  align-items: center;
  padding: 3px var(--sp-sm);
  border-radius: var(--radius-pill);
  font-size: var(--font-size-caption2);
  font-weight: 500;
  background: var(--bg-secondary);
  color: var(--text-secondary);
  border: 1px solid var(--border);
  cursor: pointer;
  transition: all var(--transition-fast);
}

.fr-chip:hover { border-color: var(--primary); }

.fr-chip.active {
  background: var(--primary-bg);
  color: var(--primary);
  border-color: var(--primary);
}

.fr-chip-val {
  display: inline-block;
  padding: 1px var(--sp-sm);
  background: var(--primary-bg);
  color: var(--primary);
  border-radius: var(--radius-pill);
  font-size: var(--font-size-caption2);
  font-weight: 500;
  margin: 1px 2px;
}

/* ── Source citation ───────────────────────────────────────────────── */
.fr-source {
  margin-top: 3px;
}

.fr-source-input {
  font-size: var(--font-size-caption2) !important;
  padding: 2px var(--sp-xs) !important;
  border-color: var(--border) !important;
  color: var(--text-tertiary);
  width: 100%;
}

/* ── Text block ───────────────────────────────────────────────────── */
.fr-text-block {
  font-size: var(--font-size-caption);
  line-height: 1.5;
  display: block;
}

/* ═══ RESPONSIVE ═══════════════════════════════════════════════════ */

@media (max-width: 1200px) {
  .editor-list-panel { max-width: 420px; }
  .editor-detail-panel { min-width: 340px; }
}

@media (max-width: 1024px) {
  .editor-layout {
    flex-direction: column;
    height: auto;
  }
  .editor-list-panel { max-width: none; max-height: 50vh; }
  .editor-detail-panel { min-width: 0; }
}

/* ═══ SESSION STATS ═══════════════════════════════════════════════ */

.session-stats {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  font-size: var(--font-size-caption2);
}
/* Hide while empty (no children rendered yet) — replaces the old
   inline style="display:none" attribute that strict CSP rejects.
   App.updateSessionStats() may also set el.style.display directly
   via CSSOM (which CSP doesn't govern); this rule covers the
   initial-empty case before any JS has run. */
.session-stats:empty {
  display: none;
}

.ss-reviewed { color: var(--status-positive); font-weight: 600; }
.ss-modified { color: var(--status-warning); font-weight: 600; }
.ss-flagged { color: var(--status-critical); font-weight: 600; }
.ss-sep { color: var(--text-tertiary); }

/* The pending tally is a link to the Pending Changes page. Underline
   only on hover so the topbar stays visually quiet at rest, mirroring
   the rest of the chrome. */
a.ss-modified {
  text-decoration: none;
  cursor: pointer;
  border-radius: var(--radius-sm);
  padding: 1px 4px;
  transition: background var(--transition-fast);
}
a.ss-modified:hover { background: var(--bg-card-hover); text-decoration: underline; }
a.ss-modified:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 1px; }

/* ═══ RESUME BANNER ═══════════════════════════════════════════════ */

.resume-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--sp-md) var(--sp-lg);
  background: var(--primary-bg);
  border: 1px solid var(--primary);
  border-radius: var(--radius-md);
  margin-bottom: var(--sp-lg);
  gap: var(--sp-lg);
}

.resume-text {
  font-size: var(--font-size-caption);
}

.resume-actions {
  display: flex;
  gap: var(--sp-sm);
  flex-shrink: 0;
}


/* === cultivars.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Cultivar Editor
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Stats bar ─────────────────────────────────────────────────────── */
.cv-stats-bar {
  display: flex;
  gap: var(--sp-md);
  margin-bottom: var(--sp-lg);
  flex-wrap: wrap;
}

.cv-stat-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-sm) var(--sp-lg);
}

/* ── Virtual list rows ───────────────────────────────────────────────
   Same two-line shape as the species editor (.ed-row): thumb on the
   left, primary identity above, parent + flags below. Reusing the
   .ed-row-thumb class from editor.css keeps the pattern in one place. */
.cv-row {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-xs) var(--sp-md);
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background var(--transition-fast);
  overflow: hidden;
}

.cv-row:hover { background: var(--primary-bg); }
.cv-row.selected { background: var(--primary-bg-hover); }
.cv-row.orphan { border-left: 3px solid var(--status-critical); }
.cv-row.modified { border-left: 3px solid var(--status-warning); }
.cv-row.orphan.modified { border-left: 3px solid var(--status-critical); }

.cv-row-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}

.cv-row-line1, .cv-row-line2 {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  min-width: 0;
}

.cv-row-name {
  font-size: var(--font-size-footnote);
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
}

.cv-row-parent {
  font-size: var(--font-size-caption);
  color: var(--text-tertiary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cv-override-dot {
  width: 6px;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--primary);
  flex-shrink: 0;
}

/* ── Genus group headers ─────────────────────────────────────────────
   Sticky inside the scroll container so the active genus name stays
   visible while you scroll its rows. Chevron flips between right
   (collapsed) and down (expanded) — the same affordance the editor
   field sections use. */
.cv-genus-header {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-sm) var(--sp-md);
  background: var(--bg-secondary);
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  user-select: none;
  position: sticky;
  top: 0;
  z-index: 1;
  transition: background var(--transition-fast);
  font-size: var(--font-size-caption);
}

.cv-genus-header:hover { background: var(--bg-pill); }
.cv-genus-header:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: -2px;
}
.cv-genus-chevron {
  display: inline-flex;
  align-items: center;
  color: var(--text-tertiary);
  flex-shrink: 0;
}
/* Genus header counter renders via .pill-count.pill-count--sm
   (see components/pill.css). Layout-only override. */
.cv-genus-count { margin-left: auto; }

/* ═══ RESPONSIVE ═══════════════════════════════════════════════════ */

@media (max-width: 1024px) {
  .cv-stats-bar { gap: var(--sp-sm); }
  .cv-stat-pill { padding: var(--sp-xs) var(--sp-md); }
}


/* === diseases.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Disease Library
   ═══════════════════════════════════════════════════════════════════════ */

.diseases-page {
  max-width: var(--page-max-width);
}

/* ── Disease Card Grid ────────────────────────────────────────────── */
.disease-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--sp-lg);
}

.disease-card {
  padding: var(--sp-xl);
  cursor: pointer;
  transition: box-shadow var(--transition-fast), border-color var(--transition-fast);
}

.disease-card:hover {
  border-color: var(--border);
}

.disease-card.expanded {
  grid-column: 1 / -1;
  cursor: default;
  border-color: var(--primary);
  box-shadow: 0 0 0 1px var(--primary-bg), var(--shadow-elevated);
}

.disease-card-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-md);
  margin-bottom: var(--sp-sm);
}

.disease-card-title h3 {
  font-size: var(--t-card-title);
  font-weight: 600;
  margin-bottom: var(--sp-xs);
}

.disease-badges {
  display: flex;
  gap: var(--sp-xs);
  flex-wrap: wrap;
}

.disease-expand-icon {
  flex-shrink: 0;
  color: var(--text-tertiary);
  margin-top: 2px;
}

.disease-excerpt {
  font-size: var(--font-size-caption);
  color: var(--text-secondary);
  line-height: 1.6;
}

/* ── Disease Detail (expanded) ────────────────────────────────────── */
.disease-detail {
  margin-top: var(--sp-lg);
  padding-top: var(--sp-lg);
  border-top: 1px solid var(--border);
}

.detail-section {
  margin-bottom: var(--sp-xl);
}

.detail-section h4 {
  /* Was caption (12px) — too small for a section heading inside the
     expanded disease card. Bumped to the sub-section role (footnote 13). */
  font-size: var(--t-sub-section);
  font-weight: 600;
  color: var(--primary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: var(--sp-sm);
}

.detail-section p {
  font-size: var(--font-size-caption);
  color: var(--text-secondary);
  line-height: 1.7;
}

/* Treatment list */
.treatment-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
}

.treatment-item {
  display: flex;
  gap: var(--sp-md);
  padding: var(--sp-md);
  background: var(--bg-secondary);
  border-radius: var(--radius-sm);
}

.treatment-category {
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--primary);
  min-width: 100px;
  flex-shrink: 0;
}

.treatment-text {
  font-size: var(--font-size-caption2);
  color: var(--text-secondary);
  line-height: 1.6;
}

/* Detail metadata */
.detail-meta {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--sp-md);
  padding: var(--sp-lg);
  background: var(--bg-secondary);
  border-radius: var(--radius-sm);
}

.meta-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* Meta-row label uses the shared .label-caps (tokens.css). Disease
   meta labels run a bit smaller than the form-field label-caps;
   override only the size, not the rest of the rule, so the spacing
   and weight stay aligned with every other uppercase label. */
.meta-label {
  font-size: var(--font-size-caption2);
  font-weight: 500;
  letter-spacing: 0.06em;
}

.meta-value {
  font-size: var(--font-size-caption);
  font-weight: 600;
  color: var(--text-primary);
}

/* ── Tips Section ─────────────────────────────────────────────────── */
.tips-toolbar {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
  margin-bottom: var(--sp-xl);
}

/* .tips-search is the shared .toolbar-search component scoped to a
   readable column width — wider than the surrounding tip cards
   would otherwise force. */
.tips-search { max-width: 400px; }

.tips-categories {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-xs);
}

/* Category buttons render via the shared .chip-filter component
   (see components/chip.css). Capitalize is enforced inline by the
   button text, no override needed. */

.tips-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}

.tip-card {
  padding: var(--sp-lg);
}

.tip-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--sp-sm);
}

.tip-content {
  font-size: var(--font-size-caption);
  color: var(--text-primary);
  line-height: 1.7;
  margin-bottom: var(--sp-sm);
}

.tip-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-md);
}

.tip-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-xs);
}

/* ── FAQ Section ──────────────────────────────────────────────────── */
.faq-search {
  margin-bottom: var(--sp-xl);
  max-width: 400px;
}

.faq-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
}

.faq-item {
  overflow: hidden;
}

.faq-question {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-md);
  padding: var(--sp-lg);
  cursor: pointer;
  transition: background var(--transition-fast);
}

.faq-question:hover {
  background: var(--primary-bg);
}

.faq-q-text {
  font-size: var(--font-size-caption);
  font-weight: 500;
  color: var(--text-primary);
  flex: 1;
}

.faq-toggle {
  flex-shrink: 0;
  color: var(--text-tertiary);
}

.faq-answer {
  padding: 0 var(--sp-lg) var(--sp-lg);
}

.faq-answer p {
  font-size: var(--font-size-caption);
  color: var(--text-secondary);
  line-height: 1.7;
  margin-bottom: var(--sp-sm);
}

.faq-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-xs);
  margin-top: var(--sp-sm);
}

.faq-item.open {
  border-color: var(--border);
}

/* ── Disease edit bar ─────────────────────────────────────────────── */
.disease-edit-bar {
  display: flex;
  gap: var(--sp-sm);
  margin-bottom: var(--sp-md);
}

/* ── Edit mode styles ─────────────────────────────────────────────── */
.tip-editing {
  border-color: var(--primary);
  box-shadow: 0 0 0 1px var(--primary-bg);
}

.tip-modified {
  border-left: 3px solid var(--status-warning);
}

.tip-edit-form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
  margin-top: var(--sp-sm);
}

.faq-editing {
  border-color: var(--primary);
  box-shadow: 0 0 0 1px var(--primary-bg);
}

.faq-modified {
  border-left: 3px solid var(--status-warning);
}

.faq-edit-form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
}

/* ── Responsive ───────────────────────────────────────────────────── */
@media (max-width: 1024px) {
  .disease-grid {
    grid-template-columns: 1fr;
  }
  .detail-meta {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* ═══════════════════════════════════════════════════════════════════════
   Plant-specific treatments edit list — used in the disease edit panel.
   Each row is a [category | textarea | remove] grid so categories line
   up vertically across rows even when the textareas grow.
   ═══════════════════════════════════════════════════════════════════════ */
.treatment-edit-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
  margin-top: var(--sp-xs);
}

.treatment-edit-row {
  display: grid;
  grid-template-columns: 160px 1fr auto;
  gap: var(--sp-sm);
  align-items: start;
  padding: var(--sp-sm);
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.treatment-cat-input {
  font-weight: 600;
  font-size: var(--font-size-caption);
}

.treatment-text-input {
  min-height: 60px;
  font-size: var(--font-size-caption);
}

.treatment-remove {
  align-self: flex-start;
  padding: var(--sp-xs);
  color: var(--text-tertiary);
}
.treatment-remove:hover {
  color: var(--status-critical);
  background: var(--status-critical-bg);
}

@media (max-width: 720px) {
  .treatment-edit-row {
    grid-template-columns: 1fr auto;
  }
  .treatment-cat-input { grid-column: 1 / -1; }
}


/* === contributions.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Contributions page — moderation list + detail.
   Two-column layout matching iOS All Plants list + Care Tasks card
   patterns. Card radius 14, warm cream surfaces, brown shadows. List
   on the left scrolls; detail pane on the right is sticky.
   ═══════════════════════════════════════════════════════════════════════ */

.contrib-page {
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
  /* Padding now lives on `.page-container` (one source of truth across
     every admin page). Earlier this rule double-padded — sp-xl on the
     container plus sp-xl/sp-section here — so the Contributions body
     started ~24px further inside than every other page. */
  max-width: var(--page-max-width);
  height: calc(100vh - var(--topbar-height) - 2 * var(--sp-xl));
  overflow: hidden;
}

/* ── Header row ───────────────────────────────────────────────────── */
.contrib-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-lg);
  flex-wrap: wrap;
}

.contrib-title {
  /* Was title1 (28) — overshot the design system's --t-page-title role
     which lives at title3 (20). All other surface titles (Database,
     Pending changes, Editor) read at the page-title token; pinning
     Contributions to the same role keeps the global hierarchy honest. */
  font-size: var(--t-page-title);
  font-weight: 700;
  line-height: 1.2;
  color: var(--text-primary);
}

.contrib-subtitle {
  /* Was subheadline (15) — same scale as a card title so the subtitle
     out-shouted the actual filter chips below (13). Footnote (13)
     keeps the page header proportioned: title (20) → subtitle (13)
     → chips (13) reads as a clear hierarchy step. */
  font-size: var(--font-size-footnote);
  color: var(--text-secondary);
  margin-top: var(--sp-xxs);
}

/* The page header counter row is a flex group of `.pill-count`
   instances rendered by contributions.js. Layout slot only. */
.contrib-counts {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
}

/* ── Filter chips ─────────────────────────────────────────────────── */
.contrib-filters {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
}

.contrib-filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-xs);
  align-items: center;
}

/* Filter row label uses the shared .label-caps (tokens.css). The
   row-prefix layout slot adds a min-width so labels in the same
   filter group right-align with each other. */
.contrib-filter-label { min-width: 64px; }

/* Filter chips render via the shared .chip-filter component (see
   components/chip.css). Embedded counts use .pill-count.pill-count--sm
   from components/pill.css; the active-state cascade for the count
   lives alongside the chip rule. */

/* ── Body: split list / detail ────────────────────────────────────── */
.contrib-body {
  display: grid;
  grid-template-columns: minmax(360px, 420px) 1fr;
  gap: var(--sp-lg);
  flex: 1;
  min-height: 0;
}

/* List pane ───────────────────────────────────────────── */
.contrib-list {
  background: var(--bg-card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.contrib-list-scroll {
  flex: 1;
  overflow-y: auto;
}

.contrib-list-empty {
  padding: var(--sp-3xl) var(--sp-xl);
  text-align: center;
  color: var(--text-tertiary);
  font-size: var(--font-size-callout);
}

.contrib-row {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-md);
  padding: var(--sp-md) var(--sp-lg);
  cursor: pointer;
  border-bottom: 1px solid var(--border);
  transition: background var(--transition-fast);
}

.contrib-row:last-child { border-bottom: none; }
.contrib-row:hover { background: var(--bg-card-hover); }

.contrib-row.selected {
  background: var(--primary-bg);
}

.contrib-row.selected::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--primary);
}

.contrib-row { position: relative; }

.contrib-row-icon {
  width: 36px;
  height: 36px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-callout);
  flex-shrink: 0;
  background: var(--bg-pill);
}

.contrib-row-body {
  flex: 1;
  min-width: 0;
}

.contrib-row-line1 {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-sm);
  margin-bottom: 2px;
}

.contrib-row-title {
  font-size: var(--font-size-subheadline);
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.contrib-row-time {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  flex-shrink: 0;
}

.contrib-row-line2 {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  font-size: var(--font-size-caption);
  color: var(--text-secondary);
}

.contrib-row-preview {
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.contrib-row-photos {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
}

/* Contribution status uses the shared .badge component (see
   components/badge.css). Mapping: new → warning, reviewing → info,
   accepted → success, rejected → danger, duplicate → neutral.
   contributions.js renders one of those modifier classes per row. */

/* Detail pane ─────────────────────────────────────────── */
.contrib-detail {
  background: var(--bg-card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}

.contrib-detail-empty {
  padding: var(--sp-3xl) var(--sp-xl);
  text-align: center;
  color: var(--text-tertiary);
  font-size: var(--font-size-callout);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-md);
  margin: auto;
}

.contrib-detail-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-lg);
  padding: var(--sp-xl) var(--sp-xl) var(--sp-md);
  border-bottom: 1px solid var(--border);
}

.contrib-detail-title {
  /* Detail panel headings use the modal-title role so they sit at the
     same step as Add Species / Create User modal headers. */
  font-size: var(--t-modal-title);
  font-weight: 700;
  color: var(--text-primary);
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
}

.contrib-detail-meta {
  font-size: var(--font-size-caption);
  color: var(--text-tertiary);
  margin-top: var(--sp-xxs);
}

.contrib-detail-body {
  padding: var(--sp-xl);
  display: flex;
  flex-direction: column;
  gap: var(--sp-xl);
}

.contrib-section-title {
  font-size: var(--font-size-caption);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-tertiary);
  margin-bottom: var(--sp-sm);
}

.contrib-fields {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
  background: var(--bg-input);
  border-radius: var(--radius-md);
  padding: var(--sp-lg);
}

.contrib-field {
  display: grid;
  grid-template-columns: 140px 1fr;
  gap: var(--sp-md);
  font-size: var(--font-size-footnote);
  align-items: baseline;
}

.contrib-field-label {
  color: var(--text-tertiary);
  font-weight: 500;
}

.contrib-field-value {
  color: var(--text-primary);
  word-break: break-word;
  white-space: pre-wrap;
}

.contrib-field-value.empty {
  color: var(--text-tertiary);
  font-style: italic;
}

.contrib-photos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--sp-sm);
}

.contrib-photo {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--radius-md);
  background: var(--bg-pill);
  border: 1px solid var(--border);
  display: block;
  cursor: zoom-in;
}

.contrib-notes {
  width: 100%;
  min-height: 100px;
  resize: vertical;
  padding: var(--sp-md);
  background: var(--bg-input);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-md);
  font-family: inherit;
  font-size: var(--font-size-footnote);
  color: var(--text-primary);
  line-height: 1.5;
}

.contrib-notes:focus {
  outline: none;
  border-color: var(--primary);
  background: var(--bg-input-focus);
  box-shadow: 0 0 0 3px var(--primary-bg);
}

/* Action footer ─────────────────────────────────────── */
.contrib-actions {
  position: sticky;
  bottom: 0;
  background: var(--bg-card);
  padding: var(--sp-lg) var(--sp-xl);
  border-top: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-md);
}

/* Size + chrome via .field-md on the markup + global tokens.css; this
   rule only carries the layout role (flex slot in the action footer)
   + the cursor affordance. */
.contrib-status-select {
  flex: 1;
  min-width: 160px;
  cursor: pointer;
}

.contrib-btn {
  font-family: inherit;
  font-size: var(--font-size-footnote);
  font-weight: 600;
  padding: 10px var(--sp-lg);
  border-radius: var(--radius-md);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background var(--transition-fast), color var(--transition-fast),
              border-color var(--transition-fast);
}

.contrib-btn-primary {
  background: var(--primary);
  color: var(--text-on-primary);
}
.contrib-btn-primary:hover { background: var(--primary-shade); }
.contrib-btn-primary:disabled { opacity: 0.5; cursor: wait; }

.contrib-btn-danger {
  background: transparent;
  color: var(--status-critical-text);
  border-color: rgba(196, 90, 74, 0.30);
}
.contrib-btn-danger:hover {
  background: var(--status-critical-bg);
  border-color: var(--status-critical);
}

/* Toast inline (success / error) ───────────────────── */
.contrib-flash {
  font-size: var(--font-size-caption);
  color: var(--text-tertiary);
  flex-basis: 100%;
  text-align: right;
}
.contrib-flash.success { color: var(--status-positive); font-weight: 500; }
.contrib-flash.error   { color: var(--status-critical-text); font-weight: 500; }

/* Photo zoom overlay ────────────────────────────────── */
.contrib-photo-zoom {
  position: fixed;
  inset: 0;
  background: rgba(15, 18, 12, 0.85);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 500;
  cursor: zoom-out;
}

.contrib-photo-zoom img {
  max-width: 92vw;
  max-height: 92vh;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-elevated);
}

/* Narrow viewport — stack list above detail */
@media (max-width: 980px) {
  .contrib-body {
    grid-template-columns: 1fr;
  }
  .contrib-page { height: auto; overflow: visible; }
}

/* ── Multi-select + bulk action bar ──────────────────────────────────
   Checkbox sits before the row icon so the icon's visual weight stays
   the affordance for "click here to open." The bulk bar is sticky to
   the bottom of the list scroll area — out of the way until the
   operator selects something. */
.contrib-row-check {
  width: 16px;
  height: 16px;
  margin: 0;
  flex-shrink: 0;
  cursor: pointer;
  accent-color: var(--primary);
}

.contrib-bulk-bar {
  position: sticky;
  bottom: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-sm) var(--sp-md);
  background: var(--bg-card);
  border-top: 1px solid var(--border-strong);
  box-shadow: var(--shadow-medium);
  font-size: var(--font-size-caption);
}

.contrib-bulk-bar strong { color: var(--primary); }


/* === pipeline.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Developer Tools — Enum Registry, Validation, Schema, Export
   ═══════════════════════════════════════════════════════════════════════ */

.pipeline-page {
  max-width: var(--page-max-width);
}

/* ── Enum Table ───────────────────────────────────────────────────── */
.enum-table-wrapper {
  overflow-x: auto;
}

.enum-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-caption);
}

.enum-table th {
  text-align: left;
  padding: var(--sp-sm) var(--sp-md);
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  border-bottom: 2px solid var(--border);
  white-space: nowrap;
}

.enum-table td {
  padding: var(--sp-md);
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}

.enum-table tr:hover td {
  background: var(--primary-bg);
}

.enum-field-name {
  font-size: var(--font-size-caption);
  font-weight: 600;
  color: var(--primary);
  background: var(--primary-bg);
  padding: 1px 6px;
  border-radius: var(--radius-sm);
}

.text-center {
  text-align: center;
}

/* Enum chips */
.enum-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  max-width: 320px;
}

/* Pipeline enum chips render via the shared .pill-tag component
   (see components/pill.css) — readonly metadata badge with the
   same warm-neutral fill as detail-panel tags. The thin border was
   pipeline-specific bleed; the component drops it for visual parity
   with the rest of the admin. */

/* Distribution bars */
.dist-bars {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 200px;
}

.dist-bar-row {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
}

.dist-label {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  min-width: 70px;
  text-align: right;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dist-bar {
  flex: 1;
  height: 8px;
  background: var(--bg-secondary);
  border-radius: var(--radius-sm);
  overflow: hidden;
  min-width: 60px;
}

.dist-fill {
  height: 100%;
  width: var(--bar-w, 0%);
  background: var(--primary);
  border-radius: var(--radius-sm);
  transition: width var(--transition-slow);
  opacity: 0.7;
}

.dist-count {
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--text-tertiary);
  min-width: 30px;
  text-align: right;
}

/* ── Validation ───────────────────────────────────────────────────── */
.validation-summary {
  margin-bottom: var(--sp-lg);
}

.validation-success {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--sp-3xl);
  text-align: center;
  gap: var(--sp-md);
}

.validation-icon {
  width: 56px;
  height: 56px;
  border-radius: var(--radius-pill);
  display: flex;
  align-items: center;
  justify-content: center;
}

.validation-icon.success {
  background: var(--status-positive-bg);
  color: var(--status-positive);
}

.validation-success h3 {
  font-size: var(--t-card-title);
  font-weight: 600;
  color: var(--text-primary);
}

.validation-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-caption);
}

.validation-table th {
  text-align: left;
  padding: var(--sp-sm) var(--sp-md);
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  border-bottom: 2px solid var(--border);
}

.validation-table td {
  padding: var(--sp-sm) var(--sp-md);
  border-bottom: 1px solid var(--border);
}

.validation-table code {
  font-size: var(--font-size-caption);
  color: var(--primary);
  background: var(--primary-bg);
  padding: 1px 6px;
  border-radius: var(--radius-sm);
}

.validation-table tr:hover td {
  background: var(--primary-bg);
}

/* ── Schema Table ─────────────────────────────────────────────────── */
.schema-table-wrapper {
  overflow-x: auto;
}

.schema-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-caption);
}

.schema-table th {
  text-align: left;
  padding: var(--sp-sm) var(--sp-md);
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  border-bottom: 2px solid var(--border);
}

.schema-table td {
  padding: var(--sp-sm) var(--sp-md);
  border-bottom: 1px solid var(--border);
}

.schema-table code {
  font-size: var(--font-size-caption);
  font-weight: 500;
}

.schema-table tr:hover td {
  background: var(--primary-bg);
}

.schema-type {
  font-size: var(--font-size-caption2);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.schema-coverage {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
}

/* ── Export ────────────────────────────────────────────────────────── */
.export-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--sp-lg);
}

.export-card {
  padding: var(--sp-xl);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--sp-md);
}

.export-icon {
  width: 48px;
  height: 48px;
  border-radius: var(--radius-pill);
  background: var(--primary-bg);
  color: var(--primary);
  display: flex;
  align-items: center;
  justify-content: center;
}

.export-card h3 {
  /* Was caption (12) — way under-sized for a card title. Brought up
     to the canonical card-title role so the export-card aligns with
     dashboard / disease / analytics card headings. */
  font-size: var(--t-card-title);
  font-weight: 600;
}

.export-filename {
  font-family: monospace;
  padding: 2px 8px;
  background: var(--bg-secondary);
  border-radius: var(--radius-sm);
}

.export-card .btn {
  margin-top: auto;
  width: 100%;
  justify-content: center;
}

/* ── Responsive ───────────────────────────────────────────────────── */
@media (max-width: 1024px) {
  .export-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 768px) {
  .export-grid {
    grid-template-columns: 1fr;
  }
  .enum-chips {
    max-width: 200px;
  }
  .dist-bars {
    min-width: 120px;
  }
}


/* === analytics.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Analytics — Chart containers, grids, legends
   ═══════════════════════════════════════════════════════════════════════ */

.analytics-page {
  max-width: var(--page-max-width);
}

/* ── Chart Grid Layout ────────────────────────────────────────────── */
.analytics-grid {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: var(--sp-xl);
}

.analytics-chart-card {
  padding: var(--sp-xl);
  overflow: hidden;
}

.analytics-chart-card h3 {
  font-size: var(--t-card-title);
  font-weight: 600;
  margin-bottom: var(--sp-lg);
  color: var(--text-primary);
}

.chart-wide {
  /* Takes 2fr column */
}

.chart-narrow {
  /* Takes 1fr column */
}

.chart-full {
  grid-column: 1 / -1;
}

/* ── Chart Container ──────────────────────────────────────────────── */
.chart-container {
  width: 100%;
  min-height: 100px;
}

.chart-container svg {
  width: 100%;
  height: auto;
  display: block;
}

/* Narrow charts (Toxicity, Quality Tiers, Care Level, Growth Habit)
   have a compact viewBox (~280-330 user units) and a single-purpose
   read. When two of them happen to land in the same grid row, the
   first sits in the 2fr column and its SVG scales 280→~840 (3x),
   which made every axis tick and bar label read like a heading. The
   cap below holds the SVG to a sensible intrinsic width so font-size
   inline values land at the size we set them, regardless of which
   column the card flows into. Centered so the card padding still
   reads as breathing room rather than offset whitespace. */
.chart-narrow .chart-container {
  max-width: 360px;
  margin-left: auto;
  margin-right: auto;
}

/* D3 axis styling */
.chart-container .domain {
  stroke: var(--border);
}

.chart-container .tick line {
  stroke: var(--border);
}

.chart-container .tick text {
  fill: var(--text-tertiary);
  font-family: 'Poppins', sans-serif;
}

/* Inline `font-size` on SVG <text> nodes is in viewBox user units, so a
   chart that scales up via `width: 100%` also scales its labels. The
   chart code now emits classed labels at 8-9 user units, which lands
   between caption2 and caption (11-15px effective on a typical narrow
   card). These class hooks are here so a future designer can re-tune
   chart typography from one place rather than chasing inline styles. */
.chart-container .chart-axis-text,
.chart-container .chart-bar-label,
.chart-container .chart-legend-text,
.chart-container .chart-center-label {
  font-family: 'Poppins', sans-serif;
}

/* ── Coverage Table ───────────────────────────────────────────────── */
.coverage-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-caption);
}

.coverage-table th {
  text-align: left;
  padding: var(--sp-sm) var(--sp-md);
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  border-bottom: 2px solid var(--border);
}

.coverage-table td {
  padding: var(--sp-sm) var(--sp-md);
  border-bottom: 1px solid var(--border);
}

.coverage-table tr:hover td {
  background: var(--primary-bg);
}

.coverage-table .progress-bar {
  height: 8px;
}

/* Bronze color for progress bar */
.progress-fill.bronze {
  background: var(--tier-bronze);
}

/* ── Responsive ───────────────────────────────────────────────────── */
@media (max-width: 1024px) {
  .analytics-grid {
    grid-template-columns: 1fr;
  }
  .chart-wide,
  .chart-narrow {
    grid-column: auto;
  }
}


/* === insights.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Insights — StoreKit V2 billing dashboard.

   Visual direction: Greenhouse's warm cream paper aesthetic for surfaces,
   but a denser data-forward composition than Dashboard / Editor. The
   prototype at server/docs/prototypes/insights-dashboard.html lays out
   the conceptual moves (Pulse ring, headline figures, Live Wire ticker,
   Subscriptions ledger, Vital Signs strip) — this stylesheet ports them
   onto Greenhouse's tokens so the page feels like the same product as
   the rest of Greenhouse, just operationally tighter.

   Defers in this file:
     - Pulse score is shown as a "Calibrating" placeholder. Composite
       formula needs ~30 days of production volume before the percentile
       inputs (churn, refund rate, sentiment) are statistically honest.
     - Live Wire here renders the most recent N events, no auto-refresh
       (task 16 wires that). The visual chrome is in place so task 16
       only adds the refresh loop, not new structure.
   ═══════════════════════════════════════════════════════════════════════ */

/* ─── Page shell ──────────────────────────────────────────────────── */
/* Insights piggybacks on the project-wide section-header pattern
   (Dashboard, Analytics, Diseases, Daily Tips, Pending all use it).
   Dropped the previous bespoke `.insights-shell + .insights-head +
   .insights-title` (largetitle 34 px) chrome because every other
   page lives at title3 (20 px) — the page felt like a different
   product. Topbar already shows "Insights" so the section-title
   here is descriptive ("Live billing signal") rather than a literal
   re-statement of the nav label. */

.insights-page {
    display: flex;
    flex-direction: column;
    gap: var(--sp-section);
}
/* ─── Toolbar — was a .section-header before. Dropped the h2
   ("Live billing signal" was redundant with the topbar "Insights")
   and renamed so the rule reads as a control surface, not a heading
   surface. Status pill on the left, controls on the right; flex-wrap
   so narrow viewports collapse to two rows naturally. */
.insights-toolbar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--sp-sm) var(--sp-md);
    margin-bottom: var(--sp-lg);
}
.insights-toolbar .insights-actions {
    margin-left: auto;
}

/* ─── Sub-route page chrome ───────────────────────────────────────
   Full-page list views opened from a "View all" link on the main
   tabs (e.g. #insights/reviews). Single-modal pattern lives on the
   detail click; the LIST stays a real page, no modal-on-modal.   */
.insights-subpage-head {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--sp-sm) var(--sp-md);
    margin-bottom: var(--sp-lg);
}
.insights-subpage-back {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-xs);
    padding: var(--sp-xs) var(--sp-sm);
    border-radius: var(--radius-md);
    font-size: var(--font-size-caption);
    font-weight: 500;
    color: var(--text-secondary);
    text-decoration: none;
    transition: background var(--transition-fast), color var(--transition-fast);
}
.insights-subpage-back:hover {
    background: var(--primary-bg);
    color: var(--primary);
    text-decoration: none;
}
.insights-subpage-sep {
    color: var(--text-tertiary);
    opacity: 0.6;
}
.insights-subpage-body {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-card);
    padding: var(--sp-xl);
}

/* "View all" footer link rendered at the bottom of capped cards
   (reviews, refunds, watchtower, milestones). Sage-on-cream pill
   that hugs the right edge so it doesn't compete with the list. */
.insights-card-foot {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-xs);
    margin-top: var(--sp-md);
    padding: var(--sp-xs) var(--sp-md);
    border-radius: var(--radius-md);
    font-size: var(--font-size-caption);
    font-weight: 600;
    color: var(--primary-shade);
    text-decoration: none;
    align-self: flex-end;
    transition: background var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
    background: var(--primary-bg);
}
.insights-card-foot:hover {
    background: var(--primary-bg-hover);
    color: var(--primary);
    text-decoration: none;
    transform: translateX(2px);
}
@media (prefers-reduced-motion: reduce) {
    .insights-card-foot:hover { transform: none; }
}

/* Sub-route list variants — drop the inner-card cap padding +
   trailing border so the body looks like a real list, not a card
   inside a card. */
.reviews-list-full,
.watch-rules-full,
.miles-list-full {
    margin: 0;
}


/* Action cluster on the right side of the section header. Period +
   env toggles + ghost + help live here. Wraps onto its own row at
   narrow widths via the section-header's own flex-wrap. */
.insights-actions {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    justify-content: flex-end;
}

/* Period toggle — same shape as env toggle, sits between Ghost mode
   and the env selector. UI-only today; backend wiring follow-up.  */
.insights-period-toggle {
    display: inline-flex;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md, 8px);
    background: var(--bg-card);
    overflow: hidden;
    box-shadow: var(--shadow-sm);
}

.insights-period-btn {
    background: transparent;
    border: none;
    color: var(--text-secondary);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.06em;
    padding: var(--sp-sm) 11px;
    cursor: pointer;
    transition: color 120ms, background 120ms;
    border-right: 1px solid var(--border);
}
.insights-period-btn:last-child { border-right: none; }
.insights-period-btn:hover { color: var(--text-primary); background: var(--bg-card-hover); }
.insights-period-btn.on {
    color: var(--text-primary);
    background: var(--bg-pill);
    font-weight: 700;
}

/* Ghost mode toggle — top-right, pairs with the env selector. */
.insights-ghost-btn {
    display: flex;
    align-items: center;
    gap: var(--sp-sm);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md, 8px);
    background: var(--bg-card);
    color: var(--text-secondary);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 7px var(--sp-md);
    cursor: pointer;
    box-shadow: var(--shadow-sm);
    transition: color 120ms, border-color 120ms;
}
.insights-ghost-btn:hover {
    color: var(--text-primary);
    border-color: var(--border-emphasis, var(--border-strong));
}

.insights-ghost-dot {
    width: 7px;
    height: 7px;
    border-radius: var(--radius-pill);
    background: var(--text-tertiary);
    transition: background 160ms, box-shadow 160ms;
}

.insights-ghost-btn.on {
    color: var(--status-warning-text);
    border-color: rgb(var(--status-warning-rgb) / 0.36);
}
.insights-ghost-btn.on .insights-ghost-dot {
    background: var(--status-warning);
    box-shadow: 0 0 6px var(--status-warning);
}

/* Ghost mode active — blur every numeric figure across the page so
   the dashboard is screenshot-safe with one toggle. Specific
   selectors (rather than a single .figure class) avoid forcing every
   render path to add a marker class.
   ──
   Transition note: blur is applied INSTANTLY (no transition during
   ghost-on). Earlier we had `transition: filter 220ms` here, which
   caused two visible bugs:
     1. Toggling on → 220ms ramp-up (steps of partial blur visible)
     2. Live Wire prepending a fresh row WHILE ghost was on → the new
        row would tween from 0 blur up to 7px because the CSS
        transition fires when the rule first applies to a freshly
        mounted element.
   Removing the transition makes the blur land in the same paint
   frame as the toggle/insert — no piecemeal rendering. The OFF
   transition stays via the .ghost-fade-out helper below if we want
   a subtle fade-out later. */
body.insights-ghost .stat-figure,
body.insights-ghost .pulse-ring-num,
body.insights-ghost .pulse-bar-num,
body.insights-ghost .stat-delta,
body.insights-ghost .subs-count,
body.insights-ghost .subs-count-family,
body.insights-ghost .subs-foot-stat .v,
body.insights-ghost .geo-num,
body.insights-ghost .geo-paying,
body.insights-ghost .geo-cc,
body.insights-ghost .cohort-cell,
body.insights-ghost .cohort-row-date,
body.insights-ghost .cohort-row-n,
body.insights-ghost .review-meta,
body.insights-ghost .refund-tenure,
body.insights-ghost .refund-prod,
body.insights-ghost .refund-when,
body.insights-ghost .mile-text .s,
body.insights-ghost .insights-card-sub,
/* Per-tab summary tiles + funnel blocks (Acquisition + Engagement +
   Reliability strips) */
body.insights-ghost .pulse-compact-value,
body.insights-ghost .pulse-compact-meta,
body.insights-ghost .pulse-compact-band,
body.insights-ghost .stat-card-summary .stat-foot,
body.insights-ghost .sticky-tile-v,
body.insights-ghost .sticky-spark-row,
body.insights-ghost .funnel-step-count,
body.insights-ghost .funnel-step-meta,
body.insights-ghost .funnel-dropoff-rate,
body.insights-ghost .funnel-source-count,
body.insights-ghost .funnel-source-conv,
body.insights-ghost .funnel-summary-value,
body.insights-ghost .perf-stat .v,
body.insights-ghost .perf-cell,
body.insights-ghost .perf-version-tag,
body.insights-ghost .versions-cell,
body.insights-ghost .versions-cell-total,
body.insights-ghost .versions-tag,
body.insights-ghost .sub-tl-total,
body.insights-ghost .sub-tl-net,
body.insights-ghost .sub-tl-type-count,
body.insights-ghost .sub-tl-type-share,
body.insights-ghost .sub-tl-offer-count,
/* Live Wire ticker (replaced .live-row) — every numeric/identifying
   span on a tick. Star + family tag also blur so a screenshot of
   the ticker mid-flow doesn't leak product/storefront context. */
body.insights-ghost .tick-ts,
body.insights-ghost .tick-product,
body.insights-ghost .tick-product .price,
body.insights-ghost .tick-cc,
body.insights-ghost .tick-env,
body.insights-ghost .tick-fam,
body.insights-ghost .tick-star,
/* Unified status bar (replaced .pipeline-stat + .vital-stat cards) */
body.insights-ghost .status-stat .v,
body.insights-ghost .status-bar-eyebrow,
/* ASO term + drill-down */
body.insights-ghost .aso-impressions,
body.insights-ghost .aso-installs,
body.insights-ghost .aso-conv,
body.insights-ghost .aso-summary-v,
body.insights-ghost .aso-country-row .aso-impressions,
body.insights-ghost .aso-country-row .aso-installs,
body.insights-ghost .aso-country-row .aso-conv,
body.insights-ghost .aso-country-row .aso-country-cc,
/* Offer funnel tiles */
body.insights-ghost .offer-num-v,
body.insights-ghost .offer-summary-v,
body.insights-ghost .offer-tile-rate-pct,
body.insights-ghost .offer-country-redeem,
body.insights-ghost .offer-country-convert,
body.insights-ghost .offer-country-rate,
/* Geo tooltip — leak guard for hover/tap reveal */
body.insights-ghost .geo-tooltip [data-tooltip-cc],
body.insights-ghost .geo-tooltip [data-tooltip-total],
body.insights-ghost .geo-tooltip [data-tooltip-paying],
body.insights-ghost .geo-tooltip-cc {
    filter: blur(7px);
    user-select: none;
}

.insights-env-toggle {
    display: inline-flex;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md, 8px);
    background: var(--bg-card);
    overflow: hidden;
    box-shadow: var(--shadow-sm);
}

.insights-env-btn {
    background: transparent;
    border: none;
    color: var(--text-secondary);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: var(--sp-sm) var(--sp-md);
    cursor: pointer;
    transition: color 120ms, background 120ms;
    border-right: 1px solid var(--border);
}
.insights-env-btn:last-child { border-right: none; }
.insights-env-btn:hover { color: var(--text-primary); background: var(--bg-card-hover); }
.insights-env-btn.on {
    color: var(--text-on-primary);
    background: var(--primary);
}

/* Compact status pill — pairs a colored state dot with one short line of
   meta. Replaces the prior plain-text "Fetching (env=…)" string that
   overflowed the controls row when env labels grew or when the status
   line tried to host both the env and a relative timestamp. The dot
   gives the operator a peripheral cue — green when fresh, amber pulse
   while loading, red when an error landed — without forcing them to
   parse the whole line. */
.insights-status {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 11px;
    color: var(--text-tertiary);
    letter-spacing: 0.04em;
    white-space: nowrap;
    max-width: 320px;
    overflow: hidden;
    text-overflow: ellipsis;
}
.insights-status::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: var(--radius-pill);
    background: var(--status-positive);
    flex-shrink: 0;
    box-shadow: 0 0 0 2px rgb(var(--status-positive-rgb) / 0.18);
}
.insights-status.loading::before {
    background: var(--status-warning);
    box-shadow: 0 0 0 2px rgb(var(--status-warning-rgb) / 0.20);
    animation: insights-status-pulse 1.4s ease-in-out infinite;
}
.insights-status.error::before {
    background: var(--status-critical);
    box-shadow: 0 0 0 2px rgb(var(--status-critical-rgb) / 0.22);
    animation: none;
}
.insights-status.error { color: var(--status-critical-text); font-weight: 500; }

@keyframes insights-status-pulse {
    0%, 100% { transform: scale(1);    opacity: 0.95; }
    50%      { transform: scale(1.35); opacity: 0.55; }
}

@media (prefers-reduced-motion: reduce) {
    .insights-status.loading::before { animation: none; }
}

/* ─── Tab bar — domain partition (Money / Acquisition / Health) ─────
   Sticky at the top of the page-container scroll so the operator's
   active domain is one glance away when they scroll the cards. The
   right-side Pulse compact rides along; it carries composite-health
   across tabs without forcing a tab switch into Health for the score.
   The bar sits ABOVE the grid in source order (so tab → grid → ops
   strip reads in DOM order); CSS keeps it pinned via position:sticky
   while the grid scrolls beneath it. */

.insights-tab-bar {
    position: sticky;
    top: 0;
    z-index: 5;
    display: flex;
    align-items: center;
    gap: var(--sp-xs);
    padding: 10px var(--sp-md);
    background: var(--bg-page);
    border-bottom: 1px solid var(--border);
}
/* Mask the page-container's padding-top zone — when the bar is pinned
   at scrollport top, scrolled content peeks through the padding-top
   slot above the bar (Chrome/WebKit include padding in the scrollport
   for sticky children). The pseudo extends the bar's solid bg upward
   by --sp-xl, covering the peek-through. Pre-pin the pseudo sits in
   the flex gap above (same bg color, invisible). */
.insights-tab-bar::before {
    content: '';
    position: absolute;
    bottom: 100%;
    left: 0;
    right: 0;
    height: var(--sp-xl);
    background: var(--bg-page);
    pointer-events: none;
}

.insights-tab-bar-spacer {
    flex: 1;
}

.insights-tab-btn {
    /* Underline-style tab — matches `.tab.active` in shell.css so the
       Greenhouse-wide tab idiom (Catalog / Editor history / Settings)
       and Insights' domain partition share one pattern. The previous
       solid-pill treatment was bespoke to this page. */
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: var(--sp-sm) var(--sp-lg);
    background: transparent;
    border: none;
    border-bottom: 2px solid transparent;
    color: var(--text-tertiary);
    font-size: var(--font-size-caption);
    font-weight: 500;
    cursor: pointer;
    transition: color 120ms, border-color 120ms;
}
.insights-tab-btn:hover {
    color: var(--text-primary);
}
.insights-tab-btn[aria-selected="true"] {
    color: var(--primary);
    border-bottom-color: var(--primary);
    font-weight: 600;
}
.insights-tab-btn:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* Pulse compact — small pill on the right side of the tab bar that
   shows the composite-health score (or "calibrating" until ~30 days
   of data) so the operator never loses peripheral health awareness
   while drilling into Money or Acquisition. Renders as <button> when
   there's something to click into (calibrating state still opens a
   "what is this?" modal); the dim/no-data variant stays a div. */
.insights-pulse-compact {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-sm);
    padding: 5px var(--sp-md);
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    font-size: 11px;
    line-height: 1;
    color: var(--text-secondary);
    box-shadow: var(--shadow-sm);
    flex-shrink: 0;
}
button.insights-pulse-compact {
    appearance: none;
    color: inherit;
    cursor: pointer;
    transition: transform 140ms, box-shadow 140ms, border-color 140ms;
}
button.insights-pulse-compact:hover {
    transform: translateY(-1px);
    box-shadow: var(--shadow-card);
    border-color: var(--border-emphasis);
}
button.insights-pulse-compact:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
    button.insights-pulse-compact:hover { transform: none; }
}
.pulse-compact-eyebrow {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}
.pulse-compact-value {
    font-size: 14px;
    font-weight: 700;
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
    letter-spacing: 0;
}
.pulse-compact-band {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    padding: var(--sp-xxs) 7px;
    border-radius: var(--radius-pill);
    font-weight: 600;
}
.pulse-compact-meta {
    color: var(--text-tertiary);
    font-size: 11px;
}
.pulse-compact-dot {
    width: 7px;
    height: 7px;
    border-radius: var(--radius-pill);
    background: var(--leaf-green);
    flex-shrink: 0;
}
.insights-pulse-compact.calibrating .pulse-compact-value { font-weight: 600; color: var(--status-warning-text); font-size: 11px; letter-spacing: 0.04em; }
.insights-pulse-compact.calibrating { border-color: rgb(var(--status-warning-rgb) / 0.32); }
.insights-pulse-compact.dim       { opacity: 0.7; }
.insights-pulse-compact.pulse-good   { border-color: rgb(var(--primary-rgb) / 0.32); }
.insights-pulse-compact.pulse-good   .pulse-compact-dot  { background: var(--primary); }
.insights-pulse-compact.pulse-good   .pulse-compact-band { background: var(--primary-bg); color: var(--primary-shade); }
.insights-pulse-compact.pulse-watch  { border-color: rgb(var(--status-warning-rgb) / 0.32); }
.insights-pulse-compact.pulse-watch  .pulse-compact-dot  { background: var(--status-warning); }
.insights-pulse-compact.pulse-watch  .pulse-compact-band { background: var(--status-warning-bg); color: var(--status-warning-text); }
.insights-pulse-compact.pulse-bad    { border-color: rgb(var(--status-critical-rgb) / 0.32); }
.insights-pulse-compact.pulse-bad    .pulse-compact-dot  { background: var(--status-critical); }
.insights-pulse-compact.pulse-bad    .pulse-compact-band { background: var(--status-critical-bg); color: var(--status-critical-text); }

/* Per-tab summary strip — tile shape mirrors .stat-card so the four
   leading tiles + status dot read identically across Money (revenue),
   Acquisition (funnel), Health (vitals). Clickable tiles render as
   <button> and pick up cursor / hover / focus styles below. */
.stat-card-summary {
    text-align: left;
    font-family: inherit;
    min-height: 110px;            /* summary tiles don't have a sparkline so we floor smaller than the stat-card's 130; 110 lets the content breathe without going hollow. */
    padding: var(--sp-md) var(--sp-lg);            /* tighter than the .insights-card baseline to match the 110 floor. */
    gap: var(--sp-xs);
}
.stat-card-summary .insights-card-eyebrow { margin-bottom: 0; }
.stat-card-summary .stat-foot {
    font-size: 11px;
    color: var(--text-tertiary);
    margin-top: var(--sp-xs);
}
button.stat-card-summary {
    /* Reset native button chrome since the card already supplies its
       own border + background + padding. width:100% so the button
       fills its grid track the same way the <div> variant does. */
    appearance: none;
    color: inherit;
    width: 100%;
    cursor: pointer;
}
.stat-card-clickable {
    transition: border-color 140ms, transform 140ms, box-shadow 140ms;
}
.stat-card-clickable:hover {
    border-color: var(--border-emphasis);
    transform: translateY(-1px);
    box-shadow: var(--shadow-card);
}
.stat-card-clickable:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}
.stat-card-clickable::after {
    /* Subtle "click me" glyph in the top-right corner — only visible
       on hover so it doesn't clutter the resting state. SF-Symbol-ish
       arrow.up.right caret built from a unicode glyph + opacity. */
    content: '↗';
    position: absolute;
    top: 8px;
    right: 10px;
    font-size: 11px;
    color: var(--text-tertiary);
    opacity: 0;
    transition: opacity 140ms;
    line-height: 1;
}
.stat-card-clickable { position: relative; }
.stat-card-clickable:hover::after,
.stat-card-clickable:focus-visible::after { opacity: 0.9; }
@media (prefers-reduced-motion: reduce) {
    .stat-card-clickable:hover { transform: none; }
}

.stat-card-summary .stat-figure-row {
    display: flex;
    align-items: baseline;
    gap: var(--sp-sm);
}
.stat-card-summary .stat-dot {
    width: 8px;
    height: 8px;
    border-radius: var(--radius-pill);
    background: var(--text-tertiary);
    flex-shrink: 0;
    align-self: center;
}
.stat-card-summary .stat-dot.dot-good { background: var(--primary); box-shadow: 0 0 0 3px rgb(var(--primary-rgb) / 0.18); }
.stat-card-summary .stat-dot.dot-warn { background: var(--status-warning); box-shadow: 0 0 0 3px rgb(var(--status-warning-rgb) / 0.20); }
.stat-card-summary .stat-dot.dot-bad  { background: var(--status-critical); box-shadow: 0 0 0 3px rgb(var(--status-critical-rgb) / 0.22); }

/* Operations strip — sticky bottom across all tabs. Houses the three
   cross-cutting status panels (Live Wire, Analytics Pipeline, Vital
   Signs). Re-uses the existing render fns for those panels so the
   polling loop's `.live-list` selector + the pipeline-just-live pulse
   keep working untouched. CSS shrinks the live-list region to ~3
   visible rows + scroll so the strip stays compact. */
.insights-ops-strip {
    position: sticky;
    bottom: 0;
    z-index: 4;
    display: flex;
    flex-direction: column;        /* live-card on top, status bar below — was 3-col grid (live + pipeline + vitals) */
    gap: var(--sp-sm);
    padding: var(--sp-md) 0 var(--sp-sm);
    background: var(--bg-page);
    border-top: 1px solid var(--border);
    box-shadow: 0 -8px 16px -8px rgba(90, 65, 30, 0.10);
    margin-top: var(--sp-md);
    padding-top: var(--sp-md);
}
/* Mirror of the tab-bar ::before mask — page-container's padding-
   bottom zone shows scrolled content peeking BELOW the pinned ops
   strip. Pseudo extends the strip's solid bg downward by --sp-xl,
   covering it. */
.insights-ops-strip::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    height: var(--sp-xl);
    background: var(--bg-page);
    pointer-events: none;
}

/* Below the existing 1100 px breakpoint the 2fr 1fr 1fr layout starts
   to squash live-list rows into one truncated column; at that point
   the strip flips to a single-column stack so each panel keeps a
   readable width. The strip gives up its sticky behaviour at this
   point too — sticky bottom on a 600 px tall stack would eat half
   the viewport. */
@media (max-width: 1100px) {
    .insights-ops-strip {
        position: static;
    }
}

/* Same bracket for the tab bar — at narrow widths the buttons + the
   pulse compact pill exceed the row width, wrapping to a second line
   that sits under the bar's blur layer awkwardly. Switch to a flex-
   wrap layout that keeps the buttons left + the pulse pill on a
   secondary row when needed. */
@media (max-width: 1100px) {
    .insights-tab-bar {
        flex-wrap: wrap;
        gap: var(--sp-sm);
    }
    .insights-tab-bar-spacer {
        flex-basis: 100%;
        height: 0;
    }
}
.insights-ops-strip > .insights-card {
    grid-column: unset !important;
    min-height: 0;             /* override the .insights-card 220 baseline — ops cards size to their natural content (compact strips), the grid then stretches each column to the row's tallest. Without this, every ops card forced 220 px even though its content was ~140-180, eating ~25% of viewport height. */
    margin: 0;
}
/* Live Wire inside ops strip — collapse to ~3 visible rows + scroll
   so the strip doesn't dominate vertical space. The full list (up to
   MAX_ROWS=30) still lives in the DOM and accepts polling pre-pends. */
/* Live Wire inside ops strip — capped at ~144px (4-5 rows + scroll).
   Earlier we let it flex-fill to row height which made the strip 360+
   px tall (matching vital-card's natural height) and ate too much of
   the visible content area. Hard cap restores ~180px ops strip total. */
.insights-ops-strip .live-card .live-list {
    max-height: 144px;
    overflow-y: auto;
}
/* Unified status bar — replaces the old separate vital-card +
   analytics-pipeline-card cards in the ops strip. Single horizontal
   row of stats (~50-60px tall) with optional eyebrow. */
.status-bar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--sp-sm) var(--sp-lg);
    padding: var(--sp-md) var(--sp-lg);
    min-height: 0;
}
.status-bar-eyebrow {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    padding-right: var(--sp-md);
    border-right: 1px solid var(--border);
    flex-shrink: 0;
}
.status-stats {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px var(--sp-lg);
    flex: 1;
}
.status-stat {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-sm);
    font-size: 12px;
    color: var(--text-secondary);
    line-height: 1.4;
    white-space: nowrap;
}
.status-stat .dot {
    width: 6px;
    height: 6px;
    border-radius: var(--radius-pill);
    background: var(--status-positive);
    box-shadow: 0 0 4px var(--status-positive);
    flex-shrink: 0;
    transform: translateY(-1px);
}
.status-stat .dot.warn { background: var(--status-warning); box-shadow: 0 0 4px var(--status-warning); }
.status-stat .dot.crit { background: var(--status-critical); box-shadow: 0 0 4px var(--status-critical); }
.status-stat .dot.dim  { background: var(--text-tertiary); box-shadow: none; opacity: 0.5; }
.status-stat .l {
    font-size: 10px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}
.status-stat .v {
    font-variant-numeric: tabular-nums;
    color: var(--text-primary);
    font-weight: 600;
    font-size: 12px;
}
.status-stat .v .muted { font-weight: 400; font-size: 11px; color: var(--text-tertiary); }

/* Awaiting → live transition pulse — was on the old
   .analytics-pipeline-card; now applies to the unified bar when
   pipeline crosses from awaiting to live. Reduce-motion users get
   the tint without the swell. */
.status-bar.pipeline-just-live {
    border-color: rgb(var(--primary-rgb) / 0.5);
    animation: pipeline-just-live-swell 1800ms ease-out 1;
}
@media (prefers-reduced-motion: reduce) {
    .status-bar.pipeline-just-live {
        animation: none;
        background: var(--primary-bg);
        transition: background 600ms ease-out;
    }
}
.ops-strip-skeleton {
    grid-column: 1 / -1;
    padding: var(--sp-md);
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    display: flex;
    flex-direction: column;
    gap: var(--sp-sm);
}

/* ─── Grid system ─────────────────────────────────────────────────── */

.insights-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: var(--sp-lg);
}

.insights-card {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg, 14px);
    box-shadow: var(--shadow-sm);
    padding: var(--sp-xl);
    overflow: hidden;
    transition: box-shadow 160ms, border-color 160ms;
}
.insights-card:hover { box-shadow: var(--shadow-card); }

.insights-card-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    margin-bottom: var(--sp-md);
    gap: var(--sp-md);
}

.insights-card-eyebrow {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}
.insights-card-sub {
    font-size: 12px;
    color: var(--text-tertiary);
    font-style: italic;
    text-align: right;
    flex-shrink: 0;
}

/* ─── Headline stat cards ─────────────────────────────────────────── */

.stat-card { grid-column: span 1; display: flex; flex-direction: column; min-height: 130px; }

.stat-figure {
    font-size: 22px;            /* hero stat — token-aligned (--t-hero-number) with dashboard's stat-value, was 36 originally then 28 before the design pass. */
    font-weight: 600;
    letter-spacing: -0.02em;
    line-height: 1;
    margin-top: 6px;
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
    display: flex;
    align-items: baseline;
    gap: var(--sp-xs);
}
.stat-figure .currency,
.stat-figure .unit {
    font-size: 15px;             /* subheadline — sits next to the 22 px hero figure as currency / unit affordance */
    color: var(--text-tertiary);
    font-weight: 500;
}
.stat-figure .currency { vertical-align: 0.45em; margin-right: 0; }

.stat-meta {
    margin-top: 10px;
    font-size: 12px;
    color: var(--text-secondary);
    display: flex;
    align-items: center;
    gap: var(--sp-sm);
}

.stat-delta {
    font-size: 11px;
    font-weight: 600;
    padding: var(--sp-xxs) 7px;
    border-radius: var(--radius-sm, 4px);
    letter-spacing: 0.04em;
}
.stat-delta.up    { color: var(--status-positive); background: var(--status-positive-bg); }
.stat-delta.down  { color: var(--status-critical-text); background: var(--status-critical-bg); }
.stat-delta.flat  { color: var(--text-tertiary); background: var(--bg-pill); }

/* Realtime additions chip — shows webhook events that landed AFTER
   the latest Sales SUMMARY processing date. Rendered in each event's
   native currency (no FX claim) so the operator sees ground truth.
   Subtle leaf-green highlight to associate with the MRR figure above
   without competing for visual weight. */
.stat-realtime-tail {
    margin-top: 6px;
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 11px;
    color: var(--text-secondary);
    cursor: help;
}
.stat-realtime-tail .rt-arrow {
    font-weight: 700;
    color: var(--leaf-green, #5e8a4b);
    font-size: 13px;
    line-height: 1;
}
.stat-realtime-tail .rt-count {
    font-weight: 600;
    color: var(--text-primary);
}
.stat-realtime-tail .rt-currencies {
    font-variant-numeric: tabular-nums;
    color: var(--text-tertiary);
    margin-left: auto;
}

/* Mini sparkline area at the bottom of each stat card */
.stat-spark { margin-top: auto; padding-top: var(--sp-md); height: 28px; }
.stat-spark svg { width: 100%; height: 100%; display: block; }

/* ─── Pulse — calibrating empty state (low confidence, n < 30) ──── */

.pulse-calibrating {
    display: grid;
    grid-template-columns: 168px 1fr;
    gap: var(--sp-2xl);
    align-items: center;
    padding: var(--sp-xs) 0;
}

.pulse-ring-empty svg { animation: none; }   /* no breathing on empty */
.pulse-ring-empty-num {
    font-size: 34px;
    font-weight: 600;
    color: var(--text-tertiary);
    line-height: 1;
}

.pulse-blurb {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.pulse-blurb-headline {
    font-size: 18px;
    font-weight: 500;
    color: var(--text-primary);
    line-height: 1.3;
}
.pulse-blurb-headline em {
    font-style: italic;
    font-weight: 600;
    color: var(--primary-shade);
}

.pulse-blurb-body {
    font-size: 12px;
    color: var(--text-secondary);
    line-height: 1.55;
    max-width: 520px;
}

.pulse-inputs {
    display: flex;
    flex-wrap: wrap;
    gap: 6px var(--sp-sm);
    margin-top: var(--sp-xs);
}

.pulse-input-chip {
    font-size: 11px;
    color: var(--text-tertiary);
    letter-spacing: 0.06em;
    border: 1px dashed var(--border-strong);
    padding: 3px var(--sp-sm);
    border-radius: var(--radius-sm, 4px);
    background: var(--bg-page);
    text-transform: lowercase;
}

/* ─── Pulse panel — composite health ring + breakdown bars ──────── */

.pulse-card { grid-column: span 3; }

.pulse-body {
    display: grid;
    grid-template-columns: 168px 1fr;
    gap: var(--sp-2xl);
    align-items: center;
    padding: var(--sp-xs) 0;
}

.pulse-ring {
    position: relative;
    width: 168px;
    height: 168px;
}

.pulse-ring svg {
    width: 100%;
    height: 100%;
    /* Subtle breathing animation — opacity oscillates so the ring
       feels alive without distracting motion. Disabled in
       prefers-reduced-motion. */
    animation: pulse-breathe 4.5s ease-in-out infinite;
}

@keyframes pulse-breathe {
    0%, 100% { opacity: 1; transform: scale(1); }
    50%      { opacity: 0.94; transform: scale(0.992); }
}

.pulse-ticks {
    stroke: var(--border-strong);
    stroke-width: 1;
}

/* Score arc color tracks the score band — pulse-good/watch/bad
   live on the parent card. */
.pulse-card.pulse-good .pulse-arc  { stroke: var(--primary); }
.pulse-card.pulse-watch .pulse-arc { stroke: var(--status-warning); }
.pulse-card.pulse-bad .pulse-arc   { stroke: var(--status-critical); }

.pulse-card.low-confidence .pulse-arc { opacity: 0.55; }

.pulse-ring-center {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

.pulse-ring-num {
    font-size: 34px;
    font-weight: 600;
    letter-spacing: -0.035em;
    color: var(--text-primary);
    line-height: 1;
    font-variant-numeric: tabular-nums;
}
.pulse-card.low-confidence .pulse-ring-num { color: var(--text-tertiary); }
.pulse-ring-num em {
    font-style: italic;
    font-size: 22px;
    color: var(--text-tertiary);
    font-weight: 400;
    margin-left: var(--sp-xxs);
}

.pulse-ring-label {
    margin-top: var(--sp-xs);
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    text-align: center;
}

/* Breakdown bars — one per contributor with weight-proportional
   visibility. Bar color tracks the contributor's score band. */
.pulse-bars {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.pulse-bar {
    display: grid;
    grid-template-columns: 96px 1fr 32px;
    gap: var(--sp-md);
    align-items: center;
}

.pulse-bar-label {
    font-size: 12px;
    color: var(--text-secondary);
}

.pulse-bar-track {
    height: 5px;
    background: var(--border);
    border-radius: var(--radius-pill);
    overflow: hidden;
}

.pulse-bar-fill {
    height: 100%;
    width: var(--bar-w, 0%);
    border-radius: var(--radius-pill);
    transition: width 360ms cubic-bezier(.18,.7,.25,1);
    /* Initial scale-in animation — when the panel mounts, bars draw
       from 0 to their target width. */
    animation: pulse-bar-grow 700ms ease-out backwards;
}
.pulse-bar-fill.good { background: linear-gradient(90deg, var(--leaf-green), var(--primary)); }
.pulse-bar-fill.warn { background: linear-gradient(90deg, #b48346, var(--status-warning)); }
.pulse-bar-fill.bad  { background: linear-gradient(90deg, #a64a2c, var(--status-critical)); }

@keyframes pulse-bar-grow {
    from { transform: scaleX(0); transform-origin: left; }
    to   { transform: scaleX(1); transform-origin: left; }
}

.pulse-bar-num {
    font-size: 12px;
    color: var(--text-secondary);
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.pulse-foot {
    margin-top: var(--sp-md);
    padding-top: var(--sp-md);
    border-top: 1px dashed var(--border-strong);
    font-size: 12px;
    color: var(--text-tertiary);
    line-height: 1.5;
}
.pulse-foot-note {
    font-style: italic;
}

@media (prefers-reduced-motion: reduce) {
    .pulse-ring svg { animation: none; }
    .pulse-bar-fill { animation: none; }
}

@media (max-width: 1100px) {
    .pulse-body { grid-template-columns: 1fr; justify-items: center; }
    .pulse-bars { width: 100%; max-width: 480px; }
}

/* ─── Subscriptions ledger ────────────────────────────────────────── */

.subs-card { grid-column: span 1; display: flex; flex-direction: column; }   /* paired 1:3 with sub-timeline-card on Money tab — ledger sits at ~290 px (room for bundle IDs), chart gets ~870 px to draw daily granularity */

.subs-rows { display: flex; flex-direction: column; }

.subs-row {
    display: grid;
    grid-template-columns: 28px 1fr 56px auto;
    gap: var(--sp-md);
    align-items: center;
    padding: var(--sp-md) 0;
    border-bottom: 1px solid var(--border);
}
.subs-row:last-child { border-bottom: none; }

/* Per-tier sparkline column — narrow, drawn flush so the line color
   visually echoes the marker badge to the left. Empty data falls back
   to a dashed line via sparklineSvg's no-data branch. */
.subs-spark {
    height: 22px;
    opacity: 0.85;
}
.subs-spark svg { width: 100%; height: 100%; display: block; }

.subs-tier-mark {
    width: 22px;
    height: 22px;
    border-radius: var(--radius-pill);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 11px;
    font-weight: 700;
    border: 1px solid var(--border-strong);
    background: var(--bg-pill);
    color: var(--text-secondary);
}
.subs-row.lifetime .subs-tier-mark {
    background: var(--tier-gold-bg);
    border-color: rgb(var(--tier-gold-rgb) / 0.36);
    color: var(--tier-gold-text);
}
.subs-row.family .subs-tier-mark {
    background: var(--status-caution-bg);
    border-color: rgb(var(--status-caution-rgb) / 0.36);
    color: var(--status-caution);
}
.subs-row.yearly .subs-tier-mark {
    background: var(--primary-bg);
    border-color: var(--primary-border);
    color: var(--primary-shade);
}
.subs-row.monthly .subs-tier-mark {
    background: var(--task-watering-bg);
    border-color: rgb(var(--task-watering-rgb) / 0.32);
    color: var(--task-watering-text);
}

.subs-name {
    font-size: 13px;
    color: var(--text-primary);
    font-weight: 500;
    display: flex;
    flex-direction: column;
    gap: var(--sp-xxs);
    min-width: 0;
}
.subs-name small {
    font-size: 11px;
    color: var(--text-tertiary);
    font-weight: 400;
    letter-spacing: 0.03em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.subs-count {
    font-size: 22px;
    font-weight: 600;
    text-align: right;
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.subs-count.zero { color: var(--text-tertiary); font-weight: 500; }
.subs-count-family {
    display: block;
    margin-top: var(--sp-xxs);
    font-size: 11px;
    color: var(--status-caution);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    font-weight: 600;
}

.subs-foot {
    margin-top: var(--sp-md);
    padding-top: var(--sp-md);
    border-top: 1px solid var(--border);
    display: flex;                       /* was 2-col grid — at narrow card widths the 1fr 1fr left an awkward gap in the middle. Flex-row lets the two stats hug the left edge as one cluster. */
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--sp-sm) var(--sp-xl);
}

.subs-foot-stat .l {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    margin-bottom: var(--sp-xs);
}
.subs-foot-stat .v {
    font-size: 16px;
    font-weight: 600;
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
    display: flex;
    align-items: baseline;
    gap: var(--sp-xs);
}
.subs-foot-stat .v small {
    font-size: 11px;
    color: var(--text-tertiary);
    font-weight: 500;
}

/* ─── Geographic — stylized world dot map ─────────────────────────── */

.geo-card { grid-column: span 4; }

.geo-body {
    display: grid;
    grid-template-columns: 1fr 260px;
    gap: var(--sp-xl);
    align-items: stretch;
}

.geo-globe {
    aspect-ratio: 1.875 / 1;
    /* Atmospheric tint — light moss radial top-left, very faint moss
       radial bottom-right. With thin-line outlines the backdrop wants
       restraint, not the dual-tone we used under filled continents. */
    background:
        radial-gradient(ellipse at 30% 25%, var(--primary-bg) 0%, transparent 60%),
        radial-gradient(ellipse at 75% 80%, var(--primary-bg) 0%, transparent 55%);
    border: 1px solid var(--border);
    border-radius: var(--radius-md, 8px);
    overflow: hidden;
    min-height: 280px;
}
.geo-globe svg { width: 100%; height: 100%; display: block; }

.geo-legend {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.geo-legend-row {
    display: grid;
    grid-template-columns: 24px 36px 1fr 36px 50px;
    align-items: center;
    gap: var(--sp-sm);
    padding: 7px 10px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm, 4px);
    font-size: 12px;
    color: var(--text-secondary);
    /* Now rendered as a <button> (legend row click highlights the
       matching map dot). Reset native button chrome. */
    cursor: pointer;
    text-align: left;
    width: 100%;
    font-family: inherit;
    transition: border-color var(--transition-fast), transform var(--transition-fast);
}
.geo-legend-row:hover { border-color: var(--border-strong); transform: translateX(2px); }
.geo-legend-row:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.geo-legend-row.has-paying { border-color: var(--primary-border); background: var(--primary-bg); }
@media (prefers-reduced-motion: reduce) {
    .geo-legend-row:hover { transform: none; }
}

/* Legend-click highlight on the matching map dot — temporary 3s
   pulse so the eye lands on it, then clears (auto-removed via
   setTimeout in wireGeoTooltip). */
.geo-dot-highlight circle {
    filter: drop-shadow(0 0 8px var(--primary));
}
@keyframes geo-dot-highlight-pulse {
    0%   { transform: scale(1); }
    50%  { transform: scale(1.4); }
    100% { transform: scale(1); }
}
.geo-dot-highlight {
    animation: geo-dot-highlight-pulse 1.2s ease-in-out 2;
    transform-origin: center;
    transform-box: fill-box;
}
@media (prefers-reduced-motion: reduce) {
    .geo-dot-highlight { animation: none; }
}

.geo-flag { font-size: 16px; line-height: 1; }
.geo-cc {
    font-size: 11px;
    letter-spacing: 0.06em;
    color: var(--text-tertiary);
    font-weight: 600;
}
.geo-bar {
    height: 5px;
    background: var(--bg-page);
    border-radius: var(--radius-pill);
    overflow: hidden;
    border: 1px solid var(--border);
}
.geo-bar-fill {
    display: block;
    height: 100%;
    width: var(--bar-w, 0%);
    background: linear-gradient(90deg, var(--leaf-green), var(--primary));
    transition: width 240ms ease-out;
}
.geo-legend-row.has-paying .geo-bar-fill {
    background: linear-gradient(90deg, var(--primary-shade), var(--primary));
}

.geo-num {
    font-size: 11px;
    color: var(--text-primary);
    text-align: right;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.geo-paying {
    font-size: 11px;
    color: var(--tier-gold-text);
    background: var(--tier-gold-bg);
    padding: var(--sp-xxs) 5px;
    border-radius: 3px;
    text-align: center;
    font-weight: 600;
    letter-spacing: 0.04em;
}
.geo-paying-spacer { width: 50px; }

.geo-empty {
    padding: var(--sp-lg);
    color: var(--text-tertiary);
    font-size: 12px;
    text-align: center;
}

/* Geo card tooltip — hover (mouse) / tap (touch) reveals country
   breakdown over the map. Positioned absolutely inside .geo-globe
   (which has position: relative implied via overflow:hidden +
   transform context); JS sets `left/top` per cursor. */
.geo-globe { position: relative; }
.geo-globe svg [data-cc] { cursor: pointer; }
.geo-tooltip {
    position: absolute;
    pointer-events: none;
    background: var(--bg-card);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    padding: var(--sp-sm) var(--sp-md);
    box-shadow: var(--shadow-elevated);
    font-size: var(--font-size-caption);
    color: var(--text-secondary);
    z-index: 10;
    min-width: 100px;
    white-space: nowrap;
}
.geo-tooltip[hidden] { display: none; }
.geo-tooltip-row {
    display: flex;
    align-items: center;
    gap: var(--sp-sm);
    margin-bottom: 2px;
}
.geo-tooltip-flag { font-size: 16px; line-height: 1; }
.geo-tooltip-cc {
    font-weight: 600;
    color: var(--text-primary);
    letter-spacing: 0.04em;
}
.geo-tooltip-stats {
    display: flex;
    align-items: baseline;
    gap: 6px;
    font-size: var(--font-size-caption2);
    color: var(--text-tertiary);
}
.geo-tooltip-stats strong,
.geo-tooltip-stats [data-tooltip-total],
.geo-tooltip-stats [data-tooltip-paying] {
    color: var(--text-primary);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.geo-tooltip-sep { opacity: 0.5; }


@media (max-width: 1100px) {
    .geo-body { grid-template-columns: 1fr; }
    .geo-globe { aspect-ratio: 2 / 1; }
}

@media (prefers-reduced-motion: reduce) {
    .geo-dot-paying circle[fill="var(--primary)"]:first-of-type { animation: none; }
    .geo-globe svg animate { display: none; }
}

/* ─── Live Wire ticker — horizontal auto-scroll ───────────────────
   Replaces the previous vertical .live-list (max-height 144px). The
   ticker pattern (prototype's `.ticker-track` + `.ticker-rail`)
   surfaces ~6-8 events at a glance in a single row, ~36 px tall.
   Card total height ~80 px (was ~144 + chrome).

   Animation: 80s linear infinite, translateX 0 → -50%. The rail is
   duplicated inline so the loop reads as seamless. Hover pauses the
   scroll (operator can read a tick); reduced-motion users get a
   static rail. Mask-image fades both edges.                        */

.live-card { grid-column: span 4; }
.live-head-eyebrow {
    display: flex;
    align-items: center;
    gap: var(--sp-sm);
}
.live-head-eyebrow .dot {
    width: 7px;
    height: 7px;
    border-radius: var(--radius-pill);
    background: var(--status-positive);
    box-shadow: 0 0 6px var(--status-positive);
    animation: live-pulse 2s ease-in-out infinite;
}
@keyframes live-pulse {
    0%, 100% { transform: scale(1); opacity: 1; }
    50%      { transform: scale(0.55); opacity: 0.45; }
}

.ticker-track {
    position: relative;
    overflow: hidden;
    margin-top: var(--sp-sm);
    /* Edge-fade so ticks entering / leaving the viewport don't pop.
       Both vendor + standard property for Safari + Firefox. */
    mask-image: linear-gradient(90deg, transparent 0, black 4%, black 96%, transparent 100%);
    -webkit-mask-image: linear-gradient(90deg, transparent 0, black 4%, black 96%, transparent 100%);
}
.ticker-rail {
    display: flex;
    gap: 0;
    flex-shrink: 0;
    width: max-content;
    animation: ticker-scroll 80s linear infinite;
}
.ticker-track:hover .ticker-rail {
    animation-play-state: paused;
}
@keyframes ticker-scroll {
    from { transform: translateX(0); }
    to   { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
    .ticker-rail { animation: none; transform: none; }
}

.tick {
    display: flex;
    align-items: center;
    gap: var(--sp-md);
    padding: 6px var(--sp-lg);
    border-right: 1px solid var(--border);
    white-space: nowrap;
    font-size: 12px;
    color: var(--text-secondary);
    transition: background 120ms;
}
.tick:hover { background: var(--bg-card-hover); }
.tick.starred {
    background: linear-gradient(90deg, var(--tier-gold-bg) 0%, transparent 100%);
}

/* Fresh-tint — brief flash on newly arrived ticks. Same flash as
   the old .live-row.fresh treatment so the visual signal "something
   landed" is preserved across the layout switch. */
.tick.fresh         { animation: tick-fresh 1.6s ease-out; }
.tick.starred.fresh { animation: tick-fresh-gold 1.6s ease-out; }
@keyframes tick-fresh {
    0%   { background: var(--primary-bg-hover); }
    60%  { background: var(--primary-bg); }
    100% { background: transparent; }
}
@keyframes tick-fresh-gold {
    0%   { background: var(--tier-gold-bg); }
    60%  { background: var(--tier-gold-bg); }
    100% { background: linear-gradient(90deg, var(--tier-gold-bg) 0%, transparent 100%); }
}

.tick-ts {
    font-size: 11px;
    color: var(--text-tertiary);
    letter-spacing: 0.02em;
}
.tick-evt {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 3px 7px;
    border-radius: var(--radius-sm, 4px);
    display: inline-block;
    text-align: center;
    white-space: nowrap;
}
.tick-evt.subscribed,
.tick-evt.renewed,
.tick-evt.offer        { color: var(--status-positive); background: var(--status-positive-bg); }
.tick-evt.expired,
.tick-evt.changed      { color: var(--text-tertiary); background: var(--bg-pill); }
.tick-evt.refund,
.tick-evt.revoke       { color: var(--status-critical-text); background: var(--status-critical-bg); }
/* DID_FAIL_TO_RENEW — billing retry, leading churn indicator. Warm
   warning tone so it reads as "watch this" without crit-red alarm. */
.tick-evt.failing      { color: var(--status-warning, #A27B1A); background: var(--status-warning-bg, rgba(162, 123, 26, 0.12)); }
.tick-evt.test         { color: var(--text-tertiary); background: var(--bg-pill); font-style: italic; }

.tick-product {
    color: var(--text-primary);
    font-weight: 500;
}
.tick-product .price {
    color: var(--text-tertiary);
    margin-left: 6px;
    font-size: 11px;
    font-weight: 400;
}
.tick-cc {
    font-size: 13px;        /* flag emoji */
    line-height: 1;
}
.tick-env {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    border: 1px solid var(--border-strong);
    padding: var(--sp-xxs) 6px;
    border-radius: var(--radius-sm, 4px);
    color: var(--text-tertiary);
    text-align: center;
    white-space: nowrap;
}
.tick-env.prod { color: var(--primary-shade); border-color: var(--primary-border); }
.tick-star {
    color: var(--tier-gold);
    font-size: 13px;
    line-height: 1;
}
.tick-fam {
    color: var(--status-caution);
    font-size: 9px;
    letter-spacing: 0.08em;
    padding: var(--sp-xxs) 4px;
    border-radius: 3px;
    background: var(--status-caution-bg);
}

.live-empty {
    padding: var(--sp-xl);
    text-align: center;
    color: var(--text-tertiary);
    font-size: 13px;
    background: var(--bg-card);
    border-radius: var(--radius-md);
    margin-top: var(--sp-sm);
}

/* ─── Refund Triage ───────────────────────────────────────────────── */

.refund-card { grid-column: span 2; }

/* Empty state — seedling illustration + reassurance copy + 4 placeholder
   field cards. Designed to be friendly: zero refunds is a GOOD signal,
   not a "no data" scolding. */
.refund-empty {
    display: grid;
    grid-template-columns: 130px 1fr;
    gap: var(--sp-xl);
    align-items: center;
    padding: var(--sp-sm) 0;
}

.refund-seedling {
    width: 130px;
    height: 130px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.refund-seedling svg { width: 100%; height: 100%; }

.refund-text {
    display: flex;
    flex-direction: column;
    gap: var(--sp-md);
    min-width: 0;
}

.refund-headline {
    font-size: 18px;
    font-weight: 500;
    line-height: 1.3;
    color: var(--text-primary);
}
.refund-headline em {
    font-style: italic;
    color: var(--primary-shade);
    font-weight: 600;
}

.refund-blurb {
    font-size: 12px;
    color: var(--text-secondary);
    line-height: 1.55;
    max-width: 460px;
}

.refund-fields {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 6px;
    margin-top: var(--sp-xs);
}

.refund-field {
    padding: 9px 10px;
    background: var(--bg-page);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-sm, 4px);
}

.refund-field .l {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    margin-bottom: 3px;
}

.refund-field .v {
    font-size: 12px;
    font-style: italic;
    color: var(--text-tertiary);
    font-weight: 400;
}

/* Populated state — header row + N data rows. Uses a CSS grid header
   so columns align without a <table>. Each row carries the same
   columns; .refund-cause picks up a tier-color based on revocation
   reason (app issue = crit terracotta, other = warn honey). */
.refund-list {
    display: flex;
    flex-direction: column;
}

.refund-head,
.refund-row {
    display: grid;
    grid-template-columns: 70px 1fr 40px 60px 80px 1.4fr;
    gap: var(--sp-md);
    align-items: center;
    padding: 9px var(--sp-xs);
}

.refund-head {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    border-bottom: 1px solid var(--border);
}
/* Header cells inherit alignment from the value cell that sits
   beneath them. Without this, headers (default left-align) drifted
   off from values (right/center aligned per column type). */
.refund-head > div:nth-child(3),  /* CC */
.refund-head > div:nth-child(5) { /* CAUSE */
    text-align: center;
}
.refund-head > div:nth-child(4) { /* TENURE */
    text-align: right;
}

.refund-row {
    font-size: 12px;
    color: var(--text-secondary);
    border-bottom: 1px solid var(--border);
}
.refund-row:last-child { border-bottom: none; }

.refund-when {
    font-size: 11px;
    color: var(--text-tertiary);
}
.refund-prod {
    color: var(--text-primary);
    font-weight: 500;
    display: flex;
    align-items: center;
    gap: 6px;
}
.refund-fam {
    font-size: 11px;
    letter-spacing: 0.08em;
    color: var(--status-caution);
    background: var(--status-caution-bg);
    padding: 1px 5px;
    border-radius: 2px;
    text-transform: uppercase;
}
.refund-cc {
    text-align: center;
    font-size: 13px;
}
.refund-tenure {
    font-size: 11px;
    color: var(--text-secondary);
    text-align: right;
}
.refund-cause {
    font-size: 11px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    padding: 3px 7px;
    border-radius: var(--radius-sm, 4px);
    text-align: center;
    font-weight: 600;
}
.cause-warn { color: var(--status-warning-text); background: var(--status-warning-bg); }
.cause-crit { color: var(--status-critical-text); background: var(--status-critical-bg); }

.refund-review {
    font-size: 12px;
    color: var(--text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.refund-review .muted { color: var(--text-tertiary); font-style: italic; }

/* ─── Milestones timeline ─────────────────────────────────────────── */

.miles-card { grid-column: span 2; }

.insights-card-title {
    font-size: 15px;
    font-weight: 600;
    color: var(--text-primary);
    margin-top: var(--sp-xs);
    line-height: 1.2;
}

/* Timeline thread runs through the centre of each mile-dot — not at
   the page-margin like the previous version. Each .mile is a grid:
   col 1 = dot column (fixed 14px), col 2 = text, col 3 = glyph. The
   thread is a 1px line absolutely-positioned over col 1's centre and
   gets clipped by mile-dot's solid background so the line "ducks
   under" each marker. Done portion solid moss, pending tail dashed
   border-strong; the boundary lands exactly where the .next class
   first appears in the list (each .mile.next acts as a stacked row
   that overlays a dashed segment on top of the solid). */
.miles-list {
    position: relative;
    margin-top: var(--sp-xs);
}

.mile {
    display: grid;
    grid-template-columns: 22px 1fr 24px;
    gap: var(--sp-md);
    align-items: center;
    padding: 9px 0;
    position: relative;
}

/* Each mile carries its own segment of the thread, centred on col 1.
   The first child mile gets top:50% so the thread starts from the
   first dot, not from the panel top. The last child gets bottom:50%
   for the same reason at the foot. Result: thread reads as connecting
   tissue between dots, not a flagpole anchored at one end. */
.mile::before {
    content: '';
    position: absolute;
    left: 10px;
    top: 0;
    bottom: 0;
    width: 1px;
    background: var(--leaf-green);
}
.mile.next::before {
    background:
        repeating-linear-gradient(
            180deg,
            var(--border-strong) 0 4px,
            transparent 4px 8px
        );
}
.miles-list .mile:first-child::before { top: 50%; }
.miles-list .mile:last-child::before  { bottom: 50%; }

.mile-dot {
    width: 11px;
    height: 11px;
    border-radius: var(--radius-pill);
    margin-left: 5px;
    flex-shrink: 0;
    /* Sit on top of the thread so the line passes BEHIND the dot. */
    position: relative;
    z-index: 1;
}
.mile.done .mile-dot {
    background: var(--primary);
    border: 1px solid var(--primary-shade);
    box-shadow: 0 0 0 4px var(--primary-bg);
}
.mile.next .mile-dot {
    background: var(--bg-card);
    border: 1px dashed var(--border-emphasis, var(--border-strong));
}

.mile-text { display: flex; flex-direction: column; gap: var(--sp-xxs); min-width: 0; }
.mile-text .t {
    font-size: 13px;
    color: var(--text-primary);
    font-weight: 500;
}
.mile.next .mile-text .t {
    color: var(--text-tertiary);
    font-style: italic;
    font-weight: 400;
}
.mile-text .s {
    font-size: 11px;
    color: var(--text-tertiary);
    letter-spacing: 0.04em;
}

/* Lucide glyph cell — small (14px) and tertiary-tinted by default.
   Featured milestone (first paying user) overrides to gold. Spacer
   variant keeps column 3 width when an entry has no icon (pending
   markers) so text columns don't shift between rows. */
.mile-glyph {
    width: 16px;
    height: 16px;
    color: var(--text-tertiary);
    display: flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
}
.mile-glyph-spacer { width: 16px; height: 16px; }
.mile.done .mile-glyph { color: var(--primary-shade); }
.mile.featured .mile-glyph { color: var(--tier-gold-text); }

/* Featured milestone — gold tint + subtle elevated background.
   Reserved for the "first paying user" today; future featured
   moments could be the 100th paying user, $1k MRR, etc.
   Tint starts AFTER the dot column (left:22px) so the timeline
   thread that runs through dot-column centre stays continuous —
   earlier we shifted the whole row left/right, breaking thread
   alignment between this row and its neighbours. */
.mile.featured {
    background: linear-gradient(
        90deg,
        transparent 0,
        transparent 22px,
        var(--tier-gold-bg) 22px,
        var(--tier-gold-bg) 60%,
        transparent 95%
    );
    border-radius: var(--radius-md, 8px);
}
.mile.featured .mile-text .t {
    font-size: 14px;
    font-weight: 600;
}
.mile.featured .mile-dot {
    background: var(--tier-gold);
    border-color: var(--tier-gold-text);
    box-shadow: 0 0 0 4px var(--tier-gold-bg);
}

.miles-empty {
    padding: var(--sp-lg);
    color: var(--text-tertiary);
    font-size: 13px;
    text-align: center;
}

/* ─── Cohort retention heatmap ────────────────────────────────────── */

.cohort-card { grid-column: span 4; }
.cohort-empty {
    padding: var(--sp-2xl) var(--sp-lg);
    color: var(--text-tertiary);
    font-size: 13px;
    line-height: 1.55;
    text-align: center;
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-md);
    background: var(--bg-page);
    margin-top: var(--sp-md);
}
.cohort-empty p { margin: 0; max-width: 480px; margin-left: auto; margin-right: auto; }
.cohort-empty em { font-style: italic; color: var(--text-secondary); }

.cohort-grid {
    display: grid;
    grid-template-columns: 86px repeat(5, 1fr);
    gap: 5px;
    margin-top: var(--sp-sm);
}

.cohort-hd {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    padding: var(--sp-sm) var(--sp-xs);
    text-align: center;
}
.cohort-hd.label { text-align: left; }

/* Cohort row label — single line "Apr 7 · n=12". The previous design
   stacked month / day / n on three lines which worked but felt over-
   engineered for what is essentially a date + sample size; single
   line lets the cells grow vertically without the row label
   dominating the rhythm. */
.cohort-row-label {
    padding: var(--sp-xs) var(--sp-sm) var(--sp-xs) 0;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
    line-height: 1.1;
    border-right: 1px solid var(--border);
    flex-wrap: wrap;
}
.cohort-row-date {
    font-size: 12px;
    font-weight: 600;
    color: var(--text-primary);
    letter-spacing: 0.04em;
}
.cohort-row-n {
    font-size: 11px;
    color: var(--text-tertiary);
    letter-spacing: 0.04em;
}

.cohort-cell {
    aspect-ratio: 2.4 / 1;       /* was 1.2 / 1 — at full card width (1160) the heatmap was 842 px tall (half a viewport). Wider-than-tall cells halve the vertical real estate while keeping the pct number readable. */
    border-radius: var(--radius-sm, 4px);
    font-size: 13px;
    font-weight: 600;
    display: flex;
    align-items: center;
    justify-content: center;
    font-variant-numeric: tabular-nums;
}

/* 6-band perceptual scale — token-driven so it carries to dark mode.
   Empty cells dashed border so they read as "not yet" not "zero". */
.cohort-cell.empty {
    background: var(--bg-page);
    border: 1px dashed var(--border-strong);
    color: var(--text-tertiary);
    font-weight: 400;
}
.cohort-cell.l5 { background: var(--bg-pill);            color: var(--text-tertiary); }
.cohort-cell.l4 { background: var(--primary-bg);         color: var(--text-secondary); }
.cohort-cell.l3 { background: var(--primary-bg-hover);   color: var(--text-primary); }
.cohort-cell.l2 { background: var(--primary-shade);      color: var(--text-on-primary); }
.cohort-cell.l1 { background: var(--primary);            color: var(--text-on-primary); }
.cohort-cell.l0 { background: var(--leaf-green);         color: var(--text-on-primary); }

/* Legend now lives ABOVE the grid (used to be cohort-foot at the
   bottom). Reading the heatmap top-down, a viewer sees the colour
   scale before the cells, so the cell colour-to-percentage mapping
   is preloaded — eliminates the double-take when scanning a row. */
.cohort-legend {
    display: flex;
    align-items: center;
    gap: var(--sp-sm);
    margin-top: var(--sp-xs);
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}
.cohort-legend-l {
    font-variant-numeric: tabular-nums;
    flex-shrink: 0;
}

.cohort-scale {
    display: flex;
    height: 8px;
    flex: 1;
    border-radius: var(--radius-pill);
    overflow: hidden;
    border: 1px solid var(--border);
}
.cohort-scale span { flex: 1; }
.cohort-scale .l5 { background: var(--bg-pill); }
.cohort-scale .l4 { background: var(--primary-bg); }
.cohort-scale .l3 { background: var(--primary-bg-hover); }
.cohort-scale .l2 { background: var(--primary-shade); }
.cohort-scale .l1 { background: var(--primary); }
.cohort-scale .l0 { background: var(--leaf-green); }

/* ─── Reviews / Field Notes ───────────────────────────────────────── */

.reviews-card { grid-column: span 4; }    /* full row on Engagement — was span 2 + watch-card partner, then span 2 alone (orphan with 588 px empty cols on the right). Reviews body benefits from full width too (long-form text + flag/version meta). */

.reviews-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.review {
    background: var(--bg-page);
    border: 1px solid var(--border);
    border-radius: var(--radius-md, 8px);
    padding: var(--sp-md) var(--sp-md);
}
.review.flagged {
    background: var(--status-critical-bg);
    border-color: rgb(var(--status-critical-rgb) / 0.3);
}

.review-flag-banner {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--status-critical-text);
    margin-bottom: var(--sp-sm);
    font-weight: 600;
}

.review-row {
    display: grid;
    grid-template-columns: auto 1fr auto 16px;
    gap: var(--sp-md);
    align-items: flex-start;
}
.review-expand {
    color: var(--text-tertiary);
    align-self: center;
    transition: transform 120ms, color 120ms;
}
button.review:hover .review-expand {
    color: var(--text-secondary);
    transform: translateX(2px);
}

.review-stars {
    font-size: 13px;
    color: var(--tier-gold);
    letter-spacing: 0.06em;
    white-space: nowrap;
}
.review-stars .off { color: var(--text-tertiary); opacity: 0.5; }

.review-body { min-width: 0; }
.review-title {
    font-size: 13px;
    font-weight: 600;
    color: var(--text-primary);
    margin-bottom: var(--sp-xs);
    line-height: 1.3;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.review-text {
    font-size: 12px;
    color: var(--text-secondary);
    line-height: 1.5;
    /* clamp to two lines; longer reviews truncate with ellipsis so the
       card heights stay roughly even across the two-column grid. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.review-meta {
    margin-top: var(--sp-sm);
    font-size: 11px;
    letter-spacing: 0.04em;
    color: var(--text-tertiary);
    display: flex;
    gap: var(--sp-md);
    flex-wrap: wrap;
}
.review-meta strong {
    color: var(--text-secondary);
    font-weight: 600;
}

.review-sentiment {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 3px 7px;
    border-radius: var(--radius-sm, 4px);
    align-self: flex-start;
    font-weight: 600;
    white-space: nowrap;
}
.sentiment-positive { color: var(--status-positive); background: var(--status-positive-bg); }
.sentiment-neutral  { color: var(--text-tertiary); background: var(--bg-pill); }
.sentiment-negative { color: var(--status-critical-text); background: var(--status-critical-bg); }

.reviews-empty {
    padding: var(--sp-2xl);
    color: var(--text-tertiary);
    font-size: 12px;
    text-align: center;
    background: var(--bg-page);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-md, 8px);
}

@media (max-width: 1100px) {
    .reviews-card { grid-column: span 4; }
}

/* ─── Watchtower (anomaly detectors) ──────────────────────────────── */

.watch-card { grid-column: span 4; }    /* full row — was paired with reviews-card 3+1, then 2+2; reviews moved to Engagement tab so watch-card sits alone on Health. Full width gives the rule list room without trailing empty cols. */

/* Triggered state — the whole card border picks up a subtle terracotta
   tint so a glance from across the page is enough to know something
   wants attention. */
.watch-card.has-trigger { border-color: rgb(var(--status-critical-rgb) / 0.3); }

.watch-rules {
    display: flex;
    flex-direction: column;
    gap: var(--sp-sm);
    margin-top: var(--sp-xs);
}

.watch-rule {
    display: grid;
    grid-template-columns: 12px auto auto 1fr 16px;
    align-items: center;
    gap: 10px;
    padding: 9px var(--sp-md);
    background: var(--bg-page);
    border: 1px solid var(--border);
    border-radius: var(--radius-md, 8px);
    font-size: 12px;
    color: var(--text-secondary);
}
.watch-expand {
    color: var(--text-tertiary);
    transition: transform 120ms, color 120ms;
}
button.watch-rule:hover .watch-expand {
    color: var(--text-secondary);
    transform: translateX(2px);
}
.watch-rule.triggered {
    background: var(--status-critical-bg);
    border-color: rgb(var(--status-critical-rgb) / 0.3);
}
.watch-rule.pending {
    background: var(--bg-card);
    opacity: 0.75;
}

.watch-dot {
    width: 7px;
    height: 7px;
    border-radius: var(--radius-pill);
    flex-shrink: 0;
    margin-top: var(--sp-xxs);
}
.watch-dot.ok   { background: var(--status-positive); box-shadow: 0 0 5px var(--status-positive); }
.watch-dot.warn { background: var(--status-warning);  box-shadow: 0 0 5px var(--status-warning); }
.watch-dot.crit {
    background: var(--status-critical);
    box-shadow: 0 0 6px var(--status-critical);
    animation: watch-pulse 1.6s ease-in-out infinite;
}
.watch-dot.dim {
    background: var(--text-tertiary);
    opacity: 0.45;
}
@keyframes watch-pulse {
    0%, 100% { transform: scale(1); opacity: 1; }
    50%      { transform: scale(0.65); opacity: 0.55; }
}

.watch-name {
    font-weight: 500;
    color: var(--text-primary);
    white-space: nowrap;
}
.watch-rule.pending .watch-name { color: var(--text-tertiary); font-style: italic; }

.watch-status {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    padding: var(--sp-xxs) 7px;
    border-radius: 3px;
    background: var(--bg-card);
    border: 1px solid var(--border);
}
.watch-rule.triggered .watch-status {
    color: var(--status-critical-text);
    background: var(--bg-card);
    border-color: rgb(var(--status-critical-rgb) / 0.3);
    font-weight: 600;
}
.watch-rule.watching .watch-status {
    color: var(--status-warning-text);
    border-color: rgb(var(--status-warning-rgb) / 0.3);
}

.watch-body {
    font-size: 11px;
    color: var(--text-tertiary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
    .watch-dot.crit { animation: none; }
}

@media (max-width: 1100px) {
    .watch-card { grid-column: span 4; }
}

/* ─── Vital Signs strip ───────────────────────────────────────────── */

.vital-card {
    grid-column: span 4;
    padding: var(--sp-md) var(--sp-xl);
    min-height: 88px;       /* same strip-shape exemption from the .insights-card 220px baseline */
}

.vital-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px var(--sp-md);          /* was --sp-2xl — tighter so 8 stats wrap to fewer rows in the narrow ops-strip column (~290 px), shrinking the strip height ~280→~120 */
}

.vital-eyebrow {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    display: flex;
    align-items: center;
    gap: var(--sp-sm);
}
.vital-eyebrow::before {
    content: '';
    width: 12px;
    height: 1px;
    background: var(--leaf-green);
}

/* Vital stat — dot column on the left, label-above-value stack on
   the right. Stack avoids the baseline-mismatch we had when label
   (9.5px mono caps) and value (11.5px tabular) tried to share a
   single horizontal line — they read at different optical heights
   so the eye couldn't align across cells. Stacked, the value's
   bottom edge anchors and the row reads cleanly. */
.vital-stat {
    display: grid;
    grid-template-columns: auto auto;
    align-items: center;
    column-gap: 9px;
    color: var(--text-secondary);
}
.vital-stat .dot {
    width: 6px;
    height: 6px;
    border-radius: var(--radius-pill);
    background: var(--status-positive);
    box-shadow: 0 0 5px var(--status-positive);
    flex-shrink: 0;
    grid-row: span 2;
}
.vital-stat .dot.warn { background: var(--status-warning); box-shadow: 0 0 5px var(--status-warning); }
.vital-stat .dot.crit { background: var(--status-critical); box-shadow: 0 0 5px var(--status-critical); }
.vital-stat .dot.dim {
    background: var(--text-tertiary);
    box-shadow: none;
    opacity: 0.5;
}
.vital-stat .l {
    grid-column: 2;
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    line-height: 1.2;
}
.vital-stat .v {
    grid-column: 2;
    font-size: 12px;
    color: var(--text-primary);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    line-height: 1.2;
    margin-top: 1px;
}
.vital-stat.placeholder .v { color: var(--text-tertiary); font-style: italic; font-weight: 500; }
.vital-stat .l {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}
.vital-stat .v {
    font-size: 12px;
    color: var(--text-primary);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

/* Modal-specific helpers used by Reviews / Watchtower bodies. */
.modal-rating {
    font-size: 16px;        /* was 18 — pictogram + status pill reads big at modal scale; 16 (callout) sits next to the title proportionally without dominating the card */
    color: var(--tier-gold);
    margin-bottom: var(--sp-md);
    display: flex;
    align-items: center;
    gap: var(--sp-md);
}
.modal-body-text {
    /* Inherits .modal-body's 13px (footnote) so review/watchtower body
       paragraphs read at the same density as pending-row text and the
       rest of the admin. Was 15px which felt big against the new
       shared modal-body baseline. */
    color: var(--text-primary);
    line-height: 1.55;
    margin-bottom: var(--sp-md);
    white-space: pre-wrap;
}
.modal-meta {
    margin-top: 10px;
    padding-top: 10px;
    border-top: 1px solid var(--border);
    font-size: 11px;
    color: var(--text-tertiary);
    display: flex;
    gap: var(--sp-md);
    flex-wrap: wrap;
}
.modal-meta strong { color: var(--text-secondary); font-weight: 600; }
.modal-flag-note {
    margin-top: var(--sp-md);
    padding: 10px var(--sp-md);
    background: var(--status-critical-bg);
    border-radius: var(--radius-md, 8px);
    font-size: 12px;
    color: var(--status-critical-text);
    font-weight: 500;
}

/* Watchtower modal — large status pill at top */
.modal-watch-status {
    display: inline-block;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.08em;
    padding: 5px 10px;
    border-radius: var(--radius-sm, 4px);
    margin-bottom: var(--sp-md);
}
.modal-watch-status.status-ok        { color: var(--status-positive); background: var(--status-positive-bg); }
.modal-watch-status.status-watching  { color: var(--status-warning-text); background: var(--status-warning-bg); }
.modal-watch-status.status-triggered { color: var(--status-critical-text); background: var(--status-critical-bg); }
.modal-watch-status.status-pending   { color: var(--text-tertiary); background: var(--bg-pill); }

/* ── Help / glossary modal ─────────────────────────────────────────
   Reference content. Sectioned with breathing room, two-column dl
   for term/definition pairs, optional 2-col acronym index grid,
   callout for the "how to read this" intro. */
.glossary {
    display: flex;
    flex-direction: column;
    gap: var(--sp-xl);
}
.glossary-section {
    display: block;
}
.glossary-section + .glossary-section {
    padding-top: var(--sp-md);
    border-top: 1px solid var(--border);
}
.glossary h3 {
    font-size: var(--font-size-callout);
    font-weight: 600;
    color: var(--text-primary);
    margin: 0 0 var(--sp-sm);
    letter-spacing: -0.005em;
}
.glossary p {
    font-size: 13px;
    color: var(--text-secondary);
    line-height: 1.55;
    margin: var(--sp-xs) 0;
}
.glossary ul.glossary-tab-rundown {
    list-style: none;
    margin: var(--sp-sm) 0;
    padding: 0;
    display: grid;
    gap: 6px var(--sp-md);
}
.glossary-tab-rundown li {
    font-size: 13px;
    color: var(--text-secondary);
    line-height: 1.5;
    padding-left: var(--sp-md);
    position: relative;
}
.glossary-tab-rundown li::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0.55em;
    width: 6px;
    height: 6px;
    border-radius: var(--radius-pill);
    background: var(--primary);
}
.glossary-tab-rundown strong {
    color: var(--text-primary);
    font-weight: 600;
}
.glossary-tab-rundown em {
    color: var(--text-tertiary);
    font-style: italic;
}

/* Callout box — for the "read top-down" intro and any high-signal
   note inside a section. Sage tint + leaf accent border. */
.glossary-callout {
    margin: var(--sp-md) 0 0 !important;
    padding: var(--sp-md) var(--sp-lg);
    background: var(--primary-bg);
    border-left: 3px solid var(--primary);
    border-radius: var(--radius-sm);
    font-size: 13px;
    color: var(--text-primary) !important;
    line-height: 1.6;
}

.glossary dl {
    display: grid;
    grid-template-columns: minmax(170px, max-content) 1fr;
    gap: 8px var(--sp-xl);
    margin: var(--sp-sm) 0 0;
}
/* Acronym index — 2-col layout so 14 entries don't dominate vertical
   real estate. Each column carries dt/dd pairs. */
.glossary-acronyms {
    grid-template-columns: minmax(110px, max-content) 1fr;
    column-gap: var(--sp-2xl);
    column-fill: balance;
}
@media (min-width: 720px) {
    .glossary-acronyms {
        grid-template-columns: minmax(110px, max-content) 1fr minmax(110px, max-content) 1fr;
    }
}
.glossary dt {
    font-size: 11px;
    letter-spacing: 0.04em;
    color: var(--text-primary);
    font-weight: 600;
    text-transform: uppercase;
    padding-top: 2px;
    overflow-wrap: anywhere;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--sp-xs);
}
.glossary dd {
    font-size: 13px;
    color: var(--text-secondary);
    line-height: 1.5;
    margin: 0;
}
.glossary dd code {
    font-family: var(--font-mono);
    font-size: 11.5px;
    padding: 1px 5px;
    background: var(--bg-pill);
    border-radius: 3px;
    color: var(--text-primary);
}
.glossary kbd {
    font-size: 10.5px;
    padding: 1px 6px;
    background: var(--bg-pill);
    border: 1px solid var(--border-strong);
    border-radius: 3px;
    font-weight: 600;
    color: var(--text-primary);
    line-height: 1.3;
}
/* Inline tag — used in dt to surface a short hint next to the term
   (e.g. "FAMILY_SHARED <FAM>", "Period toggle <24h · 7d · …>"). */
.glossary-tag {
    font-size: 9.5px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 2px var(--sp-sm);
    background: var(--bg-pill);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    color: var(--text-tertiary);
    font-weight: 500;
}
.glossary-foot {
    margin: 0 !important;
    padding: var(--sp-md);
    background: var(--bg-input);
    border-radius: var(--radius-md);
    color: var(--text-tertiary) !important;
    font-style: italic;
    font-size: 12px !important;
    text-align: center;
}

@media (max-width: 820px) {
    .glossary dl { grid-template-columns: 1fr; gap: var(--sp-xxs) 0; }
    .glossary dd { margin-bottom: var(--sp-sm); }
}

/* Help button — small icon-only, sits next to Ghost mode */
.insights-icon-btn {
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md, 8px);
    background: var(--bg-card);
    color: var(--text-secondary);
    width: 32px;
    height: 32px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: var(--shadow-sm);
    transition: color 120ms, border-color 120ms;
}
.insights-icon-btn:hover { color: var(--text-primary); border-color: var(--border-emphasis, var(--border-strong)); }

/* Make review + watchtower rows feel clickable. They're <button>s
   underneath; reset the default button look + add hover. */
button.review,
button.watch-rule {
    width: 100%;
    text-align: left;
    cursor: pointer;
    font-family: inherit;
    color: inherit;
}
button.review:hover { transform: translateY(-1px); transition: transform 120ms; }
button.watch-rule:hover {
    background: var(--bg-card-hover);
}

/* ─── Skeleton — first paint + period / env switch ──────────────────
   The skeleton pre-paints card-shaped placeholders so the operator sees
   the dashboard's rhythm immediately, instead of an empty grid + a
   plain "Fetching…" string hanging in centred white space. Each
   placeholder is a real `.insights-card` plus `.insights-skeleton-card`
   so border / radius / padding stay consistent with the eventual card
   that replaces it. The shimmer is a slow warm-toned sweep (1.6s) —
   reduced-motion strips the animation but keeps the placeholder
   geometry so the grid still feels populated. */

.insights-skeleton-card {
    pointer-events: none;
    min-height: 220px;
    display: flex;
    flex-direction: column;
    gap: var(--sp-sm);
    overflow: hidden;
}
.insights-skeleton-card[data-shape="stat"]  { min-height: 130px; }
.insights-skeleton-card[data-shape="strip"] { min-height: 88px; }

.skeleton-line {
    position: relative;
    display: block;
    background: var(--bg-pill);
    border-radius: var(--radius-sm);
    height: 12px;
    overflow: hidden;
}
.skeleton-line.tiny { width: 28%; height: 9px;  opacity: 0.65; }
.skeleton-line.sm   { width: 40%; height: 11px; }
.skeleton-line.md   { width: 60%; height: 13px; }
.skeleton-line.lg   { width: 78%; height: 15px; }
.skeleton-line.xl   { width: 90%; height: 18px; }
.skeleton-line.bar  { width: 100%; height: 36px; margin-top: auto; }
.skeleton-line.tall { width: 100%; height: 80px; margin-top: auto; }

.skeleton-line::after {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(
        90deg,
        rgb(var(--primary-rgb) / 0) 0%,
        rgb(var(--primary-rgb) / 0.10) 50%,
        rgb(var(--primary-rgb) / 0) 100%
    );
    transform: translateX(-100%);
    animation: skeleton-shimmer 1.6s ease-in-out infinite;
}

@keyframes skeleton-shimmer {
    100% { transform: translateX(100%); }
}

@media (prefers-reduced-motion: reduce) {
    .skeleton-line::after { animation: none; }
}

/* Real card fade-in when skeleton hands off. Kept short (180ms) so the
   transition isn't laggy; reduced-motion drops it to instant. */
.insights-grid > .insights-card:not(.insights-skeleton-card) {
    animation: insights-card-fade-in 180ms ease-out both;
}
@keyframes insights-card-fade-in {
    from { opacity: 0; transform: translateY(2px); }
    to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
    .insights-grid > .insights-card:not(.insights-skeleton-card) { animation: none; }
}

/* ─── Loading / error / empty states ──────────────────────────────── */

.insights-loading {
    padding: 60px;
    text-align: center;
    color: var(--text-tertiary);
    font-size: 14px;
}

.insights-error-card {
    grid-column: span 4;
    padding: var(--sp-2xl);
    background: var(--status-critical-bg);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-lg, 14px);
    color: var(--status-critical-text);
    font-size: 13px;
}
.insights-error-card strong { display: block; margin-bottom: var(--sp-xs); font-size: 14px; font-weight: 600; }

/* ─── Responsive ──────────────────────────────────────────────────── */

@media (max-width: 1100px) {
    .stat-card  { grid-column: span 2; }
    .pulse-card { grid-column: span 4; }
    .subs-card  { grid-column: span 4; }
}

@media (max-width: 720px) {
    .stat-card  { grid-column: span 4; }
    /* .insights-head + .insights-controls were dropped when the page
       moved to the project-wide section-header pattern; the section-
       header itself already wraps via flex-wrap so a narrow viewport
       just stacks naturally. */
    .live-row {
        grid-template-columns: 80px auto 1fr auto;
    }
    .live-row .live-cc, .live-row .live-env { display: none; }
}

@media (prefers-reduced-motion: reduce) {
    .live-head-eyebrow .dot { animation: none; }
    /* Fresh-row tint — keep the visual signal (something arrived) but
       drop the keyframe in favor of an instant background that fades
       within 200ms instead of an animated 1.6s transition. */
    .live-row.fresh,
    .live-row.starred.fresh {
        animation: none;
        transition: background 200ms ease-out;
    }
}

/* ─── Analytics Pipeline meta panel ─────────────────────────────────
   Sits above the Vital Signs strip — same operator-meta tone, but
   article-shaped (head + summary + report list). The card light up
   green via the .live state badge once Apple delivers; until then
   the card reads as a calm "still listening" pane.                  */

/* Health stack — wraps the 4 operations strips (Analytics Pipeline,
   Vital Signs, Stickiness, Performance Vitals) into a single card so
   the dashboard reads them as one "health" surface rather than four
   separate cards each with their own border + shadow + gap.
   The inner strips drop their own border / shadow / radius / bg and
   are separated only by hairlines. Outer wrapper carries the card
   chrome.
*/
/* The Analytics Pipeline + Vital Signs + Performance Vitals trio used
   to live inside a `.health-stack` wrapper that stripped each card's
   chrome (border, radius, shadow) and joined them with a single bottom
   divider. The result: three 49-63 px tall strips that visually fused
   into one merged surface — operators read the bottom of the page as
   "another section" instead of three independent panels. The wrapper
   is gone now; each card stands on its own with normal `.insights-card`
   chrome. Faz 4 lifts the two genuinely-strip cards (pipeline + vital)
   into the persistent Operations strip at page bottom, so the grid
   only carries the cards that earn full-card real estate. */

.analytics-pipeline-card {
    grid-column: span 4;
    padding: var(--sp-md) var(--sp-xl);
    min-height: 88px;       /* override the .insights-card baseline — pipeline is naturally a strip; full-card height would leave the line item floating in white space */
}
.pipeline-strip {
    display: flex;
    align-items: center;
    gap: var(--sp-2xl);
    flex-wrap: wrap;
}
.pipeline-eyebrow {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    padding-right: 10px;
    border-right: 1px solid var(--border);
}
.pipeline-state {
    display: flex;
    align-items: center;
    gap: var(--sp-sm);
}
.pipeline-state-text {
    color: var(--text-secondary);
    font-size: 13px;
    line-height: 1.4;
}
.pipeline-meta {
    margin-left: auto;
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}
.pipeline-stat {
    display: flex;
    align-items: baseline;
    gap: var(--sp-sm);
    font-size: 12px;
    color: var(--text-secondary);
}
.pipeline-stat .dot {
    width: 6px;
    height: 6px;
    border-radius: var(--radius-pill);
    background: var(--leaf-green, #395C2D);
    box-shadow: 0 0 4px var(--leaf-green, #395C2D);
    transform: translateY(-1px);
    flex-shrink: 0;
}
.pipeline-stat .dot.dim { background: var(--text-tertiary); box-shadow: none; opacity: 0.6; }
.pipeline-stat .l {
    color: var(--text-tertiary);
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.pipeline-stat .v {
    font-variant-numeric: tabular-nums;
    color: var(--text-primary);
    font-weight: 600;
    font-size: 13px;
}
.pipeline-stat .v .muted { font-weight: 400; font-size: 11px; }

/* Awaiting → live transition pulse. Triggered when the pipeline
   poller re-renders the card after Apple delivers the first
   instance(s). Reduce-motion users get the tint without the swell. */
@keyframes pipeline-just-live-swell {
    0%   { box-shadow: 0 0 0 0 rgb(var(--primary-rgb) / 0.45); transform: translateZ(0) scale(1); }
    40%  { box-shadow: 0 0 0 12px rgb(var(--primary-rgb) / 0); transform: translateZ(0) scale(1.005); }
    100% { box-shadow: 0 0 0 0 rgb(var(--primary-rgb) / 0); transform: translateZ(0) scale(1); }
}
.pipeline-just-live {
    animation: pipeline-just-live-swell 1800ms ease-out 1;
    border-color: rgb(var(--primary-rgb) / 0.5);
}
@media (prefers-reduced-motion: reduce) {
    .pipeline-just-live {
        animation: none;
        background: var(--primary-bg);
        transition: background 600ms ease-out;
    }
}

.analytics-header {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--sp-lg);
    margin-top: 6px;
    margin-bottom: 6px;
}

.analytics-title {
    margin: 0;
    font-size: 16px;
    font-weight: 600;
    color: var(--text-primary);
    letter-spacing: -0.01em;
}

.analytics-state-badge {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: var(--sp-xs) 10px;
    border-radius: var(--radius-pill);
    white-space: nowrap;
}
.analytics-state-badge.live {
    color: var(--leaf-green);
    background: var(--primary-bg-hover);
    border: 1px solid rgb(var(--primary-rgb) / 0.3);
}
.analytics-state-badge.pending {
    color: var(--text-tertiary);
    background: var(--bg-pill);
    border: 1px solid var(--border-strong, rgba(0,0,0,0.1));
}

.analytics-summary {
    margin: 0 0 var(--sp-md);
    color: var(--text-secondary);
    font-size: 13px;
    line-height: 1.5;
}
.analytics-summary strong {
    color: var(--text-primary);
    font-size: 12px;
}

/* Two-column grid for the report list — fits the 8-curated-report
   set neatly without scrolling on most viewports. Below 900px the
   grid collapses to 1-column for readability. */
.analytics-report-list {
    list-style: none;
    margin: 0 0 var(--sp-md);
    padding: 0;
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: var(--sp-2xl);
    row-gap: var(--sp-sm);
}
@media (max-width: 900px) {
    .analytics-report-list {
        grid-template-columns: 1fr;
    }
}

.analytics-report-row {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 12px;
    color: var(--text-secondary);
    min-height: 22px;
}
.analytics-report-row .dot {
    width: 6px;
    height: 6px;
    border-radius: var(--radius-pill);
    background: var(--status-positive);
    box-shadow: 0 0 5px var(--status-positive);
    flex-shrink: 0;
}
.analytics-report-row .dot.dim {
    background: var(--text-tertiary);
    box-shadow: none;
    opacity: 0.4;
}
.analytics-report-row .report-name {
    flex: 1;
    color: var(--text-primary);
    font-weight: 500;
}
.analytics-report-row .report-value {
    font-size: 11px;
    color: var(--text-secondary);
    text-align: right;
    letter-spacing: 0.02em;
}
.analytics-report-row .report-value.muted {
    color: var(--text-tertiary);
    font-style: italic;
}

.analytics-foot {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    padding-top: 10px;
    border-top: 1px solid var(--border);
}

/* ── Subscription Event Timeline ────────────────────────────────────
   Lifecycle pulse panel — daily stacked-column chart + event-mix
   table + offer redemption list. Spans 12 columns (full row) because
   the chart needs horizontal space to read the daily granularity. */
.sub-timeline-card {
    grid-column: span 3;       /* paired 1:3 with subs-card on Money tab — chart needs the horizontal room to read daily granularity, ledger fits at the narrower side */
    /* inherits .insights-card padding: var(--sp-lg) var(--sp-xl) */
    display: flex;
    flex-direction: column;
    gap: var(--sp-md);
}
.sub-tl-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: var(--sp-lg);
    flex-wrap: wrap;
}
.sub-tl-summary {
    display: flex;
    align-items: baseline;
    gap: var(--sp-lg);
    font-variant-numeric: tabular-nums;
    font-size: 13px;
}
.sub-tl-total {
    font-size: 18px;
    font-weight: 600;
    color: var(--text-primary);
}
.sub-tl-total .muted { font-size: 12px; font-weight: 400; }
.sub-tl-net {
    font-size: 18px;
    font-weight: 600;
    padding: var(--sp-xxs) 10px;
    border-radius: var(--radius-pill);
}
.sub-tl-net .muted { font-size: 11px; font-weight: 400; }
.sub-tl-net-pos  { color: var(--leaf-green, #395C2D); background: var(--primary-bg); }
.sub-tl-net-neg  { color: var(--status-critical, #C45A4A); background: var(--status-critical-bg); }
.sub-tl-net-zero { color: var(--text-tertiary); background: var(--bg-pill); }

/* Bucket legend — sits between the header summary and the stacked
   column chart so the colour-to-bucket mapping is preloaded before
   the operator scans a column. The chart's per-column <title>
   element still carries the precise daily breakdown on hover; the
   inline hint at the right end of the legend tells the operator
   that affordance exists. */
.sub-tl-legend {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--sp-md);
    padding: var(--sp-xs) 0 var(--sp-sm);
}
.sub-tl-legend-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-secondary);
}
.sub-tl-legend-chip .dot {
    width: 9px;
    height: 9px;
    border-radius: 2px;
    flex-shrink: 0;
}
.sub-tl-legend-chip.sub-tl-bucket-gain    .dot { background: var(--leaf-green); }
.sub-tl-legend-chip.sub-tl-bucket-neutral .dot { background: var(--task-watering); }
.sub-tl-legend-chip.sub-tl-bucket-loss    .dot { background: var(--status-critical); }
.sub-tl-legend-hint {
    font-size: 11px;
    letter-spacing: 0.04em;
    color: var(--text-tertiary);
    margin-left: auto;
}

.sub-tl-chart {
    width: 100%;
    height: auto;
    overflow: visible;
}
.sub-tl-chart .sub-tl-axis {
    font-size: 11px;
    letter-spacing: 0.08em;
    fill: var(--text-tertiary);
}
.sub-tl-chart .sub-tl-col:hover rect {
    filter: brightness(1.06);
}
.sub-tl-empty {
    padding: var(--sp-xl) 0;
    color: var(--text-secondary);
    font-size: 13px;
    line-height: 1.5;
}

.sub-tl-grid {
    display: grid;
    grid-template-columns: 1.4fr 1fr;
    gap: var(--sp-2xl);
    margin-top: var(--sp-xs);
}
@media (max-width: 980px) {
    .sub-tl-grid { grid-template-columns: 1fr; gap: var(--sp-xl); }
}
.sub-tl-section-eyebrow {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    margin-bottom: var(--sp-sm);
}

.sub-tl-type-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--sp-xs);
}
.sub-tl-type-row {
    display: grid;
    grid-template-columns: 12px 1fr 80px 38px 50px;
    align-items: center;
    gap: 10px;
    font-size: 12px;
    color: var(--text-secondary);
    padding: var(--sp-xxs) 0;
}
.sub-tl-type-swatch {
    width: 10px;
    height: 10px;
    border-radius: 2px;
    display: inline-block;
    background: var(--swatch-bg, transparent);
}
.sub-tl-type-label {
    color: var(--text-primary);
    font-size: 12px;
}
.sub-tl-type-bar {
    height: 6px;
    background: var(--bg-pill);
    border-radius: 3px;
    overflow: hidden;
}
.sub-tl-type-bar-fill {
    display: block;
    height: 100%;
    width: var(--bar-w, 0%);
    background: var(--bar-bg, var(--primary));
    border-radius: 3px;
    transition: width 200ms ease-out;
}
.sub-tl-type-count {
    font-variant-numeric: tabular-nums;
    text-align: right;
    color: var(--text-primary);
    font-size: 12px;
    font-weight: 600;
}
.sub-tl-type-share {
    font-variant-numeric: tabular-nums;
    text-align: right;
    color: var(--text-secondary);
    font-size: 12px;
}

.sub-tl-offer-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0;
}
.sub-tl-offer-row {
    display: grid;
    grid-template-columns: 1fr auto 50px;
    align-items: baseline;
    gap: var(--sp-md);
    padding: var(--sp-sm) 0;
    font-size: 12px;
    border-bottom: 1px solid var(--border);
}
.sub-tl-offer-row:last-child { border-bottom: 0; }
.sub-tl-offer-name {
    color: var(--text-primary);
    font-weight: 600;
}
.sub-tl-offer-meta {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}
.sub-tl-offer-count {
    font-variant-numeric: tabular-nums;
    text-align: right;
    color: var(--text-primary);
    font-weight: 600;
}
.sub-tl-no-offers {
    padding: var(--sp-md) 0;
    font-size: 13px;
    line-height: 1.5;
}

/* ── Performance Vitals strip ──────────────────────────────────────
   Mirrors .vital-card visually but driven by Apple's PERFORMANCE
   reports. Spans 12 columns to keep the strip readable; dot/label/
   value stat cells use the same proportions as Vital Signs so the
   eye reads both strips as one unit. */
.perf-card {
    grid-column: span 4;
    padding: var(--sp-md) var(--sp-xl);
    display: flex;
    flex-direction: column;
    gap: var(--sp-md);
}
.perf-row {
    display: flex;
    align-items: center;
    gap: var(--sp-2xl);
    flex-wrap: wrap;
}
.perf-eyebrow {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    padding-right: 10px;
    border-right: 1px solid var(--border);
}
.perf-stat {
    display: flex;
    align-items: baseline;
    gap: var(--sp-sm);
    font-size: 12px;
    color: var(--text-secondary);
}
.perf-stat .dot {
    width: 6px;
    height: 6px;
    border-radius: var(--radius-pill);
    background: var(--status-positive);
    box-shadow: 0 0 4px var(--status-positive);
    flex-shrink: 0;
    transform: translateY(-1px);
}
.perf-stat .dot.warn { background: var(--status-warning, #A27B1A); box-shadow: 0 0 4px var(--status-warning, #A27B1A); }
.perf-stat .dot.crit { background: var(--status-critical, #C45A4A); box-shadow: 0 0 4px var(--status-critical, #C45A4A); }
.perf-stat .dot.dim  { background: var(--text-tertiary); box-shadow: none; opacity: 0.5; }
.perf-stat .l {
    color: var(--text-tertiary);
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.perf-stat .v {
    font-variant-numeric: tabular-nums;
    color: var(--text-primary);
    font-weight: 600;
    font-size: 13px;
}

.perf-version-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 12px;
    margin-top: var(--sp-xs);
}
.perf-version-th, .perf-version-th-version {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-align: left;
    color: var(--text-tertiary);
    padding: var(--sp-sm) 10px var(--sp-xs);
    border-bottom: 1px solid var(--border);
}
.perf-version-th { text-align: right; }
.perf-version-row td { padding: 6px 10px; border-bottom: 1px solid var(--border); }
.perf-version-row:last-child td { border-bottom: 0; }
.perf-version-row td.perf-cell {
    font-variant-numeric: tabular-nums;
    text-align: right;
    color: var(--text-primary);
    white-space: nowrap;
}
.perf-version-row td.perf-cell .dot {
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: var(--radius-pill);
    margin-right: var(--sp-sm);
    background: var(--status-positive);
    box-shadow: 0 0 4px var(--status-positive);
    transform: translateY(-1px);
}
.perf-version-row td.perf-cell .dot.warn { background: var(--status-warning, #A27B1A); box-shadow: 0 0 4px var(--status-warning, #A27B1A); }
.perf-version-row td.perf-cell .dot.crit { background: var(--status-critical, #C45A4A); box-shadow: 0 0 4px var(--status-critical, #C45A4A); }
.perf-version-row td.perf-cell .dot.dim  { background: var(--text-tertiary); box-shadow: none; opacity: 0.5; }
.perf-version-tag {
    display: inline-block;
    padding: var(--sp-xxs) var(--sp-sm);
    background: var(--bg-pill);
    border-radius: var(--radius-pill);
    font-variant-numeric: tabular-nums;
    font-size: 11px;
    color: var(--text-primary);
}

/* ── App Versions card (webhook-side per-build subscription health)
   Sits below Performance Vitals on the Reliability tab. Reads
   real-time signal from subscription_event grouped by bundle_version.
   Mirrors perf-version-table styling so the operator's eye reads them
   as a coherent "build health" pair. */
.versions-card {
    grid-column: span 4;
}
.versions-empty {
    padding: var(--sp-lg) 0;
    text-align: center;
    font-size: 13px;
    color: var(--text-tertiary);
    line-height: 1.5;
}
.versions-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 12px;
    margin-top: var(--sp-xs);
}
.versions-th, .versions-th-version {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-align: right;
    color: var(--text-tertiary);
    padding: var(--sp-sm) 10px var(--sp-xs);
    border-bottom: 1px solid var(--border);
}
.versions-th-version { text-align: left; }
.versions-row td { padding: 6px 10px; border-bottom: 1px solid var(--border); }
.versions-row:last-child td { border-bottom: 0; }
.versions-row td.versions-cell {
    font-variant-numeric: tabular-nums;
    text-align: right;
    color: var(--text-primary);
    white-space: nowrap;
}
.versions-row td.versions-cell-total { color: var(--text-tertiary); }
.versions-row td.versions-cell .dot {
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: var(--radius-pill);
    margin-right: var(--sp-sm);
    background: var(--status-positive);
    box-shadow: 0 0 4px var(--status-positive);
    transform: translateY(-1px);
}
.versions-row td.versions-cell .dot.dot-warn { background: var(--status-warning, #A27B1A); box-shadow: 0 0 4px var(--status-warning, #A27B1A); }
.versions-tag {
    display: inline-block;
    padding: var(--sp-xxs) var(--sp-sm);
    background: var(--bg-pill);
    border-radius: var(--radius-pill);
    font-variant-numeric: tabular-nums;
    font-size: 11px;
    color: var(--text-primary);
}

/* ── Acquisition Funnel ────────────────────────────────────────────
   Horizontal trapezoid funnel — impressions at the top, first paid
   at the bottom. Source breakdown sits below. */
.funnel-card {
    grid-column: span 4;
    /* inherits .insights-card padding: var(--sp-lg) var(--sp-xl) */
    display: flex;
    flex-direction: column;
    gap: var(--sp-md);
}
.funnel-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: var(--sp-lg);
    flex-wrap: wrap;
}
.funnel-summary {
    display: flex;
    align-items: baseline;
    gap: 10px;
}
.funnel-summary-label {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
.funnel-summary-value {
    font-variant-numeric: tabular-nums;
    font-size: 22px;
    font-weight: 600;
    color: var(--leaf-green, #395C2D);
}
.funnel-empty {
    padding: var(--sp-xl) 0;
    color: var(--text-secondary);
    font-size: 13px;
    line-height: 1.5;
}

/* Step block + drop-off pill layout — replaced an SVG trapezoid that
   compressed Apple-typical 0.5-3% conversion bars to a 4% min-width
   hack, which made the count "inside" the bar visually overlap the
   bar geometry on every narrow step. The new layout reads top-to-
   bottom: each step has its own bar (proportional width) + count, and
   between steps a centered band-coloured pill calls out the step-
   over-step conversion. The leakiest junction lights up red without
   the operator having to compare three small numbers. */

.funnel-steps {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.funnel-step {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: var(--sp-xs) 0;
}
.funnel-step-row {
    display: grid;
    grid-template-columns: 180px 1fr auto;
    align-items: baseline;
    gap: var(--sp-md);
}
.funnel-step-label {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-secondary);
}
.funnel-step-meta {
    font-size: 11px;
    color: var(--text-tertiary);
    letter-spacing: 0.04em;
}
.funnel-step-count {
    font-variant-numeric: tabular-nums;
    font-size: 16px;
    font-weight: 700;
    color: var(--text-primary);
}
.funnel-step-bar {
    position: relative;
    height: 14px;
    background: var(--bg-pill);
    border-radius: var(--radius-sm);
    overflow: hidden;
}
.funnel-step-bar-fill {
    position: absolute;
    inset: 0 auto 0 0;
    width: var(--bar-w, 0%);
    background: linear-gradient(90deg, rgb(var(--primary-rgb) / 0.62), var(--leaf-green));
    border-radius: var(--radius-sm);
    min-width: 4px;
    transition: width 280ms ease-out;
}

.funnel-dropoff {
    align-self: center;
    display: inline-flex;
    align-items: center;
    gap: var(--sp-sm);
    padding: 5px var(--sp-md);
    border-radius: var(--radius-pill);
    margin: var(--sp-xs) 0;
    list-style: none;
}
.funnel-dropoff-arrow {
    font-size: 12px;
    opacity: 0.7;
    line-height: 1;
}
.funnel-dropoff-rate {
    font-variant-numeric: tabular-nums;
    font-size: 14px;
    font-weight: 700;
    letter-spacing: 0;
}
.funnel-dropoff-label {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
.funnel-dropoff-good { background: var(--status-positive-bg); color: var(--primary-shade); }
.funnel-dropoff-good .funnel-dropoff-arrow { color: var(--status-positive); }
.funnel-dropoff-warn { background: var(--status-warning-bg); color: var(--status-warning-text); }
.funnel-dropoff-warn .funnel-dropoff-arrow { color: var(--status-warning); }
.funnel-dropoff-crit { background: var(--status-critical-bg); color: var(--status-critical-text); }
.funnel-dropoff-crit .funnel-dropoff-arrow { color: var(--status-critical); }
.funnel-dropoff-dim  { background: var(--bg-pill);             color: var(--text-tertiary); }

@media (prefers-reduced-motion: reduce) {
    .funnel-step-bar-fill { transition: none; }
}

/* Transparency layer — header deep-link + per-step floor badge.

   The Acquisition Funnel surfaces numbers that are systematically
   lower than App Store Connect's web UI on low-traffic apps because
   Apple's Analytics Reports API drops any (Date × Source × Page ×
   Territory × Device) tuple with <5 users/devices. The web UI
   applies privacy AFTER aggregation so its totals look complete.

   We don't hide the gap: we surface the API number as our primary
   read AND link to Connect for ground-truth + tag the floored steps
   so the operator never confuses our 30 with their 518. */

/* Tab-level env-mismatch banner — surfaces above analytics tabs when
   the env toggle is set to Sandbox but the tab's underlying data
   sources are app-wide (Apple Analytics, Reviews, App Store Server).
   Calm informational tone: explains the gap, doesn't block the tile. */
.insights-env-mismatch-banner {
    grid-column: 1 / -1;
    margin: 0 0 var(--sp-md) 0;
    padding: var(--sp-md) var(--sp-lg);
    background: var(--bg-pill);
    border-left: 3px solid var(--leaf-green, #5e8a4b);
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    font-size: 12px;
    line-height: 1.55;
}
.insights-env-mismatch-banner strong {
    color: var(--text-primary);
    font-weight: 600;
}
.insights-env-mismatch-banner code {
    font-family: ui-monospace, SFMono-Regular, monospace;
    font-size: 11px;
    background: var(--bg-elevated, rgba(0,0,0,0.05));
    padding: 1px 5px;
    border-radius: 3px;
}

.funnel-header-titles {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.funnel-connect-link {
    align-self: flex-start;
    margin-top: 6px;
    font-size: 11px;
    color: var(--text-tertiary);
    text-decoration: none;
    border-bottom: 1px dashed var(--text-tertiary);
    padding-bottom: 1px;
    transition: color 150ms ease, border-color 150ms ease;
    cursor: help;
}
.funnel-connect-link:hover,
.funnel-connect-link:focus-visible {
    color: var(--leaf-green, #5e8a4b);
    border-bottom-color: var(--leaf-green, #5e8a4b);
    outline: none;
}
.funnel-step-floor {
    display: inline-flex;
    align-items: center;
    margin-left: 8px;
    padding: 1px 6px;
    background: var(--bg-pill);
    color: var(--text-tertiary);
    border-radius: var(--radius-sm);
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    cursor: help;
    line-height: 1.2;
    vertical-align: middle;
}
.funnel-step.is-floored .funnel-step-bar-fill {
    /* Floored steps are partial reads — dim the bar fill so the
       visual hierarchy says "this number is structurally lower than
       reality" without distorting the proportional comparison. */
    opacity: 0.7;
}

.funnel-source-section { margin-top: var(--sp-xs); }
.funnel-source-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--sp-xs);
}
.funnel-source-row {
    display: grid;
    grid-template-columns: 12px 1.4fr 1fr 70px 60px;
    align-items: center;
    gap: 10px;
    font-size: 12px;
    color: var(--text-secondary);
    padding: var(--sp-xs) 0;
}
.funnel-source-swatch {
    width: 10px;
    height: 10px;
    border-radius: 2px;
    background: var(--swatch-bg, transparent);
}
.funnel-source-name {
    color: var(--text-primary);
}
.funnel-source-bar {
    height: 6px;
    background: var(--bg-pill);
    border-radius: 3px;
    overflow: hidden;
}
.funnel-source-bar-fill { display: block; height: 100%; width: var(--bar-w, 0%); background: var(--bar-bg, var(--primary)); border-radius: 3px; transition: width 200ms ease-out; }
.funnel-source-count, .funnel-source-conv {
    font-variant-numeric: tabular-nums;
    text-align: right;
}
.funnel-source-count { color: var(--text-primary); font-weight: 600; }
.funnel-source-conv { color: var(--text-tertiary); font-size: 11px; }

/* ── Offer Redemption Funnel ───────────────────────────────────────
   Card grid — one tile per promotional offer. Each tile shows
   redemption count, conversion count, and conversion rate bar. */
.offer-funnel-card {
    grid-column: span 4;
    /* inherits .insights-card padding: var(--sp-lg) var(--sp-xl) */
    display: flex;
    flex-direction: column;
    gap: var(--sp-lg);
}
.offer-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: var(--sp-lg);
    flex-wrap: wrap;
}
.offer-summary {
    display: flex;
    gap: var(--sp-xl);
    align-items: baseline;
}
.offer-summary-cell {
    display: flex;
    flex-direction: column;
    gap: var(--sp-xxs);
    align-items: flex-end;
}
.offer-summary-l {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}
.offer-summary-v {
    font-size: 16px;
    font-weight: 600;
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
}
.offer-summary-v .muted { font-size: 11px; font-weight: 400; }
.offer-summary-rate { color: var(--leaf-green, #395C2D); font-size: 18px; }

.offer-empty { padding: var(--sp-xl) 0; color: var(--text-secondary); font-size: 13px; line-height: 1.5; }

/* auto-fit prevents orphan tiles — the 4th tile in a 3-col grid
   used to land alone on row 2. With auto-fit + minmax, the row
   reflows to fit whatever count we have (3 fits as 3-col, 4 fits
   as 4-col, 6 fits as 3+3, etc.). */
.offer-tile-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: var(--sp-md);
}

.offer-tile {
    border: 1px solid var(--border);
    border-radius: var(--radius-md, 8px);
    padding: var(--sp-md) var(--sp-md) 11px;
    display: flex;
    flex-direction: column;
    gap: var(--sp-sm);
    background: var(--bg-card, white);
}
.offer-tile-high { border-color: rgb(var(--primary-rgb) / 0.4); }
.offer-tile-mid  { border-color: rgb(var(--status-neutral-rgb) / 0.35); }
.offer-tile-low  { border-color: rgb(var(--status-warning-rgb) / 0.35); }
.offer-tile-name {
    font-size: 14px;
    font-weight: 600;
    color: var(--text-primary);
}
.offer-tile-meta {
    display: flex;
    gap: 10px;
    font-size: 11px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}
.offer-tile-numbers {
    display: flex;
    align-items: center;
    gap: var(--sp-md);
    margin-top: var(--sp-xs);
}
.offer-num {
    display: flex;
    flex-direction: column;
    gap: var(--sp-xxs);
}
.offer-num-v {
    font-variant-numeric: tabular-nums;
    font-size: 18px;
    font-weight: 600;
    line-height: 1;
    color: var(--text-primary);
}
.offer-num-l {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}
.offer-arrow {
    font-size: 18px;
    color: var(--text-tertiary);
}
.offer-tile-rate {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: var(--sp-xxs);
}
.offer-tile-rate-bar {
    flex: 1;
    height: 6px;
    background: var(--bg-pill);
    border-radius: 3px;
    overflow: hidden;
}
.offer-tile-rate-fill {
    display: block;
    height: 100%;
    width: var(--bar-w, 0%);
    border-radius: 3px;
    transition: width 250ms ease-out;
    background: var(--leaf-green, #395C2D);
}
.offer-tile-rate-fill.offer-tile-rate-mid  { background: var(--status-neutral, #7D7264); }
.offer-tile-rate-fill.offer-tile-rate-low  { background: var(--status-warning, #A27B1A); }
.offer-tile-rate-pct {
    font-variant-numeric: tabular-nums;
    font-size: 13px;
    font-weight: 600;
    min-width: 50px;
    text-align: right;
    color: var(--text-primary);
}

.offer-country-section { margin-top: var(--sp-xs); }
.offer-country-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0;
}
.offer-country-row {
    display: grid;
    grid-template-columns: 24px 60px 80px 16px 80px 60px;
    align-items: baseline;
    gap: var(--sp-md);
    padding: var(--sp-xs) 0;
    font-size: 12px;
    border-bottom: 1px solid var(--border);
}
.offer-country-row:last-child { border-bottom: 0; }
.offer-country-flag { font-size: 16px; }
.offer-country-code { color: var(--text-secondary); font-size: 11px; letter-spacing: 0.08em; }
.offer-country-redeem, .offer-country-convert {
    text-align: right;
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}
.offer-country-arrow { color: var(--text-tertiary); font-size: 12px; }
.offer-country-rate {
    text-align: right;
    color: var(--leaf-green, #395C2D);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

/* ── DAU/MAU Stickiness strip ──────────────────────────────────────
   Same skeleton as Vital Signs / Performance — compact stat cells
   with a trailing inline sparkline. */
/* Stickiness — full card now (was a 63 px-tall strip cramming
   eyebrow + four stat cells + a 220 px sparkline into one row).
   Layout: header eyebrow → 2×2 KPI grid → wide DAU sparkline. The
   left-edge tone bar reflects the stickiness band (>0.20 healthy,
   0.10-0.20 warn, <0.10 crit) so the engagement signal reads at a
   glance from across the page. */
.sticky-card {
    grid-column: span 4;
    padding: var(--sp-lg);
    display: flex;
    flex-direction: column;
    gap: var(--sp-md);
    position: relative;
    overflow: hidden;
}
.sticky-card::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 4px;
    background: var(--text-tertiary);
}
.sticky-card.sticky-tone-good::before { background: var(--primary); }
.sticky-card.sticky-tone-warn::before { background: var(--status-warning); }
.sticky-card.sticky-tone-crit::before { background: var(--status-critical); }
.sticky-card.sticky-tone-dim::before  { background: var(--text-tertiary); opacity: 0.4; }

.sticky-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: var(--sp-md);
}
@media (max-width: 900px) {
    .sticky-grid { grid-template-columns: repeat(2, 1fr); }
}
.sticky-stat-tile {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 10px var(--sp-md);
    background: var(--bg-card-hover);
    border-radius: var(--radius-md);
    border: 1px solid var(--border);
}
.sticky-tile-l {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}
.sticky-tile-v {
    font-variant-numeric: tabular-nums;
    font-size: 22px;
    font-weight: 700;
    color: var(--text-primary);
    line-height: 1.1;
}
.sticky-ratio-tile .sticky-tile-v { font-size: 22px; }

.sticky-tile-l .dot {
    width: 7px;
    height: 7px;
    border-radius: var(--radius-pill);
    background: var(--status-positive);
}
.sticky-tile-l .dot.warn { background: var(--status-warning); }
.sticky-tile-l .dot.crit { background: var(--status-critical); }
.sticky-tile-l .dot.dim  { background: var(--text-tertiary); opacity: 0.5; }

.sticky-trend-up   { color: var(--primary-shade); margin-left: var(--sp-xs); }
.sticky-trend-down { color: var(--status-critical-text); margin-left: var(--sp-xs); }

.sticky-spark-row {
    display: flex;
    align-items: center;
    gap: var(--sp-md);
    padding-top: var(--sp-xs);
}
.sticky-spark-label {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    flex-shrink: 0;
}
.sticky-sparkline {
    flex: 1;
    height: 60px;
    overflow: visible;
}
.sticky-spark-empty {
    flex: 1;
    font-size: 12px;
    color: var(--text-tertiary);
    text-align: center;
    padding: var(--sp-lg) 0;
    border-radius: var(--radius-sm);
    background: var(--bg-pill);
}

/* ── ASO Search Terms ──────────────────────────────────────────────
   Per-term performance table — sortable visually by impression bar
   width, with a colored conversion column (low / mid / high). */
.aso-card {
    grid-column: span 4;
    /* inherits .insights-card padding: var(--sp-lg) var(--sp-xl) */
    display: flex;
    flex-direction: column;
    gap: var(--sp-md);
}
.aso-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: var(--sp-lg);
    flex-wrap: wrap;
}
.aso-summary {
    display: flex;
    gap: var(--sp-xl);
    align-items: baseline;
}
.aso-summary-cell {
    display: flex;
    flex-direction: column;
    gap: var(--sp-xxs);
    align-items: flex-end;
}
.aso-summary-l {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}
.aso-summary-v {
    font-size: 16px;
    font-weight: 600;
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
}
.aso-summary-v .muted { font-size: 11px; font-weight: 400; }
.aso-summary-rate { color: var(--leaf-green, #395C2D); font-size: 18px; }
.aso-empty { padding: var(--sp-xl) 0; color: var(--text-secondary); font-size: 13px; line-height: 1.5; }

.aso-list-header,
.aso-row {
    display: grid;
    grid-template-columns: 1.6fr 90px 1fr 80px 70px 80px;
    align-items: center;
    gap: var(--sp-md);
    padding: var(--sp-xs) var(--sp-xs);
}
.aso-list-header {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    border-bottom: 1px solid var(--border);
    padding-bottom: var(--sp-sm);
}
.aso-list-header .aso-num-h { text-align: right; }
.aso-list { list-style: none; margin: 0; padding: 0; }
.aso-row {
    border-bottom: 1px solid var(--border);
    font-size: 12px;
}
.aso-row:last-child { border-bottom: 0; }
.aso-term { color: var(--text-primary); font-weight: 600; }
.aso-type {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    padding: 3px var(--sp-sm);
    border-radius: var(--radius-pill);
    width: fit-content;
}
/* Term-type badges — three should read as visually distinct at a
   glance. Branded uses the strong leaf-green (we own it), Generic
   uses task-watering blue (a separate hue lane in the palette),
   Competitor stays in the warm warning amber. The previous taupe-on-
   taupe Generic blended into Competitor amber under brief reading. */
.aso-type-branded    { color: var(--leaf-green, #395C2D); background: var(--primary-bg-hover); }
.aso-type-generic    { color: var(--task-watering-text, #2F557B); background: var(--task-watering-bg); }
.aso-type-competitor { color: var(--status-warning-text, #8C6B14); background: var(--status-warning-bg); }
.aso-type-unknown    { color: var(--text-tertiary); background: var(--bg-pill); }
.aso-bar {
    height: 8px;
    background: var(--bg-pill);
    border-radius: 4px;
    overflow: hidden;
}
.aso-bar-fill {
    display: block;
    height: 100%;
    width: var(--bar-w, 0%);
    background: var(--leaf-green, #395C2D);
    border-radius: 4px;
    transition: width 250ms ease-out;
}
.aso-impressions, .aso-installs, .aso-conv {
    font-variant-numeric: tabular-nums;
    text-align: right;
}
.aso-impressions { color: var(--text-primary); font-weight: 600; }
.aso-installs    { color: var(--text-secondary); }
/* Conversion column color follows the asoConvDotClass() rules:
   high (≥5%)   = leaf-green (strong)
   default (≥2%) = text-primary (healthy, no signal needed)
   mid  (≥1%)   = status-warning (soft)
   low  (<1%)   = status-critical (genuinely poor) */
.aso-conv                  { font-weight: 600; color: var(--text-primary); }
.aso-conv.aso-conv-high    { color: var(--leaf-green, #395C2D); }
.aso-conv.aso-conv-mid     { color: var(--status-warning, #A27B1A); }
.aso-conv.aso-conv-low     { color: var(--status-critical, #C45A4A); }

/* ── ASO row drill-down ─────────────────────────────────────────────
   Each top-N term carries a `byCountry[]` payload from the backend.
   On row click, the inline country list slides open below the parent
   row. Country rows mirror the parent column shape so the eye stays
   on the same vertical rhythm. Caret rotates 90° when open. */
.aso-row-clickable {
    cursor: pointer;
    transition: background var(--transition-fast);
}
.aso-row-clickable:hover { background: var(--bg-card-hover); }
.aso-row-clickable:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
    background: var(--primary-bg);
}
.aso-drill-caret {
    display: inline-block;
    margin-left: 6px;
    color: var(--text-tertiary);
    font-size: 14px;
    line-height: 1;
    transition: transform var(--transition-fast);
}
.aso-row-expanded .aso-drill-caret { transform: rotate(90deg); color: var(--primary); }

.aso-country-list {
    grid-column: 1 / -1;
    list-style: none;
    margin: 0;
    padding: 0;
    max-height: 0;
    overflow: hidden;
    transition: max-height 240ms ease, padding 240ms ease;
}
.aso-row-expanded .aso-country-list {
    max-height: 600px;       /* enough for ~6 country rows; clipped if more */
    padding: var(--sp-sm) 0 var(--sp-xs) var(--sp-2xl);
    border-top: 1px dashed var(--border);
    margin-top: var(--sp-sm);
}
@media (prefers-reduced-motion: reduce) {
    .aso-country-list { transition: none; }
}

/* Country row inside the drill — reuse aso column shape so columns
   align with the parent row above. */
.aso-country-row {
    display: grid;
    grid-template-columns: 1.6fr 90px 1fr 80px 70px 80px;
    align-items: center;
    gap: var(--sp-md);
    padding: 4px var(--sp-xs);
    font-size: 12px;
    color: var(--text-secondary);
}
.aso-country-row:hover { background: var(--bg-card-hover); border-radius: var(--radius-sm); }
.aso-country-row .aso-country-flag {
    font-size: 14px;
    line-height: 1;
}
.aso-country-row .aso-country-cc {
    font-size: 11px;
    letter-spacing: 0.06em;
    color: var(--text-tertiary);
    font-weight: 600;
}

/* Data-source disclosure footer on tiles that read ASC Analytics
   API (D-2 + privacy-floored). Operator sees at a glance why the
   number doesn't match Connect web UI. Quiet visual treatment so
   it doesn't compete with the metric — small, muted, italic, with
   a left accent border to signal "metadata note". */
.tile-source-note {
    margin-top: 12px;
    padding: 8px 12px;
    border-left: 2px solid var(--border-strong);
    background: var(--surface-2);
    border-radius: 4px;
    font-size: 11px;
    line-height: 1.55;
    font-style: italic;
    color: var(--text-tertiary);
}

.tile-source-note strong {
    color: var(--text-secondary);
    font-weight: 600;
    font-style: normal;
}

.tile-source-note a {
    color: var(--text-secondary);
    text-decoration: underline;
    text-decoration-style: dotted;
}

.tile-source-note a:hover {
    color: var(--primary);
}

/* ── Install / Delete Flow card ──────────────────────────────────── */

.install-delete-card {
    grid-column: span 4;
    display: flex;
    flex-direction: column;
    gap: var(--sp-md);
}
.install-delete-headline {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: var(--sp-lg);
    padding: var(--sp-md) 0;
    border-bottom: 1px solid var(--border-subtle, rgba(0,0,0,0.08));
}
.install-delete-headline .idd-stat {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.install-delete-headline .idd-stat .l {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-secondary);
}
.install-delete-headline .idd-stat .v {
    font-variant-numeric: tabular-nums;
    font-size: 22px;
    font-weight: 700;
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.install-delete-headline .idd-stat .m {
    font-size: 11px;
    color: var(--text-tertiary);
}
.install-delete-headline .idd-v-install { color: var(--leaf-green, #395C2D); }
.install-delete-headline .idd-v-delete  { color: var(--status-caution, #D27832); }

.install-delete-daily {
    display: grid;
    grid-template-columns: repeat(14, minmax(0, 1fr));
    gap: 4px;
    padding: var(--sp-md) 0;
    align-items: end;
    min-height: 80px;
}
.install-delete-day {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    position: relative;
    height: 70px;
    justify-content: flex-end;
}
.install-delete-day .idd-bar {
    display: block;
    width: 100%;
    border-radius: 2px 2px 0 0;
}
.install-delete-day .idd-bar-install {
    background: var(--leaf-green, #395C2D);
    height: var(--bar-h, 2px);
}
.install-delete-day .idd-bar-delete {
    background: var(--status-caution, #D27832);
    opacity: 0.8;
    height: var(--bar-h, 2px);
    border-radius: 0 0 2px 2px;
    margin-top: 1px;
}
.install-delete-day .idd-day-label {
    font-size: 9px;
    color: var(--text-tertiary);
    letter-spacing: 0.02em;
    margin-top: 4px;
}

.install-delete-tables {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--sp-lg);
}
.idd-table-wrap {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.idd-table-eyebrow {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-secondary);
}
.idd-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
}
.idd-table thead th {
    text-align: left;
    font-weight: 600;
    color: var(--text-secondary);
    padding: 4px 8px;
    border-bottom: 1px solid var(--border-subtle, rgba(0,0,0,0.08));
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.idd-table tbody td {
    padding: 6px 8px;
    border-bottom: 1px solid var(--border-faint, rgba(0,0,0,0.04));
    font-variant-numeric: tabular-nums;
}
.idd-table tbody tr:last-child td {
    border-bottom: none;
}
.idd-source-name, .idd-territory-name { color: var(--text-primary); }
.idd-cell-install { color: var(--leaf-green, #395C2D); font-weight: 600; }
.idd-cell-delete  { color: var(--status-caution, #D27832); font-weight: 600; }
.idd-cell-net     { color: var(--text-primary); font-weight: 600; }

/* ── CoreML Model Health card ────────────────────────────────────── */

.coreml-card {
    grid-column: span 4;
    display: flex;
    flex-direction: column;
    gap: var(--sp-md);
}
.coreml-headline {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--sp-lg);
    padding: var(--sp-md) 0;
    border-bottom: 1px solid var(--border-subtle, rgba(0,0,0,0.08));
}
.coreml-headline .coreml-stat-big {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.coreml-headline .coreml-stat-big .l {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-secondary);
}
.coreml-headline .coreml-stat-big .v {
    font-variant-numeric: tabular-nums;
    font-size: 24px;
    font-weight: 700;
    color: var(--text-primary);
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

.coreml-models {
    display: flex;
    flex-direction: column;
    gap: var(--sp-md);
}
.coreml-model-row {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: var(--sp-sm) 0;
    border-bottom: 1px solid var(--border-faint, rgba(0,0,0,0.04));
}
.coreml-model-row:last-child { border-bottom: none; }

.coreml-model-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--sp-sm);
    flex-wrap: wrap;
}
.coreml-model-name {
    font-weight: 600;
    font-size: 14px;
    color: var(--text-primary);
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.coreml-model-meta {
    font-size: 11px;
    color: var(--text-tertiary);
}

.coreml-model-stats {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: var(--sp-md);
}
.coreml-stat {
    display: flex;
    flex-direction: column;
    gap: 1px;
}
.coreml-stat .l {
    font-size: 10px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}
.coreml-stat .v {
    font-variant-numeric: tabular-nums;
    font-size: 14px;
    font-weight: 600;
    color: var(--text-primary);
}
.coreml-stat .coreml-v-fail { color: var(--status-critical, #C45A4A); }

.coreml-err-list {
    list-style: none;
    margin: 4px 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 3px;
}
.coreml-err-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--sp-sm);
    font-size: 12px;
    padding: 4px 8px;
    background: var(--bg-pill, rgba(0,0,0,0.03));
    border-radius: var(--radius-sm);
    border-left: 2px solid var(--status-caution, #D27832);
}
.coreml-err-name {
    color: var(--text-primary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 60%;
}
.coreml-err-count {
    font-variant-numeric: tabular-nums;
    color: var(--text-tertiary);
    font-size: 11px;
    flex-shrink: 0;
}

.coreml-build-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
}
.coreml-build-table thead th {
    text-align: left;
    font-weight: 600;
    color: var(--text-secondary);
    padding: 4px 8px;
    border-bottom: 1px solid var(--border-subtle, rgba(0,0,0,0.08));
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.coreml-build-table tbody td {
    padding: 6px 8px;
    border-bottom: 1px solid var(--border-faint, rgba(0,0,0,0.04));
    font-variant-numeric: tabular-nums;
}
.coreml-build-table tbody tr:last-child td { border-bottom: none; }
.coreml-build-name { font-weight: 600; }
.coreml-build-model { color: var(--text-secondary); }
.coreml-cell { color: var(--text-primary); }

/* ── Neural Engine Footprint card ────────────────────────────────── */

.neural-card {
    grid-column: span 4;
    display: flex;
    flex-direction: column;
    gap: var(--sp-md);
}
.neural-headline {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--sp-lg);
    padding: var(--sp-md) 0;
    border-bottom: 1px solid var(--border-subtle, rgba(0,0,0,0.08));
}
.neural-headline .neural-stat {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.neural-headline .neural-stat .l {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-secondary);
}
.neural-headline .neural-stat .v {
    font-variant-numeric: tabular-nums;
    font-size: 24px;
    font-weight: 700;
    color: var(--text-primary);
}

.neural-distributions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--sp-lg);
}
.neural-dist-block {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.neural-dist-eyebrow {
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-secondary);
    cursor: help;
}
.neural-bucket-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.neural-bucket-row {
    display: grid;
    grid-template-columns: 100px 1fr auto;
    align-items: center;
    gap: var(--sp-sm);
    font-size: 12px;
}
.neural-bucket-row.is-danger .neural-bucket-fill {
    background: var(--status-critical, #C45A4A);
}
.neural-bucket-row.is-danger .neural-bucket-label {
    color: var(--status-critical, #C45A4A);
    font-weight: 600;
}
.neural-bucket-label {
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
    font-size: 11px;
}
.neural-bucket-bar {
    display: block;
    height: 8px;
    background: var(--bg-pill, rgba(0,0,0,0.04));
    border-radius: 4px;
    overflow: hidden;
}
.neural-bucket-fill {
    display: block;
    height: 100%;
    width: var(--bar-w, 0%);
    background: var(--task-watering, #427096);
    border-radius: 4px;
    transition: width 200ms ease-out;
}
.neural-bucket-count {
    font-variant-numeric: tabular-nums;
    color: var(--text-tertiary);
    font-size: 11px;
}

.neural-build-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
}
.neural-build-table thead th {
    text-align: left;
    font-weight: 600;
    color: var(--text-secondary);
    padding: 4px 8px;
    border-bottom: 1px solid var(--border-subtle, rgba(0,0,0,0.08));
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.neural-build-table tbody td {
    padding: 6px 8px;
    border-bottom: 1px solid var(--border-faint, rgba(0,0,0,0.04));
    font-variant-numeric: tabular-nums;
}
.neural-build-table tbody tr:last-child td { border-bottom: none; }
.neural-build-name { font-weight: 600; }
.neural-build-version { font-size: 11px; }
.neural-cell { color: var(--text-primary); }


/* === users.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Settings page — admin-only. Two tabs: Users + Audit log.
   Uses the same warm cream + sage palette as the rest of the surface.
   ═══════════════════════════════════════════════════════════════════════ */

.users-page { max-width: var(--page-max-width); }

.settings-meta {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  margin-bottom: var(--sp-lg);
  flex-wrap: wrap;
}
.settings-meta .badge { font-size: var(--font-size-caption2); }
.settings-meta .text-tertiary { font-size: var(--font-size-caption); }

/* ── Users table ─────────────────────────────────────────────────── */
.users-table-wrap {
  background: var(--bg-card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  overflow: hidden;
}
.users-table { width: 100%; border-collapse: collapse; }
.users-table th,
.users-table td {
  padding: var(--sp-md) var(--sp-lg);
  text-align: left;
  border-bottom: 1px solid var(--border);
  font-size: var(--font-size-footnote);
}
.users-table thead th {
  background: var(--bg-input);
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: var(--font-size-caption2);
}
.users-table tbody tr:last-child td { border-bottom: none; }
.users-table tbody tr:hover { background: var(--bg-card-hover); }
.users-table .username-cell {
  /* Outer cell only — typography lives on .user-cell__name so the
     email line below can keep its own muted weight. */
  vertical-align: top;
}

/* Status uses the shared .badge family (badge-success / badge-neutral /
   badge-danger) — no role-local color rules anymore. The previous
   .status-active / .status-disabled / .status-locked spans were
   inconsistent with every other pill on the surface. */

.users-table td.actions {
  text-align: right;
  white-space: nowrap;
  /* Soft separator so the action group reads as a distinct region from
     the data columns. Keeps inline buttons but stops them from blending
     into the timestamp on the row's right edge. */
  border-left: 1px solid var(--border);
  padding-left: var(--sp-md);
}
.users-table .actions .btn { margin-left: var(--sp-xs); }

/* Destructive ghost variant — Disable button. Resting state keeps the
   neutral ghost look so a row of three actions reads evenly; hover
   tints critical so the destructive intent only surfaces on intent.
   Mirrors the iOS pattern of color-on-hover for destructive list rows. */
.btn-ghost.btn-ghost--danger:hover {
  background: var(--status-critical-bg);
  color: var(--status-critical-text);
}

/* Self-row gets a soft sage tint so the operator can never lose track
   of which row is them in a long list. */
.users-table tbody tr.is-self { background: rgba(73, 139, 82, 0.04); }
.users-table tbody tr.is-self:hover { background: rgba(73, 139, 82, 0.08); }

/* ── User cell stack (username + email line) ──────────────────────── */
.user-cell {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.user-cell__name {
  font-weight: 600;
  color: var(--text-primary);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-xs);
}
.user-cell__email {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  font-size: var(--font-size-caption);
  color: var(--text-tertiary);
  min-height: 1.5em;
}
.user-cell__email .user-email {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 28ch;
}
.user-cell__email .user-email--empty {
  color: var(--text-tertiary);
}

/* Pencil edit affordance — hidden at rest, revealed on row hover or
   keyboard focus. Keeps the email line calm in a long list and only
   surfaces the edit handle when the operator's intent is on that row. */
.user-email-edit {
  background: transparent;
  border: none;
  padding: 2px 6px;
  border-radius: var(--radius-sm, 4px);
  color: var(--text-tertiary);
  font-size: var(--font-size-caption);
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: opacity 120ms ease, background 120ms ease, color 120ms ease;
}
.users-table tr:hover .user-email-edit,
.users-table tr:focus-within .user-email-edit,
.user-email-edit:focus-visible {
  opacity: 1;
}
.user-email-edit:hover {
  background: var(--primary-bg);
  color: var(--primary);
}

/* ── Last activity stack (timestamp + IP) ─────────────────────────── */
.last-activity {
  display: flex;
  flex-direction: column;
  gap: 2px;
  line-height: 1.3;
}
.last-activity__when {
  color: var(--text-secondary);
  white-space: nowrap;
}
.last-activity__ip {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
}

/* Create-user + reset-password modals render via the shared
   .modal-overlay + .modal-card.modal-card--sm component (see
   components/modal.css). */

/* One-time password reveal — soft warning tone so the user does NOT
   miss "this is the only time you'll see it". */
.temp-password-box {
  margin-top: var(--sp-md);
  padding: var(--sp-lg);
  background: var(--bg-input);
  border: 1px dashed var(--primary);
  border-radius: var(--radius-md);
}
/* Temp-password label uses the shared .label-caps (tokens.css).
   Override only the trailing margin since this label sits above a
   monospace value block, not a regular text field. */
.temp-password-box .label-caps { margin-bottom: var(--sp-xs); }
.temp-password-box .value {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--font-size-subheadline);
  font-weight: 600;
  color: var(--text-primary);
  word-break: break-all;
  user-select: all;
}
.temp-password-box .hint {
  margin-top: var(--sp-md);
  font-size: var(--font-size-caption);
  color: var(--text-secondary);
  line-height: 1.5;
}
.temp-password-actions {
  display: flex;
  gap: var(--sp-sm);
  margin-top: var(--sp-lg);
  justify-content: flex-end;
}

/* ── Audit log table ─────────────────────────────────────────── */
.audit-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-sm);
  margin-bottom: var(--sp-md);
  align-items: center;
}
/* Audit toolbar + inline role picker carry their size via the
   .field-md utility on the markup; border / radius / bg / color come
   from the global tokens.css baseline. */

.audit-table-wrap {
  background: var(--bg-card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  overflow: hidden;
}
.audit-table { width: 100%; border-collapse: collapse; font-size: var(--font-size-caption); }
.audit-table th,
.audit-table td {
  padding: var(--sp-sm) var(--sp-md);
  text-align: left;
  border-bottom: 1px solid var(--border);
}
.audit-table thead th {
  background: var(--bg-input);
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: var(--font-size-caption2);
}
.audit-table tbody tr:hover { background: var(--bg-card-hover); }
.audit-table .ts-cell {
  white-space: nowrap;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text-tertiary);
}
.audit-table .action-cell { font-weight: 600; }
.audit-table .action-cell.action-fail { color: var(--status-critical-text); }
.audit-table .payload-cell {
  max-width: 320px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.audit-pagination {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--sp-md) var(--sp-lg);
  font-size: var(--font-size-footnote);
  color: var(--text-tertiary);
  background: var(--bg-input);
}
.audit-pagination .pager { display: flex; gap: var(--sp-xs); }


/* === pending.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Pending Changes — unified drafts feed.
   ═══════════════════════════════════════════════════════════════════════ */

.pending-page { max-width: var(--page-max-width); }

.pending-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-sm);
  align-items: center;
  margin-bottom: var(--sp-md);
}
/* Toolbar dropdown sizing comes from the .field-md utility on the
   markup; border / radius / bg / color are inherited from the global
   tokens.css input/select baseline. The previous rule reset all of
   those locally AND matched checkbox <input>s in the toggle labels,
   accidentally inflating them — narrowing the styling to the markup
   class fixes both issues at once. */
.pending-toolbar .toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  font-size: var(--font-size-footnote);
  color: var(--text-secondary);
  cursor: pointer;
}

.pending-counts {
  display: flex;
  gap: var(--sp-sm);
  flex-wrap: wrap;
  margin-bottom: var(--sp-md);
}
/* Pending feed counters render via the shared .pill-count component
   (see components/pill.css). Layout is owned by the toolbar parent. */

/* Subgrid pattern: the LIST owns the column tracks, each ROW inherits
   them via `grid-template-columns: subgrid`. Without this, every row
   ran its own independent grid — the `auto` actions column then
   sized to its OWN content width per row, so a row with "Mark applied"
   (cultivar/disease/tip/faq) ended up with a wider actions track and
   a correspondingly narrower author track than the species row next
   to it. The visible bug was "ibrahim (you)" sliding around between
   rows. Subgrid pins all six tracks list-wide so columns line up
   regardless of which buttons each row renders. */
.pending-list {
  display: grid;
  grid-template-columns: 110px 1fr 180px 140px minmax(0, 1fr) max-content;
  gap: var(--sp-sm);
}

.pending-row {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
  column-gap: var(--sp-md);
  align-items: center;
  padding: var(--sp-md) var(--sp-lg);
  background: var(--bg-card);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  font-size: var(--font-size-footnote);
}
.pending-row.applied {
  opacity: 0.55;
  background: var(--bg-input);
}

/* Record type pill renders via the shared .badge-type component
   (see components/badge.css) — categorical tints (species/cultivar/
   disease/tip/faq) live there so the pending feed and the search
   results agree on hue. */

.pending-id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text-primary); font-weight: 600; }

/* Every two-line cell in the row uses the same flex-column + gap so
   the line baselines align horizontally across cells. The title cell,
   the author/timestamp cell, and the fields-count cell all share this
   geometry — without it the title cell uses CSS gap while the author
   cell relied on default block flow with line-height padding, so the
   first line in one cell sat ~3 px lower than the first line in the
   neighbour. */
.pending-stack {
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  line-height: 1.3;
}
/* Title cell keeps its own class so we can target the title + id
   typography independent of the row layout (italic species title,
   monospace id sub). */
.pending-title-cell {
  min-width: 0;
}
.pending-title {
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Species titles ARE scientific names — italic matches the convention
   used everywhere else in the admin (species-card, search results,
   editor row). The italic also gives the title a typographic shift
   from the monospace id line below, so even when the title and id
   are the same string the two lines don't read as duplicate text. */
.pending-title--species { font-style: italic; }
.pending-id-sub {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pending-meta { color: var(--text-tertiary); font-size: var(--font-size-caption2); }
.pending-fields {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pending-actions { display: flex; flex-wrap: wrap; gap: var(--sp-xs); justify-content: flex-end; }

.pending-loading {
  /* Span every track so the loading text doesn't get cropped to
     the first column when the list owns the grid. */
  grid-column: 1 / -1;
  text-align: center;
  padding: var(--sp-xl);
  color: var(--text-tertiary);
  font-size: var(--font-size-footnote);
}

.pending-empty {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-md);
  padding: var(--sp-3xl) var(--sp-xl);
  /* Floor at 280px so the empty state isn't a pancake when only one
     filter row is rendered above it; the inbox icon needs vertical
     room or the page reads as "loading" rather than "empty". */
  min-height: 280px;
  text-align: center;
  background: var(--bg-card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  color: var(--text-tertiary);
}

.pending-empty-icon {
  color: var(--primary);
  opacity: 0.45;
  margin-bottom: var(--sp-xs);
}

.pending-empty-title {
  font-size: var(--font-size-callout);
  font-weight: 600;
  color: var(--text-primary);
}

.pending-empty-sub {
  font-size: var(--font-size-footnote);
  color: var(--text-tertiary);
  max-width: 360px;
  line-height: 1.45;
}

/* Diff modal — same lineage as settings-modal-* but simpler. */
.diff-modal-pre {
  background: var(--bg-input);
  border-radius: var(--radius-md);
  padding: var(--sp-md);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--font-size-caption);
  color: var(--text-primary);
  max-height: 50vh;
  overflow: auto;
  white-space: pre-wrap;
}

@media (max-width: 900px) {
  /* Narrow viewports collapse to a single column. Both the list AND
     the row need their grid-template overridden — leaving the list
     in 6-track subgrid mode while the row tries `1fr` would make
     subgrid inherit 6 tracks anyway. */
  .pending-list {
    grid-template-columns: 1fr;
  }
  .pending-row {
    grid-template-columns: 1fr;
    gap: var(--sp-xs);
  }
  .pending-actions { justify-content: flex-start; }
}


/* === notifications.css === */
/* ═══════════════════════════════════════════════════════════════════════
   Notifications page (admin/owner) + Profile page (every user) — shared
   stylesheet because both surface notification controls and the visual
   language is identical.
   ═══════════════════════════════════════════════════════════════════════ */

.notifications-page,
.profile-page {
  max-width: var(--page-max-width);
}

.section-subtitle {
  margin: var(--sp-xs) 0 0 0;
  font-size: var(--font-size-footnote);
  color: var(--text-secondary);
  line-height: 1.5;
  max-width: 64ch;
}

/* Read-only banner shown to admins on the Notifications page (only
   owners can mutate). */
.notice-banner {
  background: var(--primary-bg);
  border: 1px solid var(--primary-border);
  border-radius: var(--radius-md);
  padding: var(--sp-md) var(--sp-lg);
  margin-bottom: var(--sp-lg);
  font-size: var(--font-size-footnote);
  color: var(--text-primary);
  line-height: 1.5;
}
.notice-banner a { color: var(--primary-shade); }

/* ── Event card (Notifications page) ─────────────────────────── */
.notif-card {
  background: var(--bg-card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: var(--sp-lg) var(--sp-xl);
  margin-bottom: var(--sp-lg);
}

.notif-card-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-lg);
  margin-bottom: var(--sp-lg);
}
.notif-card-title {
  margin: 0 0 var(--sp-xs) 0;
  font-size: var(--font-size-headline);
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.01em;
}
.notif-card-desc {
  margin: 0;
  font-size: var(--font-size-footnote);
  color: var(--text-secondary);
  line-height: 1.5;
  max-width: 56ch;
}
.notif-card-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--sp-xs);
  white-space: nowrap;
}
.notif-preview-count {
  font-size: var(--font-size-caption);
  color: var(--primary-shade);
  font-weight: 600;
  letter-spacing: 0.02em;
}
.notif-preview-empty {
  color: var(--text-tertiary);
  font-weight: 500;
}
.notif-self-muted {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  font-style: italic;
}

/* Card sections (Roles + Specific users). */
.notif-card-section {
  margin-top: var(--sp-md);
  padding-top: var(--sp-md);
  border-top: 1px solid var(--border);
}
.notif-section-label {
  margin: 0 0 var(--sp-sm) 0;
  font-size: var(--font-size-caption2);
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* ── Role checkboxes ─────────────────────────────────────────── */
.notif-role-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-md);
}
.notif-role-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-sm);
  padding: var(--sp-sm) var(--sp-md);
  background: var(--bg-input);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-md);
  cursor: pointer;
  user-select: none;
  font-size: var(--font-size-footnote);
  color: var(--text-primary);
  transition: background 0.12s, border-color 0.12s;
}
.notif-role-toggle:hover { background: var(--bg-card-hover); }
.notif-role-toggle input[type="checkbox"] {
  accent-color: var(--primary);
  margin: 0;
}
.notif-role-toggle input[type="checkbox"]:disabled { opacity: 0.5; cursor: not-allowed; }
.notif-role-toggle:has(input:checked) {
  background: var(--primary-bg);
  border-color: var(--primary-border);
  color: var(--primary-shade);
  font-weight: 600;
}
.notif-role-label { text-transform: capitalize; }

/* ── Specific user list ──────────────────────────────────────── */
.notif-user-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-sm);
  align-items: center;
  min-height: 28px;
}
.notif-user-empty {
  font-size: var(--font-size-footnote);
  color: var(--text-tertiary);
  font-style: italic;
}
.notif-user-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-xs) var(--sp-sm);
  background: var(--bg-pill);
  border-radius: var(--radius-pill);
  font-size: var(--font-size-caption);
  color: var(--text-primary);
  font-weight: 500;
}
.notif-chip-meta {
  font-size: var(--font-size-caption2);
  color: var(--text-tertiary);
  text-transform: lowercase;
}
.notif-chip-remove {
  background: transparent;
  border: none;
  color: var(--text-tertiary);
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  padding: 0 var(--sp-xxs);
  margin-left: var(--sp-xxs);
}
.notif-chip-remove:hover { color: var(--text-primary); }

.notif-user-picker {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  margin-top: var(--sp-sm);
  padding: var(--sp-xs) var(--sp-sm);
  background: var(--bg-input);
  border-radius: var(--radius-md);
  border: 1px dashed var(--border-emphasis);
  flex-basis: 100%;
}

.notif-card-actions {
  margin-top: var(--sp-lg);
  padding-top: var(--sp-md);
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: flex-end;
}

/* ── Profile page ────────────────────────────────────────────── */
.profile-card {
  background: var(--bg-card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: var(--sp-lg) var(--sp-xl);
  margin-bottom: var(--sp-lg);
}
.profile-card-title {
  margin: 0 0 var(--sp-md) 0;
  font-size: var(--font-size-headline);
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.01em;
}
.profile-card-desc {
  margin: 0 0 var(--sp-lg) 0;
  font-size: var(--font-size-footnote);
  color: var(--text-secondary);
  line-height: 1.5;
  max-width: 64ch;
}

.profile-meta {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--sp-sm) var(--sp-lg);
  margin: 0;
  font-size: var(--font-size-footnote);
}
.profile-meta dt {
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: var(--font-size-caption2);
  font-weight: 600;
  padding-top: 2px;     /* baseline-align with the dd value */
}
.profile-meta dd {
  margin: 0;
  color: var(--text-primary);
}
.profile-meta-note {
  margin: var(--sp-md) 0 0 0;
  font-size: var(--font-size-caption);
  color: var(--text-tertiary);
  font-style: italic;
  line-height: 1.55;
}

.profile-mutes {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
}
.profile-mute-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-lg);
  padding: var(--sp-md) var(--sp-lg);
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  cursor: pointer;
}
.profile-mute-row:hover { background: var(--bg-card-hover); }
.profile-mute-text {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xxs);
  min-width: 0;
}
.profile-mute-title {
  font-size: var(--font-size-footnote);
  font-weight: 600;
  color: var(--text-primary);
}
.profile-mute-desc {
  font-size: var(--font-size-caption);
  color: var(--text-secondary);
  line-height: 1.4;
}
.profile-mute-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-sm);
  flex-shrink: 0;
}
.profile-mute-toggle input[type="checkbox"] {
  accent-color: var(--primary);
  width: 18px;
  height: 18px;
  margin: 0;
}
.profile-mute-toggle-label {
  font-size: var(--font-size-caption);
  color: var(--text-tertiary);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.profile-mute-row:has(input:checked) {
  background: var(--primary-bg);
  border-color: var(--primary-border);
}
.profile-mute-row:has(input:checked) .profile-mute-toggle-label {
  color: var(--primary-shade);
}
