/* fx.css — the app-wide visual and motion layer.
 *
 * ADDITIVE ON PURPOSE. This file is loaded AFTER style.css in both
 * document roots (base.html and dashboards/_shell.html), so all 48
 * templates inherit it without a single template being edited. A visual
 * overhaul that touches no markup cannot break a form, a table or a
 * state machine — which matters more than usual this close to a
 * presentation.
 *
 * Division of labour with fx-engine.js: three.js owns the spatial
 * effects that live in the shared WebGL scene (element edges lifting
 * toward the camera, glow volumes, click sparks, the ambient depth
 * field). This file owns everything that must still work when WebGL is
 * absent — colour, elevation, focus, layout rhythm, and the motion that
 * has to be there for the interface to feel responsive at all.
 */

/* ---------------------------------------------------------------------
   1. TOKENS — extend, never replace, the ones style.css already defines
   --------------------------------------------------------------------- */
:root {
  /* Easing. --ease-out for things arriving, --ease-spring for things
     the user pushed (a press wants a little overshoot to feel physical). */
  --ease-out: cubic-bezier(.22,.61,.36,1);
  --ease-spring: cubic-bezier(.34,1.56,.64,1);
  --ease-in-out: cubic-bezier(.65,0,.35,1);

  --dur-1: 120ms;   /* press, tap feedback */
  --dur-2: 220ms;   /* hover, focus */
  --dur-3: 420ms;   /* panel, reveal */
  --dur-4: 700ms;   /* page entrance */

  /* A real elevation ladder. The previous two shadows made everything
     sit at one of two heights, so nothing read as "above" anything. */
  --elev-1: 0 1px 2px rgba(0,0,0,.16), 0 2px 8px -4px rgba(0,0,0,.22);
  --elev-2: 0 2px 6px rgba(0,0,0,.18), 0 12px 28px -14px rgba(0,0,0,.42);
  --elev-3: 0 8px 18px rgba(0,0,0,.22), 0 28px 60px -22px rgba(0,0,0,.55);
  --elev-accent: 0 10px 30px -12px rgba(225,29,72,.45);

  --ring: 0 0 0 3px var(--accent-soft), 0 0 0 1.5px var(--accent);
}

/* ---------------------------------------------------------------------
   2. THE SHARED WebGL CANVAS
   One element, one context, behind all content and never interactive.
   --------------------------------------------------------------------- */
/* z-index:-1, NOT 0 with the content raised above it.
 *
 * The first version of this file sat the canvas at z-index:0 and then
 * lifted every direct child of <body> with
 *     body > *:not(.fx-canvas) { position: relative; z-index: 1 }
 * which silently overrode `position: fixed` on .sidebar and .dock. The
 * sidebar fell out of fixed positioning into normal flow and the whole
 * dashboard layout collapsed. Never blanket-set `position` on elements
 * this file does not own.
 *
 * Sitting at -1 puts the canvas behind all content without touching a
 * single other element. For it to remain visible, the page background
 * that would otherwise cover it is moved from <body> to <html> below —
 * html paints the canvas backdrop, the canvas draws over it, and the
 * whole interface draws over the canvas. */
.fx-canvas {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;   /* must never eat a click meant for the UI */
  opacity: 0;
  transition: opacity 900ms var(--ease-out);
}
.fx-on .fx-canvas { opacity: 1; }

/* Only while the engine is live: hand the painted background to <html>
 * so the canvas is not hidden behind body's opaque fill. With the engine
 * off (no WebGL, reduced motion, script blocked) this rule never applies
 * and body keeps its own background exactly as before. */
.fx-on body {
  background-color: transparent;
  background-image: none;
}
.fx-on {
  background: var(--bg);
  background-image:
    radial-gradient(ellipse 700px 500px at 88% 8%, var(--bg-glow-1) 0%, transparent 60%),
    radial-gradient(ellipse 600px 500px at 8% 92%, var(--bg-glow-2) 0%, transparent 55%);
  background-attachment: fixed;
}

/* ---------------------------------------------------------------------
   3. PAGE ENTRANCE
   A short staggered rise. Cheap, and it makes navigation feel authored
   rather than abrupt.
   --------------------------------------------------------------------- */
@keyframes fx-rise {
  from { opacity: 0; transform: translate3d(0, 14px, 0); }
  to   { opacity: 1; transform: none; }
}
/* :not(.dock) everywhere below.
   .dock carries class="dock glass" and centres itself with
   `transform: translateX(-50%)`. Any rule here that animates or sets
   `transform` on .glass overwrites that centring — the mobile dock jumps
   to the left edge on load and again on hover. Anything that owns its
   own transform must be excluded, not merely styled around. */
.section-label,
.glass:not(.dock),
.module-grid > *,
.dir-card,
.table-card {
  animation: fx-rise var(--dur-4) var(--ease-out) both;
}
/* Stagger the first few so the eye follows the page down rather than
   everything arriving at once. Capped at 6 — beyond that the delay
   starts to read as lag. */
.glass:not(.dock):nth-of-type(1) { animation-delay: 20ms; }
.glass:not(.dock):nth-of-type(2) { animation-delay: 60ms; }
.glass:not(.dock):nth-of-type(3) { animation-delay: 100ms; }
.glass:not(.dock):nth-of-type(4) { animation-delay: 140ms; }
.glass:not(.dock):nth-of-type(5) { animation-delay: 180ms; }
.glass:not(.dock):nth-of-type(6) { animation-delay: 220ms; }

/* ---------------------------------------------------------------------
   4. SURFACES
   --------------------------------------------------------------------- */
.glass:not(.dock), .table-card {
  box-shadow: var(--elev-2);
  transition:
    transform var(--dur-2) var(--ease-out),
    box-shadow var(--dur-2) var(--ease-out),
    border-color var(--dur-2) var(--ease-out);
}
/* The lift pairs with the three.js edge trace on the same element: CSS
   moves the surface, the WebGL layer draws its outline rising with it.
   With WebGL off, the CSS lift alone still reads correctly. */
.glass:not(.dock):hover, .table-card:hover {
  transform: translate3d(0, -2px, 0);
  box-shadow: var(--elev-3);
  border-color: var(--border-strong);
}
/* A card wrapping a form should not float away while being typed into. */
.glass:not(.dock):focus-within, .table-card:focus-within { transform: none; }

/* ---------------------------------------------------------------------
   5. BUTTONS
   --------------------------------------------------------------------- */
.btn-primary, .btn-secondary, .action-btn, .btn-otp, .chip-compact {
  transition:
    transform var(--dur-1) var(--ease-spring),
    box-shadow var(--dur-2) var(--ease-out),
    background var(--dur-2) var(--ease-out),
    color var(--dur-2) var(--ease-out);
  will-change: transform;
}
.btn-primary:hover, .btn-otp:hover { transform: translate3d(0,-2px,0); box-shadow: var(--elev-accent); }
.btn-primary:active, .btn-otp:active, .action-btn:active { transform: translate3d(0,1px,0) scale(.985); }
.btn-secondary:hover, .action-btn:hover { transform: translate3d(0,-1px,0); box-shadow: var(--elev-1); }

/* A single sheen sweep on primary actions. Purely decorative, so it is
   hidden from assistive tech by living in a pseudo-element. */
.btn-primary { position: relative; overflow: hidden; }
.btn-primary::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(115deg, transparent 30%, rgba(255,255,255,.22) 50%, transparent 70%);
  transform: translateX(-120%);
  transition: transform 620ms var(--ease-out);
}
.btn-primary:hover::after { transform: translateX(120%); }

/* ---------------------------------------------------------------------
   6. FORMS
   Focus is the one place motion is not decoration: it tells a keyboard
   user where they are, so it is always visible and never suppressed.
   --------------------------------------------------------------------- */
input, select, textarea {
  transition: border-color var(--dur-2) var(--ease-out),
              box-shadow var(--dur-2) var(--ease-out),
              background var(--dur-2) var(--ease-out);
}
input:focus, select:focus, textarea:focus {
  box-shadow: var(--ring);
  outline: none;
}
:is(a, button, [tabindex]):focus-visible {
  outline: none;
  box-shadow: var(--ring);
  border-radius: var(--radius-sm);
}

/* ---------------------------------------------------------------------
   7. TABLES
   Dense result tables are the app's real workhorse; this is where
   legibility pays off most.
   --------------------------------------------------------------------- */
table { border-collapse: separate; border-spacing: 0; }
thead th {
  position: sticky; top: 0; z-index: 2;
  backdrop-filter: blur(8px);
  background: color-mix(in srgb, var(--surface-strong) 92%, transparent);
  letter-spacing: .01em;
}
tbody tr { transition: background var(--dur-2) var(--ease-out); }
tbody tr:hover { background: var(--accent-soft); }
/* Zebra at very low contrast — enough to track a row across seven
   columns without turning the table into stripes. */
tbody tr:nth-child(even) { background: color-mix(in srgb, var(--text) 3%, transparent); }
tbody tr:nth-child(even):hover { background: var(--accent-soft); }

/* ---------------------------------------------------------------------
   8. BADGES
   --------------------------------------------------------------------- */
.status-badge {
  transition: transform var(--dur-2) var(--ease-spring), box-shadow var(--dur-2) var(--ease-out);
}
.status-badge:hover { transform: scale(1.04); }
@keyframes fx-pulse {
  0%,100% { box-shadow: 0 0 0 0 var(--accent-soft); }
  50%     { box-shadow: 0 0 0 6px transparent; }
}
/* Only the "needs a human" states pulse. If everything pulses, nothing
   is urgent. */
.status-badge.pending, .req-status.pending { animation: fx-pulse 2.6s var(--ease-in-out) infinite; }

/* ---------------------------------------------------------------------
   9. NAV
   --------------------------------------------------------------------- */
.nav-item, .dock-item {
  transition: background var(--dur-2) var(--ease-out),
              color var(--dur-2) var(--ease-out),
              transform var(--dur-1) var(--ease-spring);
}
.nav-item:hover { transform: translateX(-3px); }   /* RTL: toward the content */
.dock-item:active { transform: scale(.94); }

/* ---------------------------------------------------------------------
   10. MODALS
   --------------------------------------------------------------------- */
@keyframes fx-modal-in {
  from { opacity: 0; transform: translate3d(0, 18px, 0) scale(.985); }
  to   { opacity: 1; transform: none; }
}
.modal.show .modal-card, .modal.show > div { animation: fx-modal-in var(--dur-3) var(--ease-out) both; }
.modal { transition: opacity var(--dur-3) var(--ease-out); }

/* ---------------------------------------------------------------------
   11. SKELETON / LOADING
   --------------------------------------------------------------------- */
@keyframes fx-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
.loading-row {
  background: linear-gradient(90deg, transparent 20%, var(--sheen) 50%, transparent 80%);
  background-size: 200% 100%;
  animation: fx-shimmer 1.4s linear infinite;
}

/* ---------------------------------------------------------------------
   12. PRINT
   None of this belongs on a printed certificate or sampling form.
   --------------------------------------------------------------------- */
@media print {
  .fx-canvas { display: none !important; }
  * { animation: none !important; transition: none !important; box-shadow: none !important; }
}

/* ---------------------------------------------------------------------
   13. REDUCED MOTION
   Everything above degrades to instant. Colour, elevation and focus
   survive; only movement stops. fx-engine.js declines to start at all
   under this setting, so the WebGL layer is absent too.
   --------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .glass:hover, .table-card:hover, .btn-primary:hover { transform: none; }
  /* .dock keeps its centring transform even here — it is layout, not motion. */
  .dock { transform: translateX(-50%) !important; }
}

/* ---------------------------------------------------------------------
   14. COLLAPSIBLE SIDEBAR
   Driven by nav.js, which toggles .nav-collapsed on <html> and persists
   the choice. Everything below is width and opacity only — no `position`
   is touched, for the reason recorded in section 2.
   --------------------------------------------------------------------- */
:root { --sidebar-w-collapsed: 74px; }

.sidebar,
.main { transition: width var(--dur-3) var(--ease-out), margin var(--dur-3) var(--ease-out); }
/* Suppress that transition while restoring a saved state on load, so a
   remembered preference does not animate on every page. */
.nav-no-anim .sidebar,
.nav-no-anim .main,
.nav-no-anim .sidebar * { transition: none !important; }

.nav-collapsed .sidebar { width: var(--sidebar-w-collapsed); }
.nav-collapsed .main { margin-inline-end: var(--sidebar-w-collapsed); }

/* Labels fade and shrink out; icons stay. Using width/opacity rather
   than display:none keeps the fade smooth and the icons from jumping. */
.nav-collapsed .sidebar .logo,
.nav-collapsed .nav-section-label,
.nav-collapsed .help-card,
.nav-collapsed .nav-item > *:not(svg) {
  opacity: 0;
  max-width: 0;
  overflow: hidden;
  white-space: nowrap;
  transition: opacity var(--dur-2) var(--ease-out), max-width var(--dur-3) var(--ease-out);
}
.nav-collapsed .nav-item {
  justify-content: center;
  padding-inline: 0;
  gap: 0;   /* flex `gap` still reserves space before a 0-width label; a
               collapsed rail with a phantom half-gap next to every icon
               reads as misaligned even though nothing is visibly there. */
}
.nav-collapsed .sidebar-head { justify-content: center; }
/* The collapsed rail still needs the active item legible at a glance. */
.nav-collapsed .nav-item.active { background: var(--accent-soft); }

/* Sat at bottom:14px before, directly behind/under the "نیاز به
   راهنمایی دارید؟" help card that .sidebar-footer's margin-top:auto
   pushes to that same corner — the two overlapped, so the one control
   that opens the collapsed rail was hidden behind other chrome (reported
   as "can't find it"). Centered on the sidebar/content seam instead:
   fixed vertical position independent of scroll or footer content,
   straddling the boundary itself is the one spot a user's eye always
   crosses when scanning from nav to page, which is what makes an
   edge-handle discoverable in the first place. */
.sidebar-collapse {
  position: absolute;
  top: 50%;
  inset-inline-start: -15px;
  transform: translateY(-50%);
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface-strong);
  color: var(--text-dim);
  cursor: pointer;
  box-shadow: var(--shadow-ambient);
  z-index: 41;
  transition: transform var(--dur-2) var(--ease-spring),
              background var(--dur-2) var(--ease-out),
              color var(--dur-2) var(--ease-out);
}
.sidebar-collapse:hover { background: var(--accent-soft); color: var(--accent-2); transform: translateY(-50%) scale(1.1); }
.sidebar-collapse svg { transition: transform var(--dur-3) var(--ease-out); }
.nav-collapsed .sidebar-collapse svg { transform: rotate(180deg); }
/* The sidebar is display:none on mobile, so its toggle must go too. */
@media (max-width: 900px) { .sidebar-collapse { display: none; } }

/* ---------------------------------------------------------------------
   15. RICHER HOVER STATES
   The CSS half of each interaction; fx-engine.js draws the 3D half on
   the same element at the same moment.
   --------------------------------------------------------------------- */
.nav-item { position: relative; overflow: hidden; }
.nav-item::after {
  content: ""; position: absolute; inset-block: 0; inset-inline-start: 0;
  width: 3px; background: var(--accent);
  transform: scaleY(0); transform-origin: center;
  transition: transform var(--dur-2) var(--ease-spring);
}
.nav-item:hover::after, .nav-item.active::after { transform: scaleY(1); }

/* Table rows lift a hair toward the reader rather than only tinting. */
tbody tr { transition: background var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out); }
tbody tr:hover { transform: translateX(-2px); }

/* Chips and badges get a spring, links get an underline that grows from
   the reading edge rather than fading in flat. */
.role-chip, .module-pill, .perm-key-badge {
  transition: transform var(--dur-2) var(--ease-spring),
              background var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out);
}
.role-chip:hover, .module-pill:hover { transform: translateY(-2px) scale(1.03); }

.dir-card, .module-card { will-change: transform; }
.dir-card:hover, .module-card:hover { transform: translateY(-4px) scale(1.006); }

/* `.dock .dock-item { transition: opacity ... }` in style.css's SPATIAL
   DOCK block outranks the transform transition below for every real
   dock item (higher specificity) — deliberately, so nav.js's per-frame
   magnify lift on .dock-icon never fights a competing transition. This
   rule still governs :hover/:active press feedback on those items, and
   any lower-specificity .dock-item elsewhere. */
.dock-item { transition: transform var(--dur-2) var(--ease-spring), color var(--dur-2) var(--ease-out); }
.dock-item:hover { transform: translateY(-3px); }
