/* ═══════════════════════════════════════════════════════════════════
   VEXTALEARN — SHARED COMPONENT FOUNDATION (Phase 1 + Phase 2A)
   ═══════════════════════════════════════════════════════════════════
   Purpose: the CSS file shared by ALL FOUR layout entry points
   (base.html, admin/_layout.html, instructor/_layout.html,
   student/_layout.html). Establishes the `.vxt-*` component
   vocabulary.

   PHASE 2A — BUTTONS ARE NOW CONSOLIDATED HERE:
   `.vxt-btn` in this file is the SINGLE SOURCE OF TRUTH for buttons.
   The duplicate `.stu-btn-*` (student.css), `.instr-btn-*`
   (instructor.css) and `.pg-btn-*` (social.css) systems were deleted
   and their templates migrated onto `.vxt-btn` + a geometry variant.
   Add new button work here only — never re-introduce a portal-local
   button class.

   Still deliberately OUT of scope (documented, not migrated):
   - Bootstrap `.btn-*` in the admin portal — a thin theming layer over
     vendor Bootstrap; migrating it risks regressions across internal
     tools for no user-facing gain.
   - `.vl-btn-*`, scoped entirely inside student/learn.html's `.vl`
     lesson-player theme (its own dark/light token set).

   WHY THIS FILE IS SELF-CONTAINED
   main.css / global-experience.css / pwa.css / mobile-ui.css are only
   loaded together on base.html-derived pages (public site, auth).
   The admin, instructor, and student portal layouts are separate
   standalone HTML documents that do NOT load global-experience.css or
   mobile-ui.css, and admin.css maintains its own independent
   `--adm-*` token namespace rather than consuming main.css's tokens
   (see admin.css's own header comment). So this file:
     1. Never assumes global-experience.css/mobile-ui.css are present
        — every primitive it needs (spinner, skeleton, focus-visible,
        reduced-motion) is defined locally below, not inherited.
     2. Never assumes main.css's tokens (--primary, --card-bg, etc.)
        resolve — see the fallback layer immediately below, which
        reads main.css's tokens where available and falls back to
        admin.css's --adm-* equivalents (or a hard-coded value as a
        last resort) so ONE file works correctly in all four layouts.

   LOAD ORDER (established in base.html / admin/_layout.html /
   instructor/_layout.html / student/_layout.html):
     main.css (or admin.css's own tokens) → components.css →
     global-experience.css → pwa.css → mobile-ui.css → portal CSS
   ═══════════════════════════════════════════════════════════════════ */

/* ─── 0. Cross-portal token fallback layer ───────────────────────────
   Additive custom properties only — nothing here reassigns --primary,
   --adm-accent, or any other existing token, so every current value
   (light mode, dark mode, per-portal) is completely unaffected.
   Components below consume ONLY the --vxt-c-* names on the left. */
:root {
  --vxt-c-primary:      var(--primary, var(--adm-accent, #6D28D9));
  --vxt-c-primary-dark: var(--primary-dark, var(--adm-accent-hover, #5B21B6));
  --vxt-c-primary-tint: var(--primary-light, var(--adm-accent-glow, #F5F3FF));
  --vxt-c-surface:      var(--card-bg, var(--adm-surface, #FFFFFF));
  --vxt-c-surface-2:    var(--surface-2, var(--adm-surface-2, #F8F7FC));
  --vxt-c-surface-3:    var(--surface-3, var(--adm-surface-3, #F5F3FF));
  --vxt-c-border:       var(--border, var(--adm-border, #E9E4F0));
  --vxt-c-text:         var(--text-primary, var(--adm-text, #17121F));
  --vxt-c-text-2:       var(--text-secondary, var(--adm-text-2, #5F5668));
  --vxt-c-text-muted:   var(--text-muted, var(--adm-text-muted, #776D82));
  --vxt-c-success:      var(--success, var(--adm-success, #16A34A));
  --vxt-c-warning:      var(--warning, var(--adm-warning, #F59E0B));
  --vxt-c-danger:       var(--danger, var(--adm-danger, #DC2626));
  --vxt-c-info:         var(--adm-info, #38BDF8);
  --vxt-c-radius-sm:    var(--radius-sm, var(--adm-radius-sm, 10px));
  --vxt-c-radius-md:    var(--radius-md, var(--adm-radius-md, 14px));
  --vxt-c-radius:       var(--radius, var(--adm-radius, 16px));
  --vxt-c-radius-lg:    var(--radius-lg, 18px);
  --vxt-c-radius-pill:  var(--radius-pill, var(--adm-radius-pill, 999px));
  --vxt-c-shadow-sm:    var(--shadow-sm, var(--adm-shadow-sm, 0 1px 2px rgba(15,23,42,0.05)));
  --vxt-c-shadow:       var(--shadow, var(--adm-shadow, 0 1px 3px rgba(15,23,42,0.06), 0 6px 20px rgba(15,23,42,0.08)));
  --vxt-c-shadow-lg:    var(--shadow-lg, 0 2px 6px rgba(15,23,42,0.06), 0 16px 40px rgba(15,23,42,0.12));
  --vxt-c-font-body:    var(--font-body, 'Inter', -apple-system, sans-serif);
  --vxt-c-font-display: var(--font-display, 'Inter', -apple-system, sans-serif);
  --vxt-c-transition:   var(--transition, var(--adm-transition, all 0.2s cubic-bezier(0.4,0,0.2,1)));
}

/* ─── 1. Typography foundation ────────────────────────────────────────
   New utility classes only — no existing heading/paragraph selector
   is touched, so current typography anywhere is unaffected. */
.text-display {
  font-family: var(--vxt-c-font-display);
  font-weight: 700;
  font-size: clamp(2.5rem, 4vw, 3rem);   /* 40px → 48px */
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--vxt-c-text);
}
.text-h1 {
  font-family: var(--vxt-c-font-display);
  font-weight: 700;
  font-size: 2rem;      /* 32px */
  line-height: 1.25;
  letter-spacing: -0.02em;
  color: var(--vxt-c-text);
}
.text-h2 {
  font-family: var(--vxt-c-font-display);
  font-weight: 600;
  font-size: 1.5rem;    /* 24px */
  line-height: 1.3;
  color: var(--vxt-c-text);
}
.text-h3 {
  font-family: var(--vxt-c-font-display);
  font-weight: 600;
  font-size: 1.25rem;   /* 20px */
  line-height: 1.35;
  color: var(--vxt-c-text);
}
.text-h4 {
  font-family: var(--vxt-c-font-display);
  font-weight: 600;
  font-size: 1rem;      /* 16px */
  line-height: 1.4;
  color: var(--vxt-c-text);
}
.text-body {
  font-family: var(--vxt-c-font-body);
  font-weight: 400;
  font-size: 0.9375rem; /* 15px */
  line-height: 1.6;
  color: var(--vxt-c-text);
}
.text-body-secondary {
  font-family: var(--vxt-c-font-body);
  font-weight: 400;
  font-size: 0.875rem;  /* 14px */
  line-height: 1.55;
  color: var(--vxt-c-text-2);
}
.text-label {
  font-family: var(--vxt-c-font-body);
  font-weight: 500;
  font-size: 0.8125rem; /* 13px */
  line-height: 1.4;
  color: var(--vxt-c-text);
}
.text-caption {
  font-family: var(--vxt-c-font-body);
  font-weight: 400;
  font-size: 0.75rem;   /* 12px */
  line-height: 1.5;
  color: var(--vxt-c-text-muted);
}
.text-meta {
  font-family: var(--vxt-c-font-body);
  font-weight: 400;
  font-size: 0.75rem;   /* 12px */
  line-height: 1.5;
  color: var(--vxt-c-text-muted);
  letter-spacing: 0.01em;
}

/* ─── 2. Buttons — CANONICAL SYSTEM (Phase 2A) ─────────────────────────
   This file is now the SINGLE SOURCE OF TRUTH for VextaLearn buttons.
   Phase 2A consolidated four parallel systems into it:

     main.css .vxt-btn-primary / -outline / -light   → moved here
     global-experience.css .vxt-btn-ghost            → moved here
     student.css .stu-btn-*                          → migrated here
     instructor.css .instr-btn-*                     → migrated here

   Architecture — three orthogonal layers, combined in markup:

     1. base       .vxt-btn            layout + behaviour only
     2. geometry   .vxt-btn-portal | .vxt-btn-compact | -sm/-md/-lg
     3. colour     .vxt-btn-primary | -outline | -outline-soft | -ghost
                   | -light | -ghost-light | -white | -link | -secondary
                   | -danger | -danger-soft   (+ .vxt-btn-icon)

   Geometry modifiers intentionally use a doubled-class selector
   (`.vxt-btn.vxt-btn-portal`) so they beat the colour variants'
   own default padding/radius regardless of source order. Colour
   variants keep their default geometry so pre-existing markup that
   uses a colour variant alone (main site pages) is unchanged.

   Admin (Bootstrap `.btn-*`) is deliberately NOT migrated in this
   phase — see the notes at the end of this file.
   ------------------------------------------------------------------ */
.vxt-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-family: var(--vxt-c-font-body);
  line-height: 1.2;
  text-decoration: none;
  white-space: nowrap;
  user-select: none;
  cursor: pointer;
  transition: var(--vxt-c-transition);
}

/* ── 2a. Geometry ───────────────────────────────────────────────────
   `-portal` reproduces the exact former `.stu-btn` metrics and
   `-compact` the exact former `.instr-btn` metrics, so migrated
   student/instructor templates render pixel-identically. They are
   retained as genuine size variants (dashboard/table density), not
   as separate button systems. */
.vxt-btn.vxt-btn-portal {
  border-radius: var(--radius, 12px);
  font-weight: 600;
  font-size: 0.9rem;
  padding: 0.65rem 1.4rem;
  border: 1px solid transparent;
}
.vxt-btn.vxt-btn-compact {
  border-radius: var(--vxt-c-radius-md);
  font-weight: 600;
  font-size: 0.85rem;
  padding: 0.6rem 1.25rem;
  border: 1px solid transparent;
}

/* Sizes — modifiers, combine with any variant/geometry. */
.vxt-btn.vxt-btn-sm { padding: 6px 16px; font-size: 0.8125rem; }
.vxt-btn.vxt-btn-md { padding: 10px 24px; font-size: 0.9375rem; }
.vxt-btn.vxt-btn-lg { padding: 14px 32px; font-size: 1rem; }
/* Exact former .stu-btn-sm / .instr-btn-sm metrics preserved. */
.vxt-btn.vxt-btn-portal.vxt-btn-sm { padding: 0.4rem 0.9rem; font-size: 0.8rem; }
.vxt-btn.vxt-btn-compact.vxt-btn-sm { padding: 0.35rem 0.8rem; font-size: 0.78rem; }

/* Icon-only control — square, no label. Needs an accessible name
   (aria-label / title) on the element itself. */
.vxt-btn-icon {
  padding: 0;
  width: 36px;
  height: 36px;
  border-radius: var(--vxt-c-radius-sm);
  flex: 0 0 auto;
}
.vxt-btn.vxt-btn-icon.vxt-btn-sm { width: 30px; height: 30px; padding: 0; }

/* ── 2b. Colour variants ────────────────────────────────────────────
   Moved verbatim from main.css (primary / outline / light) and
   global-experience.css (ghost) — same declarations, same values, so
   the main site is visually unchanged; they are now available to the
   student, instructor and admin portals too, which is what makes the
   portal-specific duplicates removable. */
.vxt-btn-primary {
  background: var(--gradient-brand, linear-gradient(135deg, #6D28D9 0%, #5B21B6 100%));
  background-size: 160% 160%;
  background-position: 0% 50%;
  color: #fff;
  border: none;
  border-radius: var(--radius, 12px);
  font-weight: 600;
  padding: 10px 24px;
  transition: var(--vxt-c-transition), background-position 0.4s ease;
  box-shadow: 0 4px 15px rgba(109,40,217,0.35);
}
.vxt-btn-primary:hover {
  transform: translateY(-2px);
  background-position: 100% 50%;
  box-shadow: 0 8px 25px rgba(109,40,217,0.4);
  color: #fff;
}

.vxt-btn-outline {
  background: transparent;
  color: var(--vxt-c-primary);
  border: 2px solid var(--vxt-c-primary);
  border-radius: var(--radius, 12px);
  font-weight: 600;
  padding: 10px 24px;
  transition: var(--vxt-c-transition);
}
.vxt-btn-outline:hover {
  background: var(--vxt-c-primary);
  color: #fff;
  transform: translateY(-2px);
}

/* Soft outline — the former `.stu-btn-outline`: a surface-filled,
   hairline-bordered low-emphasis action used inside dashboard cards,
   a different hierarchy position than the 2px branded `-outline`. */
.vxt-btn-outline-soft {
  background: var(--vxt-c-surface);
  color: var(--vxt-c-primary);
  border: 1px solid var(--vxt-c-border, var(--border, #E9E4F0));
  border-radius: var(--radius, 12px);
  font-weight: 600;
  padding: 10px 24px;
}
.vxt-btn-outline-soft:hover {
  border-color: var(--vxt-c-primary);
  background: var(--vxt-c-primary-tint);
  color: var(--vxt-c-primary);
}

.vxt-btn-ghost {
  background: transparent;
  border: none;
  color: var(--vxt-c-text-2, var(--text-secondary));
  border-radius: var(--radius, 12px);
  font-weight: 600;
  padding: 10px 20px;
  transition: var(--vxt-c-transition);
}
.vxt-btn-ghost:hover { background: var(--surface, var(--vxt-c-surface-2)); color: var(--vxt-c-primary); }

/* Solid white hero pill (main site hero/CTA bands). Distinct from
   `-white` below: it carries its own larger hero geometry and weight. */
.vxt-btn-light {
  background: #fff; /* intentional: exists to read on dark/gradient sections, not to follow the surface token */
  color: var(--primary-dark, #5B21B6);
  border: none;
  border-radius: var(--radius, 12px);
  font-weight: 700;
  padding: 12px 28px;
  transition: var(--vxt-c-transition);
  box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}
.vxt-btn-light:hover { transform: translateY(-2px); box-shadow: 0 12px 32px rgba(0,0,0,0.2); color: var(--primary-dark, #5B21B6); }

/* Translucent action on a dark/gradient surface (former
   `.instr-btn-light`) — 14+ real "back to X" instances inside hero
   headers. Named -ghost-light because `.vxt-btn-light` above already
   means the solid white hero pill. */
.vxt-btn-ghost-light {
  background: rgba(255,255,255,0.14);
  color: #fff;
  border: 1px solid rgba(255,255,255,0.4);
  border-radius: var(--vxt-c-radius);
  font-weight: 600;
  padding: 10px 24px;
}
.vxt-btn-ghost-light:hover { background: rgba(255,255,255,0.22); color: #fff; }

/* Solid white action on a dark/gradient surface (former
   `.instr-btn-white`) — pairs with `-ghost-light` in hero headers and
   inherits its geometry from the geometry layer. */
.vxt-btn-white {
  background: #fff; /* intentional: reads on the dark hero gradient, not a mode-following surface */
  color: var(--vxt-c-primary);
  box-shadow: 0 8px 20px rgba(0,0,0,0.18);
}
.vxt-btn-white:hover { transform: translateY(-2px); color: var(--vxt-c-primary); }

/* Text-only action (former `.instr-link-btn`) — used for "Clear"
   filter resets next to a submit button. No hover fill, matching the
   legacy treatment exactly. */
.vxt-btn-link {
  background: transparent;
  color: var(--vxt-c-primary);
  border-color: transparent;
}

/* Solid Secondary CTA (success/positive actions). `--secondary`
   (#10B981) is bright enough that white text fails WCAG AA (~2.5:1),
   so — per design-system rule, never darken the brand token itself —
   this pairs the unchanged brand fill with `--dark` text instead:
   `--dark` stays a fixed near-black navy in both light and dark site
   themes (unlike `--text-primary`, which flips to a light color under
   `html[data-theme="dark"]` and would go low-contrast on this fill),
   giving 7:1+ contrast at rest. Hover/active move to the deliberately
   separate `--secondary-dark` token — reserved for exactly this kind
   of strong CTA state — where white text clears AA again (5.5:1),
   matching `.vxt-btn-primary`'s solid-fill-white-text language for
   its emphasized states. */
.vxt-btn-secondary {
  background: var(--vxt-c-success);
  color: var(--dark);
  border: none;
  border-radius: var(--vxt-c-radius);
  font-weight: 600;
  padding: 10px 24px;
}
.vxt-btn-secondary:hover {
  background: var(--secondary-dark, #047857);
  color: #fff;
  transform: translateY(-2px);
}
.vxt-btn-secondary:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--secondary-a35, rgba(16,185,129,0.35)); }

.vxt-btn-danger {
  background: transparent;
  color: var(--vxt-c-danger);
  border: 2px solid var(--vxt-c-danger);
  border-radius: var(--vxt-c-radius);
  font-weight: 600;
  padding: 10px 24px;
}
.vxt-btn-danger:hover { background: var(--vxt-c-danger); color: #fff; }

/* Soft destructive (former `.stu-btn-danger-soft` /
   `.instr-btn-danger-soft`): tint fill → solid on hover, for
   low-emphasis destructive actions in table rows and modals. */
.vxt-btn-danger-soft {
  background: color-mix(in srgb, var(--vxt-c-danger) 12%, transparent);
  color: var(--vxt-c-danger);
  border: none;
  border-radius: var(--vxt-c-radius);
  font-weight: 600;
  padding: 10px 24px;
}
.vxt-btn-danger-soft:hover { background: var(--vxt-c-danger); color: #fff; }

/* Dark-mode tint, reproducing student.css's deliberate translucent
   red (its `--stu-danger-light` dark override) rather than the solid
   pink light-mode value. */
html[data-theme="dark"] .vxt-btn-danger-soft {
  background: color-mix(in srgb, var(--vxt-c-danger) 16%, transparent);
}

/* ── 2c. States ─────────────────────────────────────────────────────
   One canonical treatment for every variant: the existing VextaLearn
   press-scale and 3px brand focus ring (moved from main.css /
   global-experience.css — no new focus philosophy introduced). */
.vxt-btn-primary,
.vxt-btn-outline,
.vxt-btn-light {
  letter-spacing: 0.01em;
  transition: transform 0.22s cubic-bezier(0.4,0,0.2,1), box-shadow 0.22s ease, background 0.3s ease, color 0.22s ease, background-position 0.4s ease;
}

.vxt-btn:active:not(:disabled):not([aria-disabled="true"]) { transform: translateY(0) scale(0.97); }
.vxt-btn-primary:active,
.vxt-btn-light:active,
.vxt-btn-white:active { transform: translateY(0) scale(0.97); }
.vxt-btn-outline:active { transform: translateY(0) scale(0.97); background: var(--primary-dark, #5B21B6); border-color: var(--primary-dark, #5B21B6); }

.vxt-btn:focus-visible,
.vxt-btn-primary:focus-visible,
.vxt-btn-outline:focus-visible,
.vxt-btn-light:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--primary-a35, rgba(109,40,217,0.35));
}

.vxt-btn:disabled,
.vxt-btn[aria-disabled="true"],
.vxt-btn-primary:disabled,
.vxt-btn-outline:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  pointer-events: none;
  transform: none;
}

/* Loading state — pairs with a nested .vxt-btn-spinner rather than
   hiding label text, so existing markup patterns that swap an icon
   for a spinner on click keep working unchanged. */
.vxt-btn--loading { position: relative; color: transparent !important; pointer-events: none; }
.vxt-btn--loading .vxt-btn-spinner {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}


/* ─── 3. Cards — CANONICAL CARD FOUNDATION (Phase 2B) ─────────────────
   `.vxt-card` in THIS file is the single source of truth for the card
   surface. The duplicate definition that used to live in
   global-experience.css was removed in Phase 2B (that file is only
   loaded on base.html pages, so portals silently missed the base and
   it also won the cascade over this file — two problems at once).

   ARCHITECTURE: shared foundation → semantic variant → page composition

     .vxt-card                     surface: background, border, radius,
                                   shadow, transition (NO layout)
       .vxt-card--interactive      hover lift + focus-visible ring for
                                   clickable / linked cards
       .vxt-card--flat             no shadow (nested / inside a panel)
       .vxt-card--bordered         no shadow, stronger border
       .vxt-card--elevated         stronger resting shadow
       .vxt-card--list             edge-to-edge list container
       .vxt-card--stat / --kpi     metric-tile padding + stack

   Product/portal card types COMPOSE with it in markup and keep their
   own semantic class for layout and identity:
     class="vxt-card course-card"      course catalog / profile
     class="vxt-card stu-card"         student portal panels
     class="vxt-card instr-kpi-card"   instructor KPI tiles
     class="vxt-card admin-card"       admin panels
   Those semantic classes stay in main.css / student.css /
   instructor.css / admin.css, which all load AFTER this file (except
   main.css — see the `.course-card.vxt-card` compound selector there),
   so each type's own surface/geometry still wins where it genuinely
   differs. Do NOT move layout (grid/flex/padding of a specific card
   type) into `.vxt-card`. */
.vxt-card {
  background: var(--vxt-c-surface);
  border: 1px solid var(--vxt-c-border);
  border-radius: var(--vxt-c-radius-lg);
  box-shadow: var(--vxt-c-shadow-sm);
  transition: transform 0.3s cubic-bezier(0.16,1,0.3,1), box-shadow 0.3s ease, border-color 0.3s ease;
}
/* Interactive: opt-in, so a static panel never lifts. Used for cards
   that are (or wrap) a link/button. Keeps the <a> semantics — this is
   styling only. */
.vxt-card--interactive { cursor: pointer; }
.vxt-card--interactive:hover {
  transform: translateY(-4px);
  box-shadow: 0 16px 36px -10px color-mix(in srgb, var(--vxt-c-primary) 20%, transparent);
  border-color: color-mix(in srgb, var(--vxt-c-primary) 16%, transparent);
}
.vxt-card--interactive:focus-visible,
.vxt-card--interactive a:focus-visible {
  outline: 2px solid var(--vxt-c-primary);
  outline-offset: 2px;
}
.vxt-card--flat     { box-shadow: none; }
.vxt-card--bordered { box-shadow: none; border-color: var(--vxt-c-border); }
.vxt-card--elevated { box-shadow: var(--vxt-c-shadow); }
.vxt-card--stat {
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.vxt-card--kpi {
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.vxt-card--list {
  padding: 0;
  overflow: hidden;
}
.vxt-card--list > * + * { border-top: 1px solid var(--vxt-c-border); }
@media (prefers-reduced-motion: reduce) {
  .vxt-card { transition: none; }
  .vxt-card--interactive:hover { transform: none; }
}
@media (max-width: 575.98px) {
  .vxt-card--interactive:hover { transform: translateY(-2px); }
}

/* ─── 9b. KPI icon tile — color modifiers ─────────────────────────────
   Phase 2 migration target. Combined with the existing structural
   class already on each icon element (`instr-kpi-icon`, `stat-icon`,
   `stu-stat-icon`, `admin-stat-icon` — left completely untouched,
   still providing size/radius/layout), these modifiers replace the
   inline `style="background:...; color:...;"` pattern the audit
   flagged. `.vxt-kpi-icon` itself is also provided as a standalone
   structural class for future full-card migrations that adopt the
   `.vxt-card--kpi` / `.vxt-kpi-icon` markup shape end to end.

   Two intentional visual harmonizations happened during migration
   (documented in CHANGES-phase2-component-consolidation.md):
   - Admin's brand and semantic alpha values remain scoped to admin surfaces.
     translucent-overlay treatment now renders as the same soft tint
     used everywhere else (`--primary-light` / `--*-success-light` /
     `--*-warning-light`), rather than a separate visual language.
   - The few `#FFF7E8` / `#EAF6FD` hardcoded backgrounds (near-
     duplicates of the light tokens, off by a couple of hex values —
     copy-paste drift) now resolve to the real token. */
.vxt-kpi-icon {
  width: 40px;
  height: 40px;
  border-radius: var(--vxt-c-radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1rem;
  flex-shrink: 0;
}
.vxt-kpi-icon--primary { background: var(--vxt-c-primary-tint); color: var(--vxt-c-primary); }
.vxt-kpi-icon--success { background: color-mix(in srgb, var(--vxt-c-success) 12%, transparent); color: var(--vxt-c-success); }
.vxt-kpi-icon--warning {
  /* Darker text than --vxt-c-warning itself (matches the codebase's
     own existing convention in .instr-badge-warning) for legible
     contrast on a light amber tint. */
  background: color-mix(in srgb, var(--vxt-c-warning) 12%, transparent);
  color: #B45309;
}
.vxt-kpi-icon--danger  { background: color-mix(in srgb, var(--vxt-c-danger) 12%, transparent); color: var(--vxt-c-danger); }
.vxt-kpi-icon--info    { background: color-mix(in srgb, var(--vxt-c-info) 14%, transparent); color: var(--vxt-c-info); }
.vxt-kpi-icon--neutral { background: var(--vxt-c-surface-2); color: var(--vxt-c-text-muted); }

/* ─── 3b. Card ↔ KPI icon composition helpers ─────────────────────────
   Referenced by the audit's example markup (.vxt-card.vxt-card--kpi
   wrapping .vxt-kpi-icon + .vxt-kpi-content); not yet consumed by any
   template (that's a full-card migration, later phase) but declared
   now so the two pieces are ready to compose together. */
.vxt-kpi-content { display: flex; flex-direction: column; gap: 2px; }
.vxt-kpi-label { font-size: 0.8125rem; color: var(--vxt-c-text-muted); }
.vxt-kpi-value { font-family: var(--vxt-c-font-display); font-size: 1.375rem; font-weight: 700; color: var(--vxt-c-text); line-height: 1.15; }
.vxt-kpi-meta { font-size: 0.75rem; color: var(--vxt-c-text-muted); }

/* ─── 4. Badges ────────────────────────────────────────────────────────
   Fully new — no existing `.vxt-badge*` class anywhere in the codebase
   (confirmed by inspection), so this cannot conflict with the
   portal-specific badge systems (badge-role-*, fin-badge, pg-badge,
   instr-badge) which are left exactly as-is for now. */
.vxt-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--vxt-c-font-body);
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1.4;
  padding: 3px 10px;
  border-radius: var(--vxt-c-radius-pill);
  white-space: nowrap;
}
.vxt-badge--success { background: color-mix(in srgb, var(--vxt-c-success) 14%, transparent); color: var(--vxt-c-success); }
.vxt-badge--warning { background: color-mix(in srgb, var(--vxt-c-warning) 16%, transparent); color: var(--vxt-c-warning); }
.vxt-badge--danger  { background: color-mix(in srgb, var(--vxt-c-danger) 14%, transparent); color: var(--vxt-c-danger); }
.vxt-badge--info    { background: color-mix(in srgb, var(--vxt-c-info) 16%, transparent); color: var(--vxt-c-info); }
.vxt-badge--neutral { background: var(--vxt-c-surface-2); color: var(--vxt-c-text-2); }

/* ─── 5. Tabs ──────────────────────────────────────────────────────────
   Fully new — no existing `.vxt-tab*` class in the codebase. Does not
   replace Bootstrap's `.nav-tabs`, which several portal pages already
   use; this is an alternative primitive for future/custom tab bars. */
.vxt-tabs {
  display: flex;
  gap: 4px;
  border-bottom: 1px solid var(--vxt-c-border);
}
.vxt-tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 16px;
  font-family: var(--vxt-c-font-body);
  font-weight: 500;
  font-size: 0.875rem;
  color: var(--vxt-c-text-2);
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  cursor: pointer;
  transition: var(--vxt-c-transition);
}
.vxt-tab:hover { color: var(--vxt-c-text); }
.vxt-tab--active {
  color: var(--vxt-c-primary);
  border-bottom-color: var(--vxt-c-primary);
}
.vxt-tab:focus-visible {
  outline: 2px solid var(--vxt-c-primary);
  outline-offset: -2px;
  border-radius: var(--vxt-c-radius-sm);
}

/* ─── 6. Forms ─────────────────────────────────────────────────────────
   New, separate from Bootstrap's `.form-control`/`.form-select`
   (already refined in global-experience.css for base.html pages —
   left untouched here). These are for any future custom form markup
   that doesn't route through Bootstrap's classes. */
.vxt-form-group { margin-bottom: 16px; }
.vxt-label {
  display: block;
  margin-bottom: 6px;
  font-family: var(--vxt-c-font-body);
  font-weight: 500;
  font-size: 0.8125rem;
  color: var(--vxt-c-text);
}
.vxt-input,
.vxt-select,
.vxt-textarea {
  width: 100%;
  font-family: var(--vxt-c-font-body);
  font-size: 0.9375rem;
  color: var(--vxt-c-text);
  background: var(--vxt-c-surface);
  border: 1px solid var(--vxt-c-border);
  border-radius: var(--vxt-c-radius);
  padding: 10px 14px;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.vxt-textarea { resize: vertical; min-height: 96px; }
.vxt-input:focus,
.vxt-select:focus,
.vxt-textarea:focus {
  outline: none;
  border-color: var(--vxt-c-primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--vxt-c-primary) 15%, transparent);
}
.vxt-input:disabled,
.vxt-select:disabled,
.vxt-textarea:disabled {
  background: var(--vxt-c-surface-2);
  color: var(--vxt-c-text-muted);
  cursor: not-allowed;
}
.vxt-input--error,
.vxt-select--error,
.vxt-textarea--error { border-color: var(--vxt-c-danger); }
.vxt-input--error:focus,
.vxt-select--error:focus,
.vxt-textarea--error:focus { box-shadow: 0 0 0 3px color-mix(in srgb, var(--vxt-c-danger) 15%, transparent); }
.vxt-input--success,
.vxt-select--success,
.vxt-textarea--success { border-color: var(--vxt-c-success); }
.vxt-form-hint { margin-top: 6px; font-size: 0.75rem; color: var(--vxt-c-text-muted); }
.vxt-form-hint--error { color: var(--vxt-c-danger); }
.vxt-form-hint--success { color: var(--vxt-c-success); }

/* ─── 6b. Table foundation ─────────────────────────────────────────────
   Phase 2 establishes this as a shared pattern only — no existing
   admin/instructor/student table markup is migrated onto it yet
   (that's a template-by-template job for a later phase, once each
   table's column structure and any JS row-selection logic has been
   individually checked). Available now for any NEW table markup. */
.vxt-table-wrapper {
  overflow-x: auto;
  border: 1px solid var(--vxt-c-border);
  border-radius: var(--vxt-c-radius);
  background: var(--vxt-c-surface);
}
.vxt-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--vxt-c-font-body);
  font-size: 0.875rem;
}
.vxt-table thead th {
  text-align: left;
  padding: 12px 16px;
  font-weight: 600;
  font-size: 0.75rem;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--vxt-c-text-muted);
  background: var(--vxt-c-surface-2);
  border-bottom: 1px solid var(--vxt-c-border);
  white-space: nowrap;
}
.vxt-table tbody td {
  padding: 12px 16px;
  color: var(--vxt-c-text);
  border-bottom: 1px solid var(--vxt-c-border);
  vertical-align: middle;
}
.vxt-table tbody tr:last-child td { border-bottom: none; }
.vxt-table tbody tr:hover { background: var(--vxt-c-surface-2); }
.vxt-table tbody tr.is-selected { background: color-mix(in srgb, var(--vxt-c-primary) 8%, transparent); }
.vxt-table td.vxt-table-actions { text-align: right; white-space: nowrap; }
.vxt-table-empty,
.vxt-table-loading { padding: 40px 16px; text-align: center; }

/* ─── 7. Loading / Empty / Error primitives ──────────────────────────── */

/* Self-contained spinner — intentionally NOT reusing global-experience
   .vxt-spinner's exact class name to avoid any dependency on that file
   being loaded (it isn't, on admin/instructor/student portals). Same
   visual idea, independent implementation. */
.vxt-loading {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--vxt-c-text-2);
  font-family: var(--vxt-c-font-body);
  font-size: 0.875rem;
}
.vxt-loading::before {
  content: '';
  width: 18px;
  height: 18px;
  border: 2.5px solid color-mix(in srgb, var(--vxt-c-primary) 20%, transparent);
  border-top-color: var(--vxt-c-primary);
  border-radius: 50%;
  animation: vxtComponentSpin 0.7s linear infinite;
  flex-shrink: 0;
}
@keyframes vxtComponentSpin { to { transform: rotate(360deg); } }

/* Skeleton — matches mobile-ui.css's existing .vxt-skeleton values
   exactly (same background/shimmer/timing), so on base.html-derived
   pages the two definitions produce an identical result regardless of
   cascade order. On admin/instructor/student portals, where
   mobile-ui.css isn't loaded, this is the only definition and is what
   makes `.vxt-skeleton` usable there for the first time. Background
   uses the --vxt-c-surface-3 token (falls back through --surface-3 /
   --adm-surface-3, both already dark-mode-aware) instead of a
   hardcoded color, so it stays in sync with mobile-ui.css's version
   in both themes rather than becoming a second, conflicting source of
   truth for the same color. */
.vxt-skeleton {
  position: relative;
  overflow: hidden;
  background: var(--vxt-c-surface-3);
  border-radius: var(--vxt-c-radius-md);
}
.vxt-skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.6), transparent);
  animation: vxtComponentShimmer 1.4s infinite;
}
@keyframes vxtComponentShimmer { 100% { transform: translateX(100%); } }

.vxt-empty {
  text-align: center;
  padding: 48px 24px;
  color: var(--vxt-c-text-2);
}
.vxt-empty i,
.vxt-empty svg {
  font-size: 1.75rem;
  color: var(--vxt-c-text-muted);
  margin-bottom: 12px;
  opacity: 0.7;
}
.vxt-empty h4 {
  font-family: var(--vxt-c-font-display);
  font-size: 1.0625rem;
  color: var(--vxt-c-text);
  margin-bottom: 4px;
}
.vxt-empty p { max-width: 380px; margin: 0 auto; font-size: 0.875rem; }

.vxt-error {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 14px 16px;
  border-radius: var(--vxt-c-radius);
  background: color-mix(in srgb, var(--vxt-c-danger) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--vxt-c-danger) 25%, transparent);
  color: var(--vxt-c-danger);
  font-size: 0.875rem;
}
.vxt-error i { margin-top: 2px; flex-shrink: 0; }

/* ─── 8. Focus-visible foundation ─────────────────────────────────────
   Scoped to this file's own new component classes only. A broad
   `a, button, input, select, textarea, [tabindex]` rule already exists
   in global-experience.css for base.html-derived pages; duplicating
   that globally here would be redundant on those pages and premature
   on admin/instructor/student pages (native form controls there are
   still on Bootstrap's own focus styling until a later phase touches
   them). This block only guarantees the NEW custom components above
   always have a visible focus ring, everywhere they're used. */
.vxt-tabs .vxt-tab:focus-visible,
.vxt-btn:focus-visible,
.vxt-input:focus-visible,
.vxt-select:focus-visible,
.vxt-textarea:focus-visible {
  outline: 2px solid var(--vxt-c-primary);
  outline-offset: 2px;
}

/* ─── 9. Reduced motion ───────────────────────────────────────────────
   Covers only the animations this file introduces. Existing motion in
   global-experience.css / mobile-ui.css / splash.css / social.css /
   student.css already has its own reduced-motion handling and is
   unaffected by this block. */
@media (prefers-reduced-motion: reduce) {
  .vxt-loading::before,
  .vxt-skeleton::after {
    animation-duration: 1.4s;
  }
  .vxt-btn,
  .vxt-tab,
  .vxt-input,
  .vxt-select,
  .vxt-textarea {
    transition: none;
  }
}

/* ─── 10. Migration record (documentation only) ───────────────────────
   Phase 2A COMPLETED the button half of this mapping. The legacy
   selectors below no longer exist; the templates that used them now
   carry the canonical classes:

     instr-btn                 →  vxt-btn + vxt-btn-compact   [done]
     instr-btn-primary-solid   →  vxt-btn-primary             [done]
     instr-btn-light           →  vxt-btn-ghost-light         [done]
     instr-btn-white           →  vxt-btn-white               [done]
     instr-btn-danger-soft     →  vxt-btn-danger-soft         [done]
     instr-link-btn            →  vxt-btn-link                [done]
     instr-btn-sm              →  vxt-btn-sm                  [done]
     stu-btn                   →  vxt-btn + vxt-btn-portal    [done]
     stu-btn-primary           →  vxt-btn-primary             [done]
     stu-btn-outline           →  vxt-btn-outline-soft        [done]
     stu-btn-ghost             →  vxt-btn-ghost               [done]
     stu-btn-danger-soft       →  vxt-btn-danger-soft         [done]
     stu-btn-sm                →  vxt-btn-sm                  [done]
     pg-btn                    →  vxt-btn + vxt-btn-portal    [done]
     pg-btn-primary/outline/ghost/sm  →  canonical equivalents [done]

   Note the two `-light` variants were NOT collapsed: the instructor
   `-light` was a translucent-on-brand-background button, so it maps to
   `vxt-btn-ghost-light`, while `vxt-btn-light` stays the solid neutral
   variant used on the public site. Merging them (as the Phase 1 note
   guessed) would have broken instructor hero headers.

   Still pending for later phases: `cb-tab-btn → vxt-tab`, and the
   non-goals listed in the file header (admin Bootstrap, `.vl-btn-*`). */

/* ══════════════════════════════════════════════════════════════════════
   PREMIUM LAUNCH COUNTDOWN (.vxt-premium-countdown)
   Shared by main/premium.html, student dashboard, premium course
   library, and subscription management — see
   app/templates/premium/_countdown_widget.html.

   BUGFIX: this block used to live in admin.css, scoped so it "never
   leaks into the admin panel" - but NONE of the four pages above load
   admin.css, so the horizontal flex layout below never actually
   applied anywhere it was meant to. Every one of those pages was
   rendering the four <div class="premium-countdown-unit"> as plain
   block-level elements (default browser stacking = vertical) instead
   of the intended horizontal row. Moved here, to the one stylesheet
   components.css that's genuinely shared by every page that includes
   the widget (base.html and student/_layout.html both load it), so
   it now actually reaches all four. admin.css keeps its own separate
   #premiumLaunchCard-scoped copy for the admin panel, untouched.
   ══════════════════════════════════════════════════════════════════════ */
.vxt-premium-countdown .premium-countdown-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 16px 12px;
  border-radius: 12px;
  background: rgba(99,102,241,0.08);
  border: 1px solid rgba(99,102,241,0.25);
}
html[data-theme="dark"] .vxt-premium-countdown .premium-countdown-row {
  background: rgba(99,102,241,0.16);
  border-color: rgba(129,140,248,0.4);
}
.vxt-premium-countdown .premium-countdown-unit {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 58px;
}
.vxt-premium-countdown .premium-countdown-unit span {
  display: inline-block;
  font-size: 1.6rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  /* BUGFIX/feature: numbers used to just snap to the new value every
     second with no transition at all. premium-countdown.js now toggles
     .is-tick on the <span> for one animation-duration each time its
     text actually changes (skipped on every-second no-op re-renders
     that don't change the displayed value isn't possible here since
     seconds always change, but the class is still keyed off a real
     value change, not a blind per-tick toggle, so a slower unit like
     Days only animates on the second it actually changes). */
  transform-origin: 50% 100%;
}
.vxt-premium-countdown .premium-countdown-unit span.is-tick {
  animation: vxtCountdownTick 0.4s var(--transition-base, ease);
}
@keyframes vxtCountdownTick {
  0% { transform: translateY(-6px) scale(0.92); opacity: 0.4; }
  60% { transform: translateY(1px) scale(1.03); opacity: 1; }
  100% { transform: translateY(0) scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .vxt-premium-countdown .premium-countdown-unit span.is-tick { animation: none; }
}
.vxt-premium-countdown .premium-countdown-unit small {
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  opacity: 0.75;
  margin-top: 2px;
}
.vxt-premium-countdown .premium-countdown-sep {
  font-size: 1.4rem;
  font-weight: 700;
  opacity: 0.6;
  padding-bottom: 16px;
}
@media (max-width: 480px) {
  .vxt-premium-countdown .premium-countdown-unit { min-width: 46px; }
  .vxt-premium-countdown .premium-countdown-unit span { font-size: 1.25rem; }
}
