﻿/* ============================================================
   Oletro â€” shared page loader (used on every page)
   Full-screen brand overlay hidden by assets/js/loader.js
   once window "load" fires.
   ============================================================ */

#app-loader {
  position: fixed;
  inset: 0;
  z-index: 999999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(120% 120% at 20% 10%, #0d1b2a 0%, #0e2236 45%, #0b1a2a 100%);
  transition: opacity 0.45s ease, visibility 0.45s ease;
}

#app-loader .loader-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
}

#app-loader .loader-logo {
  font-family: 'Poppins', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  font-size: 2.6rem;
  font-weight: 800;
  letter-spacing: 3px;
  background: linear-gradient(45deg, #ff8a00, #e52e71);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}

#app-loader .loader-bar {
  position: relative;
  width: 132px;
  height: 5px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.14);
  overflow: hidden;
}

#app-loader .loader-bar::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 38%;
  border-radius: 999px;
  background: linear-gradient(90deg, #ff8a00, #e52e71);
  animation: oletra-loader-slide 1.1s ease-in-out infinite;
}


#app-loader.hide {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

/* ============================================================
   Data-gated pages (admin + data pages)
   Pages that opt in with <html data-gate> stay behind the loader
   until their backend data has loaded (see assets/js/loader.js).
   Until then, main content is hidden; once revealed it fades in.
   ============================================================ */
html.pg-loading main {
  visibility: hidden;
  opacity: 0;
}
html.pg-gated:not(.pg-loading) main {
  /* IMPORTANT: fill-mode must be `backwards`, never `both`. The keyframe
     ends on transform: translateY(0), and `both` keeps that non-none
     transform on <main> forever â€” which makes every position:fixed
     descendant (the navbar) position relative to <main> instead of the
     viewport, so the navbar scrolls away with the page. `backwards` only
     applies the from-state during any delay, then reverts to no transform. */
  animation: oletra-page-reveal 0.4s ease backwards;
}
/* Reduced motion: these are STATUS indicators, not decoration. A user who
   cannot tolerate movement still needs to know the page is working, so the
   motion is degraded (slide -> fade) instead of deleted. Spinner rotation
   further down is deliberately left alone: a 18px circle spinning in place
   is not a vestibular trigger, and removing it strands the user with a
   disabled button and no explanation. */
@media (prefers-reduced-motion: reduce) {
  html.pg-gated:not(.pg-loading) main {
    animation: oletra-page-reveal-fade 0.3s ease backwards;
  }

  #app-loader .loader-bar::after {
    animation: oletra-loader-fade 1.1s ease-in-out infinite;
  }

  #app-loader {
    transition: opacity 0.2s linear;
  }
}

/* The indeterminate bar. The bar itself is 132px wide and the highlight is
   38% of it, so it must travel 100% of its own width plus the remaining 62%
   of the track before it fully exits - hence 162%, not 100%. */
@keyframes oletra-loader-slide {
  from { transform: translateX(-100%); }
  to   { transform: translateX(162%); }
}

/* Reduced-motion counterpart: the highlight stays put and pulses instead of
   travelling, so the bar still reads as "working" with no lateral motion. */
@keyframes oletra-loader-fade {
  0%, 100% { opacity: 0.3; }
  50%      { opacity: 0.85; }
}

@keyframes oletra-page-reveal {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Reduced-motion counterpart: opacity only. Deliberately ends on
   transform: none is impossible in a keyframe, which is why the rule using
   this uses animation-fill-mode: backwards - see the note above it. */
@keyframes oletra-page-reveal-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}



/* ============================================================
   SKELETON LOADERS â€” reusable placeholder shapes for content
   that is still loading. Apply .skel to any container; children
   use .skel-line, .skel-circle, .skel-rect.

   Theme-aware by construction: every colour comes from the
   --skel-* tokens in assets/css/tokens.css, so light and dark
   are the SAME rules. This sheet used to carry a hardcoded light
   ramp plus a near-identical [data-theme="dark"] override block;
   that duplication is gone, so a palette tweak is a one-line
   change in tokens.css instead of an edit in two places.
   ============================================================ */

@keyframes skel-shimmer {
  0%   { background-position: calc(var(--skel-shimmer-span) * -0.5) 0; }
  100% { background-position: var(--skel-shimmer-span) 0; }
}

/* Reduced-motion counterpart for every placeholder. A breath is a change in
   brightness only - nothing crosses the screen, so it does not provoke the
   vestibular symptoms that reduced-motion users are avoiding. */
@keyframes skel-breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.55; }
}

/* The single source of the skeleton surface. Anything that should read as
   "content is on its way" inherits from this one rule. */
.skel,
.skel-line,
.skel-circle,
.skel-rect,
.skel-media,
.skel-para,
.skel-card,
.skel-row .skel-cell,
.skel-card .skel-icon {
  background-color: var(--skel-base);
  background-image: linear-gradient(
    90deg,
    var(--skel-base) 25%,
    var(--skel-sheen) 37%,
    var(--skel-base) 63%
  );
  background-size: var(--skel-shimmer-span) 100%;
  background-repeat: no-repeat;
  animation: skel-shimmer 1.6s ease-in-out infinite;
  border-radius: 6px;
}

.skel {
  position: relative;
  overflow: hidden;
}

.skel-line {
  height: 14px;
  margin-bottom: 10px;
  width: 100%;
}

.skel-line.w60 { width: 60%; }
.skel-line.w40 { width: 40%; }
.skel-line.w80 { width: 80%; }
.skel-line:last-child { margin-bottom: 0; }

/* Body-copy placeholder for long text (about / legal / article). */
.skel-para {
  height: 12px;
  margin-bottom: 9px;
  border-radius: 4px;
}
.skel-para.w90 { width: 90%; }
.skel-para.w75 { width: 75%; }

/* Matches the 4:3 ratio the product and service cards actually use, so the
   layout does not jump when the real media lands. */
.skel-media {
  width: 100%;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-md, 10px);
}

.skel-circle {
  border-radius: 50%;
  width: 40px;
  height: 40px;
  flex-shrink: 0;
}

.skel-rect {
  width: 100%;
  height: 120px;
}

/* Dashboard card skeleton */
.skel-card {
  padding: 18px;
  border-radius: var(--radius-lg, 14px);
  border: 1px solid var(--skel-edge);
  display: flex;
  align-items: center;
  gap: 14px;
}

.skel-card .skel-icon {
  width: 44px;
  height: 44px;
  border-radius: var(--radius-md, 10px);
}

.skel-card .skel-text { flex: 1; }
.skel-card .skel-text .skel-line:first-child {
  width: 80px;
  height: 12px;
}
.skel-card .skel-text .skel-line:last-child {
  width: 50%;
  height: 20px;
  margin-bottom: 0;
}

/* Table row skeleton */
.skel-row {
  display: flex;
  gap: 14px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--skel-edge);
  align-items: center;
}

.skel-row .skel-cell {
  height: 14px;
  flex: 1;
}

.skel-row .skel-cell:first-child { flex: 0.3; }
.skel-row .skel-cell:nth-child(2) { flex: 1.5; }

/* Stagger siblings so a list of placeholders cascades in instead of pulsing
   as one flat block. JS sets --skel-i per child (0,1,2...). */
.skel-stagger > * {
  animation-delay: calc(var(--skel-stagger-step, 90ms) * var(--skel-i, 0));
}

/* The skeleton shimmer sweeps a highlight across the placeholder. Under
   reduced motion the sweep is swapped for a gentle opacity breath, so the
   placeholder still reads as "content is arriving" without anything
   travelling across the screen. Listed once against the shared surface
   rule above so the two can never drift apart. */
@media (prefers-reduced-motion: reduce) {
  .skel,
  .skel-line,
  .skel-circle,
  .skel-rect,
  .skel-media,
  .skel-para,
  .skel-card,
  .skel-row .skel-cell,
  .skel-card .skel-icon {
    animation-name: skel-breathe;
    animation-duration: 1.8s;
    background-image: none;
  }

  /* Staggering is a delay-based effect; drop it so every placeholder
     breathes in sync instead of arriving late. */
  .skel-stagger > * {
    animation-delay: 0s;
  }
}


/* ============================================================
   CANONICAL MOTION KEYFRAMES
   ------------------------------------------------------------
   These three lived as 28 separate copy-pasted definitions across 23
   stylesheets (`spin` alone was duplicated 7 times under 4 names, plus
   `btn-spin` x2, `send-spin` x2, `mf-pulse` x2, `voice-pulse` x2).
   Every one was the same two or three lines, so a fix or a timing tweak
   had to be applied a dozen times and half of them drifted.

   They are declared here, in the one stylesheet every page already loads,
   and all usages point at these names.
   ============================================================ */

/* The single spin. Used by button spinners, table loaders and every
   inline "loading" indicator. */
@keyframes oletra-spin {
  to { transform: rotate(360deg); }
}

/* The single "this thing is live / pending" pulse. */
@keyframes oletra-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.45; }
}

/* ============================================================
   BUTTON LOADING STATES â€” add .btn-loading to any button to
   show a spinner and disable clicks.
   ============================================================ */

.btn-loading {
  position: relative;
  pointer-events: none;
  opacity: 0.7;
}

.btn-loading::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 18px;
  height: 18px;
  margin: -9px 0 0 -9px;
  /* currentColor, not a colour token: buttons on this site carry light text
     on navy AND deep text on orange (see .login-btn / .signup-btn), so a
     fixed token makes the spinner invisible on half of them. currentColor
     tracks whatever the button already does. */
  border: 2px solid color-mix(in srgb, currentColor 28%, transparent);
  border-top-color: currentColor;
  border-radius: 50%;
  animation: oletra-spin 0.6s linear infinite;
}

.btn-loading > *:not(.skel) {
  visibility: hidden;
}

/* ============================================================
   ERROR / RETRY STATE â€” reusable component for failed fetches.
   Use: <div class="error-state"><p>Message</p><button class="retry-btn">Retry</button></div>
   ============================================================ */

.error-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 32px 20px;
  text-align: center;
  gap: 12px;
}

.error-state svg {
  color: #e74c3c;
  opacity: 0.7;
}

.error-state p {
  font-size: 0.88rem;
  color: #6b7d95;
  margin: 0;
  max-width: 280px;
}

.retry-btn {
  font-size: 0.8rem;
  font-weight: 600;
  padding: 7px 18px;
  border: 1px solid #d0d7e2;
  border-radius: 8px;
  background: #fff;
  color: #0b3c5d;
  cursor: pointer;
  transition: background 0.2s;
}

.retry-btn:hover {
  background: #eef3f9;
}

[data-theme="dark"] .retry-btn {
  background: #1a2640;
  border-color: #2a3854;
  color: #a0c4e8;
}

/* ============================================================
   LEGACY LOADER CONTAINERS -> SKELETONS
   ------------------------------------------------------------
   Around 70 places across admin/ and user/ still emit one of these
   markup shapes while data loads:

     <div class="loading"><div class="spinner"></div><p>Loading...</p></div>
     <tr><td class="adm-table-loading" colspan="8">Loading coupons.</td></tr>
     <div class="loading-text">Loading promos...</div>

   Rewriting 70 call sites in page JS risks breaking whichever one is
   subtly wrong, and they would drift apart again the moment someone adds
   a new admin list. Instead the SHAPE is upgraded once, here, in the one
   stylesheet every page already loads. The old markup keeps working, the
   spinner and the "Loading..." caption are replaced by placeholder
   geometry, and the result reads as the page's own content arriving.

   Note what is deliberately NOT touched: .btn-loading::after and any
   bare .spinner used inside a <button>. A spinner on a button is the only
   signal that the click registered, so it stays a spinner.
   ============================================================ */

/* Shared surface for every legacy container, so they are all the same
   material as the .skel-* primitives above and follow the theme tokens. */
.loading,
.loading-text,
.adm-table-loading,
.sm-loading,
.mk-loading,
.oc-loading,
.sp-loading,
.followers-loading,
.cd-loading,
.co-loading,
.ur-loading,
.cmp-loading,
.perms-loading {
  background-color: var(--skel-base);
  background-image: linear-gradient(
    90deg,
    var(--skel-base) 25%,
    var(--skel-sheen) 37%,
    var(--skel-base) 63%
  );
  background-size: var(--skel-shimmer-span) 100%;
  background-repeat: no-repeat;
  animation: skel-shimmer 1.6s ease-in-out infinite;
  border-radius: var(--radius-md, 10px);
  color: transparent;
  min-height: 14px;
}

/* The old spinner and caption become invisible, but are NOT display:none -
   if the fetch fails and the code swaps in an error message inside this
   container, it must still be readable. */
.loading > .spinner,
.loading > .spinner-wrap,
.loading > .loader,
.loading > p,
.loading > span,
.adm-table-loading > .spinner,
.loading-text > .spinner {
  visibility: hidden;
}

/* Block form: three ragged lines, like a paragraph of copy arriving. */
.loading {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 18px;
  min-height: 84px;
}

/* Table-cell form: a few full-width rules so the row reads as a list
   rather than one lonely bar. */
.adm-table-loading {
  padding: 16px;
  min-height: 52px;
  background-image:
    repeating-linear-gradient(
      180deg,
      var(--skel-edge) 0 1px,
      transparent 1px 22px
    ),
    linear-gradient(
      90deg,
      var(--skel-base) 25%,
      var(--skel-sheen) 37%,
      var(--skel-base) 63%
    );
  background-size: 100% 100%, var(--skel-shimmer-span) 100%;
}

/* Single-line form: inline counters and "Loading x..." captions. */
.loading-text,
.cd-loading,
.co-loading,
.ur-loading,
.cmp-loading,
.perms-loading,
.followers-loading {
  display: block;
  min-height: 14px;
  max-width: 320px;
}

.sm-loading,
.mk-loading,
.oc-loading,
.sp-loading {
  min-height: 120px;
}

@media (prefers-reduced-motion: reduce) {
  .loading,
  .loading-text,
  .adm-table-loading,
  .sm-loading,
  .mk-loading,
  .oc-loading,
  .sp-loading,
  .followers-loading,
  .cd-loading,
  .co-loading,
  .ur-loading,
  .cmp-loading,
  .perms-loading {
    animation-name: skel-breathe;
    animation-duration: 1.8s;
    background-image: none;
  }

  /* The row-rule variant loses its sheen, so restore the rule pattern. */
  .adm-table-loading {
    background-image: repeating-linear-gradient(
      180deg,
      var(--skel-edge) 0 1px,
      transparent 1px 22px
    );
  }
}
