/* Component styles - Buttons, cards, inputs, badges, etc. */

/* ============ Button ============ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  padding: var(--space-3) var(--space-6);
  border-radius: var(--radius-4);
  border: none;
  cursor: pointer;
  transition: all var(--duration-normal) var(--ease-in-out);
  text-decoration: none;
  white-space: nowrap;
  min-height: 44px;
  min-width: 44px;
  user-select: none;
}

.btn:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

@media (hover: hover) {
  .btn:hover:not(:disabled) {
    transform: translateY(-2px);
    box-shadow: var(--shadow-md);
  }
}

.btn:active:not(:disabled) {
  transform: scale(0.98);
}

.btn:disabled {
  background-color: var(--color-locked);
  color: var(--color-text-light);
  cursor: not-allowed;
  opacity: 0.6;
}

/* Button variants */
.btn-primary {
  background-color: var(--color-primary);
  color: var(--color-text-light);
}

@media (hover: hover) {
  .btn-primary:hover:not(:disabled) {
    background-color: var(--color-sky-bright);
  }
}

.btn-secondary {
  background-color: var(--color-secondary);
  color: var(--color-text-light);
}

@media (hover: hover) {
  .btn-secondary:hover:not(:disabled) {
    background-color: var(--color-sky-light);
  }
}

.btn-outline {
  background-color: transparent;
  border: 2px solid var(--color-primary);
  color: var(--color-primary);
}

@media (hover: hover) {
  .btn-outline:hover:not(:disabled) {
    background-color: var(--color-primary);
    color: var(--color-text-light);
  }
}

.btn-ghost {
  background-color: transparent;
  color: var(--color-primary);
}

@media (hover: hover) {
  .btn-ghost:hover:not(:disabled) {
    background-color: var(--color-white-translucent);
  }
}

.btn-success {
  background-color: var(--color-success);
  color: var(--color-text-light);
}

.btn-error {
  background-color: var(--color-error);
  color: var(--color-text-light);
}

.btn-warning {
  background-color: var(--color-warning);
  color: var(--color-text-light);
}

.btn-premium {
  background-color: var(--btn-premium-bg);
  color: var(--btn-premium-text);
}

@media (hover: hover) {
  .btn-premium:hover:not(:disabled) {
    box-shadow: var(--shadow-premium);
  }
}

.btn-travel {
  background-color: var(--btn-travel-bg);
  color: var(--btn-travel-text);
}

@media (hover: hover) {
  .btn-travel:hover:not(:disabled) {
    background-color: var(--color-primary);
  }
}

.btn-link {
  background-color: transparent;
  color: var(--btn-link-text);
  padding: var(--space-1) var(--space-2);
  min-width: 0;
  text-decoration: none;
  border-bottom: 1px solid transparent;
}

@media (hover: hover) {
  .btn-link:hover:not(:disabled) {
    border-bottom-color: var(--btn-link-text);
    transform: none;
    box-shadow: none;
  }
}

.btn-floating {
  border-radius: var(--radius-circle);
  box-shadow: var(--btn-floating-shadow);
  width: 56px;
  height: 56px;
  padding: 0;
}

@media (hover: hover) {
  .btn-floating:hover:not(:disabled) {
    box-shadow: var(--shadow-modal);
  }
}

/* Loading state — usable on any .btn variant. Sets aria-busy so screen
   readers announce it (never rely on the spinner/opacity alone, per
   CLAUDE.md regla 42 "sin color único"). */
.btn-loading {
  color: transparent;
  pointer-events: none;
  position: relative;
}

.btn-loading::after {
  content: "";
  position: absolute;
  width: 18px;
  height: 18px;
  top: 50%;
  left: 50%;
  margin: -9px 0 0 -9px;
  border: 2px solid var(--color-white-translucent);
  border-top-color: currentColor;
  border-radius: var(--radius-circle);
  color: var(--color-text-light);
  animation: spin 1s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) .btn-loading::after {
    animation: none;
  }
}

/* Button sizes */
.btn-sm {
  padding: var(--space-2) var(--space-4);
  font-size: var(--font-size-sm);
  min-height: 36px;
  min-width: 36px;
}

.btn-lg {
  padding: var(--space-4) var(--space-8);
  font-size: var(--font-size-lg);
  min-height: 48px;
  min-width: 48px;
}

.btn-full {
  width: 100%;
}

.btn-icon {
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: var(--radius-full);
}

.btn-icon svg {
  width: 24px;
  height: 24px;
}

/* ============ Card ============ */
.card {
  background-color: var(--color-bg-primary);
  border-radius: var(--radius-4);
  padding: var(--space-6);
  box-shadow: var(--shadow-md);
  transition: box-shadow var(--duration-normal) var(--ease-in-out);
}

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

.card-header {
  margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--color-bg-secondary);
  padding-bottom: var(--space-4);
}

.card-body {
  margin: var(--space-4) 0;
}

.card-footer {
  margin-top: var(--space-4);
  border-top: 1px solid var(--color-bg-secondary);
  padding-top: var(--space-4);
}

/* Card variants (Fase 8 Parte 2) — additive to the base .card above,
   consumed via js/components/card.js createCard(). Interactivity is
   never implied by CSS alone: a card marked .card--interactive still
   needs a real <button>/<a> inside it, enforced by the factory. */
.card--elevated {
  box-shadow: var(--shadow-lg);
}

.card--glass {
  background: var(--color-glass-bg);
  backdrop-filter: var(--blur-glass);
  border: 1px solid var(--border-color-glass);
  color: var(--color-text-light);
  box-shadow: var(--shadow-glass);
}

.card--outlined {
  background: transparent;
  border: var(--border-width-thin) solid var(--border-color-default);
  box-shadow: none;
}

.card--interactive {
  cursor: pointer;
}

.card--premium {
  border: var(--border-width-thin) solid var(--color-premium);
  background: var(--color-premium-bg-soft);
}

.card--locked {
  opacity: var(--opacity-disabled);
}

.card__title {
  font-size: var(--type-title-size);
  font-weight: var(--type-title-weight);
  line-height: var(--type-title-line-height);
  color: var(--color-text-primary);
  margin: 0;
}

.card--glass .card__title {
  color: var(--color-text-light);
}

.card__subtitle {
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  margin: var(--space-1) 0 0 0;
}

.card--glass .card__subtitle {
  color: var(--color-white-translucent-strong);
}

/* ============ Input ============ */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="search"],
textarea,
select {
  display: block;
  width: 100%;
  padding: var(--space-3) var(--space-4);
  font-size: var(--font-size-base);
  border: 1px solid #D0D5DD;
  border-radius: var(--radius-2);
  background-color: var(--color-bg-primary);
  color: var(--color-text-primary);
  transition: all var(--duration-normal) var(--ease-in-out);
}

input::placeholder,
textarea::placeholder {
  color: var(--color-text-secondary);
  opacity: 0.7;
}

input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(109, 213, 231, 0.1);
}

input:disabled,
textarea:disabled,
select:disabled {
  background-color: var(--color-bg-secondary);
  cursor: not-allowed;
  opacity: 0.6;
}

/* ============ Label ============ */
label {
  display: block;
  font-weight: var(--font-weight-semibold);
  margin-bottom: var(--space-2);
  color: var(--color-text-primary);
  font-size: var(--font-size-sm);
}

label[required]::after {
  content: " *";
  color: var(--color-error);
}

.form-group {
  margin-bottom: var(--space-6);
}

/* ============ Badge & Chip ============ */
.badge,
.chip {
  display: inline-block;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-full);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  background-color: var(--color-bg-secondary);
  color: var(--color-text-primary);
  white-space: nowrap;
}

.badge-success {
  background-color: var(--color-success);
  color: var(--color-text-light);
}

.badge-error {
  background-color: var(--color-error);
  color: var(--color-text-light);
}

.badge-warning {
  background-color: var(--color-warning);
  color: var(--color-text-light);
}

.badge-info {
  background-color: var(--color-primary);
  color: var(--color-text-light);
}

.badge-gem {
  background-color: var(--color-gem);
  color: var(--color-text-light);
}

.badge-xp {
  background-color: var(--color-xp);
  color: var(--color-text-light);
}

.badge-accent {
  background-color: var(--color-accent);
  color: var(--color-text-light);
}

.badge-premium {
  background-color: var(--color-premium);
  color: var(--color-text-light);
}

.badge-locked {
  background-color: var(--color-locked);
  color: var(--color-text-light);
}

/* "Coming soon" must never rely on color alone (CLAUDE.md regla 42) — the
   text content itself ("Próximamente" etc.) is the actual signal; this
   variant just keeps it visually distinct from success/error/info badges. */
.badge-coming-soon {
  background-color: var(--color-neutral-100);
  color: var(--color-neutral-600);
  border: 1px solid var(--color-neutral-200);
}

/* ============ Alert ============ */
.alert {
  padding: var(--space-4);
  border-radius: var(--radius-4);
  border-left: 4px solid;
  margin-bottom: var(--space-4);
}

.alert-success {
  background-color: rgba(39, 174, 96, 0.1);
  border-left-color: var(--color-success);
  color: var(--color-success);
}

.alert-error {
  background-color: rgba(231, 76, 60, 0.1);
  border-left-color: var(--color-error);
  color: var(--color-error);
}

.alert-warning {
  background-color: rgba(243, 156, 18, 0.1);
  border-left-color: var(--color-warning);
  color: #8a5f0f;
}

.alert-info {
  background-color: rgba(109, 213, 231, 0.1);
  border-left-color: var(--color-primary);
  color: var(--color-primary);
}

/* Toast types added in Fase 8 Parte 2 (js/components/toast.js) — reuse
   the same .alert base as info/success/warning/error above. */
.alert-xp,
.alert-level-up {
  background-color: var(--color-xp-bg-soft);
  border-left-color: var(--color-xp);
  color: var(--color-text-primary);
}

.alert-achievement {
  background-color: var(--color-achievement-bg-soft);
  border-left-color: var(--color-travel-gold);
  color: var(--color-text-primary);
}

.alert-mission {
  background-color: var(--color-info-bg-soft);
  border-left-color: var(--color-accent);
  color: var(--color-text-primary);
}

.alert-premium {
  background-color: var(--color-premium-bg-soft);
  border-left-color: var(--color-premium);
  color: var(--color-text-primary);
}

/* ============ Toast internals (close button, queue) ============ */
.toast__row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
}

.toast__content {
  flex: 1;
  min-width: 0;
}

.toast__close {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  cursor: pointer;
  color: inherit;
  opacity: 0.6;
  border-radius: var(--radius-circle);
}

.toast__close:hover,
.toast__close:focus-visible {
  opacity: 1;
}

.toast__close:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

/* ============ Spinner/Loader ============ */
.spinner {
  display: inline-block;
  width: 40px;
  height: 40px;
  border: 3px solid var(--color-bg-secondary);
  border-top-color: var(--color-primary);
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) .spinner {
    animation: none;
  }
}

/* ============ Avatar ============ */
/* Image with an initials fallback: put the initials as textContent on the
   .avatar element itself, and add an <img> child only when a photo URL
   exists — CSS alone can't detect a broken/missing image src, so the
   initials text stays in the DOM and is simply covered by the image
   when one loads. */
.avatar {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  border-radius: var(--radius-circle);
  overflow: hidden;
  background-color: var(--color-bg-secondary);
  color: var(--color-text-secondary);
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
}

/* Positioned absolutely so it covers the initials text underneath
   instead of sitting beside it in the flex layout — if the image fails
   to load, js/components/avatar.js removes the <img> and the initials
   already in the DOM show through with no extra logic needed. */
.avatar img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.avatar-sm {
  width: 32px;
  height: 32px;
  font-size: var(--font-size-xs);
}

.avatar-md {
  width: 44px;
  height: 44px;
  font-size: var(--font-size-base);
}

.avatar-lg {
  width: 120px;
  height: 120px;
  font-size: var(--font-size-3xl);
}

.avatar-bordered {
  border: 3px solid var(--color-primary);
  box-shadow: var(--shadow-avatar-ring);
}

/* Predefined local avatar colors (Perfiles múltiples locales, Parte 2) -
   see js/data/avatars.js for the catalog + getAvatarClassName(). Every
   value here is an existing token, never a new raw color (regla 80). */
.avatar-color-traveler-cyan {
  background-color: var(--color-primary);
  color: var(--color-ocean-dark);
}

.avatar-color-traveler-purple {
  background-color: var(--color-accent);
  color: var(--color-white);
}

.avatar-color-traveler-green {
  background-color: var(--color-success);
  color: var(--color-white);
}

.avatar-color-traveler-orange {
  background-color: var(--color-warning);
  color: var(--color-ocean-dark);
}

.avatar-color-traveler-azure {
  background-color: var(--color-travel-azure);
  color: var(--color-white);
}

.avatar-color-traveler-gold {
  background-color: var(--color-travel-gold);
  color: var(--color-ocean-dark);
}

/* ============ Page Header ============ */
/* Consumed via js/components/page-header.js createPageHeader(). Migrates
   css/courses.css .courses-page__header (single real usage) — that
   file's rules are left in place for backward compatibility (regla 15
   del prompt: alias temporal) rather than deleted, since removing them
   now would require touching js/pages/course-detail.js's hero too. */
.page-header {
  padding: var(--space-4);
  padding-top: var(--safe-area-top);
}

.page-header--solid {
  background-color: var(--color-bg-primary);
  border-bottom: 1px solid var(--color-divider);
}

.page-header--transparent {
  background-color: transparent;
}

.page-header--sticky {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
}

.page-header__row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.page-header__titles {
  flex: 1;
  min-width: 0;
}

/* font-size-3xl matches the exact size .courses-page__title already used
   (its only real consumer today) — not --type-heading-size, which would
   render 6px larger and silently change the page, not just generalize it. */
.page-header__title {
  font-size: var(--font-size-3xl);
  font-weight: var(--font-weight-bold);
  line-height: var(--type-heading-line-height);
  color: var(--color-text-primary);
  margin: 0;
}

.page-header__subtitle {
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  margin: var(--space-1) 0 0 0;
}

.page-header__actions {
  display: flex;
  gap: var(--space-2);
  flex-shrink: 0;
}

/* ============ Section Header ============ */
/* Consumed via js/components/section-header.js createSectionHeader().
   Migrates css/courses.css's repeated (4x) h2 + grid pattern in
   js/pages/courses.js renderSection(). */
.section-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

.section-header__main {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.section-header__title {
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-primary);
  margin: 0;
}

.section-header__description {
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  margin: var(--space-1) 0 0 0;
}

.section-header__action {
  flex-shrink: 0;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-primary);
  background: transparent;
  border: none;
  cursor: pointer;
  padding: var(--space-2);
  min-height: 44px;
}

.section-header__action:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

/* ============ Stat Card ============ */
/* Consumed via js/components/stat-card.js createStatCard(). Migrates the
   4x-repeated .card.text-center pattern in js/pages/progress.js (Total
   XP/Gemas/Racha Actual/Mejor Racha) — the Fase 2 placeholder Progreso
   page, direct precursor of the real Fase 9 page. Values match the
   original .text-4xl/.text-primary/.text-gem/.text-warning utility
   classes exactly (same tokens), not a new size scale. */
/* Padding trimmed to --space-4 below the 768px tier (was --space-6 at
   every width) - on a 2-column mobile card as narrow as ~134px, 24px of
   padding per side left only ~86px for content, still not quite enough
   room for a 10-character word like "disponible" to wrap into 2 lines
   instead of 3. */
.stat-card {
  background: var(--color-bg-primary);
  border-radius: var(--radius-4);
  padding: var(--space-4);
  box-shadow: var(--shadow-md);
  text-align: center;
}

@media (min-width: 768px) {
  .stat-card {
    padding: var(--space-6);
  }
}

.stat-card__icon {
  display: flex;
  justify-content: center;
  margin-bottom: var(--space-2);
  color: var(--color-text-secondary);
}

/* overflow-wrap: anywhere here used to break every label letter by
   letter ("Ciuda/des") - not because labels need it (they're normal
   multi-word text that already wraps at spaces with the browser
   default), but because the grid column itself was too narrow (see the
   4-column-at-414px fix in css/personal-center.css). With 2 comfortable
   columns on mobile, plain normal wrapping is enough and never forces a
   mid-word break. */
.stat-card__label {
  color: var(--color-text-secondary);
  margin: 0 0 var(--space-2) 0;
  font-size: var(--font-size-base);
  word-break: normal;
  overflow-wrap: normal;
  white-space: normal;
}

/* Values can be a single unbreakable word ("Precisión", "Próximamente")
   at a large font size - unlike labels, these can legitimately need to
   wrap mid-word as a last resort if the card is narrower than the word
   itself. overflow-wrap: break-word only breaks when a line genuinely
   has no other option (unlike "anywhere", which also breaks inside
   words that would otherwise fit, producing the letter-by-letter look
   this hotfix is fixing) - with 2 comfortable mobile columns this reads
   as a normal 1-2 line wrap, never single characters. */
/* --font-size-4xl (36px) is sized for a short number (e.g. "80"), not
   for the longer text fallbacks this same field can hold ("No
   disponible", "Próximamente") - at that size even a comfortable
   2-column mobile card can't fit a 10+ character word without breaking
   it into several fragments. --font-size-2xl on mobile (below the 4-col
   tablet/desktop tier) gives those fallbacks room to wrap into 1-2
   natural lines instead; the always-short numeric values just render
   slightly smaller, still clearly the visual focus of the card. */
.stat-card__value {
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-bold);
  margin: 0;
  color: var(--color-text-primary);
  overflow-wrap: break-word;
}

@media (min-width: 768px) {
  .stat-card__value {
    font-size: var(--font-size-4xl);
  }
}

.stat-card__value--primary {
  color: var(--color-primary);
}

.stat-card__value--gem {
  color: var(--color-gem);
}

.stat-card__value--warning {
  color: var(--color-warning);
}

.stat-card__value--success {
  color: var(--color-success);
}

.stat-card__trend {
  font-size: var(--font-size-xs);
  margin-top: var(--space-1);
}

.stat-card__trend--up {
  color: var(--color-success);
}

.stat-card__trend--down {
  color: var(--color-danger);
}

.stat-card__aux {
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
  margin: var(--space-1) 0 0 0;
}

/* ============ Empty State ============ */
/* Consumed via js/components/empty-state.js createEmptyState(). Migrates
   the 5x-repeated css/passport.css .passport-empty-message pattern
   (js/components/passport-stamps.js/-postcards.js/-boarding-passes.js/
   -souvenirs.js/-monuments.js) — .passport-empty-message itself is kept
   in css/passport.css as a compatibility alias in case another consumer
   is added later, but the 5 JS files now render through this component. */
.empty-state {
  text-align: center;
  padding: var(--space-6);
  background: var(--color-bg-primary);
  border-radius: var(--radius-2);
}

.empty-state--locked {
  background: var(--color-bg-secondary);
}

.empty-state--coming-soon {
  background: var(--color-badge-neutral-bg);
}

.empty-state--error {
  background: var(--color-danger-bg-soft);
}

.empty-state--success {
  background: var(--color-success-bg-soft);
}

.empty-state__icon {
  display: flex;
  justify-content: center;
  margin-bottom: var(--space-2);
  color: var(--color-neutral-400);
}

.empty-state__title {
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-primary);
  margin: 0 0 var(--space-2) 0;
}

.empty-state__description {
  color: var(--color-text-primary);
  font-size: var(--font-size-base);
  margin: 0;
}

.empty-state__action {
  margin-top: var(--space-4);
}

/* ============ Tabs (visual definition only) ============ */
/* No page in the app has a tabbed interface today (confirmed by grep
   during Fase 8 Parte 2 — see docs/COMPONENT_LIBRARY.md). Building the
   keyboard-navigation/ARIA-tablist JS behavior now would be functionality
   with no consumer (CLAUDE.md regla 61), so only the visual CSS exists
   here, ready for Parte 3 if/when an actual tabbed screen is designed. */
.tabs {
  display: flex;
  gap: var(--space-2);
  border-bottom: 1px solid var(--color-divider);
  overflow-x: auto;
}

.tab {
  min-height: 44px;
  padding: var(--space-3) var(--space-4);
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  color: var(--color-text-secondary);
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  cursor: pointer;
  white-space: nowrap;
}

.tab:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: -2px;
}

.tab[aria-selected="true"] {
  color: var(--color-primary);
  border-bottom-color: var(--color-primary);
}

/* ============ Toast ============ */
/* js/components/toast.js builds #toast-container with the utility classes
   "flex flex-col gap-2" (css/layout.css) plus this id-scoped rule for
   positioning. Previously the container also carried "fixed bottom-4
   right-4" — .fixed exists in layout.css but .bottom-4/.right-4 never
   did, so the toast stack rendered unpositioned (top-left, no offset).
   Fixed here (Fase 8 Parte 2) by giving #toast-container its own
   position/offset instead of relying on missing utility classes. */
#toast-container {
  position: fixed;
  bottom: var(--safe-area-bottom);
  right: var(--space-4);
  left: var(--space-4);
  z-index: var(--elevation-toast);
  pointer-events: none;
}

#toast-container > * {
  pointer-events: auto;
  margin-left: auto;
  max-width: 360px;
}

@media (min-width: 414px) {
  #toast-container {
    left: auto;
  }
}

/* Toast dismiss transition — replaces the inline toast.style.opacity/
   toast.style.transition previously set directly in
   js/components/toast.js (CLAUDE.md regla 31/33). TOAST_REMOVE_DELAY_MS
   in toast.js must stay equal to --duration-normal's 300ms so the removal
   timeout matches this transition. */
.toast--removing {
  opacity: 0;
  transition: opacity var(--duration-normal) var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) .toast--removing {
    transition: none;
  }
}

/* ============ Loading Indicator ============ */
/* Wraps the existing .spinner (defined below, Fase 1) with an
   aria-busy/label container. Consumed via js/components/loading.js. */
.loading-indicator {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--color-text-secondary);
}

.loading-indicator__label {
  font-size: var(--font-size-sm);
}

/* ============ Skeleton ============ */
/* Zero current consumers — the app has no asynchronous loading today
   (see docs/COMPONENT_LIBRARY.md), built because it's an explicit
   requirement of this part's component list, ready for future use
   (e.g. a backend-synced Perfil/Progreso in a later phase). */
.skeleton {
  background: linear-gradient(
    90deg,
    var(--color-neutral-100) 25%,
    var(--color-neutral-50) 50%,
    var(--color-neutral-100) 75%
  );
  background-size: 200% 100%;
  animation: skeleton-shimmer var(--duration-shimmer) var(--ease-in-out) infinite;
}

@keyframes skeleton-shimmer {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) .skeleton {
    animation: none;
    background: var(--color-neutral-100);
  }
}

.skeleton--text {
  height: 1em;
  border-radius: var(--radius-1);
  margin-bottom: var(--space-2);
}

.skeleton--text:last-child {
  margin-bottom: 0;
  width: 70%;
}

.skeleton--circle {
  border-radius: var(--radius-circle);
}

.skeleton--rect {
  border-radius: var(--radius-2);
}

.skeleton-card {
  padding: var(--space-6);
  background-color: var(--color-bg-primary);
  border-radius: var(--radius-4);
  box-shadow: var(--shadow-sm);
}

/* ============ Divider ============ */
.divider {
  height: 1px;
  background-color: var(--color-bg-secondary);
  margin: var(--space-4) 0;
}

.divider-vertical {
  width: 1px;
  height: 100%;
  background-color: var(--color-bg-secondary);
}

/* ============ XP Progress Bar ============ */
.xp-progress-bar {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-4);
  background-color: var(--color-bg-secondary);
  border-radius: var(--radius-3);
  margin-bottom: var(--space-4);
}

.xp-level-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 60px;
  width: 60px;
  height: 60px;
  background: linear-gradient(135deg, var(--color-accent), var(--color-primary));
  color: white;
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-bold);
  border-radius: 50%;
  box-shadow: var(--shadow-md);
}

@media (prefers-reduced-motion: no-preference) {
  .xp-level-badge.level-up-animation {
    animation: levelUpPulse 0.6s ease-out;
  }
}

@keyframes levelUpPulse {
  0% {
    transform: scale(1);
    box-shadow: var(--shadow-md);
  }
  50% {
    transform: scale(1.15);
    box-shadow: 0 0 20px rgba(109, 213, 231, 0.6);
  }
  100% {
    transform: scale(1);
    box-shadow: var(--shadow-md);
  }
}

.xp-bar-container {
  flex: 1;
  min-width: 0;
}

.xp-bar {
  width: 100%;
  height: 24px;
  background-color: var(--color-bg-tertiary);
  border-radius: var(--radius-2);
  overflow: hidden;
  position: relative;
}

.xp-bar-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--color-primary), var(--color-accent));
  border-radius: var(--radius-2);
  transition: width 0.3s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) .xp-bar-fill.no-animation {
    transition: none;
  }
}

.xp-stats {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-1);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  min-width: 60px;
}

.xp-current {
  font-weight: var(--font-weight-semibold);
  color: var(--color-accent);
}

.xp-percent {
  font-weight: var(--font-weight-bold);
  color: var(--color-primary);
}

@media (min-width: 414px) {
  .xp-progress-bar {
    padding: var(--space-5);
  }
}

/* ============ Mission Items ============ */
.mission-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  background-color: var(--color-bg-secondary);
  border-radius: var(--radius-3);
  margin-bottom: var(--space-2);
  transition: all var(--duration-normal) var(--ease-in-out);
  border-left: 4px solid var(--color-accent);
}

.mission-item--claimed {
  opacity: 0.6;
  border-left-color: var(--color-success);
}

.mission-item__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 40px;
  width: 40px;
  height: 40px;
  color: var(--color-accent);
}

.mission-item__icon svg {
  width: 100%;
  height: 100%;
}

.mission-item__content {
  flex: 1;
  min-width: 0;
}

.mission-item__title {
  display: block;
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-primary);
  margin-bottom: var(--space-1);
}

.mission-item__description {
  display: block;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}

.mission-item__reward {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: var(--font-weight-semibold);
  color: var(--color-warning);
  min-width: 80px;
}

.mission-item__claim {
  min-width: 44px;
  min-height: 44px;
  padding: var(--space-2);
}

@media (max-width: 375px) {
  .mission-item {
    flex-direction: column;
    align-items: flex-start;
  }

  .mission-item__reward {
    width: 100%;
    justify-content: space-between;
  }
}

/* ============ Progress Bar (generic) ============ */
/* Generalizes the .xp-bar/.xp-bar-fill pattern above for non-XP uses
   (course completion %, city completion %). .xp-bar/.xp-bar-fill are
   untouched — this is a new sibling, not a replacement, so
   xp-progress-bar.js keeps working exactly as before.
   Consumer contract: apply role="progressbar" plus
   aria-valuenow/aria-valuemin="0"/aria-valuemax="100" on .progress-bar,
   and set the fill width inline (style="width: N%") on .progress-bar-fill,
   the same way js/components/xp-progress-bar.js already sets .xp-bar-fill
   — that file does not set those aria-value* attributes today either;
   noted in docs/COMPONENT_LIBRARY.md as a pre-existing gap, not
   introduced or fixed here. */
.progress-bar {
  width: 100%;
  height: 12px;
  background-color: var(--color-bg-tertiary);
  border-radius: var(--radius-full);
  overflow: hidden;
  position: relative;
}

.progress-bar-fill {
  height: 100%;
  background-color: var(--color-primary);
  border-radius: var(--radius-full);
  transition: width var(--duration-normal) var(--ease-out);
}

.progress-bar-sm {
  height: 6px;
}

.progress-bar-lg {
  height: 24px;
}

.progress-bar-fill--success {
  background-color: var(--color-success);
}

.progress-bar-fill--premium {
  background: linear-gradient(90deg, var(--color-primary), var(--color-premium));
}

/* ============ Circular Progress ============ */
/* Generalizes the single existing ring (js/components/home-panels.js
   "course completion" circle, previously .course-circle/.course-circle__svg/
   .course-circle__progress in css/home.css with raw literal stroke colors
   #1A5490/#6DD5E7) — migrated below, not left duplicated. */
.circular-progress {
  position: relative;
  display: inline-flex;
  max-width: 100%;
}

/* Lets the ring shrink proportionally (aspect-ratio preserved by the SVG
   viewBox) when its container is narrower than the `size` option passed
   to createCircularProgress() — see the .home-panel min-width: 0 fix in
   css/home.css for the grid-overflow bug this closes. */
.circular-progress__svg {
  max-width: 100%;
  height: auto;
}

.circular-progress__track {
  fill: none;
  stroke: var(--color-ocean-light);
}

.circular-progress__fill {
  fill: none;
  stroke: var(--color-primary);
  stroke-linecap: round;
  transition: stroke-dasharray var(--duration-slow) var(--ease-smooth);
}

.circular-progress__fill--success {
  stroke: var(--color-success);
}

.circular-progress__fill--premium {
  stroke: var(--color-premium);
}

@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) .circular-progress__fill {
    transition: none;
  }
}

.circular-progress__text {
  fill: var(--color-text-light);
  font-weight: var(--font-weight-bold);
  text-anchor: middle;
}

/* ============ Dialog ============ */
/* Not wired into any page yet (Fase 8 Parte 3). js/components/dialog.js
   provides openDialog()/closeDialog() with focus management, mirroring
   the pattern already proven in js/components/side-drawer.js (Fase 7):
   a module-level instance + keydown handler, removed on close so
   listeners never pile up. */
.dialog-overlay {
  position: fixed;
  inset: 0;
  background-color: var(--color-overlay);
  backdrop-filter: var(--blur-modal);
  z-index: var(--elevation-dialog);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  animation: fade var(--duration-normal) var(--ease-smooth);
}

.dialog {
  background-color: var(--color-bg-primary);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-modal);
  max-width: var(--max-width-sm);
  width: 100%;
  max-height: 90vh;
  overflow-y: auto;
  padding: var(--space-6);
  animation: scale var(--duration-normal) var(--ease-smooth);
  border-top: 4px solid transparent;
}

/* Dialog type accents (Fase 8 Parte 2) — same idea as .card--premium/
   --locked, just a top border instead of a full background tint so it
   doesn't compete with the dialog's own content. */
.dialog--warning {
  border-top-color: var(--color-warning);
}

.dialog--error {
  border-top-color: var(--color-danger);
}

.dialog--success {
  border-top-color: var(--color-success);
}

.dialog--premium {
  border-top-color: var(--color-premium);
}

.dialog--confirm {
  border-top-color: var(--color-primary);
}

.dialog__header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

.dialog__title {
  font-size: var(--type-title-size);
  font-weight: var(--type-title-weight);
  line-height: var(--type-title-line-height);
  margin: 0;
  color: var(--color-text-primary);
}

.dialog__close {
  flex-shrink: 0;
  min-width: 44px;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  cursor: pointer;
  border-radius: var(--radius-circle);
  color: var(--color-text-secondary);
}

.dialog__close:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

@media (hover: hover) {
  .dialog__close:hover {
    background-color: var(--color-bg-secondary);
  }
}

.dialog__body {
  margin-bottom: var(--space-6);
  color: var(--color-text-primary);
}

.dialog__actions {
  display: flex;
  gap: var(--space-3);
  justify-content: flex-end;
  flex-wrap: wrap;
}

/* ============ Bottom Sheet ============ */
.bottom-sheet-overlay {
  position: fixed;
  inset: 0;
  background-color: var(--color-overlay);
  z-index: var(--elevation-bottom-sheet);
}

.bottom-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--elevation-bottom-sheet);
  background-color: var(--color-bg-primary);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  box-shadow: var(--shadow-floating);
  backdrop-filter: var(--blur-drawer);
  max-height: 85vh;
  overflow-y: auto;
  padding: var(--space-6) var(--space-4) var(--safe-area-bottom);
  animation: fade-up var(--duration-normal) var(--ease-smooth);
}

.bottom-sheet__handle {
  width: 40px;
  height: 4px;
  background-color: var(--color-neutral-300);
  border-radius: var(--radius-full);
  margin: 0 auto var(--space-4);
}

/* Implements the "fade" and "scale" animation names reserved in
   css/tokens.css (Fase 8 Parte 1). "fade-up" here is the closest reserved
   name to the bottom sheet's upward entrance. */
@keyframes fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes scale {
  from { opacity: 0; transform: scale(0.95); }
  to { opacity: 1; transform: scale(1); }
}

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

@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) .dialog-overlay,
  :root:not([data-motion="full"]) .dialog,
  :root:not([data-motion="full"]) .bottom-sheet {
    animation: none;
  }
}
