/* ═══════════════════════════════════════════════════════════
   Control Room Academy — Design System v3
   "Mission Critical" identity — Navy / Royal / Electric Blue
   Reference register: Genetec · Milestone · Motorola Solutions
   (enterprise security operations, NOT consumer e-learning)
═══════════════════════════════════════════════════════════ */

@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=Montserrat:wght@600;700;800;900&family=Inter:wght@300;400;500;600;700&family=Roboto+Mono:wght@400;500;700&family=Noto+Sans+Arabic:wght@300;400;500;600;700;800;900&display=swap');

/* ── Dark Theme (default — the control room is a dark room) ── */
:root, [data-theme="dark"] {
  /* ── BRAND CONSTANTS — LOCKED v1.0 (2026-07-25) ──────────
     Do not alter without a Brand Book revision. */
  --navy:          #081C3A;   /* Deep Navy      */
  --control-blue:  #0057FF;   /* Control Blue   */
  --signal-cyan:   #1EC8FF;   /* Signal Cyan    */
  --slate-gray:    #5D6978;   /* Slate Gray     */
  --brand-bg:      #F5F7FA;   /* Background     */

  /* Neutral ramp (BOS) */
  --gray-0:    #FFFFFF;
  --gray-50:   #F5F7FA;
  --gray-100:  #E9EDF2;
  --gray-500:  #64748B;
  --gray-900:  #111827;

  /* Link colour on LIGHT surfaces. Control Blue on #F5F7FA computes to
     4.50:1 — passing with zero margin, which fails on any off-white drift.
     #0052CC gives 5.3:1 and is visually indistinguishable at text sizes. */
  --link-on-light: #0052CC;

  /* Backgrounds — navy is the console surface */
  --bg-1:        #081C3A;   /* Deep Navy — primary canvas */
  --bg-2:        #0B2447;   /* elevated panel */
  --bg-3:        #0F2D55;   /* raised card */
  --bg-4:        #163866;   /* highest / hover fill */
  --bg-deep:     #050F22;   /* recessed: sidebars, wells */
  --bg-hover:    rgba(30,200,255,0.07);
  --bg-active:   rgba(0,87,255,0.18);
  --bg-glass:    rgba(8,28,58,0.72);

  /* Text — contrast verified against --bg-1 (#081C3A) */
  --text-primary:   #EAF0F7;  /* 16.1:1 */
  --text-secondary: #AFBAC9;  /*  8.3:1 */
  --text-tertiary:  #94A0B2;  /*  6.3:1 */
  --text-muted:     #7C8896;  /*  4.7:1 — smallest size still safe for body */
  --text-disabled:  #5D6978;  /* Slate Gray — 3.0:1, large/decorative/disabled ONLY */

  /* Borders */
  --border:         rgba(234,240,247,0.10);
  --border-2:       rgba(234,240,247,0.16);
  --border-focus:   rgba(30,200,255,0.70);
  --border-strong:  rgba(234,240,247,0.26);

  /* Brand / Primary */
  --brand-300:  #8FDCFF;
  --brand-400:  #1EC8FF;   /* Signal Cyan */
  --brand-500:  #0057FF;   /* Electric Blue */
  --brand-600:  #0052D6;
  --brand-700:  #0041AB;
  --brand-800:  #00337F;
  --brand-900:  #081C3A;   /* Deep Navy */
  --primary:    #0057FF;
  --primary-hover: #1EC8FF;

  /* Operational status accents (SOC semantics, not decoration) */
  --accent-cyan:   #1EC8FF;
  --accent-green:  #16A34A;
  --accent-orange: #F59E0B;

  /* Semantic state colours — locked, palette-independent */
  --state-success: #16A34A;
  --state-warning: #F59E0B;
  --state-danger:  #DC2626;
  --state-info:    #0284C7;

  /* Semantic */
  --success:    #16A34A;
  --warning:    #F59E0B;
  --error:      #DC2626;
  --danger:     #DC2626; /* alias — pages use both names */
  --info:       #0284C7;

  /* Aliases used by some pages */
  --bg:          var(--bg-1);
  --card-shadow: var(--shadow-md);

  /* Layout */
  --topbar-bg:  rgba(8,28,58,0.88);
  --card-bg:    rgba(11,36,71,0.80);
  --card-border: rgba(234,240,247,0.10);
  --sidebar-bg: #050F22;
  --input-bg:   rgba(234,240,247,0.06);
  --overlay:    rgba(5,15,34,0.80);

  /* Typography */
  --font-display: 'Montserrat', 'Noto Sans Arabic', system-ui, sans-serif;
  --font-body:    'Inter', 'Noto Sans Arabic', system-ui, sans-serif;
  --font-mono:    'Roboto Mono', ui-monospace, SFMono-Regular, monospace;

  /* Career ladder — 4 ranks, LOCKED (ADR-016, supersedes ADR-004).
     A career path, not "Level 1..5".
     Progression reads as ascending prestige: slate → blue → cyan → gold */
  /* Six-rank ladder, ADR-018 (supersedes the four of ADR-016).
     Progression reads slate -> blue -> cyan -> gold -> copper -> deep red:
     cool and neutral at entry, warming as autonomy and accountability rise.
     Ranks 5 and 6 continue past gold rather than repeating it, so the top of
     the ladder is distinguishable at a glance.

     ⚠ THESE ARE ACCENT TOKENS, NOT TEXT COLOURS. Measured against --bg-1 on
     both surfaces (dark #081C3A / light #F5F7FA):

         rank-1  3.04 / 5.21      rank-4  7.66 / 2.07
         rank-2  3.08 / 5.14      rank-5  4.67 / 3.39
         rank-3  8.70 / 1.82      rank-6  2.82 / 5.61

     Not one clears 4.5:1 on BOTH — the cool ranks fail on dark, the warm ones
     on light. That is inherent to a hue ramp spanning the wheel and was already
     true of ranks 1-4 before 5-6 were added. Use them for badge fills, rules,
     dots and icon strokes. For rank text on a surface, pair with a tinted
     background or use the --cra-fg-* text tokens, which are solved against the
     worst-case surface. Verify before using one as a foreground colour. */
  --rank-1-cadet:        #5D6978;  /* Cadet — free, credential only        */
  --rank-2-operator:     #0057FF;  /* CCTV Operator                · CCO   */
  --rank-3-senior:       #1EC8FF;  /* Senior CCTV Operator         · CSCO  */
  --rank-4-supervisor:   #E0A33C;  /* CCTV Supervisor              · CCS   */
  --rank-5-manager:      #D2691E;  /* CCTV Control Room Manager    · CCM   */
  --rank-6-headops:      #B03A2E;  /* Head of Surveillance Ops     · CHSO  */

  /* Deferred — post-V1 (ADR-015/016). Retained, NOT used in V1 UI.
     Ranks 6–7 of the superseded ladder return when scope broadens
     beyond CCTV into full security operations.
       --rank-2-junior:     #6E8CA8;   Junior Operator            (retired)
       --rank-5-supervisor: #C08A4A;   Shift Supervisor           (retired)
       --rank-6-manager:    #C8D2E0;   Control Room Manager       (deferred)
       --rank-7-socmanager: #E0A33C;   Security Operations Manager(deferred)
     Old progression: slate → steel → blue → cyan → bronze → silver → gold */

  /* Spacing — strict 8px grid */
  --s-1:  4px;
  --s-2:  8px;
  --s-3:  12px;
  --s-4:  16px;
  --s-5:  24px;
  --s-6:  32px;
  --s-7:  48px;
  --s-8:  64px;
  --s-9:  96px;

  /* Radius — BOS scale: cards 16 · buttons & inputs 12 · badges pill */
  --r-xs:      4px;
  --r-sm:      8px;
  --r-md:      12px;  /* buttons, inputs */
  --r-lg:      16px;  /* cards */
  --r-xl:      20px;
  --r-2xl:     28px;
  --r-card:    16px;
  --r-button:  12px;
  --r-input:   12px;
  --r-badge:   9999px;
  --r-full: 9999px;

  /* Shadows */
  --shadow-sm:  0 1px 4px rgba(0,0,0,0.4);
  --shadow-md:  0 4px 16px rgba(0,0,0,0.5);
  --shadow-lg:  0 12px 40px rgba(0,0,0,0.6);
  --shadow-xl:  0 24px 64px rgba(0,0,0,0.7);
  --shadow-brand: 0 8px 32px rgba(0,87,255,0.35);
  --glow:       0 0 40px rgba(30,200,255,0.18);

  /* Transitions */
  --t-fast:   0.15s ease;
  --t-med:    0.25s ease;
  --t-normal: 0.30s ease;
  --t-slow:   0.50s ease;

  /* Easing — natural deceleration curves */
  --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
  --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-out-expo:  cubic-bezier(0.16, 1, 0.3, 1);

  /* Gradients — brand blues only; no purple drift */
  --grad-brand: linear-gradient(135deg, #0057FF, #1EC8FF);

  /* Text-bearing brand surfaces. --grad-brand terminates on Signal Cyan, where
     white measures 1.95:1 — unreadable at any size, and it fails in BOTH
     themes because the fill does not follow the page background. This ramp
     stops at Control Blue instead: white is 8.96:1 on --brand-700 and 5.52:1
     on --brand-500, so it clears AA across the whole sweep (worst point 5.52).
     Same navy-to-royal hue family — only the cyan terminus is dropped, no hue
     is introduced. Mirrors --cra-grad-ink in components.css, which solved the
     identical problem for the `cra-` layer and documents the same 2.0:1.
     --grad-brand itself is UNCHANGED and stays correct for fills that carry
     no text: meters, bars, rules, the enterprise-console fill. */
  --grad-brand-ink: linear-gradient(135deg, var(--brand-700), var(--brand-500));
  --grad-hero:  linear-gradient(135deg, rgba(4,14,29,0.94) 0%, rgba(7,26,53,0.90) 55%, rgba(10,49,117,0.86) 100%);
}

/* ── Light Theme ──────────────────────────────────────── */
[data-theme="light"] {
  --bg-1:        #F5F7FA;   /* Background */
  --bg-2:        #FFFFFF;
  --bg-3:        #E9EEF5;
  --bg-4:        #D9E2EC;
  --bg-deep:     #E9EEF5;
  --bg-hover:    rgba(8,28,58,0.04);
  --bg-active:   rgba(0,87,255,0.08);
  --bg-glass:    rgba(255,255,255,0.88);

  --text-primary:   #081C3A;  /* Deep Navy — 15.81:1 on --bg-1 */
  --text-secondary: #3A4557;  /*  9.02:1 on --bg-1 */
  --text-tertiary:  #4B5667;  /*  6.92:1 on --bg-1 */
  /* Slate Gray read 5.21:1 on --bg-1 but only 4.27:1 on --bg-4, the hover
     fill it lands on in tables and list rows — under AA for body text.
     Two steps darker clears every light surface. */
  --text-muted:     #586472;  /*  5.62:1 on --bg-1 · 4.61:1 on --bg-4 */
  /* Was #98A3B2 = 2.38:1. Disabled text is exempt from 1.4.3, but at that
     ratio it was invisible rather than merely de-emphasised; 1.4.11's 3:1
     is the floor that keeps a disabled control identifiable. */
  --text-disabled:  #78859A;  /*  3.48:1 on --bg-1 */

  --border:         rgba(8,28,58,0.10);   /*  1.22:1 on --bg-1 — DIVIDERS ONLY */
  --border-2:       rgba(8,28,58,0.16);   /*  1.39:1 on --bg-1 — DIVIDERS ONLY */
  /* Focus rings and control outlines are what 1.4.11 is actually about, so
     these two carry enough alpha to clear 3:1.
     ⚠ CORRECTION (contrast pass): the two hairline tokens above are NOT only
     decorative. --border draws the .input/.select/.textarea box and the
     .btn-ghost / .ctrl-btn / .nav-toggle boundary, and --border-2 draws
     .btn-outline — all of which are interface components under 1.4.11 and
     were rendering at 1.22-1.39:1, i.e. an invisible form field on white.
     The tokens keep their light values (27 files draw dividers with them);
     the control boundaries are re-pointed at --border-strong in the
     "light-theme control boundaries" block below. */
  --border-focus:   rgba(0,87,255,0.85);  /*  4.09:1 composited on --bg-1 */
  --border-strong:  rgba(8,28,58,0.50);   /*  3.26:1 on --bg-1 · 3.31 --bg-2
                                              3.20 --bg-3 · 3.11 --bg-4      */

  /* Signal Cyan is solved for navy. On a white console it collapses to
     1.82:1, so the light theme carries the same hue at working lightness —
     hue 194° vs the brand's 196°, i.e. the same cyan, not a new colour.
     --signal-cyan itself is a locked brand constant and is NOT touched. */
  --brand-400:      #0A6C8F;  /*  5.50:1 on --bg-1 (was #1EC8FF, 1.82:1) */
  --accent-cyan:    #0A6C8F;

  /* Semantic accents re-solved for light surfaces. Same hues, lower
     lightness — these are read as text (.form-error, .stat-delta,
     .status-live, alert and badge foregrounds), not just as fills. */
  --success:        #0F7A37;  /*  5.07:1 on --bg-1 (was #16A34A, 3.07:1) */
  --warning:        #8F5C00;  /*  5.29:1 on --bg-1 (was #F59E0B, 2.00:1) */
  --error:          #C81E1E;  /*  5.35:1 on --bg-1 (was #DC2626, 4.50:1) */
  --danger:         #C81E1E;
  --info:           #0369A1;  /*  5.53:1 on --bg-1 (was #0284C7, 3.82:1) */
  --state-success:  #0F7A37;
  --state-warning:  #8F5C00;
  --state-danger:   #C81E1E;
  --state-info:     #0369A1;
  --accent-green:   #0F7A37;
  --accent-orange:  #8F5C00;

  --topbar-bg:  rgba(255,255,255,0.92);
  --card-bg:    #FFFFFF;
  --card-border: rgba(8,28,58,0.10);
  --sidebar-bg: #FFFFFF;
  --input-bg:   #F5F7FA;
  --overlay:    rgba(8,28,58,0.55);

  --shadow-sm:  0 1px 4px rgba(8,28,58,0.08);
  --shadow-md:  0 4px 16px rgba(8,28,58,0.10);
  --shadow-lg:  0 12px 40px rgba(8,28,58,0.12);
  --shadow-xl:  0 24px 64px rgba(8,28,58,0.16);
  --shadow-brand: 0 8px 32px rgba(0,87,255,0.22);
  --glow:       0 0 40px rgba(0,87,255,0.10);

  --grad-hero:  linear-gradient(135deg, rgba(245,247,250,0.94) 0%, rgba(233,238,245,0.90) 100%);
}

/* ── Light-theme control boundaries — WCAG 1.4.11 (3:1) ───────────────────
   A control's visible boundary is the only thing that says "this is a field
   you can type in". On the navy console the hairline reads fine; on white it
   did not. Measured before this block, composited over the surface each
   control actually sits on:

     .input / .select / .textarea   --border   over --input-bg   1.22:1
     .btn-ghost / .ctrl-btn         --border   over --bg-3       1.22:1
     .nav-toggle                    --border   over --bg-3       1.22:1
     .btn-outline                   --border-2 over --bg-1       1.39:1

   All four now use --border-strong, an EXISTING token whose whole purpose is
   this: 3.26:1 on --bg-1, 3.31 on --bg-2, 3.20 on --bg-3, 3.11 on --bg-4.
   The .ctrl-btn boundary against the page reads 3.48:1 and .btn-ghost:hover
   3.79:1, because the fill underneath it is darker than the page.

   No token value changed and no token was added — --border and --border-2
   keep their light values, since 27 files draw genuine dividers with them and
   darkening those would put a heavy rule under every table row and card.
   Dividers are decoration and are exempt; these controls are not.       */
[data-theme="light"] .input,
[data-theme="light"] .select,
[data-theme="light"] .textarea,
[data-theme="light"] .btn-ghost,
[data-theme="light"] .btn-outline,
[data-theme="light"] .ctrl-btn,
[data-theme="light"] .ctrl-btn:hover,
[data-theme="light"] .nav-toggle {
  border-color: var(--border-strong);
}

/* ══════════════════════════════════════════════════════════
   BASE RESET & GLOBALS
══════════════════════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html {
  scroll-behavior: smooth;
  scroll-padding-top: 80px; /* fixed navbar — keep anchor targets visible */
  font-size: 16px;
}

body {
  font-family: var(--font-body);
  background: var(--bg-1);
  color: var(--text-primary);
  line-height: 1.65;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* Montserrat has no Arabic coverage — Noto leads in RTL */
[dir="rtl"] {
  --font-display: 'Noto Sans Arabic', 'Montserrat', system-ui, sans-serif;
  --font-body:    'Noto Sans Arabic', 'Inter', system-ui, sans-serif;
}

/* Headings carry the brand voice: Montserrat ExtraBold, tight but never cramped */
h1, h2, h3, h4,
.section-title, .hero-title, .page-title, .auth-title, .cta-title, .stat-value {
  font-family: var(--font-display);
  font-weight: 800;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
[dir="rtl"] h1, [dir="rtl"] h2, [dir="rtl"] h3, [dir="rtl"] h4 { letter-spacing: 0; }

/* Numerals read like an operations console */
.mono, .stat-value, .stat-num, .pricing-amount, .hero-stat-num,
.cert-number-display, .progress-txt, .course-price, code, kbd {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

a { color: inherit; text-decoration: none; }
button { cursor: pointer; font-family: inherit; }
img { max-width: 100%; display: block; }

/* ══════════════════════════════════════════════════════════
   NAVBAR
══════════════════════════════════════════════════════════ */
.navbar {
  position: fixed;
  top: 0;
  inset-inline-start: 0;
  inset-inline-end: 0;
  z-index: 200;
  height: 64px;
  background: var(--topbar-bg);
  border-bottom: 1px solid var(--border);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  transition: background var(--t-normal), box-shadow var(--t-normal);
}

.navbar.scrolled {
  box-shadow: var(--shadow-md);
}

.nav-inner {
  max-width: 1280px;
  margin: 0 auto;
  height: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 24px;
}

.nav-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  color: var(--text-primary);
  flex-shrink: 0;
  margin-inline-end: 8px;
}

/* Nav brand tile. Carries the Command Ring mark, not the letters "CRA".
   Done here rather than in markup so all 16 pages using <div class="brand-logo">CRA</div>
   pick it up from one rule and no HTML changes are needed.

   The tile is navy with the mark laid over it — the same composition as
   cra-appicon.svg — rather than the appicon itself, so the corner radius stays on
   --r-md (12px) and matches every other rounded element. The appicon's own rx
   would scale to ~7.9px at this size and read as a different shape.

   cra-mark.svg is drawn for dark grounds (white outer ring, Control Blue inner,
   Signal Cyan vector), which is why the tile is --navy and not --grad-brand.

   font-size:0 hides the "CRA" text node visually while leaving it in the
   accessibility tree, so the link keeps a text accessible name. */
.brand-logo {
  width: 36px;
  height: 36px;
  border-radius: var(--r-md);
  background: var(--navy) url("/assets/brand/cra-mark.svg") center / 24px 24px no-repeat;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0;
  color: transparent;
  box-shadow: var(--shadow-brand);
  flex-shrink: 0;
}

.brand-text {
  font-weight: 700;
  font-size: 15px;
  white-space: nowrap;
  display: none;
}

@media (min-width: 600px) { .brand-text { display: block; } }

.nav-links {
  display: flex;
  align-items: center;
  gap: 2px;
  flex: 1;
}

.nav-link {
  padding: 6px 14px;
  border-radius: var(--r-md);
  font-size: 14px;
  font-weight: 500;
  color: var(--text-secondary);
  transition: all var(--t-fast);
  white-space: nowrap;
}

.nav-link:hover, .nav-link.active {
  color: var(--text-primary);
  background: var(--bg-hover);
}

.nav-end {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-inline-start: auto;
  flex-shrink: 0;
}

.ctrl-bar {
  display: flex;
  align-items: center;
  gap: 4px;
}

.ctrl-btn {
  height: 34px;
  min-width: 34px;
  padding: 0 10px;
  border-radius: var(--r-md);
  background: var(--bg-3);
  border: 1px solid var(--border);
  color: var(--text-secondary);
  font-size: 13px;
  font-weight: 500;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  transition: all var(--t-fast);
  cursor: pointer;
  white-space: nowrap;
}

.ctrl-btn:hover {
  background: var(--bg-4);
  color: var(--text-primary);
  border-color: var(--border-2);
}

.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--r-md);
  background: var(--bg-3);
  border: 1px solid var(--border);
  color: var(--text-primary);
  font-size: 18px;
  cursor: pointer;
}

/* ── Mobile navigation ────────────────────────────────────
   The drawer itself was never the problem — the button that opens it was.
   .nav-inner is a flex row whose brand, .nav-end and .ctrl-bar are all
   flex-shrink:0, so at 375px its content measured 533px. .nav-toggle is the
   last child and the only shrinkable one, so it collapsed to 18px and was
   pushed past the inline-end edge, where body{overflow-x:hidden} clipped it.
   The hamburger was unreachable, and with it every link on the site.

   Fix: stop competing for one row. .nav-inner wraps; the bar keeps only the
   brand, the language/theme controls and the toggle (~200px at 375px), and
   the links and the auth buttons each take a full-width row below it. The
   fixed navbar grows downward when open instead of overflowing sideways, so
   there are no magic panel heights to keep in sync with its 64px.

   .nav-end becomes display:contents so its two children can be placed
   independently — .ctrl-bar stays in the bar, #nav-auth-slot drops into the
   drawer — without touching markup shared by 13 pages.

   Every offset here is logical (inset-inline / margin-inline), and order+wrap
   follow the flex writing mode, so RTL and LTR both lay out correctly.

   Breakpoint is 940px, not the old 768px: measured across the widest nav
   (contact.html, five links) the desktop row does not actually fit until
   940px, so 769-939px was overflowing with the toggle already hidden — the
   same unreachable-nav bug, just quieter. 940px is also what home.html's
   own nav already uses. */
@media (max-width: 940px) {
  .navbar { height: auto; }

  .nav-inner {
    flex-wrap: wrap;
    height: auto;
    min-height: 64px;
    row-gap: 0;
    padding: 0 16px;
  }

  /* Row 1. The explicit height keeps the bar at 64px once the rows below it
     wrap in — otherwise the line box would collapse to the 36px controls. */
  .nav-brand  { order: 1; height: 64px; min-width: 0; }
  .ctrl-bar   { order: 2; margin-inline-start: auto; flex-shrink: 0; }
  .nav-toggle { order: 3; display: flex; flex-shrink: 0; margin-inline-start: 8px; }

  /* .nav-end stops being a box; its children become items of .nav-inner. */
  .nav-end { display: contents; }

  /* Once .nav-end is display:contents its children are flex items of
     .nav-inner and need an explicit order — the default is 0, which sorts
     BEFORE .nav-brand's 1. On the 13 drawer pages this was invisible because
     the slot is display:none until the drawer opens, but on payment.html
     (no .nav-links, so nothing hides it) row 1 rendered as
     auth · brand · controls. 3 is free there: it is .nav-toggle's order, and
     payment.html has no toggle. When the drawer opens, the rule below
     re-orders the slot to 5 on higher specificity. */
  .nav-end > *:not(.ctrl-bar) { order: 3; }

  /* Rows 2 and 3. flex-basis:100% is what forces the wrap. */
  .nav-links {
    order: 4;
    display: none;
    flex: 0 0 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
    padding: 12px 0;
    background: var(--bg-2);
    border-top: 1px solid var(--border);
    max-height: calc(100vh - 64px);
    overflow-y: auto;
  }
  .nav-links.open { display: flex; animation: nav-drop 0.28s var(--ease-out-quart); }

  /* The auth pair is what made the row overflow, so it lives in the drawer.
     Matched structurally rather than by id: the slot is #nav-auth-slot on 12
     pages but #nav-auth on course.html, and it carries an inline
     display:flex, so !important is the only way to reach it from here.

     ⚠ The leading `.nav-links ~` is load-bearing, not decoration. payment.html
     has a .nav-end and a .ctrl-bar but NO .nav-links and no .nav-toggle — its
     navbar is brand + controls + auth slot only. An unscoped
     `.nav-end > *:not(.ctrl-bar)` hid that page's auth slot at <=940px with no
     drawer and no hamburger to bring it back, so Log in / Log out was
     unreachable on a phone: the same defect this block exists to fix, moved to
     a different page. Scoping the hide to navbars that actually own a drawer
     means a page without .nav-links simply keeps its auth buttons inline.
     Sibling order is safe to rely on — .nav-links precedes .nav-end in the
     markup of all 13 pages that have both. */
  .nav-links ~ .nav-end > *:not(.ctrl-bar) { display: none !important; }
  .nav-links.open ~ .nav-end > *:not(.ctrl-bar) {
    order: 5;
    display: flex !important;
    flex: 0 0 100%;
    gap: 8px;
    padding: 0 0 16px;
    background: var(--bg-2);
    box-shadow: var(--shadow-lg);
    animation: nav-drop 0.28s var(--ease-out-quart);
  }
  /* Share the row evenly — "Log in" and "Start free" are different widths. */
  .nav-links.open ~ .nav-end > *:not(.ctrl-bar) > * { flex: 1; }

  .nav-link { width: 100%; padding: 10px 16px; }
}

@keyframes nav-drop {
  from { opacity: 0; transform: translateY(-10px); }
  to   { opacity: 1; transform: none; }
}

/* ══════════════════════════════════════════════════════════
   BUTTONS
══════════════════════════════════════════════════════════ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 10px 22px;
  border-radius: var(--r-md);
  font-size: 14px;
  font-weight: 600;
  border: none;
  cursor: pointer;
  transition: all var(--t-fast);
  white-space: nowrap;
  text-decoration: none;
}

/* White on --grad-brand was 1.95:1 at the cyan terminus — the site's primary
   call to action, unreadable across roughly its last third, in both themes.
   --grad-brand-ink keeps the navy-to-royal sweep and the same hue family;
   white is 8.96:1 at the start and 5.52:1 at the end. Hover applies
   brightness(1.08), which lands the end stop on #005EFF — white 5.19:1. */
.btn-primary {
  background: var(--grad-brand-ink);
  color: white;
  box-shadow: var(--shadow-brand);
}
.btn-primary:hover {
  transform: translateY(-1px);
  box-shadow: 0 12px 40px rgba(37,99,235,0.45);
  filter: brightness(1.08);
}

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

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

.btn-sm { padding: 7px 14px; font-size: 13px; }
.btn-lg { padding: 14px 32px; font-size: 16px; border-radius: var(--r-lg); }
.btn-full { width: 100%; }

.btn:active { transform: translateY(0) scale(0.97); }
.btn:focus-visible,
.ctrl-btn:focus-visible,
.nav-link:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}
.btn[disabled] { opacity: 0.6; cursor: not-allowed; }

/* ══════════════════════════════════════════════════════════
   CARDS
══════════════════════════════════════════════════════════ */
.card {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: var(--r-xl);
  backdrop-filter: blur(12px);
  transition: all var(--t-med);
}

.card:hover {
  border-color: rgba(59,130,246,0.25);
  box-shadow: var(--shadow-lg);
  transform: translateY(-3px);
}

/* ══════════════════════════════════════════════════════════
   BADGES
══════════════════════════════════════════════════════════ */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 12px;
  border-radius: var(--r-full);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.3px;
}

.badge-blue   { background: rgba(59,130,246,0.15);  color: var(--brand-400); border: 1px solid rgba(59,130,246,0.25); }
.badge-green  { background: rgba(16,185,129,0.15);  color: #34d399; border: 1px solid rgba(16,185,129,0.25); }
.badge-purple { background: rgba(139,92,246,0.15);  color: #a78bfa; border: 1px solid rgba(139,92,246,0.25); }
.badge-orange { background: rgba(249,115,22,0.15);  color: #fb923c; border: 1px solid rgba(249,115,22,0.25); }
.badge-red    { background: rgba(239,68,68,0.15);   color: #f87171; border: 1px solid rgba(239,68,68,0.25); }
.badge-gray   { background: var(--bg-hover);        color: var(--text-secondary); border: 1px solid var(--border); }

/* The badge foregrounds above are the 300/400 tints of each hue — solved for a
   navy console. On a white one they land between 1.5:1 and 2.1:1. Light theme
   swaps in the 700-ish end of the same hues and drops the fill from 15% to 10%,
   because at 15% the tint darkens the surface enough to pull orange and green
   back under 4.5:1. Badge text is 12px/600, so 4.5:1 applies — it is not
   "large text". Ratios measured on the composited fill over --bg-1. */
[data-theme="light"] .badge-blue   { background: rgba(59,130,246,0.10);  color: var(--brand-400); }   /* 4.92:1 */
[data-theme="light"] .badge-green  { background: rgba(16,185,129,0.10);  color: var(--success); }     /* 4.64:1 */
[data-theme="light"] .badge-purple { background: rgba(139,92,246,0.10);  color: #6D28D9; }            /* 5.89:1 */
[data-theme="light"] .badge-orange { background: rgba(249,115,22,0.10);  color: var(--warning); }     /* 4.80:1 */
[data-theme="light"] .badge-red    { background: rgba(239,68,68,0.10);   color: var(--error); }       /* 4.70:1 */

/* ══════════════════════════════════════════════════════════
   FORMS
══════════════════════════════════════════════════════════ */
.input, .select, .textarea {
  width: 100%;
  padding: 11px 16px;
  background: var(--input-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  color: var(--text-primary);
  font-size: 14px;
  font-family: inherit;
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
  outline: none;
}

.input:focus, .select:focus, .textarea:focus {
  border-color: var(--border-focus);
  box-shadow: 0 0 0 3px rgba(59,130,246,0.12);
}

.input::placeholder { color: var(--text-muted); }
.label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
.form-group { margin-bottom: 16px; }
.form-error { font-size: 12px; color: var(--error); margin-top: 4px; }

/* ══════════════════════════════════════════════════════════
   ALERTS / NOTIFICATIONS
══════════════════════════════════════════════════════════ */
.alert {
  padding: 14px 18px;
  border-radius: var(--r-md);
  font-size: 14px;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin-bottom: 16px;
}

.alert-success { background: rgba(16,185,129,0.10); border: 1px solid rgba(16,185,129,0.25); color: #34d399; }
.alert-error   { background: rgba(239,68,68,0.10);  border: 1px solid rgba(239,68,68,0.25);  color: #f87171; }
.alert-info    { background: rgba(59,130,246,0.10); border: 1px solid rgba(59,130,246,0.25); color: var(--brand-400); }
.alert-warn    { background: rgba(245,158,11,0.10); border: 1px solid rgba(245,158,11,0.25); color: #fbbf24; }

/* Same problem as the badges, and it matters more here: this is the element
   UI.showAlert() writes every server error into, so on light theme the
   failure mode was an error message the user cannot read. The 10% fills
   already work on white; only the foregrounds move. */
[data-theme="light"] .alert-success { color: var(--success); }  /* 4.95:1 */
[data-theme="light"] .alert-error   { color: var(--error); }    /* 5.02:1 */
[data-theme="light"] .alert-info    { color: var(--info); }     /* 5.31:1 */
[data-theme="light"] .alert-warn    { color: var(--warning); }  /* 5.25:1 */

/* ══════════════════════════════════════════════════════════
   DASHBOARD SIDEBAR LAYOUT
══════════════════════════════════════════════════════════ */
.dash-layout {
  display: flex;
  min-height: 100vh;
}

.sidebar {
  width: 240px;
  background: var(--sidebar-bg);
  border-inline-end: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  position: fixed;
  top: 0;
  bottom: 0;
  inset-inline-start: 0;
  z-index: 100;
  overflow-y: auto;
}

.sidebar-logo {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 20px 20px 16px;
  text-decoration: none;
  color: var(--text-primary);
  border-bottom: 1px solid var(--border);
  margin-bottom: 8px;
}

/* 11px/900 white lettering — 4.5:1 applies, it is not "large text". */
.sidebar-logo-icon {
  width: 32px;
  height: 32px;
  border-radius: var(--r-md);
  background: var(--grad-brand-ink);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 900;
  font-size: 11px;
  color: white;
  flex-shrink: 0;
}

.sidebar-nav { padding: 0 12px; flex: 1; }

.sidebar-label {
  font-size: 10px;
  font-weight: 700;
  color: var(--text-muted);
  letter-spacing: 1.2px;
  text-transform: uppercase;
  padding: 12px 8px 6px;
}

.sidebar-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  border-radius: var(--r-md);
  font-size: 14px;
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  transition: all var(--t-fast);
  margin-bottom: 2px;
}

.sidebar-item:hover { background: var(--bg-hover); color: var(--text-primary); }
.sidebar-item.active { background: var(--bg-active); color: var(--brand-400); font-weight: 600; }
.sidebar-item .si { width: 18px; text-align: center; flex-shrink: 0; }

.topbar {
  height: 56px;
  background: var(--topbar-bg);
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  padding: 0 24px;
  gap: 12px;
  backdrop-filter: blur(16px);
  position: sticky;
  top: 0;
  z-index: 50;
}

.main-content {
  margin-inline-start: 240px;
  flex: 1;
  min-width: 0;
}

.page-content { padding: 24px; max-width: 1100px; }

@media (max-width: 900px) {
  .sidebar { transform: translateX(-100%); transition: transform var(--t-normal); }
  [dir="rtl"] .sidebar { transform: translateX(100%); }
  .sidebar.open { transform: translateX(0); }
  .main-content { margin-inline-start: 0; }
}

/* ══════════════════════════════════════════════════════════
   UTILITIES
══════════════════════════════════════════════════════════ */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-2 { gap: 8px; }
.gap-3 { gap: 12px; }
.gap-4 { gap: 16px; }
.text-sm { font-size: 13px; }
.text-xs { font-size: 11px; }
.font-bold { font-weight: 700; }
.text-muted { color: var(--text-muted); }
.text-secondary { color: var(--text-secondary); }
.text-primary { color: var(--primary); }

/* Scroll reveal — content is visible by default; the .js class (added by
   app.js) opts in to the animated entrance, so nothing ships blank if JS
   fails or the observer never fires. */
.js .reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.7s var(--ease-out-quint), transform 0.7s var(--ease-out-quint);
}
.js .reveal.visible {
  opacity: 1;
  transform: none;
}
.reveal-delay-1 { transition-delay: 0.08s; }
.reveal-delay-2 { transition-delay: 0.16s; }
.reveal-delay-3 { transition-delay: 0.24s; }
.reveal-delay-4 { transition-delay: 0.32s; }
.reveal-delay-5 { transition-delay: 0.40s; }

/* Staggered list entrance — set style="--i:N" per item */
@keyframes rise-in {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}
.rise-in {
  animation: rise-in 0.55s var(--ease-out-quart) both;
  animation-delay: calc(var(--i, 0) * 60ms);
}

/* Pulse animation */
@keyframes pulse-dot {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%       { opacity: 0.5; transform: scale(0.8); }
}
@keyframes float {
  0%, 100% { transform: translateY(0px); }
  50%       { transform: translateY(-8px); }
}
@keyframes spin-slow {
  to { transform: rotate(360deg); }
}
@keyframes gradient-shift {
  0%   { background-position: 0% 50%; }
  50%  { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

/* Spinner (used by course/payment/dashboard/admin loading states) */
.spinner {
  display: inline-block;
  width: 22px;
  height: 22px;
  border: 2.5px solid var(--border-2);
  border-top-color: var(--primary);
  border-radius: 50%;
  animation: spin-slow 0.7s linear infinite;
  flex-shrink: 0;
}
.spinner-sm { width: 15px; height: 15px; border-width: 2px; }

/* Skeleton loader */
.skeleton {
  background: linear-gradient(90deg, var(--bg-3) 25%, var(--bg-4) 50%, var(--bg-3) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: var(--r-md);
}
@keyframes shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* Progress bar */
.progress {
  height: 6px;
  background: var(--bg-4);
  border-radius: var(--r-full);
  overflow: hidden;
}
.progress-bar {
  height: 100%;
  background: var(--grad-brand);
  border-radius: var(--r-full);
  transition: width 0.6s ease;
}
/* The fill IS the information here, so 1.4.11's 3:1 applies against the
   track. On navy the cyan reads 6.4:1 against --bg-4 and --grad-brand is
   right. On the light track (--bg-4 #D9E2EC) the cyan end collapses to
   1.49:1 — a bar you cannot see. The ink ramp gives 6.84:1 at the start and
   4.21:1 at the end. Dark theme is deliberately left on --grad-brand. */
[data-theme="light"] .progress-bar { background: var(--grad-brand-ink); }

/* Avatar — white initials, 12px/700 at .avatar-sm, so 4.5:1 applies there. */
.avatar {
  border-radius: 50%;
  object-fit: cover;
  background: var(--grad-brand-ink);
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: 700;
  flex-shrink: 0;
}
.avatar-sm  { width: 32px;  height: 32px;  font-size: 12px; }
.avatar-md  { width: 42px;  height: 42px;  font-size: 15px; }
.avatar-lg  { width: 56px;  height: 56px;  font-size: 20px; }

/* Empty state */
.empty-state {
  text-align: center;
  padding: 56px 24px;
  color: var(--text-muted);
}
.empty-state .es-icon { font-size: 52px; margin-bottom: 16px; }
.empty-state .es-title { font-size: 18px; font-weight: 700; color: var(--text-secondary); margin-bottom: 8px; }
.empty-state .es-sub   { font-size: 14px; margin-bottom: 20px; }

/* Divider */
.divider {
  height: 1px;
  background: var(--border);
  margin: 24px 0;
}

/* Stat card */
.stat-card {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: var(--r-xl);
  padding: 20px 24px;
}
.stat-value { font-size: 28px; font-weight: 900; color: var(--text-primary); }
.stat-label { font-size: 13px; color: var(--text-muted); margin-top: 4px; }
.stat-delta { font-size: 12px; font-weight: 600; margin-top: 8px; }
.stat-delta.up   { color: var(--success); }
.stat-delta.down { color: var(--error); }

/* Table */
.table { width: 100%; border-collapse: collapse; }
.table th { padding: 10px 16px; text-align: start; font-size: 12px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.6px; border-bottom: 1px solid var(--border); }
.table td { padding: 12px 16px; font-size: 14px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.table tr:hover td { background: var(--bg-hover); }

/* Notification dot */
.notif-dot {
  width: 8px;
  height: 8px;
  background: var(--error);
  border-radius: 50%;
  position: absolute;
  top: -2px;
  inset-inline-end: -2px;
}

/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--border-strong); }

/* ══════════════════════════════════════════════════════════
   COMMAND RING — the brand mark as a reusable component
   Inherits currentColor unless the accent classes are used.
══════════════════════════════════════════════════════════ */
.cra-mark { width: 36px; height: 36px; flex-shrink: 0; display: block; }
.cra-mark .ring,
.cra-mark .arc { fill: none; stroke: currentColor; stroke-width: 3; }
.cra-mark .arc { stroke-linecap: round; }
.cra-mark .point { fill: currentColor; }

/* Two-tone brand version */
.cra-mark.duo .ring,
.cra-mark.duo .arc-outer { stroke: var(--royal-blue); }
.cra-mark.duo .arc-inner { stroke: var(--sky-blue); }
.cra-mark.duo .point     { fill: var(--sky-blue); }
[data-theme="dark"] .cra-mark.duo .ring,
[data-theme="dark"] .cra-mark.duo .arc-outer { stroke: var(--brand-400); }

/* Radar sweep on hover — the ring is "live" */
.cra-brand:hover .cra-mark .arc-inner {
  animation: ring-pulse 1.4s var(--ease-out-quart);
  transform-origin: center;
}
@keyframes ring-pulse {
  0%   { opacity: 1;    transform: rotate(0deg); }
  100% { opacity: 1;    transform: rotate(360deg); }
}

/* Rank badge */
.rank-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-1) var(--s-3);
  border-radius: var(--r-full);
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border: 1px solid currentColor;
}
.rank-badge::before {
  content: '';
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
}
/* Six-rank ladder (ADR-018). .rank-7 stays absent — no seventh rank exists. */
.rank-1, .rank-cadet       { color: var(--rank-1-cadet); }
.rank-2, .rank-operator    { color: var(--rank-2-operator); }
.rank-3, .rank-senior      { color: var(--rank-3-senior); }
.rank-4, .rank-supervisor  { color: var(--rank-4-supervisor); }
.rank-5, .rank-manager     { color: var(--rank-5-manager); }
.rank-6, .rank-headops     { color: var(--rank-6-headops); }

/* Deprecated aliases — remove once no template emits them. */
.rank-junior               { color: var(--rank-1-cadet); }   /* folded into Cadet */
.rank-socmanager           { color: var(--rank-6-headops); } /* retired name, nearest live rank */

/* ══════════════════════════════════════════════════════════
   BRAND PATTERN — control-room motifs, not decoration
   Apply to a positioned parent; each renders into ::before.
══════════════════════════════════════════════════════════ */
.pattern-grid,
.pattern-radar,
.pattern-scan { position: relative; isolation: isolate; }

.pattern-grid::before,
.pattern-radar::before,
.pattern-scan::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}

/* Console grid — the operations map */
.pattern-grid::before {
  background-image:
    linear-gradient(to right,  rgba(54,163,255,0.07) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(54,163,255,0.07) 1px, transparent 1px);
  background-size: 48px 48px;
  mask-image: radial-gradient(ellipse 80% 70% at 50% 40%, #000 30%, transparent 78%);
  -webkit-mask-image: radial-gradient(ellipse 80% 70% at 50% 40%, #000 30%, transparent 78%);
}

/* Radar sweep rings — situational awareness */
.pattern-radar::before {
  background-image:
    radial-gradient(circle at 50% 50%, transparent 0 14%, rgba(54,163,255,0.09) 14% calc(14% + 1px), transparent calc(14% + 1px)),
    radial-gradient(circle at 50% 50%, transparent 0 27%, rgba(54,163,255,0.07) 27% calc(27% + 1px), transparent calc(27% + 1px)),
    radial-gradient(circle at 50% 50%, transparent 0 40%, rgba(54,163,255,0.05) 40% calc(40% + 1px), transparent calc(40% + 1px));
  mask-image: radial-gradient(circle at 50% 50%, #000 20%, transparent 72%);
  -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 20%, transparent 72%);
}

/* Monitor scan lines — subtle, CRT-adjacent */
.pattern-scan::before {
  background: repeating-linear-gradient(
    to bottom,
    rgba(232,237,244,0.030) 0 1px,
    transparent 1px 4px
  );
  opacity: 0.6;
}

[data-theme="light"] .pattern-grid::before {
  background-image:
    linear-gradient(to right,  rgba(7,26,53,0.06) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(7,26,53,0.06) 1px, transparent 1px);
}
[data-theme="light"] .pattern-scan::before { display: none; }

/* Live status dot — "system is watching" */
.status-live {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--success);
}
.status-live::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 0 0 currentColor;
  animation: status-ping 2s var(--ease-out-quart) infinite;
}
@keyframes status-ping {
  0%        { box-shadow: 0 0 0 0 rgba(16,185,129,0.55); }
  70%, 100% { box-shadow: 0 0 0 7px rgba(16,185,129,0); }
}

/* Toast */
.toast {
  position: fixed;
  bottom: 24px;
  inset-inline-end: 24px;
  z-index: 900;
  padding: 12px 20px;
  border-radius: var(--r-md);
  color: white;
  font-size: 14px;
  font-weight: 600;
  box-shadow: var(--shadow-lg);
  max-width: 340px;
  line-height: 1.5;
  animation: toast-in 0.35s var(--ease-out-quint);
  transition: opacity 0.3s ease, transform 0.3s ease;
}
.toast.leaving { opacity: 0; transform: translateY(8px); }
@keyframes toast-in {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

/* Alert entrance */
.alert { animation: alert-in 0.3s var(--ease-out-quart); }
@keyframes alert-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

/* Direction-aware arrow: flips when the page direction flips */
.dir-arrow { display: inline-block; }
[dir="ltr"] .dir-arrow { transform: scaleX(-1); }

/* Gentle attention shake (invalid results, form errors) */
@keyframes shake-x {
  10%, 90%      { transform: translateX(-1px); }
  20%, 80%      { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-3px); }
  40%, 60%      { transform: translateX(3px); }
}
.shake { animation: shake-x 0.5s ease both; }

/* Pop-in for success/result cards */
@keyframes pop-in {
  from { opacity: 0; transform: scale(0.94); }
  to   { opacity: 1; transform: none; }
}
.pop-in { animation: pop-in 0.4s var(--ease-out-quint) both; }

/* ══════════════════════════════════════════════════════════
   REDUCED MOTION — every animation gets an instant fallback
══════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
  }
  .js .reveal { opacity: 1; transform: none; }
  /* spinners convey loading state — keep them moving, just slower */
  .spinner { animation-duration: 1.2s !important; animation-iteration-count: infinite !important; }
}

