/* ==========================================================
   تيسير — Taysir landing (RTL Arabic)
   Palette: purple #6C3EF4 | white #FFFFFF | black #111111
   Font: Almarai (400, 800)

   Dark mode: added via [data-theme="dark"] on <html>.
   Every color, surface, shadow and illustration fill has a
   dedicated dark counterpart — chosen (not inverted) — so
   the site looks intentionally designed in both modes.
   ==========================================================

   ┌────────────────────────────────────────────────────────┐
   │ PROVENANCE — sections 1 … 16 below are the FINISHED    │
   │ visual prototype's styles.css, reproduced verbatim.    │
   │ Nothing in them was redesigned, re-tuned or           │
   │ "improved": the tokens, the white canvas, the faint    │
   │ decorative doodles, the Almarai type, the light/dark   │
   │ theme with its animated sun/moon toggle and every      │
   │ animation are the prototype's own values.              │
   │                                                        │
   │ Section 17 onward is ADDITIVE only. It maps the        │
   │ prototype's tokens onto the parts of the app the       │
   │ prototype never covered (the internal library, the     │
   │ file viewer, the shelf and the admin console) and adds │
   │ the illustration slots for the downloading state and   │
   │ the copy-link / share view. It introduces no new       │
   │ palette — every value resolves to a token defined      │
   │ above.                                                 │
   │                                                        │
   │ This stylesheet is PRESENTATION ONLY. It contains no   │
   │ behaviour and is loaded by pages whose markup, routing │
   │ and data flow are untouched.                           │
   └────────────────────────────────────────────────────────┘
   ========================================================== */

/* ==========================================================
   1. THEME TOKENS
   ========================================================== */
:root,
:root[data-theme="light"] {
  /* Brand */
  --purple:       #6C3EF4;
  --purple-dark:  #5828E0;
  --purple-soft:  rgba(108, 62, 244, 0.08);

  /* Neutrals */
  --bg:           #FFFFFF;
  --bg-elevated:  #FFFFFF;         /* cards, header background */
  --bg-input:     #FAFAFC;
  --bg-tabs:      #F5F4FA;
  --footer-bg:    #17151E;

  --text:         #111111;
  --text-soft:    rgba(17, 17, 17, 0.7);
  --text-mute:    rgba(17, 17, 17, 0.4);

  --border:       rgba(17, 17, 17, 0.06);
  --border-strong:rgba(17, 17, 17, 0.08);
  --border-input: rgba(17, 17, 17, 0.08);
  --check-border: rgba(17, 17, 17, 0.25);

  /* Header on scroll (frosted) */
  --header-bg:    rgba(255, 255, 255, 0.92);

  /* Decorative doodles */
  --doodle-color:   #111111;
  --doodle-opacity: 0.06;
  --doodle-opacity-soft: 0.05;

  /* Illustration semantic fills (matches SVG hex map in script.js) */
  --illu-outline:    #111111;
  --illu-mid:        #222222;
  --illu-accent:     #6C3EF4;
  --illu-skin:       #EFD9CC;
  --illu-paper:      #F2F2F5;
  --illu-paperAlt:   #E8E8EE;
  --illu-paperPure:  #FFFFFF;

  /* Shadows */
  --shadow-sm:      0 2px 8px rgba(17, 17, 17, 0.04);
  --shadow-md:      0 8px 24px rgba(17, 17, 17, 0.06);
  --shadow-lg:      0 24px 60px rgba(17, 17, 17, 0.10);
  --shadow-purple:  0 12px 30px rgba(108, 62, 244, 0.28);
  --shadow-purple-hover: 0 16px 36px rgba(108, 62, 244, 0.35);
  --shadow-tab:     0 4px 14px rgba(17, 17, 17, 0.08);
  --shadow-focus:   0 0 0 4px rgba(108, 62, 244, 0.14);

  /* Radii */
  --radius-card: 20px;
  --radius-btn:  30px;
  --container:   1100px;

  /* Motion */
  --theme-duration: 0.42s;
  --theme-ease:     cubic-bezier(0.4, 0, 0.2, 1);
}

/* -----------------------  DARK THEME  ----------------------
   Carefully designed second version — NOT auto-inverted.
   - deep soft slate background (#0E1016 / #14161F cards)
   - soft off-white text
   - brighter purple that pops on dark
   - illustrations recolored to purple + soft light neutrals
   ----------------------------------------------------------- */
:root[data-theme="dark"] {
  --purple:       #8F6BFF;
  --purple-dark:  #7A54FF;
  --purple-soft:  rgba(143, 107, 255, 0.16);

  --bg:           #0E1016;
  --bg-elevated:  #17192200;        /* set via layered surfaces below */
  --bg-input:     #1A1D28;
  --bg-tabs:      #191C25;
  --footer-bg:    #0A0B10;

  --text:         #F2F2F5;
  --text-soft:    rgba(242, 242, 245, 0.72);
  --text-mute:    rgba(242, 242, 245, 0.38);

  --border:        rgba(255, 255, 255, 0.07);
  --border-strong: rgba(255, 255, 255, 0.10);
  --border-input:  rgba(255, 255, 255, 0.09);
  --check-border:  rgba(255, 255, 255, 0.28);

  --header-bg:    rgba(14, 16, 22, 0.78);

  --doodle-color:   #F2F2F5;
  --doodle-opacity: 0.055;
  --doodle-opacity-soft: 0.045;

  /* Illustrations recolored for dark: no more black silhouettes;
     replace with a deep desaturated purple. Accent is brighter.
     Skin tone is softened. Paper tones are a family of dark
     purples so cards inside the illustrations still feel
     paper-like without being white. */
  --illu-outline:    #201B33;    /* was black — now deep indigo */
  --illu-mid:        #2B2445;    /* mid gray -> slightly lighter deep indigo */
  --illu-accent:     #A78BFF;    /* brighter purple pops on dark */
  --illu-skin:       #D9B7A0;    /* softer skin so it doesn't glow */
  --illu-paper:      #E6E2F5;    /* off-white with a lilac cast */
  --illu-paperAlt:   #C9C2E8;    /* darker paper for depth */
  --illu-paperPure:  #F3F0FF;

  --shadow-sm:      0 2px 8px rgba(0, 0, 0, 0.35);
  --shadow-md:      0 10px 28px rgba(0, 0, 0, 0.45);
  --shadow-lg:      0 28px 70px rgba(0, 0, 0, 0.55);
  --shadow-purple:  0 12px 30px rgba(143, 107, 255, 0.35);
  --shadow-purple-hover: 0 18px 40px rgba(143, 107, 255, 0.45);
  --shadow-tab:     0 4px 14px rgba(0, 0, 0, 0.4);
  --shadow-focus:   0 0 0 4px rgba(143, 107, 255, 0.22);
}

/* ==========================================================
   2. GLOBAL COLOR TRANSITIONS
   Applied only during a toggle (root gets .theme-transitioning)
   so we don't pay for transitions during normal interactions.
   ========================================================== */
:root.theme-transitioning,
:root.theme-transitioning *:not(.no-theme-fade) {
  transition:
    background-color var(--theme-duration) var(--theme-ease),
    color            var(--theme-duration) var(--theme-ease),
    border-color     var(--theme-duration) var(--theme-ease),
    box-shadow       var(--theme-duration) var(--theme-ease),
    fill             var(--theme-duration) var(--theme-ease),
    stroke           var(--theme-duration) var(--theme-ease),
    opacity          var(--theme-duration) var(--theme-ease) !important;
}

/* ==========================================================
   3. BASE
   ========================================================== */
* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: 'Almarai', sans-serif;
  font-weight: 400;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

html, body { overflow-x: hidden; max-width: 100%; }

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

h1, h2, h3, h4 {
  font-weight: 800;
  color: var(--text);
  line-height: 1.2;
  margin: 0;
  letter-spacing: -0.01em;
}

p { margin: 0; color: var(--text); }

.container {
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 24px;
  position: relative;
  z-index: 1;
}

/* ==========================================================
   4. BACKGROUND DOODLES
   ========================================================== */
.bg-decor {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  color: var(--doodle-color);
}
.doodle {
  position: absolute;
  opacity: var(--doodle-opacity);
}
.doodle.d1 { width: 420px; top: 40px; right: -80px; }
.doodle.d2 { width: 380px; top: 480px; left: -60px; }
.doodle.d3 { width: 180px; top: 700px; right: 40px; }
.doodle.d4 { width: 60px;  top: 240px; left: 12%; }
.doodle.d5 { width: 220px; top: 1200px; left: -60px; opacity: var(--doodle-opacity-soft); }
.doodle.d6 { width: 200px; top: 1500px; right: 5%; }
.doodle.d7 { width: 420px; top: 1900px; left: 8%; }
.doodle.d8 { width: 44px;  top: 900px; right: 20%; }

/* ==========================================================
   5. HEADER
   ========================================================== */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--header-bg);
  backdrop-filter: saturate(180%) blur(10px);
  -webkit-backdrop-filter: saturate(180%) blur(10px);
  border-bottom: 1px solid var(--border);
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 72px;
}

.brand {
  font-weight: 800;
  font-size: 28px;
  color: var(--text);
  letter-spacing: -0.02em;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Nav on desktop */
.nav-desktop {
  display: flex;
  gap: 36px;
  align-items: center;
}
.nav-desktop a {
  font-weight: 700;
  font-size: 15px;
  color: var(--text);
  position: relative;
  padding: 4px 0;
  transition: color 0.2s;
}
.nav-desktop a::after {
  content: '';
  position: absolute;
  bottom: -2px;
  inset-inline-end: 0;
  height: 2px;
  width: 0;
  background: var(--purple);
  transition: width 0.25s ease;
}
.nav-desktop a:hover { color: var(--purple); }
.nav-desktop a:hover::after { width: 100%; }

/* Hamburger */
.hamburger {
  display: none;
  background: transparent;
  border: 0;
  width: 44px;
  height: 44px;
  padding: 0;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 5px;
  border-radius: 12px;
  transition: background 0.2s;
}
.hamburger:hover { background: var(--purple-soft); }
.hamburger span {
  display: block;
  width: 22px;
  height: 2px;
  background: var(--text);
  border-radius: 2px;
  transition: transform 0.25s, opacity 0.25s;
}
.hamburger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.hamburger.open span:nth-child(2) { opacity: 0; }
.hamburger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

.mobile-menu {
  display: none;
  flex-direction: column;
  padding: 12px 24px 20px;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}
.mobile-menu a {
  padding: 14px 0;
  font-weight: 700;
  font-size: 16px;
  border-bottom: 1px solid var(--border);
  color: var(--text);
}
.mobile-menu a:last-child { border-bottom: 0; }
.mobile-menu[data-open="true"] { display: flex; }

/* ==========================================================
   6. THEME TOGGLE — sun/moon pill switch
   Distinctive, elegant, and animated. The thumb slides, the
   two icons cross-fade + rotate, and tiny stars fade in on
   the moon side when dark.
   ========================================================== */
.theme-toggle {
  --w: 62px;
  --h: 32px;
  --thumb: 26px;

  appearance: none;
  border: 0;
  background: transparent;
  padding: 0;
  width: var(--w);
  height: var(--h);
  border-radius: 999px;
  position: relative;
  cursor: pointer;
  outline: none;
  flex-shrink: 0;
}
.theme-toggle:focus-visible .theme-toggle-track {
  box-shadow: 0 0 0 3px var(--purple-soft), inset 0 0 0 1px var(--purple);
}

/* Lottie host — fills the 62x32 pill. /static/theme-lottie.js mounts the
   dotLottie canvas inside this span (it searches for `.theme-toggle-lottie`
   inside `[data-theme-toggle]`). Purely presentational: if the module never
   loads the span stays empty and the button still toggles the theme. */
.theme-toggle-lottie {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 999px;
  overflow: hidden;
}
.theme-toggle-lottie canvas {
  width: 100%;
  height: 100%;
  display: block;
}

.theme-toggle-track {
  position: absolute;
  inset: 0;
  border-radius: 999px;
  /* Two gradient stacks — day and night — cross-fade via background-image.
     Fallback: solid color so the thing never looks broken. */
  background:
    linear-gradient(135deg, #FFE7A3 0%, #FFB88A 55%, #FF9DBF 100%);
  border: 1px solid rgba(17, 17, 17, 0.08);
  overflow: hidden;
  transition:
    background var(--theme-duration) var(--theme-ease),
    border-color var(--theme-duration) var(--theme-ease),
    box-shadow 0.2s ease;
}
:root[data-theme="dark"] .theme-toggle-track {
  background:
    radial-gradient(circle at 78% 28%, rgba(255,255,255,0.10), transparent 45%),
    linear-gradient(135deg, #1B1A3B 0%, #2A2360 55%, #3F2A78 100%);
  border-color: rgba(255, 255, 255, 0.12);
}

/* Tiny star pinpricks — only visible in dark mode */
.theme-toggle-star {
  position: absolute;
  width: 3px;
  height: 3px;
  background: #F2F2F5;
  border-radius: 999px;
  opacity: 0;
  transform: scale(0.6);
  transition:
    opacity var(--theme-duration) var(--theme-ease),
    transform var(--theme-duration) var(--theme-ease);
}
.theme-toggle-star.s1 { top: 8px;  left: 10px; }
.theme-toggle-star.s2 { top: 20px; left: 22px; width: 2px; height: 2px; }
.theme-toggle-star.s3 { top: 6px;  left: 26px; width: 2px; height: 2px; }
:root[data-theme="dark"] .theme-toggle-star {
  opacity: 0.9;
  transform: scale(1);
}
:root[data-theme="dark"] .theme-toggle-star.s2 { transition-delay: 0.05s; opacity: 0.65; }
:root[data-theme="dark"] .theme-toggle-star.s3 { transition-delay: 0.1s;  opacity: 0.85; }

/* The thumb slides right when dark (in RTL "start" is right;
   we intentionally position it visually to reflect day-left/night-right). */
.theme-toggle-thumb {
  position: absolute;
  top: 50%;
  left: 3px;                              /* light = left (sun) */
  transform: translateY(-50%) rotate(0deg);
  width: var(--thumb);
  height: var(--thumb);
  border-radius: 999px;
  background: linear-gradient(160deg, #FFFFFF 0%, #FFF3D4 100%);
  box-shadow:
    0 2px 6px rgba(17, 17, 17, 0.18),
    inset 0 -2px 4px rgba(255, 190, 90, 0.35);
  display: grid;
  place-items: center;
  color: #F59E2A;                         /* sun color */
  transition:
    left var(--theme-duration) var(--theme-ease),
    transform var(--theme-duration) var(--theme-ease),
    background var(--theme-duration) var(--theme-ease),
    color var(--theme-duration) var(--theme-ease),
    box-shadow var(--theme-duration) var(--theme-ease);
}
:root[data-theme="dark"] .theme-toggle-thumb {
  left: calc(var(--w) - var(--thumb) - 3px);
  transform: translateY(-50%) rotate(360deg);
  background: linear-gradient(160deg, #F5F3FF 0%, #D9CFFF 100%);
  color: #6B5FFF;                         /* moon color */
  box-shadow:
    0 2px 6px rgba(0, 0, 0, 0.5),
    inset 0 -2px 4px rgba(107, 95, 255, 0.35);
}

.theme-toggle-thumb .icon-sun,
.theme-toggle-thumb .icon-moon {
  position: absolute;
  width: 16px;
  height: 16px;
  transition:
    opacity var(--theme-duration) var(--theme-ease),
    transform var(--theme-duration) var(--theme-ease);
}
.theme-toggle-thumb .icon-sun  { opacity: 1; transform: rotate(0deg)   scale(1); }
.theme-toggle-thumb .icon-moon { opacity: 0; transform: rotate(-90deg) scale(0.6); }

:root[data-theme="dark"] .theme-toggle-thumb .icon-sun  { opacity: 0; transform: rotate(90deg)  scale(0.6); }
:root[data-theme="dark"] .theme-toggle-thumb .icon-moon { opacity: 1; transform: rotate(0deg)   scale(1); }

/* Hover flourish */
@media (hover: hover) and (pointer: fine) {
  .theme-toggle:hover .theme-toggle-thumb {
    box-shadow:
      0 4px 10px rgba(17, 17, 17, 0.25),
      inset 0 -2px 4px rgba(255, 190, 90, 0.4);
  }
  :root[data-theme="dark"] .theme-toggle:hover .theme-toggle-thumb {
    box-shadow:
      0 4px 12px rgba(0, 0, 0, 0.65),
      inset 0 -2px 4px rgba(107, 95, 255, 0.45);
  }
}

/* ==========================================================
   7. BUTTONS
   ========================================================== */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 14px 30px;
  border-radius: var(--radius-btn);
  font-weight: 800;
  font-size: 15px;
  border: 0;
  cursor: pointer;
  transition: transform 0.2s ease, box-shadow 0.25s ease, background 0.2s;
  text-align: center;
  white-space: nowrap;
}

.btn-primary {
  background: var(--purple);
  color: #FFFFFF;
  box-shadow: var(--shadow-purple);
}
.btn-primary:hover {
  background: var(--purple-dark);
  transform: translateY(-2px);
  box-shadow: var(--shadow-purple-hover);
}
.btn-primary:active { transform: translateY(0); }

.btn-ghost {
  background: transparent;
  color: var(--text);
  padding: 14px 22px;
  border-radius: var(--radius-btn);
  position: relative;
}
.btn-ghost::after {
  content: '';
  position: absolute;
  inset: auto 22px 8px 22px;
  height: 2px;
  background: var(--text);
  transform: scaleX(0.6);
  transform-origin: right;
  transition: transform 0.25s;
}
.btn-ghost:hover::after { transform: scaleX(1); }

.btn-block { width: 100%; }

/* ==========================================================
   8. HERO
   ========================================================== */
.hero { padding: 80px 0 40px; }

.hero-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 40px;
  align-items: center;
}

.hero-copy h1 {
  font-size: clamp(38px, 5.4vw, 62px);
  margin-bottom: 20px;
}

.hero-copy .lead {
  font-size: 19px;
  max-width: 460px;
  margin-bottom: 32px;
  color: var(--text-soft);
}

.hero-cta {
  display: flex;
  gap: 14px;
  align-items: center;
  flex-wrap: wrap;
}

.hero-illu {
  display: flex;
  justify-content: center;
  align-items: center;
}
.hero-illu img,
.hero-illu svg {
  width: 100%;
  max-width: 480px;
  height: auto;
  background: transparent;
}

/* ==========================================================
   9. GLIMPSE STRIP
   ========================================================== */
.glimpse { padding: 40px 0 40px; }
.glimpse-row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 40px;
  align-items: center;
  justify-items: center;
  opacity: 0.9;
}
.glimpse-item {
  width: 100%;
  max-width: 160px;
  display: flex;
  justify-content: center;
}
.glimpse-item img,
.glimpse-item svg {
  width: 100%;
  height: 120px;
  object-fit: contain;
  background: transparent;
}

/* ==========================================================
   10. SECTION HEAD
   ========================================================== */
.section-head {
  text-align: center;
  margin-bottom: 56px;
}
.section-head h2 {
  font-size: clamp(30px, 3.6vw, 44px);
  margin-bottom: 12px;
}
.section-head p {
  font-size: 17px;
  color: var(--text-soft);
}

/* ==========================================================
   11. GUIDE
   ========================================================== */
.guide { padding: 80px 0; }

.guide-grid {
  display: grid;
  grid-template-columns: 1fr 1.1fr;
  gap: 60px;
  align-items: center;
}

.guide-illu { display: flex; justify-content: center; }
.guide-illu img,
.guide-illu svg {
  width: 100%;
  max-width: 440px;
  background: transparent;
}

.steps {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: 18px;
}

.step {
  position: relative;
  display: grid;
  grid-template-columns: auto auto 1fr;
  gap: 18px;
  align-items: center;
  background: var(--bg-elevated);
  padding: 22px 24px;
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-md);
  border: 1px solid transparent;
  transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
}
:root[data-theme="dark"] .step {
  background: #171923;
  border-color: rgba(255, 255, 255, 0.05);
}
.step:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-lg);
}
:root[data-theme="dark"] .step:hover {
  border-color: rgba(143, 107, 255, 0.35);
}

.step-num {
  width: 44px;
  height: 44px;
  border-radius: 14px;
  background: var(--text);
  color: var(--bg);
  font-weight: 800;
  font-size: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
:root[data-theme="dark"] .step-num {
  background: linear-gradient(135deg, #8F6BFF, #6B4CFF);
  color: #FFFFFF;
}

.step-icon {
  width: 44px;
  height: 44px;
  border-radius: 14px;
  background: var(--purple-soft);
  color: var(--purple);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.step-icon svg { width: 22px; height: 22px; }

.step-body h3 { font-size: 19px; margin-bottom: 4px; }
.step-body p  { font-size: 15px; color: var(--text-soft); }

/* ==========================================================
   12. ACCOUNT / LOGIN
   ========================================================== */
.account { padding: 80px 0 100px; }

.account-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 60px;
  align-items: center;
}

.account-illu {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 420px;
}
.account-illu .illu-primary {
  width: 100%;
  max-width: 460px;
  background: transparent;
}
.account-illu .illu-mini {
  position: absolute;
  width: 110px;
  height: auto;
  background: transparent;
  opacity: 0.85;
}
.account-illu .illu-mini-a { bottom: 0; inset-inline-end: 0; }
.account-illu .illu-mini-b { top: 10px; inset-inline-start: 0; width: 90px; }

.auth-card {
  background: var(--bg-elevated);
  border-radius: 24px;
  padding: 36px 32px;
  box-shadow: var(--shadow-lg);
  position: relative;
}
:root[data-theme="dark"] .auth-card {
  background: #171923;
  border: 1px solid rgba(255, 255, 255, 0.06);
}
.auth-card::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 24px;
  padding: 1px;
  background: linear-gradient(180deg, rgba(108, 62, 244, 0.18), rgba(17, 17, 17, 0.04));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
  transition: background var(--theme-duration) var(--theme-ease);
}
:root[data-theme="dark"] .auth-card::before {
  background: linear-gradient(180deg, rgba(143, 107, 255, 0.45), rgba(255, 255, 255, 0.04));
}

.tabs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  padding: 6px;
  background: var(--bg-tabs);
  border-radius: 999px;
  margin-bottom: 28px;
}
.tab {
  border: 0;
  background: transparent;
  padding: 12px 18px;
  border-radius: 999px;
  font-weight: 800;
  font-size: 15px;
  color: var(--text-soft);
  transition: background 0.25s, color 0.2s, box-shadow 0.25s;
}
.tab.active {
  background: var(--bg-elevated);
  color: var(--purple);
  box-shadow: var(--shadow-tab);
}
:root[data-theme="dark"] .tab.active {
  background: #22253A;
}

.tab-panel {
  display: none;
  flex-direction: column;
  gap: 18px;
}
.tab-panel.active { display: flex; }
.tab-panel.active.is-switching { animation: fade 0.25s ease; }

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

.field { display: flex; flex-direction: column; gap: 8px; }
.field-label {
  font-weight: 700;
  font-size: 14px;
  color: var(--text);
}

.input-wrap {
  position: relative;
  display: flex;
  align-items: center;
  background: var(--bg-input);
  border: 1.5px solid var(--border-input);
  border-radius: 14px;
  padding: 0 16px;
  transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
}
.input-wrap:focus-within {
  border-color: var(--purple);
  background: var(--bg-elevated);
  box-shadow: var(--shadow-focus);
}
:root[data-theme="dark"] .input-wrap:focus-within {
  background: #1F2231;
}
.input-wrap svg {
  width: 18px;
  height: 18px;
  color: var(--text);
  opacity: 0.7;
  flex-shrink: 0;
}
.input-wrap input {
  flex: 1;
  border: 0;
  background: transparent;
  padding: 14px 12px;
  font-family: inherit;
  font-size: 15px;
  font-weight: 400;
  color: var(--text);
  outline: none;
  text-align: start;
}
.input-wrap input::placeholder { color: var(--text-mute); }

.row-between {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: -4px 0 4px;
}
.check {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
  cursor: pointer;
}
.check input {
  appearance: none;
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  border-radius: 6px;
  border: 1.5px solid var(--check-border);
  background: var(--bg-elevated);
  display: inline-grid;
  place-content: center;
  cursor: pointer;
  transition: background 0.2s, border-color 0.2s;
}
:root[data-theme="dark"] .check input { background: #1A1D28; }
.check input:checked {
  background: var(--purple);
  border-color: var(--purple);
}
.check input:checked::after {
  content: '';
  width: 10px;
  height: 10px;
  background: #FFFFFF;
  clip-path: polygon(14% 44%, 0 60%, 40% 100%, 100% 20%, 84% 6%, 40% 68%);
}

.link {
  font-size: 14px;
  font-weight: 700;
  color: var(--purple);
  transition: color 0.2s;
}
.link:hover { color: var(--purple-dark); text-decoration: underline; text-underline-offset: 3px; }

/* ==========================================================
   13. FOOTER
   ========================================================== */
.site-footer {
  background: var(--footer-bg);
  color: #FFFFFF;
  padding: 60px 0 24px;
  position: relative;
  z-index: 2;
}
:root[data-theme="dark"] .site-footer {
  border-top: 1px solid rgba(255, 255, 255, 0.05);
}

.footer-inner .footer-top {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr;
  gap: 40px;
  align-items: start;
  padding-bottom: 40px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

.brand-white {
  font-weight: 800;
  font-size: 30px;
  color: #FFFFFF;
  margin-bottom: 8px;
}
.footer-brand p {
  color: rgba(255, 255, 255, 0.7);
  font-size: 15px;
  max-width: 320px;
}

.footer-links {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.footer-links a {
  color: rgba(255, 255, 255, 0.78);
  font-weight: 700;
  font-size: 14px;
  transition: color 0.2s;
}
.footer-links a:hover { color: #FFFFFF; }

.footer-social {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}
.footer-social a {
  width: 40px;
  height: 40px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.06);
  color: #FFFFFF;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background 0.2s, transform 0.2s;
}
.footer-social a:hover {
  background: var(--purple);
  transform: translateY(-2px);
}
.footer-social svg { width: 18px; height: 18px; }

.footer-bottom {
  padding-top: 20px;
  text-align: center;
  font-size: 13px;
  color: rgba(255, 255, 255, 0.55);
}

/* ==========================================================
   14. ILLUSTRATION RECOLORING
   Applied to any inline <svg> that came from an <img.float-illu>
   or that lives inside an illustration container. The script
   swapped every fill hex we care about into one of these
   semantic classes. In dark mode, the CSS variables shift.
   ========================================================== */
.float-illu .illu-fill-outline,
.hero-illu .illu-fill-outline,
.guide-illu .illu-fill-outline,
.account-illu .illu-fill-outline,
.glimpse-item .illu-fill-outline { fill: var(--illu-outline); }

.float-illu .illu-fill-mid,
.hero-illu .illu-fill-mid,
.guide-illu .illu-fill-mid,
.account-illu .illu-fill-mid,
.glimpse-item .illu-fill-mid { fill: var(--illu-mid); }

.float-illu .illu-fill-accent,
.hero-illu .illu-fill-accent,
.guide-illu .illu-fill-accent,
.account-illu .illu-fill-accent,
.glimpse-item .illu-fill-accent { fill: var(--illu-accent); }

.float-illu .illu-fill-skin,
.hero-illu .illu-fill-skin,
.guide-illu .illu-fill-skin,
.account-illu .illu-fill-skin,
.glimpse-item .illu-fill-skin { fill: var(--illu-skin); }

.float-illu .illu-fill-paper,
.hero-illu .illu-fill-paper,
.guide-illu .illu-fill-paper,
.account-illu .illu-fill-paper,
.glimpse-item .illu-fill-paper { fill: var(--illu-paper); }

.float-illu .illu-fill-paperAlt,
.hero-illu .illu-fill-paperAlt,
.guide-illu .illu-fill-paperAlt,
.account-illu .illu-fill-paperAlt,
.glimpse-item .illu-fill-paperAlt { fill: var(--illu-paperAlt); }

.float-illu .illu-fill-paperPure,
.hero-illu .illu-fill-paperPure,
.guide-illu .illu-fill-paperPure,
.account-illu .illu-fill-paperPure,
.glimpse-item .illu-fill-paperPure { fill: var(--illu-paperPure); }

.float-illu .illu-stroke-mid,
.hero-illu .illu-stroke-mid,
.guide-illu .illu-stroke-mid,
.account-illu .illu-stroke-mid,
.glimpse-item .illu-stroke-mid { stroke: var(--illu-mid); }

.float-illu .illu-stroke-outline,
.hero-illu .illu-stroke-outline,
.guide-illu .illu-stroke-outline,
.account-illu .illu-stroke-outline,
.glimpse-item .illu-stroke-outline { stroke: var(--illu-outline); }

/* Smooth color fade on illustrations — always on during theme toggle.
   Also on for hover/mouse events (~150ms) so any interactive tint
   feels alive but doesn't compete with the toggle. */
.float-illu [class*="illu-fill-"],
.float-illu [class*="illu-stroke-"],
.hero-illu   [class*="illu-fill-"],
.guide-illu  [class*="illu-fill-"],
.account-illu [class*="illu-fill-"],
.glimpse-item [class*="illu-fill-"] {
  transition: fill var(--theme-duration) var(--theme-ease),
              stroke var(--theme-duration) var(--theme-ease);
}

/* ==========================================================
   15. ANIMATIONS — floats + reveals (unchanged behavior)
   ========================================================== */
.float-illu {
  will-change: transform;
  animation: floatY 3.6s ease-in-out infinite;
  transition: transform 0.35s ease;
}
.float-illu.float-a { animation-duration: 4.2s; animation-delay: -0.4s; }
.float-illu.float-b { animation-duration: 3.4s; animation-delay: -0.9s; }
.float-illu.float-c { animation-duration: 3.8s; animation-delay: -1.7s; }
.float-illu.float-d { animation-duration: 4.0s; animation-delay: -0.2s; }
.float-illu.float-e { animation-duration: 3.6s; animation-delay: -2.3s; }
.float-illu.float-f { animation-duration: 4.4s; animation-delay: -0.7s; }
.float-illu.float-g { animation-duration: 4.0s; animation-delay: -1.2s; }
.float-illu.float-h { animation-duration: 3.2s; animation-delay: -0.5s; }
.float-illu.float-i { animation-duration: 3.8s; animation-delay: -1.9s; }

@keyframes floatY {
  0%,100% { transform: translateY(0); }
  50%     { transform: translateY(-8px); }
}

@media (hover: hover) and (pointer: fine) {
  .float-illu:hover { transform: translateY(-4px) scale(1.03); }
}

/* Reveal-on-scroll */
[data-reveal] {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.9s ease, transform 0.9s ease;
  will-change: opacity, transform;
}
[data-reveal].is-visible {
  opacity: 1;
  transform: translateY(0);
}
.float-illu[data-reveal] { animation: none; }
.float-illu[data-reveal].is-visible { animation: floatY 3.6s ease-in-out infinite; }
.float-illu.float-a[data-reveal].is-visible { animation-duration: 4.2s; animation-delay: -0.4s; }
.float-illu.float-b[data-reveal].is-visible { animation-duration: 3.4s; animation-delay: -0.9s; }
.float-illu.float-c[data-reveal].is-visible { animation-duration: 3.8s; animation-delay: -1.7s; }
.float-illu.float-d[data-reveal].is-visible { animation-duration: 4.0s; animation-delay: -0.2s; }
.float-illu.float-e[data-reveal].is-visible { animation-duration: 3.6s; animation-delay: -2.3s; }
.float-illu.float-f[data-reveal].is-visible { animation-duration: 4.4s; animation-delay: -0.7s; }
.float-illu.float-g[data-reveal].is-visible { animation-duration: 4.0s; animation-delay: -1.2s; }

.btn { will-change: transform; }
@media (hover: hover) and (pointer: fine) {
  .btn-ghost:hover {
    transform: translateY(-2px);
    box-shadow: 0 10px 24px rgba(17, 17, 17, 0.08);
  }
  :root[data-theme="dark"] .btn-ghost:hover {
    box-shadow: 0 10px 24px rgba(0, 0, 0, 0.5);
  }
}

/* Respect reduced-motion — kill every animation we added
   INCLUDING the theme fade. */
@media (prefers-reduced-motion: reduce) {
  .float-illu,
  .float-illu[data-reveal].is-visible,
  .float-illu.float-a[data-reveal].is-visible,
  .float-illu.float-b[data-reveal].is-visible,
  .float-illu.float-c[data-reveal].is-visible,
  .float-illu.float-d[data-reveal].is-visible,
  .float-illu.float-e[data-reveal].is-visible,
  .float-illu.float-f[data-reveal].is-visible,
  .float-illu.float-g[data-reveal].is-visible {
    animation: none !important;
  }
  [data-reveal] {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  .float-illu:hover { transform: none !important; }
  .btn:hover, .btn-ghost:hover, .btn-primary:hover { transform: none !important; }

  /* Kill the theme transition — snap instantly instead */
  :root.theme-transitioning,
  :root.theme-transitioning * {
    transition: none !important;
  }
  .theme-toggle-thumb,
  .theme-toggle-thumb .icon-sun,
  .theme-toggle-thumb .icon-moon,
  .theme-toggle-track,
  .theme-toggle-star {
    transition: none !important;
  }
}

/* ==========================================================
   16. RESPONSIVE
   ========================================================== */
@media (max-width: 900px) {
  .nav-desktop { display: none; }
  .hamburger { display: inline-flex; }

  .hero { padding: 48px 0 24px; }
  .hero-grid { grid-template-columns: 1fr; gap: 24px; text-align: start; }
  .hero-illu { order: -1; }
  .hero-illu img, .hero-illu svg { max-width: 320px; }

  .glimpse-row { grid-template-columns: repeat(2, 1fr); gap: 24px; }

  .guide { padding: 60px 0; }
  .guide-grid { grid-template-columns: 1fr; gap: 32px; }
  .guide-illu img, .guide-illu svg { max-width: 320px; }

  .account { padding: 60px 0 80px; }
  .account-grid { grid-template-columns: 1fr; gap: 32px; }
  .account-illu { min-height: 300px; }
  .account-illu .illu-primary { max-width: 340px; }
  .account-illu .illu-mini { display: none; }

  .auth-card { padding: 28px 22px; }

  .footer-inner .footer-top {
    grid-template-columns: 1fr;
    gap: 28px;
  }
}

@media (max-width: 520px) {
  .container { padding: 0 18px; }
  .brand { font-size: 24px; }
  .hero-copy h1 { font-size: 34px; }
  .hero-copy .lead { font-size: 17px; }
  .btn { padding: 13px 22px; font-size: 14px; }
  .step { grid-template-columns: auto 1fr; padding: 18px 18px; }
  .step-icon { display: none; }
  .section-head { margin-bottom: 40px; }
}

/* ==========================================================
   ==========================================================
   17. APP BRIDGE  —  ADDITIVE ONLY
   ==========================================================

   Everything from here down maps the prototype tokens defined
   in §1 onto the parts of the product the prototype never drew:

     • the internal library browser   (.gd-* / library.css)
     • the in-app file viewer         (.viewer-* / components.css)
     • the shelf scene                (.shelf-root / shelf.css)
     • the admin console              (tokens.css --color-*)

   It declares NO new colours. Every value below resolves to a
   token from §1, so the whole product shares one palette and
   one light/dark switch.

   IMPORTANT: this section only re-points CSS custom properties
   and adds decorative pseudo-elements. It changes no markup
   contract — every selector targets a class the application
   already emitted before this redesign.
   ========================================================== */

/* ---------- 17.1 Almarai everywhere -----------------------
   The prototype's type is Almarai. The internal pages used to
   ask for Fraunces / Public Sans / Cairo through the
   --font-heading / --font-body / shelf font stacks, so we
   re-point those variables rather than rewriting each rule. */
:root {
  --font-heading: 'Almarai', 'Segoe UI', Tahoma, sans-serif;
  --font-body:    'Almarai', 'Segoe UI', Tahoma, sans-serif;
  --taysir-font:  'Almarai', 'Segoe UI', Tahoma, sans-serif;
}

body,
.gd-body,
.shelf-root,
.viewer-shell {
  font-family: var(--taysir-font);
}

/* The prototype's weights: 400 body, 700 strong, 800 headings. */
.gd-brand-name,
.gd-modal-title,
.gd-empty-title,
.gd-section-label,
.viewer-title {
  font-family: var(--taysir-font);
  font-weight: 800;
  letter-spacing: -0.01em;
}

/* ---------- 17.2 Library browser tokens -------------------
   library.css is authored entirely against --gd-*, so mapping
   those onto the prototype palette re-skins the whole browser
   without editing a single library.css rule. */
html[data-theme="light"] .gd-body,
html:not([data-theme="dark"]) .gd-body {
  --gd-blue:        var(--purple);
  --gd-blue-hover:  var(--purple-dark);
  --gd-blue-soft:   var(--purple-soft);
  --gd-blue-tint:   rgba(108, 62, 244, 0.42);

  --gd-text:        var(--text);
  --gd-text-2:      var(--text-soft);
  --gd-text-3:      var(--text-mute);

  --gd-line:        var(--border-strong);
  --gd-line-soft:   var(--border);
  --gd-surface:     var(--bg-elevated);
  --gd-surface-2:   #FAFAFC;
  --gd-bg:          var(--bg);
  --gd-hover:       rgba(108, 62, 244, 0.05);
  --gd-hover-2:     rgba(108, 62, 244, 0.09);
  --gd-selected:    var(--purple-soft);
  --gd-chip:        var(--bg-tabs);
  --gd-scrim:       rgba(17, 17, 17, 0.45);
  --gd-shimmer:     rgba(255, 255, 255, 0.72);
  --gd-lock-veil:   rgba(17, 17, 17, 0.06);
  --gd-on-accent:   #FFFFFF;
  --gd-tooltip-bg:  var(--footer-bg);
  --gd-tooltip-fg:  #FFFFFF;

  --gd-folder:      var(--purple);
  --gd-red:         var(--purple);
  --gd-green:       var(--purple);
  --gd-yellow:      var(--purple);
  --gd-cyan:        var(--purple);
  --gd-cyan-strong: var(--purple-dark);
  --gd-gold:        var(--purple);

  --gd-shadow-1:    var(--shadow-sm);
  --gd-shadow-2:    var(--shadow-md);
  --gd-shadow-menu: var(--shadow-lg);

  /* Base tokens the JS-injected file/folder glyphs read. */
  --color-primary:       var(--purple);
  --color-accent:        var(--purple);
  --color-success:       var(--purple);
  --color-warning:       var(--purple);
  --color-danger:        #C2410C;
  --color-ink-secondary: var(--text-soft);
  --color-on-primary:    #FFFFFF;
  --color-on-accent:     #FFFFFF;
  --color-accent-soft:   var(--purple-soft);
  --color-warning-soft:  var(--purple-soft);
  --color-danger-soft:   rgba(194, 65, 12, 0.12);
  --color-border-strong: var(--border-strong);
}

html[data-theme="dark"] .gd-body {
  --gd-blue:        var(--purple);
  --gd-blue-hover:  var(--purple-dark);
  --gd-blue-soft:   var(--purple-soft);
  --gd-blue-tint:   rgba(143, 107, 255, 0.42);

  --gd-text:        var(--text);
  --gd-text-2:      var(--text-soft);
  --gd-text-3:      var(--text-mute);

  --gd-line:        var(--border-strong);
  --gd-line-soft:   var(--border);
  --gd-surface:     #171923;
  --gd-surface-2:   #14161F;
  --gd-bg:          var(--bg);
  --gd-hover:       rgba(255, 255, 255, 0.06);
  --gd-hover-2:     rgba(255, 255, 255, 0.10);
  --gd-selected:    var(--purple-soft);
  --gd-chip:        var(--bg-tabs);
  --gd-scrim:       rgba(6, 7, 11, 0.66);
  --gd-shimmer:     rgba(255, 255, 255, 0.12);
  --gd-lock-veil:   rgba(6, 7, 11, 0.36);
  --gd-on-accent:   #14101F;
  --gd-tooltip-bg:  #22253A;
  --gd-tooltip-fg:  var(--text);

  --gd-folder:      var(--purple);
  --gd-red:         var(--purple);
  --gd-green:       var(--purple);
  --gd-yellow:      var(--purple);
  --gd-cyan:        var(--purple);
  --gd-cyan-strong: #A78BFF;
  --gd-gold:        var(--purple);

  --gd-shadow-1:    var(--shadow-sm);
  --gd-shadow-2:    var(--shadow-md);
  --gd-shadow-menu: var(--shadow-lg);

  --color-primary:       var(--purple);
  --color-accent:        var(--purple);
  --color-success:       var(--purple);
  --color-warning:       var(--purple);
  --color-danger:        #F0A58F;
  --color-ink-secondary: var(--text-soft);
  --color-on-primary:    #14101F;
  --color-on-accent:     #14101F;
  --color-accent-soft:   var(--purple-soft);
  --color-warning-soft:  var(--purple-soft);
  --color-danger-soft:   rgba(240, 165, 143, 0.16);
  --color-border-strong: var(--border-strong);
}

/* The prototype's radii + pill buttons, applied to the browser chrome. */
.gd-body {
  --gd-radius:    14px;
  --gd-radius-lg: var(--radius-card);
  --gd-radius-sm: 10px;
}
.gd-btn,
.gd-storage-cta {
  border-radius: var(--radius-btn);
  font-weight: 800;
}
.gd-btn-primary {
  background: var(--purple);
  color: #FFFFFF;
  box-shadow: var(--shadow-purple);
  border: 0;
}
.gd-btn-primary:hover {
  background: var(--purple-dark);
  box-shadow: var(--shadow-purple-hover);
  transform: translateY(-2px);
}
.gd-modal { border-radius: 24px; }

/* ---------- 17.3 Faint decorative doodles -----------------
   The prototype's signature: a very faint, fixed doodle layer
   behind everything. `.bg-decor` is injected as the FIRST child
   of <body> by the markup of each page; it is inert
   (pointer-events:none, aria-hidden) and sits at z-index 0. */
.gd-body .bg-decor,
.shelf-root .bg-decor,
.viewer-shell .bg-decor {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  color: var(--doodle-color);
}

/* The library uses fixed chrome + an inner scroller, so its own layers must
   sit above the doodles. IMPORTANT: raise z-index ONLY — do NOT set
   `position: relative` here. These selectors are (0,2,0) and would override
   library.css's (0,1,0) `position: fixed` architecture
   (.gd-topbar:171, .gd-shell:371, and the <=1000px drawer .gd-sidebar:1038),
   which demotes the fixed chrome into document flow. That caused two bugs:
   the off-canvas mobile drawer kept reserving 248px of flex width (blank
   band on the left, folders squeezed right), and the document started
   overflowing by 64-128px with boxes at negative x, making the page
   draggable sideways on touch devices. z-index works on fixed boxes, so
   `fixed` is restated here at matching specificity instead. */
.gd-body .gd-topbar { position: fixed; z-index: 40; }
.gd-body .gd-shell  { position: fixed; z-index: 1; }

/* Flex child of the shell — `relative` is required for z-index and is
   harmless (it is not part of the fixed chrome). */
.gd-body .gd-main   { position: relative; z-index: 1; }

/* Desktop: the sidebar is an in-flow 248px rail (library.css:377-386). */
@media (min-width: 1001px) {
  .gd-body .gd-sidebar { position: relative; z-index: 1; }
}
/* Phone/tablet: the sidebar is an OFF-CANVAS DRAWER (library.css:1037-1049).
   It MUST stay out of flow, or it reserves width while invisible. */
@media (max-width: 1000px) {
  .gd-body .gd-sidebar { position: fixed; z-index: 30; }
}

/* Belt-and-braces against sideways drag: the fixed shell means the document
   never needs to scroll, so clip any residual horizontal overflow from the
   decorative doodle layer (.doodle.d1 right:-80px etc.) on the library page
   specifically. `clip` cannot be scrolled or rubber-banded, unlike
   `hidden`, which iOS Safari can still drag. */
.gd-body {
  overflow-x: clip;
  max-width: 100%;
}

/* Let the doodles show through the library's large flat panes. */
html[data-theme="light"] .gd-body .gd-main,
html[data-theme="light"] .gd-body .gd-scroller { background: transparent; }
html[data-theme="dark"] .gd-body .gd-main,
html[data-theme="dark"] .gd-body .gd-scroller { background: transparent; }

/* ---------- 17.4 In-app file viewer ------------------------
   The standalone viewer page (/library/view/:id) and the SPA
   viewer overlay. Only colours/surfaces change — the zoom
   controls, the PDF canvas stack and the media elements keep
   their existing geometry. */
html[data-theme="light"] .viewer-shell,
html:not([data-theme="dark"]) .viewer-shell {
  --color-bg:             var(--bg);
  --color-surface:        var(--bg-elevated);
  --color-surface-2:      #FAFAFC;
  --color-surface-sunken: var(--bg-tabs);
  --color-border:         var(--border-strong);
  --color-border-strong:  var(--border-strong);
  --color-primary:        var(--purple);
  --color-primary-hover:  var(--purple-dark);
  --color-primary-soft:   var(--purple-soft);
  --color-primary-border: rgba(108, 62, 244, 0.42);
  --color-accent:         var(--purple);
  --color-accent-hover:   var(--purple-dark);
  --color-accent-soft:    var(--purple-soft);
  --color-ink:            var(--text);
  --color-ink-secondary:  var(--text-soft);
  --color-ink-muted:      var(--text-mute);
  --color-text-muted:     var(--text-mute);
  --color-on-primary:     #FFFFFF;
  --color-on-accent:      #FFFFFF;
  --shadow-focus:         var(--shadow-focus);
}
html[data-theme="dark"] .viewer-shell {
  --color-bg:             var(--bg);
  --color-surface:        #171923;
  --color-surface-2:      #14161F;
  --color-surface-sunken: var(--bg-tabs);
  --color-border:         var(--border-strong);
  --color-border-strong:  var(--border-strong);
  --color-primary:        var(--purple);
  --color-primary-hover:  var(--purple-dark);
  --color-primary-soft:   var(--purple-soft);
  --color-primary-border: rgba(143, 107, 255, 0.42);
  --color-accent:         var(--purple);
  --color-accent-hover:   var(--purple-dark);
  --color-accent-soft:    var(--purple-soft);
  --color-ink:            var(--text);
  --color-ink-secondary:  var(--text-soft);
  --color-ink-muted:      var(--text-mute);
  --color-text-muted:     var(--text-mute);
  --color-on-primary:     #14101F;
  --color-on-accent:      #14101F;
  --shadow-focus:         var(--shadow-focus);
}
.viewer-shell { background: var(--bg); color: var(--text); }
.viewer-bar {
  background: var(--header-bg);
  backdrop-filter: saturate(180%) blur(10px);
  -webkit-backdrop-filter: saturate(180%) blur(10px);
  border-bottom: 1px solid var(--border);
}
.viewer-title { color: var(--text); }

/* ==========================================================
   17.5  ILLUSTRATION SLOTS  (Task C)
   ==========================================================

   The SVG illustrations are attached to two product moments:

     A) the DOWNLOADING / IN-PROGRESS state
     B) the COPY-LINK / SHARE view (a link opened from
        another site / an in-app browser)

   HOW THE THEME SWITCH WORKS
   Two techniques are used, chosen so that NO application
   JavaScript had to be modified:

   1. INLINE (preferred). Where the illustration sits in markup
      we control, it is an <img class="taysir-illu">, which
      illustrations.js swaps for an inline <svg> whose fills
      read the --illu-* custom properties. Those properties
      flip with the theme, and §14 above already gives them a
      `transition: fill var(--theme-duration)`, so the artwork
      cross-fades smoothly between light and dark.

   2. LAYERED BACKGROUNDS. Where the element is produced by
      application logic we must not touch (most importantly the
      library viewer's spinner, whose markup comes from
      public/static/library.js), the illustration is attached
      from CSS as a background-image. A CSS background cannot
      inherit custom properties, so scripts/build-illustrations.mjs
      pre-bakes a `-light` and a `-dark` copy of the artwork,
      and BOTH are mounted as stacked pseudo-elements. The theme
      only animates their `opacity`, which gives a genuine
      cross-fade (not a hard swap) and never shows a gap while
      the other file decodes.
   ---------------------------------------------------------- */

/* ---- shared illustration frame ---------------------------
   One reusable stage: the two baked variants are absolutely
   stacked and cross-faded by opacity. */
.taysir-illu-stage {
  position: relative;
  width: 100%;
  max-width: 340px;
  aspect-ratio: 4 / 3;
  margin: 0 auto;
  pointer-events: none;
}
.taysir-illu-stage::before,
.taysir-illu-stage::after {
  content: '';
  position: absolute;
  inset: 0;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  transition: opacity var(--theme-duration) var(--theme-ease);
}
/* ::before = light copy, ::after = dark copy */
.taysir-illu-stage::before { opacity: 1; }
.taysir-illu-stage::after  { opacity: 0; }
html[data-theme="dark"] .taysir-illu-stage::before { opacity: 0; }
html[data-theme="dark"] .taysir-illu-stage::after  { opacity: 1; }

/* Gentle float, matching the prototype's floatY on illustrations. */
.taysir-illu-stage {
  animation: floatY 4.2s ease-in-out infinite;
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .taysir-illu-stage { animation: none !important; }
  .taysir-illu-stage::before,
  .taysir-illu-stage::after { transition: none !important; }
}

/* ==========================================================
   (A) DOWNLOADING / IN-PROGRESS STATE
   ==========================================================

   `.gd-viewer-spinner` is emitted by library.js the moment a
   file starts opening, and replaced by the file's own content
   once the bytes have arrived. We therefore dress THAT element
   — the existing, untouched progress indicator — with the
   "file searching" illustration plus the prototype's Arabic
   progress copy, instead of introducing any new state.

   Because we only style an element the app already creates and
   already removes, the download lifecycle is completely
   unchanged: no new fetch, no new listener, no new timing. */
.gd-viewer-spinner {
  flex-direction: column;
  gap: 26px;
}

/* The illustration, stacked light/dark, above the spinner. */
.gd-viewer-spinner::before {
  content: '';
  display: block;
  width: min(62vw, 300px);
  aspect-ratio: 880 / 737;
  background-image: url('/static/illustrations/file-searching-light.svg');
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  animation: floatY 4.2s ease-in-out infinite;
  transition: opacity var(--theme-duration) var(--theme-ease);
}
html[data-theme="dark"] .gd-viewer-spinner::before {
  background-image: url('/static/illustrations/file-searching-dark.svg');
}

/* Arabic progress caption under the spinner. */
.gd-viewer-spinner::after {
  content: 'جارٍ تحضير الملف…';
  display: block;
  font-family: var(--taysir-font);
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--text-soft);
  letter-spacing: -0.01em;
}

/* Re-tune the existing ring so it reads as part of the scene. */
.gd-spin {
  border-color: var(--purple-soft);
  border-top-color: var(--purple);
}

/* Same treatment for the standalone viewer page's PDF host while
   PDF.js is still rendering its first page. */
.viewer-pdf:empty {
  min-height: 60vh;
  display: flex;
  align-items: center;
  justify-content: center;
}
.viewer-pdf:empty::before {
  content: '';
  width: min(62vw, 300px);
  aspect-ratio: 880 / 737;
  background-image: url('/static/illustrations/file-searching-light.svg');
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  animation: floatY 4.2s ease-in-out infinite;
}
html[data-theme="dark"] .viewer-pdf:empty::before {
  background-image: url('/static/illustrations/file-searching-dark.svg');
}

/* The library's own listing skeleton gets the "searching" art, so
   the in-progress feeling is consistent across the product. */
.gd-skeleton-wrap .gd-section-label {
  position: relative;
  padding-top: 200px;
  text-align: center;
  color: var(--text-soft);
  font-weight: 700;
}
.gd-skeleton-wrap .gd-section-label::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 190px;
  height: 190px;
  background-image: url('/static/illustrations/the-search-light.svg');
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  animation: floatY 4.4s ease-in-out infinite;
}
html[data-theme="dark"] .gd-skeleton-wrap .gd-section-label::before {
  background-image: url('/static/illustrations/the-search-dark.svg');
}
@media (prefers-reduced-motion: reduce) {
  .gd-viewer-spinner::before,
  .viewer-pdf:empty::before,
  .gd-skeleton-wrap .gd-section-label::before { animation: none !important; }
}

/* "No matches" search state — reuse the same family. */
.gd-empty-art { display: none; }
.gd-search-results .gd-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.gd-search-results .gd-empty::before {
  content: '';
  width: min(56vw, 240px);
  aspect-ratio: 649 / 728;
  background-image: url('/static/illustrations/the-search-light.svg');
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  margin-bottom: 18px;
}
html[data-theme="dark"] .gd-search-results .gd-empty::before {
  background-image: url('/static/illustrations/the-search-dark.svg');
}

/* ==========================================================
   (B) COPY-LINK / SHARE VIEW
   ==========================================================

   When تيسير is opened from ANOTHER site — a TikTok /
   Instagram / Facebook / Telegram link — the page loads inside
   that app's embedded web view, and the visitor is shown the
   "open this link in your browser / copy the link" panel
   (`.inapp-notice`, rendered by the React exterior).

   That panel is exactly the copy-link/share view, so it gets
   the share illustration. The panel is React markup we own, so
   here the INLINE technique is used: the component renders an
   <img class="taysir-illu">, illustrations.js inlines it, and
   the fills follow the --illu-* tokens — which means the
   artwork re-tints itself the instant the theme changes, with
   no second request.
   ---------------------------------------------------------- */
.inapp-notice__illu {
  display: block;
  width: min(58vw, 260px);
  margin: 0 auto 20px;
  animation: floatY 4s ease-in-out infinite;
  will-change: transform;
}
.inapp-notice__illu svg,
.inapp-notice__illu img {
  width: 100%;
  height: auto;
  display: block;
  background: transparent;
}
/* The inlined <svg> recolours through the shared illustration
   variables, transitioning on a theme toggle. */
.inapp-notice__illu .illu-fill-outline   { fill: var(--illu-outline); }
.inapp-notice__illu .illu-fill-mid       { fill: var(--illu-mid); }
.inapp-notice__illu .illu-fill-accent    { fill: var(--illu-accent); }
.inapp-notice__illu .illu-fill-skin      { fill: var(--illu-skin); }
.inapp-notice__illu .illu-fill-paper     { fill: var(--illu-paper); }
.inapp-notice__illu .illu-fill-paperAlt  { fill: var(--illu-paperAlt); }
.inapp-notice__illu .illu-fill-paperPure { fill: var(--illu-paperPure); }
.inapp-notice__illu .illu-stroke-mid     { stroke: var(--illu-mid); }
.inapp-notice__illu .illu-stroke-outline { stroke: var(--illu-outline); }
.inapp-notice__illu [class*="illu-fill-"],
.inapp-notice__illu [class*="illu-stroke-"] {
  transition: fill var(--theme-duration) var(--theme-ease),
              stroke var(--theme-duration) var(--theme-ease);
}
@media (prefers-reduced-motion: reduce) {
  .inapp-notice__illu { animation: none !important; }
  .inapp-notice__illu [class*="illu-fill-"],
  .inapp-notice__illu [class*="illu-stroke-"] { transition: none !important; }
}

/* A compact variant for short viewports, so the illustration
   never pushes the copy/dismiss buttons off screen. */
@media (max-height: 720px) {
  .inapp-notice__illu { width: min(40vw, 168px); margin-bottom: 12px; }
}

/* The subscribe modal's lock badge keeps the brand accent. */
.gd-modal-lock {
  background: var(--purple-soft);
  color: var(--purple);
}

/* ---- generic inline-illustration recolour ----------------
   Any illustration dropped anywhere in the app (not only the
   prototype's own containers from §14) recolours per theme. */
.taysir-illu .illu-fill-outline,
svg.taysir-illu .illu-fill-outline   { fill: var(--illu-outline); }
.taysir-illu .illu-fill-mid,
svg.taysir-illu .illu-fill-mid       { fill: var(--illu-mid); }
.taysir-illu .illu-fill-accent,
svg.taysir-illu .illu-fill-accent    { fill: var(--illu-accent); }
.taysir-illu .illu-fill-skin,
svg.taysir-illu .illu-fill-skin      { fill: var(--illu-skin); }
.taysir-illu .illu-fill-paper,
svg.taysir-illu .illu-fill-paper     { fill: var(--illu-paper); }
.taysir-illu .illu-fill-paperAlt,
svg.taysir-illu .illu-fill-paperAlt  { fill: var(--illu-paperAlt); }
.taysir-illu .illu-fill-paperPure,
svg.taysir-illu .illu-fill-paperPure { fill: var(--illu-paperPure); }
.taysir-illu .illu-stroke-mid,
svg.taysir-illu .illu-stroke-mid     { stroke: var(--illu-mid); }
.taysir-illu .illu-stroke-outline,
svg.taysir-illu .illu-stroke-outline { stroke: var(--illu-outline); }
.taysir-illu [class*="illu-fill-"],
.taysir-illu [class*="illu-stroke-"] {
  transition: fill var(--theme-duration) var(--theme-ease),
              stroke var(--theme-duration) var(--theme-ease);
}

/* ==========================================================
   17.6  SHELF SCENE + ADMIN CONSOLE
   ==========================================================

   The shelf (/shelf, /shelf/folder) is an ILLUSTRATED room —
   its wood, lamps and book spines are artwork, not chrome, so
   the scene itself is left intact. What is re-skinned is the
   surrounding chrome: the glass information panel, its
   typography and the page canvas, so the shelf now sits on the
   prototype's white (or deep-slate) canvas and speaks Almarai.
   ---------------------------------------------------------- */
html[data-theme="light"] .shelf-root,
html:not([data-theme="dark"]) .shelf-root {
  --shelf-glass-bg:    rgba(255, 255, 255, 0.72);
  --shelf-glass-bg-2:  rgba(108, 62, 244, 0.05);
  --shelf-glass-line:  rgba(255, 255, 255, 0.72);
  --shelf-glass-edge:  rgba(108, 62, 244, 0.22);
  --shelf-glass-text:  #111111;
  --shelf-glass-title: #6C3EF4;
  --shelf-glass-chip:  rgba(108, 62, 244, 0.08);
  --shelf-radius:      var(--radius-card);
}
html[data-theme="dark"] .shelf-root {
  --shelf-glass-bg:    rgba(23, 25, 35, 0.72);
  --shelf-glass-bg-2:  rgba(143, 107, 255, 0.14);
  --shelf-glass-line:  rgba(255, 255, 255, 0.10);
  --shelf-glass-edge:  rgba(143, 107, 255, 0.30);
  --shelf-glass-text:  #F2F2F5;
  --shelf-glass-title: #A78BFF;
  --shelf-glass-chip:  rgba(255, 255, 255, 0.08);
  --shelf-radius:      var(--radius-card);
}
.shelf-root { font-family: var(--taysir-font); }

/* The shelf page's own canvas follows the prototype background. */
body.shelf-page { background: var(--bg); color: var(--text); }

/* Admin console: keep its calm, functional layout but adopt the
   brand accent, Almarai and the prototype radii. Scoped to the
   admin body class so nothing else is affected. */
body.admin-page {
  --color-primary:       var(--purple);
  --color-primary-hover: var(--purple-dark);
  --color-primary-soft:  var(--purple-soft);
  --color-accent:        var(--purple);
  --color-accent-hover:  var(--purple-dark);
  --color-accent-soft:   var(--purple-soft);
  --color-bg:            var(--bg);
  --color-surface:       var(--bg-elevated);
  --color-surface-sunken:var(--bg-tabs);
  --color-border:        var(--border-strong);
  --color-ink:           var(--text);
  --color-ink-secondary: var(--text-soft);
  --color-ink-muted:     var(--text-mute);
  --color-on-primary:    #FFFFFF;
  --color-on-accent:     #FFFFFF;
  --radius-lg:           var(--radius-card);
  background: var(--bg);
  color: var(--text);
  font-family: var(--taysir-font);
}
html[data-theme="dark"] body.admin-page {
  --color-surface:       #171923;
  --color-surface-sunken:#14161F;
  --color-on-primary:    #14101F;
  --color-on-accent:     #14101F;
}

/* ==========================================================
   17.7  HEADER / FOOTER CHROME FOR INTERNAL PAGES
   Gives the internal pages the same frosted sticky header and
   ink footer the prototype uses, driven by the same tokens.
   ========================================================== */
.gd-topbar {
  background: var(--header-bg);
  backdrop-filter: saturate(180%) blur(10px);
  -webkit-backdrop-filter: saturate(180%) blur(10px);
  border-bottom: 1px solid var(--border);
}
.gd-brand-name { color: var(--purple); }

/* The sidebar rail becomes a quiet surface instead of a slab. */
.gd-sidebar { background: transparent; border-inline-end: 1px solid var(--border); }
.gd-storage {
  background: var(--bg-tabs);
  border-radius: var(--radius-card);
  border: 1px solid var(--border);
}

/* Cards / rows pick up the prototype's soft elevation + lift. */
.gd-card {
  background: var(--bg-elevated);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--border);
  transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
}
html[data-theme="dark"] .gd-card { background: #171923; }
@media (hover: hover) and (pointer: fine) {
  .gd-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
}
.gd-row { border-radius: 14px; }

/* Inputs match the prototype's rounded, tinted fields. */
.gd-search {
  background: var(--bg-input);
  border: 1.5px solid var(--border-input);
  border-radius: 14px;
  transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
}
.gd-search:focus-within {
  border-color: var(--purple);
  background: var(--bg-elevated);
  box-shadow: var(--shadow-focus);
}
.gd-search-input { font-family: var(--taysir-font); color: var(--text); }
.gd-search-input::placeholder { color: var(--text-mute); }

/* ==========================================================
   17.8  THEME TOGGLE PLACEMENT ON INTERNAL PAGES
   The toggle itself is the prototype component from §6; these
   rules only position it inside the existing top bars.
   ========================================================== */
.gd-topbar .theme-toggle,
.viewer-bar .theme-toggle { flex: 0 0 auto; margin-inline: 4px; }
.viewer-bar { gap: 10px; }

/* ==========================================================
   17.8b THEME TOGGLE SIZE LOCK ON THE REACT HOME PAGE
   WHY THIS EXISTS
   On /library the toggle sits in `.gd-topbar` / `.viewer-bar`,
   which pin it with `flex: 0 0 auto` (see §17.8 directly
   above). The React HomePage header is different markup —
   `.site-header > .header-inner > .header-actions > button.theme-toggle`
   — so NONE of those rules apply and nothing caps the pill.

   Two things can then stretch it on that page only:
     1. `.header-actions` is a flex row, so the button is a flex
        ITEM. The §6 base rule sizes it with `width: var(--w)`
        (a custom property), which loses to any later/equal
        cascade winner and gives no flex floor of its own.
     2. The React bundle (frontend/src/styles.css, compiled into
        /react/assets/*.css for its own ThemeToggle component)
        also declares a bare `.theme-toggle` TWICE. It is a
        same-specificity selector loaded from a DIFFERENT
        stylesheet, so whichever link resolves last wins the
        `width` / `display` declarations — and because the
        HomePage injects the prototype sheet at RUNTIME (see
        HomePage.tsx's useEffect) that order is not guaranteed.
        A stretched button then stretches the Lottie host
        (`width: 100%`) and the dotLottie canvas inside it, which
        is the scene overflow that was reported.

   FIX: re-assert the same 62x32 contract the §6 base rule
   intends, scoped to the HomePage header only, in explicit px
   (not `var(--w)`) and with the `flex: 0 0 auto` floor that
   `.gd-topbar` already gives the internal pages. The shared §6
   values and the §17.8 rule above are both left untouched, and
   this cannot reach `.gd-topbar`, `.viewer-bar`, or the
   Tailwind-built `SiteHeader` (it has no `.header-actions`).
   ========================================================== */
.site-header .header-actions .theme-toggle {
  flex: 0 0 auto;
  width: 62px;
  height: 32px;
  min-width: 62px;
  max-width: 62px;
  min-height: 32px;
  max-height: 32px;
  box-sizing: border-box;
}
/* Keep the decorative Lottie host (and its canvas) inside that box, so an
   oversized backing store can never paint outside the pill. */
.site-header .header-actions .theme-toggle .theme-toggle-lottie {
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  overflow: hidden;
}

/* ==========================================================
   17.8b-2  THEME TOGGLE — CLIP THE LOTTIE CANVAS TO THE PILL
   (append-only; §17.8b directly above is untouched)

   THE BUG
   A grey halo ringed the pill: two nested rounded shapes that
   disagreed. Measured, not guessed:

     • The dotLottie canvas does NOT fill the 62x32 box. The
       scene is 590x320 (aspect 1.844) and the host is 62x32
       (aspect 1.938), so the runtime's "contain" fit leaves a
       TRANSPARENT margin of ~2.3px left/right and ~0.5px top /
       ~1.6px bottom. The painted artwork is ~57.4 x 29.9.

     • Through that transparent margin the BUTTON's own
       `box-shadow` was showing:
           inset 0 1px 2px rgba(0,0,0,.5),
           inset 0 0 0 1px rgba(255,255,255,.04)
       An INSET shadow paints over the background but UNDER
       child content — and the canvas is transparent exactly
       where the ring sits, so it showed through. That ring is
       drawn on the HOST's 999px radius at 62x32 while the
       artwork's own pill is 57.4x29.9: the two rounded shapes
       that "disagree". The dark blurred inset read as the grey
       halo, the 1px white inset as the pale outline.

   WHERE THAT SHADOW CAME FROM — AND WHY IT IS A RACE
   It is NOT in this file and NOT in the prototype sheet. It is
   the bare `.theme-toggle` rule in frontend/src/styles.css,
   compiled into /react/assets/*.css. That rule also declares a
   `background` gradient and a `border`. The prototype sheet's
   §6 `.theme-toggle` (same 0,1,0 specificity) sets
   `background: transparent` and `border: 0` but declares NO
   box-shadow — so background/border depend on which sheet lands
   last, and the box-shadow simply has no competitor and always
   won. Because HomePage.tsx injects the prototype sheet at
   RUNTIME, that order is not guaranteed (same reasoning as
   §17.8b and §17.8c).

   THE FIX
   Re-assert the pill contract at (0,3,0), which outranks every
   bare `.theme-toggle` rule from either sheet regardless of load
   order: clip the host to the pill (matching radius + overflow)
   and strip every halo-capable property (box-shadow / border /
   background / filter) from the host, so the only rounded shape
   left on screen is the artwork's own. The canvas is pinned to
   display:block + 100%/100% with no intrinsic margin and no
   inline gap, so it can neither offset nor overflow the clip.

   NOT DONE ON PURPOSE — NO CANVAS SCALE.
   The pale frame is NOT baked into the .lottie (verified by
   rasterising it: zero stroke items, no drop-shadow effects, and
   the outer edge steps straight from alpha 0 to saturated fill
   with no pale ramp). So there is nothing to crop, and scaling
   the canvas would only magnify the artwork and risk clipping
   the sun/moon edge. The residual ~2.3px transparent margin is
   pure aspect mismatch; with the halo gone the host is fully
   transparent there, so nothing misaligns. Closing that margin
   is an ASSET job (re-export the scene at the pill's aspect),
   not a CSS one.

   SCOPE: `.header-actions` exists only in HomePage.tsx, so this
   cannot reach the Tailwind SiteHeader.tsx (which renders the
   separate React ThemeToggle component), `.gd-topbar`, or
   `.viewer-bar`. Behaviour is untouched: applyTheme, themeBoot,
   the 'taysir-theme' key, the [data-theme-toggle] delegated
   listener and all aria wiring live in JS and are not styled
   here. Focus stays visible — the focus ring is an `outline`
   (outlines are not clipped by the element's own overflow), and
   the `:focus-visible` track shadow targets a child, not this.
   No colour literals: only `transparent` / `none` / `0`.
   ========================================================== */
.site-header .header-actions .theme-toggle {
  /* Clip the animation to the pill. */
  border-radius: 999px;
  overflow: hidden;
  /* Remove every halo-capable property from the host, so the
     artwork's pill is the only rounded shape that paints. */
  background: transparent;
  border: 0;
  box-shadow: none;
  filter: none;
  /* No inline gap can be contributed around the canvas. */
  line-height: 0;
  font-size: 0;
}
.site-header .header-actions .theme-toggle .theme-toggle-lottie {
  border-radius: inherit;
  background: transparent;
  border: 0;
  box-shadow: none;
  filter: none;
  line-height: 0;
  font-size: 0;
}
.site-header .header-actions .theme-toggle .theme-toggle-lottie > canvas {
  display: block;          /* no inline baseline gap */
  width: 100%;
  height: 100%;
  margin: 0;               /* no intrinsic offset inside the clip */
  padding: 0;
  border: 0;
  border-radius: inherit;
  background: transparent;
  box-shadow: none;
  filter: none;
  vertical-align: top;
}

/* ==========================================================
   17.8c REACT HOME PAGE HEADER LAYOUT
   (hamburger visual-right / brand centred / toggle visual-left,
    and no separator line under the bar)

   WHY EVERY SELECTOR HERE IS DOUBLE-CLASSED
   `.site-header`, `.header-inner` and `.brand` are each declared
   TWICE in this codebase: once above in §5 of this file, and once
   again in the vendored, byte-for-byte prototype sheet at
   /static/prototype/styles.css §5. The React HomePage INJECTS that
   prototype sheet at runtime from its own useEffect, so it is
   appended to <head> AFTER this file's <link> and wins every
   equal-specificity selector. A bare `.site-header { border: 0 }`
   here would therefore be overruled by the prototype's
   `border-bottom` and silently do nothing.
   So each rule below is qualified with `.home-header` (0,2,0),
   which outranks the prototype's single-class `.site-header`
   (0,1,0) regardless of sheet order, and needs no `!important`.

   That scoping is also a hard requirement in the other direction:
   frontend/src/components/SiteHeader.tsx is a COMPLETELY different
   Tailwind header that reuses the same bare `.site-header` class
   name. Only the HomePage renders `.home-header`, so nothing here
   can reach it, nor `.gd-topbar` / `.viewer-bar` on /library.

   RTL NOTE: the document is `dir="rtl"`, so grid track 1 is the
   inline-START track, which the user sees on the RIGHT, and track 3
   is inline-END = the visual LEFT. `justify-self: start` likewise
   resolves to the visual right.

   MIRRORED ROW: the row is now assigned the OPPOSITE tracks from
   the original layout, using ONE mechanism — an explicit
   `grid-column` on each of the three children, so DOM order is
   untouched and no per-property override is scattered around.
   The three positions now map to: track 1 (visual right) = theme
   toggle, track 2 = brand (centre), track 3 (visual left) = desktop
   nav + hamburger. Each child's `justify-self` is flipped with it so
   it still hugs the OUTER edge of its own track. Nothing else moves:
   the `1fr auto 1fr` tracks, the 72px height, the 8px gap and every
   colour/font are exactly as before.

   §17.8b directly above is untouched: the toggle still matches
   `.site-header .header-actions .theme-toggle`, so its 62x32 lock
   and its Lottie clipping both still apply unchanged.
   ========================================================== */

/* (1) REMOVE THE SEPARATOR LINE UNDER THE HEADER.
   Neutralises the `border-bottom: 1px solid var(--border)` that §5
   of this file and §5 of the prototype sheet both put on
   `.site-header`. The shared declaration is left in place for
   SiteHeader.tsx and the internal pages; only this page's header
   opts out.

   `box-shadow: none` is a defensive reset of the §5 / prototype
   baseline only. It does NOT neutralise the light-theme `--scrolled`
   shadow in frontend/src/styles.css: that selector is
   `html[data-theme="light"] .site-header--scrolled` (0,2,1), which
   OUTRANKS this rule (0,2,0) and would win. The shadow is moot here
   for a different reason — `.site-header--scrolled` is only ever
   applied by SiteHeader.tsx, and App.tsx renders SiteHeader with
   `{!isHomeRoute && <SiteHeader />}`, so it never mounts on the
   HomePage route. Were that ever to change, this rule would need
   the `--scrolled` selector restated at equal-or-higher
   specificity. */
.site-header.home-header {
  border-bottom: 0;
  box-shadow: none;
}

/* (2) THREE-TRACK BAR. The two side tracks are both `1fr`, so they
   are always equal width and the `auto` middle track lands dead
   centre of the bar — the brand is therefore optically centred on
   mobile AND desktop, independently of how wide the nav or the
   toggle happen to be. Replaces the inherited
   `justify-content: space-between` flex row from §5. */
.site-header.home-header .header-inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  height: 72px; /* unchanged from §5 */
}

/* (3) VISUAL LEFT (inline-end in RTL) after the mirror: desktop nav
   + hamburger, pinned to the LAST track via `grid-column: 3` and
   `justify-self: end` so the cluster still sits against the outer
   edge of the bar.
   `min-width: 0` lets this track shrink instead of pushing the
   brand off-centre when the nav is wide. `overflow: hidden` is the
   collision guard that makes that shrink safe: min-width alone lets
   the track shrink, but the flex CONTENT can still paint past the
   track edge, and the nav's intrinsic width depends on the Almarai
   webfont resolving (§17.9). If the font fails and the fallback
   metrics are wider, the nav would otherwise spill out of track 1
   and overlap the centred brand. Clipping keeps it inside its own
   track no matter how wide it measures. */
.site-header.home-header .header-lead {
  display: flex;
  align-items: center;
  gap: 8px;
  grid-column: 3;
  justify-self: end;
  min-width: 0;
  overflow: hidden;
}

/* (4) CENTRE: the brand. `grid-column: 2` is stated explicitly
   because rules (3) and (5) now place their items definitely, and
   an auto-placed brand would otherwise be dropped into the first
   free track instead of the middle one.
   Rule (2) already does all the centring:
   the two `1fr` side tracks absorb every bit of free space, so the
   `auto` middle track resolves to exactly the brand's max-content
   width and lands dead centre of the bar. No `justify-self` is
   needed here — the item and its track are the same width, so
   `center` / `start` / the default `stretch` are indistinguishable.
   `white-space: nowrap` is NOT cosmetic: it keeps the brand on one
   line, which is what makes that max-content track width stable.
   Font/size/colour still come from §5 `.brand` and the <=520px
   override. */
.site-header.home-header .brand {
  grid-column: 2;
  white-space: nowrap;
}

/* (5) VISUAL RIGHT (inline-start in RTL) after the mirror: the theme
   toggle. Same flex row as §5 `.header-actions`, just pinned to the
   FIRST track now, hugging the bar's outer start edge. The button's
   own internals — the 62x32 lock in §17.8b, its Lottie host, its
   padding and its colours — are not referenced here and are
   unchanged. */
.site-header.home-header .header-actions {
  grid-column: 1;
  justify-self: start;
  min-width: 0;
}

/* (6) The hamburger is `display: none` by default in §5 and is
   switched to `inline-flex` at <=900px by §16. Because rule (3)
   makes `.header-lead` a flex container, restate that toggle at the
   same breakpoint with matching `.home-header` specificity so the
   button reliably appears on mobile and stays hidden on desktop. */
@media (max-width: 900px) {
  .site-header.home-header .header-lead .nav-desktop { display: none; }
  .site-header.home-header .header-lead .hamburger { display: inline-flex; }
}
/* `not all and (max-width: 900px)` — NOT `(min-width: 901px)`. Viewport
   width is a floating-point length, not an integer: browser zoom and
   fractional device-pixel ratios routinely produce widths like 900.5px,
   which satisfy neither `max-width: 900px` nor `min-width: 901px`. At
   those widths the old pair left BOTH the desktop nav and the hamburger
   visible at once. Negating the exact same query is the precise
   complement of the block above, so every possible width matches
   exactly one of the two and the gap closes by construction. */
@media not all and (max-width: 900px) {
  .site-header.home-header .header-lead .hamburger { display: none; }
}

/* ==========================================================
   17.9  ALMARAI WEBFONT (self-hosted, same-origin)
   The prototype loads Almarai from Google Fonts. The internal
   pages are served from our own origin and must keep working
   offline (the service worker caches the shell), so the family
   is declared here against files under /static/fonts/.
   A Google-Fonts <link> is ALSO present on the pages as a
   progressive enhancement; whichever resolves first is used.
   ========================================================== */
@font-face {
  font-family: 'Almarai';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/almarai-arabic-400-normal.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0898-08E1,
    U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74,
    U+FE76-FEFC;
}
@font-face {
  font-family: 'Almarai';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/static/fonts/almarai-arabic-700-normal.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0898-08E1,
    U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74,
    U+FE76-FEFC;
}
@font-face {
  font-family: 'Almarai';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url('/static/fonts/almarai-arabic-800-normal.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0898-08E1,
    U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74,
    U+FE76-FEFC;
}
@font-face {
  font-family: 'Almarai';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/almarai-latin-400-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Almarai';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/static/fonts/almarai-latin-700-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Almarai';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url('/static/fonts/almarai-latin-800-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ==========================================================
   18. MOBILE MENU — STEP 2A (LAYOUT) + 2B (COLOUR)
   ==========================================================
   Gives the STEP-1 scaffold in HomePage.tsx its geometry (2A)
   and its palette (2B).
   This block is still DELIBERATELY MOTIONLESS: no transition,
   no transform, no animation, no @keyframes, no opacity
   animation, no will-change. Motion arrives in 2C (the
   staggered slide-in). Everything below is geometry + paint.

   STRATEGY — sibling root escapes the header, then
               fixed root + absolute inner layers
   --------------------------------------------------------
   `<header class="site-header home-header">` carries a
   `backdrop-filter`. A `backdrop-filter` (like `filter`,
   `transform` and `contain`) makes the element a CONTAINING
   BLOCK for `position: fixed` descendants, so ANY `fixed`
   element inside the header — including the menu root itself —
   is trapped inside the 72px header box instead of filling the
   viewport. That was bug B1.

   The fix is structural: HomePage.tsx now renders the menu as a
   SIBLING of the header, immediately after `</header>` and
   before `<main>`, so it is no longer a descendant of the
   backdrop-filtered header and its `position: fixed` resolves
   against the initial containing block (the viewport) again.

   Because the menu left the header, the old
   `.site-header.home-header .mobile-menu` scope can no longer
   match it. Every rule below is therefore re-scoped onto the
   new `.home-mobile-menu` hook class that the root carries
   alongside the original `.mobile-menu`. `.home-mobile-menu`
   is the class that "escapes the header containing-block
   trap": it marks the one root that lives outside the header
   and owns the full-viewport fixed box.

   Layering is unchanged: the MENU ROOT ITSELF is the single
   fixed, viewport-filling box, and every inner layer is
   positioned `absolute` against that root.

   B2 — NEUTRALISING THE `.mobile-menu` DROPDOWN-CARD LEAK
   --------------------------------------------------------
   This root also carries the class `.mobile-menu`, and
   frontend/src/styles.css styles that class as a dark, rounded,
   blurred dropdown CARD. It is NOT one rule: FOUR separate
   top-level rules there match this root, and all four load
   AFTER this file (see the load-order note below):

     :198  `.mobile-menu`                          0-1-0
           the full card — absolute, padding, 1px border,
           22px radius, opaque background, backdrop-filter,
           box-shadow, plus `will-change` and `transform-origin`
     :472  `html[data-theme="light"] .mobile-menu` 0-2-1
           re-applies background-color + border-color + box-shadow
     :839  `.mobile-menu`                          0-1-0
           re-applies border-color, background, box-shadow, radius
     :845  `html[data-theme="dark"]  .mobile-menu` 0-2-1
           re-applies background-color

   The two THEME-SCOPED rules (:472 light, :845 dark) are the
   reason the root selector below needs 0-2-1-or-higher. They sit
   at 0-2-1, so the previous 0-2-0 root rule was simply outranked
   and its `background` / `box-shadow` resets were discarded — the
   card fill and drop shadow leaked straight back onto the open
   full-screen root, in BOTH themes.

   LOAD ORDER — WHY A TIE IS NOT ENOUGH
   --------------------------------------------------------
   The HTML shells load this sheet FIRST and the bundled
   styles.css SECOND (see public/react/index.html and
   frontend/index.html: `/static/taysir-theme.css` precedes the
   Vite CSS bundle that `frontend/src/main.tsx` imports). With
   equal specificity the LAST declaration wins, so styles.css
   wins every tie. A merely-equal 0-2-1 root selector (e.g. a
   bare `html ` prefix) would tie :472/:845 and still LOSE.

   The root selector is therefore
   `html[data-theme] .home-mobile-menu[data-open="true"]`, which
   is 0-3-1 — `[data-theme]`, `.home-mobile-menu` and
   `[data-open="true"]` are three class/attribute components,
   plus the `html` type selector. That outranks 0-2-1 outright,
   so source order never comes into play and the reset holds in
   light AND dark. The unquoted `[data-theme]` matches whichever
   value is set; the pre-paint inline script in every shell
   always sets one, so the attribute is guaranteed present.
   Verified empirically against all four rules in both themes.

   Note that the card's own `backdrop-filter` had to go too: left
   in place it would have re-created the containing-block trap
   for the fixed root from the inside. `will-change` and
   `transform-origin` are reset for the same reason — see the
   per-property notes on the rule itself.

   All of this is plain specificity. No `!important` anywhere.

   CLOSED / OPEN GEOMETRY
   --------------------------------------------------------
   CLOSED is left exactly as §5 already defines it:
   `.mobile-menu { display: none }`, so the root and all its
   layers are removed from layout and nothing here applies.
   Everything below is keyed on `[data-open="true"]`, the flag
   prototype/script.js already toggles, which §5 turns into
   `display: flex`. So OPEN geometry = full-viewport fixed
   root, 70vw left-anchored waves + panel; CLOSED geometry =
   untouched. No off-canvas transform is introduced here — the
   layers sit at their final, open positions so 2C has a known
   destination to animate toward.

   RTL NOTE: the document is `<html dir="rtl">`, so the panel
   and the waves are anchored with the logical
   `inset-inline-end: 0`, which resolves to the VISUAL LEFT
   edge in RTL (and would flip correctly if the page were ever
   served LTR).

   SCOPING: every selector below is scoped under the
   `.home-mobile-menu` hook class that only this page's menu
   root carries, so nothing here can leak into the internal
   pages' `.gd-topbar` header or into any other `.mobile-menu`.
   Every selector keeps the `[data-open="true"]` attribute, so
   specificity is 0-3-1 for the root rule (which has to outrank
   the 0-2-1 theme rules listed above) and 0-3-0 or more for the
   descendant rules. The descendants deliberately keep their
   original specificity: no rule in frontend/src/styles.css
   targets `.mm-scrim` / `.mm-wave*` / `.mm-panel` / `.mm-close`
   / `.mm-links` at all, so they compete with nothing and need
   no `html[data-theme]` prefix. Only the ROOT shares the
   `.mobile-menu` class, and only the root needed the bump.
   No `!important` is used.

   STEP 2B — THE PALETTE
   --------------------------------------------------------
   Re-verified before writing 2B: `.mm-scrim`, `.mm-wave*`,
   `.mm-panel`, `.mm-close`, `.mm-links`, `.mm-link-label` and
   `.mm-num` appear NOWHERE outside this section. The only other
   file that mentions them is HomePage.tsx, and only inside a
   comment. frontend/src/styles.css styles the BEM variants
   `.mobile-menu__scrim` / `.mobile-menu__link`, which this DOM
   does not use. So the inner layers compete with nothing and
   keep their plain 0-3-0 / 0-4-0 specificity — no `html[data-
   theme]` bump is needed for any of them. Only two weak
   inherited rules exist and both are outranked here by orders
   of magnitude: `.mobile-menu a` (§5, 0-1-1) and the bare
   `a { color: inherit }` / `button { font: inherit }` element
   resets.

   DERIVATION — the four wave shades come from the BRAND token
   `--purple`, not from a new invented hue. `--purple` is
   `#6C3EF4` = hsl(255.2, 89.2%, 60%) in light and `#8F6BFF` =
   hsl(254.6, 100%, 71%) in dark; `--purple-dark` is `#5828E0` =
   hsl(255.7, 74.8%, 51.8%). Every wave therefore locks the hue
   to the brand's ~255deg and only walks LIGHTNESS, producing one
   continuous family rather than four unrelated purples:

     --mm-wave-1  hsl(255, 100%, 84%)  #C2ADFF   lightest
                  a touch lighter than the dark theme's
                  `--illu-accent` (#A78BFF, L 77%), so the
                  first wave reads as a pale brand tint.
     --mm-wave-2  hsl(255,  92%, 72%)  #9776F9
                  effectively the DARK theme's `--purple`
                  (#8F6BFF, L 71%) — an existing brand value.
     --mm-wave-3  hsl(255,  89%, 60%)  #6C3EF4
                  EXACTLY the light theme's `--purple`. The
                  ladder's anchor: the middle wave IS the brand
                  colour.
     --mm-wave-4  hsl(256,  75%, 46%)  #4C1DCD   darkest
                  `--purple-dark` (#5828E0, L 51.8%) taken ~6
                  points deeper so the back of the stack still
                  separates from wave-3 once 2C staggers them.

   The 12-point lightness step (84 -> 72 -> 60 -> 46) is even
   enough that the four layers will read as distinct bands while
   they travel in 2C. These are written as LITERAL hsl() values,
   not `var(--purple)`, on purpose: the waves must be the SAME
   four shades in light and dark. `--purple` changes value
   between themes, so referencing it would silently re-shuffle
   the ladder and break its light -> dark ordering in dark mode.
   The literals are documented above against the tokens they
   derive from.

   --mm-scrim  rgba(12, 9, 22, 0.58)
   A near-black with a faint violet cast (12/9/22 sits on the
   brand hue rather than neutral grey, and is a shade of the
   existing `--footer-bg` #17151E family) at 58% — inside the
   50-60% the reference shows. Composited it gives #727078 over
   the light page and #0D0C16 over the dark page, so the white
   panel keeps 4.88:1 against the dimmed area in light and
   19.42:1 in dark. Fixed rgba rather than a token because a
   scrim's job is to darken whatever is behind it identically in
   both themes.

   --mm-num  #6C3EF4 in light / #8F6BFF in dark — this one DOES
   track `var(--purple)`, because unlike the waves it sits on the
   always-white panel and only has to stay legible there: 5.78:1
   for the light value, and the dark value is only used if a
   theme ever darkens the panel. See 18.7.

   BOTH THEMES: the panel is white in light AND dark, matching
   the reference. It is an overlay surface, not a page surface,
   so it does not follow `--bg`. Consequently every colour
   painted ON the panel (link text, number, close icon) is also
   theme-independent and tuned for white — that is what keeps
   the menu readable in dark mode instead of going
   light-text-on-white. Stated explicitly on each rule below.
   ========================================================== */

/* ---- 18.1  The fixed, viewport-filling root ---------------
   Specificity 0-3-1: `[data-theme]` + `.home-mobile-menu` +
   `[data-open="true"]` are three class/attribute components and
   `html` is one type component. That is what it takes to outrank
   the 0-2-1 theme rules at frontend/src/styles.css:472 and :845,
   which load after this file and would otherwise win any tie on
   source order (see the LOAD ORDER note in the header above).
   `z-index: 1000` lifts the whole menu above the sticky header
   and the page content. */
html[data-theme] .home-mobile-menu[data-open="true"] {
  position: fixed;
  inset: 0;
  z-index: 1000;

  /* 2B — the menu's own palette, declared once here and
     inherited by every inner layer. Custom properties are
     inert paint values: declaring them applies nothing on its
     own and triggers no motion. Derivations and the reasoning
     for each value are in the STEP 2B block of the section
     header above. */
  --mm-wave-1:  hsl(255, 100%, 84%);   /* #C2ADFF  lightest */
  --mm-wave-2:  hsl(255,  92%, 72%);   /* #9776F9           */
  --mm-wave-3:  hsl(255,  89%, 60%);   /* #6C3EF4 = --purple */
  --mm-wave-4:  hsl(256,  75%, 46%);   /* #4C1DCD  darkest  */
  --mm-scrim:   rgba(12, 9, 22, 0.58);
  --mm-panel:   #FFFFFF;
  --mm-ink:     #141019;               /* near-black, violet-cast */
  --mm-num:     var(--purple);

  /* B2 — neutralise the `.mobile-menu` dropdown-card leak that
     frontend/src/styles.css applies across FOUR rules (:198 and
     :839 at 0-1-0, plus the theme-scoped :472 and :845 at
     0-2-1). All four are outranked by the 0-3-1 selector above,
     with no `!important`. Each declaration here zeroes one card
     decoration those rules apply:
       border-width     kills its 1px border on ALL four sides.
                        On a fixed `inset: 0` root a border
                        shrinks the PADDING BOX — the containing
                        block the absolute layers below resolve
                        `inset: 0` / `bottom: 0` against — so any
                        border left standing would leave every
                        layer short of the viewport edge. This
                        also covers the 1px `border-bottom` that
                        §5 puts on `.mobile-menu`. Only the WIDTH
                        is zeroed, never the colour, and it is
                        scoped to the open state, so the §5
                        closed state is untouched.
       border-radius    the 22px / `--radius-card` rounding — a
                        full-screen surface must have square
                        corners.
       background       its opaque fill (dark from :198/:839,
                        near-white from :472, #171923 from :845),
                        which would hide the scrim and the four
                        wave layers.
       backdrop-filter  its blur/saturate. MUST be removed: on
                        the root it would make the root its own
                        containing block and re-trap the fixed
                        box — the very B1 failure mode.
       box-shadow       the dropdown drop shadow + inset hairline
                        (re-applied by :472 and :839).
       padding          its .75rem inset, which would push the
                        absolute layers' containing block in on
                        every side.
       will-change      :198 declares `transform, opacity` for
                        its scale-fade dropdown reveal. Left
                        leaking it would pin a permanent
                        compositor layer on a full-viewport
                        element for the entire session, wasting
                        GPU memory even while the menu is shut.
                        2C will re-declare it deliberately, for
                        the elements and the duration it needs.
       transform-origin :198 sets `top center` so its scaleY
                        grows the dropdown downward from under
                        the nav bar. That anchor is wrong here:
                        this menu is a left-edge slide-in, so it
                        is reset to `left center` — the visual
                        LEFT edge, vertically centred, which is
                        the edge the panel and waves enter from
                        after the mirror. Resetting it now
                        keeps 2C's slide-in from being mis-
                        anchored to the top-centre point. */
  border-width: 0;
  border-radius: 0;
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: none;
  padding: 0;
  will-change: auto;
  transform-origin: left center;
}

/* ---- 18.2  Scrim — backdrop area, lowest layer -----------
   Specificity 0-3-0. Fills the whole fixed root.

   2B COLOUR: `rgba(12, 9, 22, 0.58)` — a violet-cast near-black
   at 58%, the reference's ~50-60% dim. Because the panel and
   the waves cover the inline-end 70vw, the visible result is
   the darkened ~30% strip on the visual RIGHT that the design
   calls for, while the remaining 42% sits harmlessly UNDER the
   opaque panel. A single flat fill, no gradient, no blur: a
   `backdrop-filter` here would make this layer a containing
   block and risk re-introducing the B1 trap for anything fixed
   inside the menu.

   BOTH THEMES: identical in light and dark — a scrim darkens
   what is behind it, and the page behind is already light or
   dark accordingly. Over the light page it composites to
   #727078, over the dark page to #0D0C16; the white panel keeps
   4.88:1 and 19.42:1 against it respectively, so the panel edge
   stays clearly defined either way. */
.home-mobile-menu[data-open="true"] .mm-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--mm-scrim);
}

/* ---- 18.3  The four wave layers --------------------------
   Specificity 0-3-0. Full-height, 70vw wide, all four
   anchored to the same inline-END (visual LEFT in RTL)
   edge, so they sit exactly on top of one another. 2B gives
   them their four graduated purple shades and 2C staggers
   their slide-in.

   MIRRORED: the anchor was `inset-inline-start: 0` (visual
   RIGHT). It is now `inset-inline-end: 0`, the mirror edge,
   which is the other half of the entry-side flip — §19.2's
   parked transform flips sign to match. */
.home-mobile-menu[data-open="true"] .mm-wave {
  position: absolute;
  top: 0;
  bottom: 0;
  inset-inline-end: 0;
  width: 70vw;
}

/* Stacking order inside the root, back to front, each paired
   with its shade from the ladder. Back to front:
   scrim(1) < wave-1(2) < wave-2(3) < wave-3(4) < wave-4(5)
   < panel(6). Each rule is 0-3-0.

   2B COLOUR: the four graduated purples, LIGHTEST at the back
   (wave-1) to DARKEST at the front (wave-4). Flat `background`
   fills only — no gradients, no shadows, nothing that would
   imply motion.

   EXPECTED IN 2B: with no animation yet, all four are
   coincident at the same 70vw left-anchored box, so wave-4
   (#4C1DCD, the darkest and the frontmost) is the only one
   actually visible, and it is itself almost entirely hidden
   behind the opaque panel that sits above it at z-index 6.
   That is CORRECT for this step. The ladder only reveals
   itself in 2C, when the stagger offsets each layer and the
   trailing edges fan out into visible bands. The shades are
   defined now so 2C has nothing left to do but move them.

   BOTH THEMES: fixed literals, identical in light and dark, so
   the light -> dark ordering of the ladder can never invert.
   The waves are pure decoration and carry no text, so they have
   no contrast requirement of their own. */
.home-mobile-menu[data-open="true"] .mm-wave-1 { z-index: 2; background: var(--mm-wave-1); }
.home-mobile-menu[data-open="true"] .mm-wave-2 { z-index: 3; background: var(--mm-wave-2); }
.home-mobile-menu[data-open="true"] .mm-wave-3 { z-index: 4; background: var(--mm-wave-3); }
.home-mobile-menu[data-open="true"] .mm-wave-4 { z-index: 5; background: var(--mm-wave-4); }

/* ---- 18.4  The panel — topmost layer ---------------------
   Specificity 0-3-0. Same 70vw / full-height / left-anchored
   box as the waves (mirrored: `inset-inline-start: 0` ->
   `inset-inline-end: 0`), but above all of them. A flex COLUMN so
   `.mm-close` takes the top row and `nav.mm-links` flows
   below it. `overflow-y: auto` is layout insurance: on a very
   short viewport the link list scrolls inside the panel
   instead of spilling past the bottom edge.

   2B COLOUR: flat `#FFFFFF`. A pure white, not a warm off-white
   — the panel sits directly against the saturated violet waves,
   and any warmth in it reads as a dirty cream next to that hue,
   whereas pure white reads as crisp paper. Opaque, so it fully
   hides the coincident waves beneath it, which is what makes
   the 2C reveal work: the waves are only ever seen in the gap
   the panel has not yet covered.

   BOTH THEMES: the panel stays WHITE in dark mode too, exactly
   as the reference shows. It is an overlay surface, not a page
   surface, so it deliberately does NOT follow `--bg` (which
   would turn it #0E1016 in dark). Everything painted on it
   below is therefore also pinned to dark-on-white values rather
   than `--text`; if the panel followed the theme and the text
   did not, dark mode would render near-black text on a
   near-black panel. Pinning both sides together is what keeps
   the menu readable in BOTH themes.

   DEPTH: one soft, static drop shadow along the panel's outer
   edge, purely to lift it off the waves. It is a plain
   decorative `box-shadow` with no transition and no spread
   animation — nothing here re-triggers the dropdown-card shadow
   that 18.1 removes from the ROOT; this is a different element.
   Kept deliberately minimal: low alpha, no inset hairline, no
   coloured glow. */
.home-mobile-menu[data-open="true"] .mm-panel {
  position: absolute;
  top: 0;
  bottom: 0;
  inset-inline-end: 0;
  width: 70vw;
  z-index: 6;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  background: var(--mm-panel);
  box-shadow: 0 0 40px rgba(12, 9, 22, 0.18);
  padding: 18px 26px 32px;
}

/* ---- 18.5  Close button — pinned to the panel's top row --
   Specificity 0-4-0. `flex: 0 0 auto` keeps it at its natural
   height so it can never be stretched or squashed by the link
   list.

   2B COLOUR: a black X on nothing. The inline SVG in
   HomePage.tsx is `stroke="currentColor"`, so setting `color`
   to the near-black ink is all it takes to paint the icon —
   no fill, no separate stroke declaration. The button itself
   gets a TRANSPARENT background and `border: 0`: the reference
   shows a bare glyph, and any chip or circle behind it would
   compete with the numbered links for attention. 18.79:1
   against the white panel.

   TAP TARGET: 44x44 — the same floor the links use — kept
   clean by sizing the BUTTON rather than the 22px glyph, which
   stays centred inside it via flex. `align-self: flex-end` puts
   it at the panel's inline-end, which in this RTL document is
   the visual LEFT, i.e. the top corner away from the links'
   right-aligned edge. `-8px` inline-end margin pulls its
   generous tap padding back so the GLYPH optically lines up
   with the panel padding instead of sitting inset from it.

   BOTH THEMES: pinned dark, because the panel under it is white
   in both. `currentColor` means the glyph can never drift away
   from the value set here. */
.home-mobile-menu[data-open="true"] .mm-panel .mm-close {
  flex: 0 0 auto;
  align-self: flex-start;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  margin-inline-end: -8px;
  border: 0;
  border-radius: 12px;
  background: transparent;
  color: var(--mm-ink);
}

/* ---- 18.6  Link list — vertical stack -------------------
   Specificity 0-3-0 for the nav, 0-3-1 for the anchors (both
   comfortably above the 0-1-1 `.mobile-menu a` rule in §5 they
   need to extend). Each anchor becomes a full-width row with a
   44px minimum height — the standard touch-target floor — so
   the three links stack vertically and stay tappable.
   `space-between` pushes `.mm-num` to the far end of the row
   from `.mm-link-label`.

   2B: the nav is pushed down off the close row with a top
   margin and given breathing room between rows via `gap`, so
   the three links read as a deliberate list rather than a
   stack. `margin-block-start: auto` is NOT used — the
   reference has the links sitting in the upper-middle of the
   panel, not pinned to its floor. */
.home-mobile-menu[data-open="true"] .mm-links {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-block-start: 6vh;
}

/* The link rows. Specificity 0-3-1, comfortably above the
   0-1-1 `.mobile-menu a` rule in §5 whose 16px / border-bottom
   / `var(--text)` treatment this replaces, and above the bare
   `a { color: inherit }` element reset.

   2B TYPE + COLOUR: large, heavy, near-black. `clamp()` scales
   the label from 24px on a small phone up to 34px on a wide
   one without a media query, matching the reference's oversized
   menu type. Weight 800 lines up with the `h1..h4` weight this
   sheet already uses, so the links read as headings rather than
   body links. `line-height: 1.25` is tight enough to keep the
   big Arabic type compact while still clearing its ascenders
   and descenders. `letter-spacing: -0.01em` mirrors the
   heading rule in §3.

   RTL: the document is `dir="rtl"`, so the flex main axis runs
   right-to-left. `.mm-link-label` is first in the DOM and lands
   at the row's visual RIGHT; `space-between` throws `.mm-num`
   to the opposite end, the visual LEFT. That is exactly the
   reference layout — right-aligned label, small number off to
   its left — achieved with no positioning at all.

   BOTH THEMES: pinned to `--mm-ink` (#141019), NOT `--text`.
   `--text` flips to #F2F2F5 in dark, which on this permanently
   white panel would be white-on-white. Pinning holds 18.79:1
   in both themes. `text-decoration: none` is inherited from the
   base reset and left alone. */
.home-mobile-menu[data-open="true"] .mm-links a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  min-height: 44px;
  gap: 16px;
  padding-block: 10px;
  color: var(--mm-ink);
  font-size: clamp(24px, 6.4vw, 34px);
  font-weight: 800;
  line-height: 1.25;
  letter-spacing: -0.01em;
}

/* ---- 18.7  The 01 / 02 / 03 counters -------------------
   Specificity 0-4-0.

   2B COLOUR: PURPLE, and the only purple on the panel — it is
   the single accent tying the white panel back to the violet
   waves behind it. This one tracks `var(--purple)` rather than
   a literal, because unlike the waves it has no ordering to
   preserve and simply wants to be "the brand accent": #6C3EF4
   in light (5.78:1 on white, comfortably readable) and #8F6BFF
   in dark.

   SIZE / PLACEMENT: small and light against the heavy label —
   weight 700 instead of 800 — so it reads as a quiet counter,
   not a second word. The size is an ABSOLUTE `14px`, not an
   `em` fraction of the label: the label is itself a `clamp()`
   that bottoms out at 24px on a narrow phone, and a relative
   `0.4em` would drag the counter down to ~9.6px there, below
   readable. A fixed 14px stays legible at the small end and
   still reads as clearly subordinate at the label's 34px
   maximum.

   Placement is already handled: `space-between` on the row
   parks it at the visual LEFT end in this RTL layout, so
   nothing here needs to position it. `flex: 0 0 auto` stops it
   being squeezed by a long label, and `font-variant-numeric:
   tabular-nums` locks 01/02/03 to a single column width so
   their left edges line up down the list. `line-height: 1`
   keeps the small glyphs optically centred against the much
   taller label.

   BOTH THEMES: both `--purple` values clear 4.5:1 on the
   always-white panel, so the accent stays legible either way. */
.home-mobile-menu[data-open="true"] .mm-links .mm-num {
  flex: 0 0 auto;
  color: var(--mm-num);
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
}

/* ==========================================================
   19. MOBILE MENU — STEP 2C: THE STAGGERED SLIDE-IN
   ==========================================================
   §18 gave the multi-layer menu on the HomePage its geometry
   (2A) and its palette (2B) but left it DELIBERATELY
   MOTIONLESS. This section is the motion, and nothing else:
   not one box, not one colour and not one z-index from §18 is
   restated or overridden below. §19 adds `transform`,
   `opacity`, `transition` and the per-layer stagger delays —
   that is the entire surface area of this section, plus the
   two small affordances in 19.7 and the scroll lock in 19.9.

   WHY THE MENU LOOKED DEAD BEFORE THIS SECTION
   --------------------------------------------------------
   HomePage.tsx renders the whole layer stack already
   (`#mobile-menu.home-mobile-menu` > `.mm-scrim`,
   `.mm-wave-1..4`, `.mm-panel` > `.mm-close` + `nav.mm-links`)
   and prototype/script.js already flips `data-open`. But for a
   while the only rule that reacted to that flag was §5's
   `.mobile-menu[data-open="true"] { display: flex }`, written
   for the OLD FLAT TEXT MENU: the layers existed in the DOM
   and no stylesheet painted them, so the hamburger appeared to
   do nothing at all. §18 fixed the paint. §19 fixes the
   movement.

   ── THE CENTRAL DESIGN DECISION: `data-open` IS NOT THE
      ANIMATION FLAG — `.mm-in` IS ────────────────────────────
   This is the one thing to understand before changing
   anything here.

   §18 keys EVERYTHING on `[data-open="true"]`: the fixed
   full-viewport root, the 70vw wave boxes, the panel, the
   z-index ladder AND the entire 2B palette (`--mm-wave-1..4`,
   `--mm-scrim`, `--mm-panel`, `--mm-ink`, `--mm-num` are all
   declared on the `[data-open="true"]` root rule). §5, in two
   different sheets, additionally ties `display` to the same
   flag.

   So `data-open` is a PAINT flag, not a motion flag. If the
   closing animation were driven by flipping it to "false",
   then on the very first frame of the close the root would
   lose `position: fixed`, the waves would lose their width and
   their colour, the panel would lose its white fill, and §5
   would set `display: none` on top — the element would simply
   vanish and there would be nothing left on screen to animate.
   That is exactly the "hidden cuts off the closing animation"
   failure this step has to fix, and driving the exit from
   `data-open` would have re-created it in a subtler form.

   The contract is therefore:

     data-open="true"   the menu is ON SCREEN — either sliding
                        in, resting open, or sliding out. §18
                        paints it for this entire window.
     .mm-in             the menu is at its RESTING OPEN
                        position. Present = layers in place;
                        absent = layers parked off-canvas.
     data-open="false"  the menu is fully gone. §5's
                        `display: none` applies again and the
                        subtree leaves the a11y tree and
                        hit-testing entirely.

   The close sequence is consequently: remove `.mm-in` (the
   layers animate out while §18 keeps painting them, because
   `data-open` is still "true"), and only when the last
   transition has finished does the script set
   `data-open="false"` and let `display: none` take over. That
   is the whole reason the JS in prototype/script.js waits for
   `transitionend` instead of hiding the menu immediately, and
   the reason it no longer sets the `hidden` attribute while
   the menu is closing — `hidden` is `display: none` in the UA
   sheet and would cut the exit off on frame one.

   BENEFIT OF THIS SPLIT: §18 needs NO changes whatsoever, and
   the closed state stays byte-for-byte what §5 always did
   (`display: none`), so the closed menu is genuinely removed
   from the box tree, unfocusable and untappable, with no
   `visibility` / `pointer-events` juggling and no risk of an
   invisible full-viewport overlay swallowing taps on the page
   underneath. It also means no rule in §19 has to fight §5
   over `display` at all.

   THE ONE COST: a transition cannot start from a
   `display: none` element, because there is no previous
   computed style to interpolate from. The script therefore
   sets `data-open="true"` (which makes the element rendered
   and parked off-canvas by 19.2), forces a synchronous reflow,
   and only then adds `.mm-in` on the next animation frame.
   That is class toggling, not JS animation — every value that
   moves is interpolated by the CSS engine.

   ── SPECIFICITY / LOAD-ORDER CONTRACT ─────────────────────
   Verified before writing this section:

     frontend/src/styles.css :198 `.mobile-menu`             0-1-0
     frontend/src/styles.css :472 html[light] .mobile-menu   0-2-1
     frontend/src/styles.css :839 `.mobile-menu`             0-1-0
     frontend/src/styles.css :845 html[dark]  .mobile-menu   0-2-1
     taysir-theme.css §5          `.mobile-menu`             0-1-0
     taysir-theme.css §5          `.mobile-menu[data-open]`  0-1-1
     prototype/styles.css §5      the same two, injected LAST

   NONE of them uses `!important`, and none of them sets
   `transform`, `transition`, `transition-delay` or (except the
   dropdown-card `will-change`, which §18.1 already resets)
   anything else this section declares. Grepping the whole of
   taysir-theme.css for `!important` returns only §2's theme
   fade, §15's `prefers-reduced-motion` block
   (`[data-reveal]`, `.float-illu`, `.btn:hover`,
   `.theme-toggle*`, `:root.theme-transitioning *`) and the
   `.gd-skeleton` / `.inapp-notice` / `.taysir-illu-stage`
   reduced-motion resets — not one of them targets
   `.mobile-menu`, `#mobile-menu`, `[data-open]`, `.mm-*` or
   `.hamburger`. So there is no `!important` to beat here.

   The one `!important` that DOES reach this subtree is §15's
   `:root.theme-transitioning * { transition: none !important }`.
   That class is only on <html> for ~0.5s during a theme
   toggle, and suppressing the menu transition inside that
   window is harmless.

   Because the prototype sheet is injected at runtime by
   HomePage.tsx and therefore lands LAST in <head>, an
   equal-specificity selector here would lose on source order.
   Every selector below is consequently ID-scoped
   (`#mobile-menu`, 1-x-x), which outranks all of the 0-x-x
   rules above outright regardless of sheet order. Each one
   also keeps `.home-mobile-menu`, so these rules can only
   match this one HomePage root and can never reach
   SiteHeader.tsx, `.gd-topbar` or the internal pages.

   ── RTL / DIRECTION (deliberate — do not "fix") ───────────
   The entrance is a PHYSICAL left-edge slide. `translateX()`
   is a physical transform and is NOT flipped by `dir="rtl"`,
   so `translateX(-100%)` means "displaced to the left" in both
   directions. §18 anchors the waves and the panel with
   `inset-inline-end: 0`, which in this RTL document is the
   VISUAL LEFT edge. Together that parks each layer just off
   the left edge of the viewport and slides it RIGHTWARD into
   place. This pairing of a physical transform with a logical
   inset is intentional and correct.

   MIRRORED ENTRY SIDE: the pair was `translateX(100%)` +
   `inset-inline-start: 0` (enter from the visual RIGHT). BOTH
   halves are flipped together — the sign of the parked
   transform AND the anchor offset — so the closed panel is
   still fully off-screen (100% of its own 70vw width beyond
   the new edge) and the open panel is still flush with it.
   Every duration, easing token and stagger delay is
   untouched: nothing in §19.1 / §20.1 encodes a side, and
   the wave ladders encode DEPTH (back-to-front), not
   direction, so they are left exactly as they were.

   ── PERFORMANCE (low-end phones over LTE) ─────────────────
   Pure CSS transitions. No JS animation, no library, no
   `@keyframes`. Only `transform` and `opacity` are animated,
   both compositor-only, so no frame triggers layout or paint.
   `will-change` is declared ONLY on the layers that actually
   move and ONLY while `.mm-in` is being applied, so no
   compositor layer is retained while the menu is shut — §18.1
   resets `will-change: auto` on the root for that exact
   reason and is left untouched.

   ── NO HARD-CODED COLOURS ─────────────────────────────────
   §19 declares no colour of its own. Every surface keeps the
   value §18 gave it, and the two affordances added in 19.7
   resolve through `--purple`, `--purple-dark`, `--purple-soft`
   and `--border`, so both themes keep working with no
   theme-scoped rule.
   ========================================================== */

/* ---- 19.1  Motion tokens --------------------------------
   Declared on the root regardless of state, so the parked
   rules and the resting rules read the same values and one
   edit re-times the whole sequence. Custom properties are
   inert: declaring them paints nothing and moves nothing.

   --mm-dur-panel  the panel's travel. 380ms reads as a
                   deliberate reveal without feeling sluggish
                   when a student taps the hamburger twice.
   --mm-dur-wave   slightly faster, so the waves snap out from
                   behind the panel rather than lagging it.
   --mm-dur-scrim  the backdrop only cross-fades.
   --mm-step       the stagger interval. Four layers x 55ms
                   spreads the fan-out over ~165ms, well
                   inside the panel's own 380ms travel, so the
                   whole thing still reads as ONE gesture
                   rather than a sequence of separate moves.
   --mm-ease-out   fast off the mark, long settle — this is
                   what makes the layers look like they
                   decelerate into place.
   --mm-ease-in    its mirror, used on the way out so the exit
                   accelerates away instead of crawling.

   --mm-exit-total is the single source of truth for "how long
   does the close take", and is consumed by the JS fallback
   timer in prototype/script.js. Longest exit path = the
   panel's duration plus the largest exit delay (the waves'
   mirrored ladder tops out at 3 steps). */
#mobile-menu.home-mobile-menu {
  --mm-dur-panel: 380ms;
  --mm-dur-wave:  340ms;
  --mm-dur-scrim: 260ms;
  --mm-step:       55ms;
  --mm-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --mm-ease-in:  cubic-bezier(0.55, 0, 1, 0.45);
  --mm-exit-total: calc(var(--mm-dur-panel) + var(--mm-step) * 3);
}

/* ---- 19.1b  The parked offset, as a direction-aware token -
   `translateX()` is a PHYSICAL transform: it is never flipped
   by `dir`. The anchor stays LOGICAL (`inset-inline-end: 0`
   in §18.3 / §18.4 — deliberately NOT converted to
   left/right), so the visual edge the layers are pinned to
   moves when the document direction flips:

     dir="rtl"  inline-end = visual LEFT  -> park LEFTWARD
     dir="ltr"  inline-end = visual RIGHT -> park RIGHTWARD

   A hard-coded `-100%` is therefore only correct in RTL; in
   LTR it parks the layers ON TOP of the viewport instead of
   off it, so the panel is visible while shut. `--mm-park`
   carries the SIGN only — the magnitude is always 100% of the
   layer's own 70vw width, i.e. exactly fully off-canvas past
   whichever edge the logical anchor resolved to.

   The unprefixed declaration is the RTL fallback, so an
   ancestor with no `dir` attribute behaves exactly as before
   this token existed.

   SOURCE ORDER IS LOAD-BEARING. The shell still ships
   `<html dir="rtl">` and only the `.app-shell` element flips
   to `dir="ltr"` on the home route, so on that route BOTH
   attribute selectors match (one via <html>, one via the
   nearer shell) at the same 1-2-0 specificity. The tie is
   broken by order, so the `[dir="ltr"]` rule is declared
   LAST and wins wherever an LTR context exists. Do not
   reorder these three blocks.

   Nothing else is touched: no duration, no easing, no stagger
   delay, no width, no z-index, no padding, no background, no
   box-shadow, no colour, and no `.mm-close` rule. */
#mobile-menu.home-mobile-menu {
  --mm-park: -100%;
}

[dir="rtl"] #mobile-menu.home-mobile-menu {
  --mm-park: -100%;
}

[dir="ltr"] #mobile-menu.home-mobile-menu {
  --mm-park: 100%;
}

/* ---- 19.2  PARKED — on screen, layers off-canvas ---------
   Matches whenever the menu is RENDERED but not at rest:
   during the first frame of the opening (before `.mm-in` is
   added) and for the whole of the closing (after `.mm-in` is
   removed, while `data-open` is still "true" so §18 keeps
   painting everything).

   Specificity 1-2-1 for the layers, comfortably above §18's
   0-3-0 equivalents, and ID-scoped so the runtime-injected
   prototype sheet cannot outrank it on source order.

   Only `transform` / `opacity` / `transition` are set. The
   position, the 70vw width, the z-index ladder and every
   colour still come from §18 and are deliberately not
   repeated.

   `pointer-events: none` on the root during this state means
   a menu that is still visibly sliding away can no longer
   intercept taps: the student can hit the page underneath
   immediately, without waiting out the animation. It is not
   transitioned, so it applies on the first frame of the
   close. */
#mobile-menu.home-mobile-menu[data-open="true"]:not(.mm-in) {
  pointer-events: none;
}

#mobile-menu.home-mobile-menu[data-open="true"] .mm-scrim {
  opacity: 0;
  transition: opacity var(--mm-dur-scrim) var(--mm-ease-in);
}

#mobile-menu.home-mobile-menu[data-open="true"] .mm-wave {
  transform: translateX(var(--mm-park));
  transition: transform var(--mm-dur-wave) var(--mm-ease-in);
}

#mobile-menu.home-mobile-menu[data-open="true"] .mm-panel {
  transform: translateX(var(--mm-park));
  transition: transform var(--mm-dur-panel) var(--mm-ease-in);
}

/* The close button and the three link rows ride the panel in,
   then settle a beat later, so the panel reads as a surface
   that CARRIES its content rather than one with the text
   baked onto it. `translateY` + `opacity` only — still
   compositor-only.

   Specificity 1-2-2 / 1-2-3, above §18.5 (0-4-0) and §18.6
   (0-3-1). */
#mobile-menu.home-mobile-menu[data-open="true"] .mm-panel .mm-close,
#mobile-menu.home-mobile-menu[data-open="true"] .mm-links a {
  opacity: 0;
  transform: translateY(10px);
  transition:
    opacity   160ms var(--mm-ease-in),
    transform 160ms var(--mm-ease-in);
}

/* ---- 19.3  EXIT LADDER — mirrored, front to back ---------
   On the way OUT the DARKEST, FRONTMOST wave leaves first and
   the lightest, backmost one trails, so the stack peels away
   front-to-back instead of looking like the entrance played
   backwards. The panel leaves immediately (delay 0), which
   uncovers the waves behind it and is what makes the peel
   visible at all.

   The rows and the close button just fade with no per-row
   delay: the panel is sliding out from under them anyway, a
   staggered exit would be invisible, and the cheaper
   transition is the right call on a low-end phone.

   These delays live on the PARKED state, so they apply during
   the close. The entrance ladder in 19.4 overrides each of
   them while `.mm-in` is present. Specificity 1-2-2. */
#mobile-menu.home-mobile-menu[data-open="true"] .mm-wave-4 { transition-delay: 0ms; }
#mobile-menu.home-mobile-menu[data-open="true"] .mm-wave-3 { transition-delay: var(--mm-step); }
#mobile-menu.home-mobile-menu[data-open="true"] .mm-wave-2 { transition-delay: calc(var(--mm-step) * 2); }
#mobile-menu.home-mobile-menu[data-open="true"] .mm-wave-1 { transition-delay: calc(var(--mm-step) * 3); }

/* ---- 19.4  RESTING OPEN — the staggered entrance ---------
   `.mm-in` is added by the script on the frame AFTER the menu
   becomes rendered, which is what lets these values animate
   from the parked ones in 19.2 rather than being applied
   instantly.

   Every layer travels to `translateX(0)` — the position §18
   already defines — but each starts later than the one behind
   it. Because the four waves are coincident 70vw boxes
   stacked back to front (wave-1 lightest at the back, wave-4
   darkest at the front, panel above all four), delaying the
   FRONT layers briefly exposes the ones behind: that is what
   produces the fan of graduated purple bands the design calls
   for. Once the sequence settles they are coincident again
   and only the panel shows, exactly as §18 describes.

   The panel is last in, so the bands are on screen before it
   covers them.

   `will-change: transform` is declared here and ONLY here —
   on the children, on the open state — so the compositor
   layers exist exactly while they are needed and are dropped
   again the moment the menu closes. §18.1's
   `will-change: auto` reset on the ROOT (which undoes a
   permanent leak from frontend/src/styles.css) stands
   untouched; this is the deliberate, scoped re-declaration
   §18 anticipated.

   Specificity: 1-3-1 for the layers, 1-3-2 / 1-3-3 for the
   rows — one class above their 19.2 / 19.3 counterparts. */
#mobile-menu.home-mobile-menu[data-open="true"].mm-in .mm-scrim {
  opacity: 1;
  transition: opacity var(--mm-dur-scrim) var(--mm-ease-out);
}

#mobile-menu.home-mobile-menu[data-open="true"].mm-in .mm-wave {
  transform: translateX(0);
  transition: transform var(--mm-dur-wave) var(--mm-ease-out);
  will-change: transform;
}

/* IN — lightest/backmost leads, darkest/frontmost trails. */
#mobile-menu.home-mobile-menu[data-open="true"].mm-in .mm-wave-1 { transition-delay: 0ms; }
#mobile-menu.home-mobile-menu[data-open="true"].mm-in .mm-wave-2 { transition-delay: var(--mm-step); }
#mobile-menu.home-mobile-menu[data-open="true"].mm-in .mm-wave-3 { transition-delay: calc(var(--mm-step) * 2); }
#mobile-menu.home-mobile-menu[data-open="true"].mm-in .mm-wave-4 { transition-delay: calc(var(--mm-step) * 3); }

#mobile-menu.home-mobile-menu[data-open="true"].mm-in .mm-panel {
  transform: translateX(0);
  transition: transform var(--mm-dur-panel) var(--mm-ease-out) calc(var(--mm-step) * 2);
  will-change: transform;
}

#mobile-menu.home-mobile-menu[data-open="true"].mm-in .mm-panel .mm-close,
#mobile-menu.home-mobile-menu[data-open="true"].mm-in .mm-links a {
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity   240ms var(--mm-ease-out),
    transform 240ms var(--mm-ease-out);
}
#mobile-menu.home-mobile-menu[data-open="true"].mm-in .mm-panel .mm-close {
  transition-delay: calc(var(--mm-step) * 3);
}
#mobile-menu.home-mobile-menu[data-open="true"].mm-in .mm-links a:nth-child(1) {
  transition-delay: calc(var(--mm-step) * 4);
}
#mobile-menu.home-mobile-menu[data-open="true"].mm-in .mm-links a:nth-child(2) {
  transition-delay: calc(var(--mm-step) * 5);
}
#mobile-menu.home-mobile-menu[data-open="true"].mm-in .mm-links a:nth-child(3) {
  transition-delay: calc(var(--mm-step) * 6);
}

/* ---- 19.5  The hamburger's own transition ----------------
   §5 already morphs the three bars into an X via
   `.hamburger.open`, and already transitions `transform` and
   `opacity` on the bars. Nothing is added to that. This rule
   only matches the menu's easing to the bars' so the two do
   not visibly disagree, and is scoped to the HomePage header
   so the identical `.hamburger` in the prototype sheet and on
   any other page is untouched.

   The 62x32 theme toggle, its Lottie host and every
   `.theme-toggle*` rule are deliberately NOT referenced
   anywhere in §19. */
.site-header.home-header .hamburger span {
  transition:
    transform 0.25s var(--mm-ease-out),
    opacity   0.25s var(--mm-ease-out);
}

/* ---- 19.6  Overscroll containment inside the panel -------
   §18.4 gives the panel `overflow-y: auto` so a long link
   list can scroll on a short viewport. `overscroll-behavior:
   contain` stops that scroll from chaining to the document
   underneath once the panel hits its end — the standard
   scroll-trap fix, and it costs nothing. */
#mobile-menu.home-mobile-menu .mm-panel {
  overscroll-behavior: contain;
}

/* ---- 19.7  Interaction affordances — TOKEN-DRIVEN --------
   Both colours resolve through the theme tokens, so dark mode
   keeps working with no theme-scoped rule: §1 flips the tokens
   themselves.

   FOCUS RING: the menu is now keyboard-reachable (the script
   moves focus into it on open and traps Escape), so there must
   be a visible focus indicator. `:focus-visible` only paints
   for keyboard / AT navigation and never on a touch tap, so it
   costs a phone user nothing. `--purple` is the brand accent
   and clears contrast on §18's always-white panel in both
   themes (#6C3EF4 light / #8F6BFF dark).

   PRESSED STATE: `--purple-soft`, a translucent brand tint
   that is defined in BOTH themes. `:active` rather than
   `:hover`, because this is a touch surface; the hover variant
   is gated behind `hover: hover` so it can never stick on a
   phone after a tap. Radius matches the 12px §18.5 already
   puts on the close button.

   NO NEW COLOUR VALUE IS INTRODUCED ANYWHERE IN §19. */
#mobile-menu.home-mobile-menu .mm-panel .mm-close:focus-visible,
#mobile-menu.home-mobile-menu .mm-links a:focus-visible {
  outline: 2px solid var(--purple);
  outline-offset: 3px;
  border-radius: 12px;
}
#mobile-menu.home-mobile-menu .mm-links a:active,
#mobile-menu.home-mobile-menu .mm-panel .mm-close:active {
  background: var(--purple-soft);
  border-radius: 12px;
}
@media (hover: hover) and (pointer: fine) {
  #mobile-menu.home-mobile-menu .mm-links a:hover {
    color: var(--purple-dark);
  }
  #mobile-menu.home-mobile-menu .mm-panel .mm-close:hover {
    background: var(--purple-soft);
    border-radius: 12px;
  }
}

/* ---- 19.8  Desktop — the menu can never be left open -----
   VERIFIED BREAKPOINT: 900px, not 901px. Both this file's §16
   and the runtime-injected prototype/styles.css §16 switch the
   hamburger on at `max-width: 900px`, and §17.8c rule (6)
   restates the same value at `.home-header` specificity.

   The form is `not all and (max-width: 900px)` rather than
   `min-width: 901px`, copying §17.8c verbatim and for its
   documented reason: a viewport width is a floating-point
   length, so 900.5px (browser zoom, fractional DPR) satisfies
   NEITHER `max-width: 900px` NOR `min-width: 901px` and would
   fall straight through the gap. Negating the exact query the
   hamburger uses is its precise complement, so the guard here
   can never disagree with the guard on the button.

   `display: none` is safe in THIS rule — unlike anywhere else
   in §19 — because on desktop there is no animation to cut
   off: the only control that can open the menu is hidden at
   these widths. This is purely the backstop for a visitor who
   opens the menu on a narrow window and then widens it; the
   menu is removed outright instead of being stranded over the
   desktop layout. Specificity 1-2-1, above §5's
   `display: flex`. */
@media not all and (max-width: 900px) {
  #mobile-menu.home-mobile-menu,
  #mobile-menu.home-mobile-menu[data-open="true"] {
    display: none;
  }
}

/* ---- 19.9  Scroll lock ----------------------------------
   While the menu is open the page behind it must not scroll
   under the finger. prototype/script.js toggles `mm-open` on
   <html>; this is the CSS half of that contract.

   `overflow: hidden` on <html> and <body> is the
   widest-supported lock and needs no scroll-position
   bookkeeping in JS, so it costs nothing on a low-end device.
   `overscroll-behavior: contain` additionally stops the
   pull-to-refresh / rubber-band chain from reaching the
   document on mobile Safari and Chrome.

   The class is removed as soon as the close begins, so the
   lock never outlives the gesture. Specificity 0-2-0 — no
   other rule in taysir-theme.css, prototype/styles.css or
   frontend/src/styles.css sets `overflow` on <html> or
   <body>, so this competes with nothing. */
html.mm-open,
html.mm-open body {
  overflow: hidden;
  overscroll-behavior: contain;
}

/* ---- 19.10  Reduced motion ------------------------------
   Honour `prefers-reduced-motion: reduce` the way §15 already
   does for the rest of the site: keep the FUNCTION, drop the
   MOVEMENT. The menu still opens, still closes, still traps
   Escape and still locks the scroll — the layers simply appear
   at their final positions instead of travelling there.

   `transition-duration` / `transition-delay` are zeroed rather
   than writing `transition: none`, so the script's
   `transitionend` listener still fires (a 0s transition on a
   changing property still dispatches the event) and the close
   still completes. The script also carries its own timeout
   fallback, so the menu can never get stuck open even if no
   event arrives.

   The `.mm-in` resting values are then forced onto the parked
   state so nothing is left stranded off-canvas for the frame
   before the class lands.

   `!important` is used here and nowhere else in §19, for one
   narrow reason: these rules must override the per-layer
   `transition-delay` ladders in 19.3 / 19.4, which sit at
   equal-or-higher specificity, without restating every rung.
   §15 of this file uses `!important` in its own
   reduced-motion block for exactly the same reason, so this
   follows the established pattern. Nothing outside this media
   query is affected. */
@media (prefers-reduced-motion: reduce) {
  #mobile-menu.home-mobile-menu .mm-scrim,
  #mobile-menu.home-mobile-menu .mm-wave,
  #mobile-menu.home-mobile-menu .mm-panel,
  #mobile-menu.home-mobile-menu .mm-panel .mm-close,
  #mobile-menu.home-mobile-menu .mm-links a,
  .site-header.home-header .hamburger span {
    transition-duration: 0s !important;
    transition-delay: 0s !important;
  }
  #mobile-menu.home-mobile-menu[data-open="true"] .mm-scrim {
    opacity: 1;
  }
  #mobile-menu.home-mobile-menu[data-open="true"] .mm-wave,
  #mobile-menu.home-mobile-menu[data-open="true"] .mm-panel {
    transform: none;
  }
  #mobile-menu.home-mobile-menu[data-open="true"] .mm-panel .mm-close,
  #mobile-menu.home-mobile-menu[data-open="true"] .mm-links a {
    opacity: 1;
    transform: none;
  }
}

/* ==========================================================
   20. MOBILE MENU — MOTION SMOOTHNESS PASS  (append-only)
   ==========================================================

   §19 built the staggered slide and it is CORRECT: the
   geometry, the colours, the z-index ladder, the
   `data-open` / `.mm-in` contract and the exit ladder all
   stay exactly as they are. Not one byte of §5, §17.8c, §18
   or §19 is edited; this section only appends, and it
   appends only what a mid-range Android phone needs to
   render that same gesture at a steady frame rate.

   ── THE SYMPTOM ───────────────────────────────────────────
   On a mid-range Android the slide reads as roughly 20fps:
   fast and juddery rather than silky. The movement itself is
   right, the FRAME DELIVERY is not.

   ── THE FOUR CAUSES, AND WHAT IS DONE ABOUT EACH ──────────

   1) SIX LARGE SURFACES COMPOSITE AT ONCE. The scrim, four
      full-height 70vw wave layers and the panel all move or
      fade in the same window. That per-frame compositing
      cost is fixed by the design and is not being changed
      here — instead the sequence is given MORE TIME to
      absorb it (20.1) and the layers are handed to the
      compositor BEFORE the first moving frame (20.2).

   2) THE TIMINGS WERE TOO SHORT TO ABSORB A DROPPED FRAME.
      §19.1's 340ms wave over a 55ms ladder leaves ~6 frames
      per layer at 60fps, and barely 2 if the device is
      actually delivering 20fps — so a single long frame eats
      a third of the travel and the eye reads a jump rather
      than a slide. 20.1 lengthens the travel and TIGHTENS
      the stagger: a longer distance-per-layer spread over a
      shorter interval means each layer has more frames to
      itself while the fan still reads as one gesture.

   3) THE EXIT EASE WAS ABRUPT. `cubic-bezier(0.55,0,1,0.45)`
      ends at full speed, so on a phone that is already
      dropping frames the close reads as a snap. 20.1
      replaces it with a curve that decelerates into the
      final position.

   4) `will-change` WAS DECLARED ONLY IN THE `.mm-in` STATE.
      The compositor layer was therefore being created on the
      very frame the transform starts changing — the browser
      pays for the promotion (layer allocation, one full
      re-raster of a 70vw x 100vh surface) inside the first
      animated frame, which is exactly the frame the user
      sees judder on. 20.2 moves the promotion one frame
      earlier, onto the PARKED state, which is applied when
      `data-open` flips and one frame BEFORE `.mm-in` lands.

   5) THE STICKY HEADER'S `backdrop-filter` KEPT RE-SAMPLING.
      §5 gives `.site-header` `saturate(180%) blur(10px)`. A
      backdrop-filter re-samples and re-blurs everything
      painted behind it whenever those pixels change — and
      six large surfaces sliding across the viewport change
      them on EVERY frame. The header is covered by the menu
      while it is open, so that blur is invisible work. 20.3
      suspends it for exactly the duration of the gesture.

   ── WHAT THIS SECTION DELIBERATELY DOES NOT DO ────────────
   No `@keyframes`, no JS, no library, no new file. No new
   colour value: §20 declares no colour at all. No geometry,
   no z-index, no `display`, no `position`, no width. It does
   NOT declare `transform` or `opacity` VALUES anywhere —
   only durations, easings and compositor hints — which is
   what keeps §19.10's `prefers-reduced-motion` block, which
   appears ABOVE this section, fully authoritative: its
   `transition-duration: 0s !important` /
   `transition-delay: 0s !important` beat every normal
   declaration below regardless of source order, and its
   `transform: none` / `opacity: 1` resets have nothing here
   to fight. Retiming through the tokens is therefore
   automatically neutralised under reduced motion.

   The 62x32 theme toggle, its Lottie host, `applyTheme` and
   `themeBoot` are not referenced anywhere in §20.
   ========================================================== */

/* ---- 20.1  Retimed motion tokens ------------------------
   Same selector as §19.1 (`#mobile-menu.home-mobile-menu`,
   specificity 1-1-0) and later in source order, so these
   values win on the cascade without `!important` and without
   editing §19. Every rule in §19 reads the tokens, so
   re-declaring them here re-times the ENTIRE sequence —
   entrance, exit ladder, rows, close button — from one
   place. Nothing else has to be restated.

   --mm-dur-panel  380ms -> 460ms. At a real-world 20-30fps
                   that is the difference between ~8 and
                   ~12 frames of travel; enough that losing
                   one or two no longer registers as a jump.
   --mm-dur-wave   340ms -> 420ms, kept just under the panel
                   so the waves still snap out from behind it
                   exactly as §19.1 describes.
   --mm-dur-scrim  260ms -> 300ms. The scrim is a full-screen
                   cross-fade and is the cheapest surface to
                   lengthen; matching it to the slide stops
                   the backdrop finishing visibly early.
   --mm-step        55ms ->  42ms. Counter-intuitive but
                   deliberate: with the layer travel now
                   longer, a SHORTER interval keeps the four
                   waves overlapping heavily instead of
                   arriving as four discrete events. Four
                   layers x 42ms fans out over 126ms, still
                   well inside the panel's own travel, so the
                   whole thing still reads as ONE gesture —
                   and each layer's own motion now occupies
                   more frames than the gap between layers,
                   which is what reads as silky.
   --mm-ease-out   unchanged. The entrance deceleration was
                   never the problem.
   --mm-ease-in    (0.55,0,1,0.45) -> (0.4,0,0.2,1). The old
                   curve is still travelling at full speed
                   when it hits the end point, so a dropped
                   frame near the end removes the only part
                   of the exit the eye could have tracked.
                   The replacement accelerates away just as
                   promptly but settles instead of stopping
                   dead.

   --mm-exit-total is restated here in the SAME calc() form
   §19.1 uses, so it remains the single source of truth for
   "how long does the close take" and keeps resolving from
   whichever values are in force. Under these tokens it is
   460ms + (42ms x 3) = 586ms. prototype/script.js reads this
   property at close time via getComputedStyle, so its
   fallback timer follows this retime automatically and no JS
   is touched. */
#mobile-menu.home-mobile-menu {
  --mm-dur-panel: 460ms;
  --mm-dur-wave:  420ms;
  --mm-dur-scrim: 300ms;
  --mm-step:       42ms;
  --mm-ease-in:  cubic-bezier(0.4, 0, 0.2, 1);
  --mm-exit-total: calc(var(--mm-dur-panel) + var(--mm-step) * 3);
}

/* ---- 20.2  Pre-promotion — one frame earlier ------------
   §19.4 declares `will-change` only under `.mm-in`. The
   script sets `data-open="true"` on one frame and adds
   `.mm-in` on the NEXT, so under §19 alone the compositor
   layer for six large surfaces is allocated and rastered in
   the same frame the transform first changes. That single
   frame is where the judder is born, and because the layers
   are full-height 70vw boxes the raster is not cheap.

   Declaring the hint on the PARKED state moves the
   promotion onto the earlier frame — the one where nothing
   is moving yet and there is budget to spare — so the first
   ANIMATED frame only has to hand an already-composited
   layer a new transform matrix.

   This does NOT reintroduce the leak §18.1 removed. The
   selector still requires `[data-open="true"]`, so while the
   menu is shut there is no hint and no retained layer;
   §18.1's `will-change: auto` on the ROOT stands untouched.
   The hint also correctly persists through the close (the
   parked state is what the closing menu matches), which is
   the other half of the same problem: previously the layer
   was DROPPED the instant `.mm-in` was removed, i.e. on the
   first frame of the exit.

   `backface-visibility: hidden` is the long-standing hint
   that keeps a promoted layer on the fast path on Android
   GPUs; it paints nothing, moves nothing and changes no
   geometry.

   Specificity 1-2-1 — identical to the §19.2 rules it sits
   beside, later in source order, and it declares only
   compositor hints, so no transform, opacity, transition or
   colour is affected. */
#mobile-menu.home-mobile-menu[data-open="true"] .mm-scrim {
  will-change: opacity;
  backface-visibility: hidden;
}

#mobile-menu.home-mobile-menu[data-open="true"] .mm-wave,
#mobile-menu.home-mobile-menu[data-open="true"] .mm-panel {
  will-change: transform;
  backface-visibility: hidden;
}

/* ---- 20.3  Suspend the header blur during the gesture ---
   §5 gives `.site-header` `backdrop-filter: saturate(180%)
   blur(10px)`. A backdrop-filter must re-sample the pixels
   painted behind its box and re-blur them whenever those
   pixels change. While the menu slides, six large surfaces
   are moving across the viewport directly behind the sticky
   header, so the blur is recomputed on EVERY frame of the
   animation — on a mid-range Android that alone can cost
   more per frame than the slide it is decorating.

   The menu is a full-viewport fixed root that covers the
   header, so this blur is invisible for the whole time it is
   being recomputed. Suspending it is pure profit: nothing
   the user can see changes, and a per-frame full-width blur
   disappears from the budget.

   `html.mm-open` is the scroll-lock class prototype/script.js
   already toggles for §19.9 — no new contract, no new JS. It
   is added as the open begins and removed as the close
   begins, so the suspension covers exactly the animated
   window and the frosted header returns the moment the
   gesture is over.

   Only the two filter properties are touched. The header's
   background token, border, sticky position and z-index are
   left exactly as §5 sets them, so the bar keeps its fill
   and its hairline throughout. Specificity 0-2-0, above §5's
   0-1-0 `.site-header`, and it can only apply while the menu
   is open. */
html.mm-open .site-header {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* ---- §20.4  Keep the blur suspended through the CLOSE too ----
   §20.3 keys the suspension on html.mm-open, but script.js
   drops that class on the FIRST line of close(), while the
   exit still has --mm-exit-total (586ms) of motion left. The
   blur therefore returned exactly when the heaviest part of
   the gesture was still running, so the close stayed juddery
   while the open got smoother.

   #mobile-menu[data-open="true"] is true for the whole
   on-screen lifetime — entrance, rest AND exit — so keying
   the sibling header off it covers the window §20.3 misses.
   :has() is supported by every browser that ships
   backdrop-filter in 2026; where it is not, §20.3 still
   covers the entrance. */
html:has(#mobile-menu.home-mobile-menu[data-open="true"]) .site-header {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* ==========================================================
   §21 — HOME SUBJECT PEEK
   ----------------------------------------------------------
   The home page's quiet "peek" at the library: a short,
   scrollable list of subject names, sitting where the old
   decorative glimpse strip used to be.

   THE CARD SURFACE IS GONE. The rows used to be faithful
   copies of `.gd-card.gd-folder` — elevated background, 1px
   hairline, 20px radius, --shadow-sm. Every one of those is
   now removed. A row is nothing but the purple folder tile
   and the subject name, floating directly on the page
   background. Nothing that reads as a card or a chip remains.

   That inverts the old weight balance, and the rest of the
   section follows from it:

     • THE FRAME (.subject-peek-list) is now the ONLY thing
       defining the area, so it can no longer be a whisper.
       It is still exactly one 1px hairline at radius 24px —
       no fill, no shadow, no inset, no second border, no
       dividers, no header bar, no ::before/::after — but its
       colour is derived from --text instead of --border.
       --border is too weak a token to build a visible line
       from: at 100% it is only 0.06 alpha in light, so any
       percentage of it composites below the perception
       threshold on a phone. --text is a strong token, so a
       LOW percentage of it gives a clean thin line that is
       comfortably visible without reading as a heavy box.
       Still color-mix, still an existing token, still no new
       colour literal — see §21.1 for the measured figures.

     • THE ROWS (.subject-peek-item) are compact. With the
       surface gone, most of the old 74px row height was
       padding around a card that no longer exists, so the
       padding is cut to almost nothing and the icon defines
       the height. See §21.2.

     • THE GAP between rows is widened, because the rows no
       longer have their own surface or a separating border
       to hold them apart. No divider lines are added.

   The rows stay inert decoration: no hover state, no pointer
   cursor, no transform, no transition, nothing focusable.

   Append-only section. Nothing above it is modified.
   ========================================================== */
.subject-peek { padding: 40px 0; }

/* ---- §21.1  The frame: one hairline, now actually visible ----

   THE TOKEN CHANGED, NOT THE PERCENTAGE.

   The frame used to be color-mix(var(--border) 35%). That path is
   exhausted and cannot be rescued by raising the percentage, because
   --border is simply too weak a token to build a visible line from:

     light  --border = rgba(17,17,17,0.06)
            x 35%  -> 0.0210 alpha -> rgb(250,250,250) on #FFFFFF
                      L* 98.27 vs page L* 100.00  ->  dL* 1.73
     dark   --border = rgba(255,255,255,0.07)
            x 70%  -> 0.0490 alpha -> rgb(26,28,33) on #0E1016
                      L* 10.15 vs page L*  4.71  ->  dL* 5.44

   A dL* of 1.73 is below the perception threshold — on a phone at
   arm's length the light frame is not merely faint, it is absent.
   And even at 100% of --border the light line only reaches 0.06
   alpha / dL* 4.9, still too weak. The ceiling of the token is the
   problem, so the token has to change.

   --text is a STRONG existing token (#111111 light, #F2F2F5 dark),
   so a LOW percentage of it produces a clean thin line rather than
   a heavy box. Measured, composited against the real page
   background, not guessed:

     light  color-mix(var(--text) 12%)
            -> 0.12 alpha -> rgb(226,226,226) on #FFFFFF
               L* 90.04 vs 100.00   ->  dL*  9.96
     dark   color-mix(var(--text) 14%)
            -> 0.14 alpha -> rgb( 46, 48, 53) on #0E1016
               L* 19.73 vs   4.71   ->  dL* 15.02

   Dark needs the higher percentage for the usual reason: the same
   dL* costs more alpha low down the L* curve. Both land in the
   "clearly and comfortably visible hairline" band — roughly 6x the
   light contrast the old frame had, and well short of anything that
   reads as a box. Still one 1px line, still color-mix, still an
   existing token, still no colour literal.

   This matters more than it used to: with §21.2's card surface
   removed, this hairline is the ONLY thing defining the area. */
.subject-peek-list {
  box-sizing: border-box;

  border: 1px solid color-mix(in srgb, var(--text) 12%, transparent);
  border-radius: 24px;
  /* 12px vertical as before. The horizontal padding is raised to 14px
     to clear the 6px scrollbar of §21.5: the scrollbar gutter is
     carved out of the padding box on the inline-start side, and 14px
     keeps a comfortable channel between the thumb and the names. */
  padding: 12px 14px;

  background: transparent;
  box-shadow: none;
  outline: none;

  /* Reset the <ul> */
  list-style: none;
  margin: 0;

  display: flex;
  flex-direction: column;
  /* Was 10px. The rows no longer have a surface or a border of their
     own to hold them apart, so without a wider channel the names
     crowd each other. 14px replaces the separation the card edges
     used to provide — no divider lines are added. */
  gap: 14px;

  /* Exactly four rows are visible, the fifth is scrolled to.
     RE-MEASURED after §21.2 stripped the surface. The old 352px was
     derived from a 74px row that was mostly card padding plus the
     card's own 1px top and bottom borders; that row is now 36px, so
     352px would have shown four rows plus most of a fifth.

     The row height is MEASURED in a browser, not inferred from
     min-height — §21.2 zeroes min-height precisely so that the icon,
     and nothing else, decides the height:

       4 rows  x 36px measured     = 144px
       3 gaps  x 14px              =  42px
       padding 12px top + bottom   =  24px
       border   1px top + bottom   =   2px
                                   -------
                                      212px

     box-sizing: border-box above folds the padding and border into
     the figure, and flex-shrink: 0 on the rows (§21.2) stops them
     collapsing when a fifth row exists, so the fourth row ends flush
     with the frame. */
  max-height: 212px;

  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;

  /* NO mask-image.
     There used to be an 18px top/bottom fade here. A mask applies to
     the element's BORDER box, so its transparent ends were erasing
     the top and bottom edges of this very hairline — the frame was
     being cut open by its own scroll fade. It also faded the
     scrollbar thumb of §21.5 at both ends.

     Fading only the CONTENT while sparing the 1px border cannot be
     expressed on a single element: mask-clip: content-box does not
     spare the border, it clips the border away entirely, and every
     other route needs an extra wrapper or a ::before, both of which
     this section forbids. So the mask is REMOVED rather than
     reworked. The frame is now the thing that communicates the
     boundary, and the §21.5 scrollbar communicates the overflow,
     which is what the fade was standing in for. */
}

/* Dark: 14% rather than light's 12%, for the reason measured in the
   comment above — the same perceived hairline costs more alpha
   against #0E1016 than against #FFFFFF. */
html[data-theme="dark"] .subject-peek-list {
  border-color: color-mix(in srgb, var(--text) 14%, transparent);
}

/* ---- §21.2  The rows: NO SURFACE, and compact ----

   The card is gone. A row is now the purple tile and the name,
   floating directly on the page background, with nothing behind
   them. Removed outright:

     background: var(--bg-elevated)   -> none (transparent)
     border: 1px solid var(--border)  -> none
     border-radius: var(--radius-card) (20px) -> 0
     box-shadow: var(--shadow-sm)     -> none

   Nothing that reads as a card or a chip may remain, so the radius
   is zeroed rather than merely reduced — on a surface-less row a
   leftover radius has nothing to round anyway, but it is set to 0
   explicitly so no cascade can reintroduce a chip shape.

   Kept exactly as they were: the flex layout, align-items: center,
   the icon-then-name source order, and the RTL text alignment.

   And the row is now SMALL. With the surface gone, the old 74px was
   almost entirely padding wrapped around a card that no longer
   exists: 14px + 44px icon + 14px, plus the card's own 1px borders.
   The padding is dropped to zero and min-height to zero, so the
   icon — and only the icon — decides the height. Measured result:
   74px -> 36px per row.

   No hover state, no cursor, no transition: the rows stay inert. */
.subject-peek-item {
  display: flex;
  align-items: center;
  gap: 14px;

  /* Was min-height: 64px. Zeroed so the measurement is honest: the
     icon defines the row box, nothing floors it. */
  min-height: 0;
  /* Still required. The rows are flex items in a height-capped
     column, so without this they shrink below their intrinsic
     height as soon as a fifth row exists, and the four-row
     max-height of §21.1 stops resolving to four rows. */
  flex-shrink: 0;
  /* Was 14px 18px. The row has no surface left to inset the content
     from, so the padding goes entirely and the icon sets the height. */
  padding: 0;

  background: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;

  text-align: start;   /* RTL-friendly, unchanged */
}

/* ---- §21.3  The glyph tile ----
   The purple treatment is UNCHANGED: same
   color-mix(in srgb, var(--purple) 18%, transparent) fill, same
   var(--purple) glyph colour. Only the box is scaled down, in
   proportion, to match the compacted row:

     width/height   44px -> 36px
     border-radius  12px -> 10px   (12/44 = 0.273; 36 x 0.273 = 9.8)
     svg            26px -> 21px   (26/44 = 0.591; 36 x 0.591 = 21.3)

   The tile stays square and keeps its radius-to-size proportion, so
   it reads as the same rounded folder tile, just smaller. */
.subject-peek-icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 10px;
  background: color-mix(in srgb, var(--purple) 18%, transparent);
  color: var(--purple);
}
.subject-peek-icon svg { width: 21px; height: 21px; }
.subject-peek-icon svg path { fill: currentColor; }

/* ---- §21.4  The name ---- */
.subject-peek-name {
  flex: 1 1 auto;
  min-width: 0;
  /* Was 16px (inherited). Eased down slightly to sit with the
     smaller tile; the weight and colour are unchanged. */
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- §21.5  A scrollbar you can actually see ----

   Four rows are shown and the rest are scrolled to, so there has to
   be a visible indicator that the list continues. Both syntaxes are
   given because they are not interchangeable: the standard
   scrollbar-width / scrollbar-color pair covers Firefox, and the
   ::-webkit-scrollbar pseudo-elements cover Chrome and the Android
   WebView, which is what most visitors here are using. Chromium
   honours the standard properties when present and ignores the
   pseudo-elements in that case, so the two agree by construction —
   same tokens, same percentages.

   SIDE: the list is RTL (dir="rtl" on <html>), and a vertical
   scrollbar renders on the INLINE-START edge, which in RTL is the
   LEFT. Verified in the browser, not assumed: the names begin at
   the right edge and the thumb sits at the left. §21.1 carries
   14px of horizontal padding for exactly this reason — the 6px
   gutter is taken out of the padding box on the left, leaving an
   8px channel, so the thumb never overlaps a name. The names are
   also ellipsised (§21.4), so the longest one ends before the
   channel rather than running under it.

   MASK: §21.1's 18px fade is gone. Had it stayed, it would have
   clipped both ends of this thumb as well as the frame's own top
   and bottom edges.

   Thumb colour by color-mix from --text, measured composited
   against the real page background:

     light  28% -> 0.28 alpha -> rgb(188,188,188) on #FFFFFF
                   L* 76.38 vs 100.00  ->  dL* 23.62
     dark   26% -> 0.26 alpha -> rgb( 73, 75, 80) on #0E1016
                   L* 31.82 vs   4.71  ->  dL* 27.11

   Clearly visible in both themes while staying a thin indicator
   rather than a UI element. The track is barely tinted at 5% so the
   channel reads as a groove, not a bar. No hex, no rgb(), no named
   colour, no new custom property. */
.subject-peek-list {
  scrollbar-width: thin;
  scrollbar-color:
    color-mix(in srgb, var(--text) 28%, transparent)
    color-mix(in srgb, var(--text) 5%, transparent);
}
html[data-theme="dark"] .subject-peek-list {
  scrollbar-color:
    color-mix(in srgb, var(--text) 26%, transparent)
    color-mix(in srgb, var(--text) 5%, transparent);
}

.subject-peek-list::-webkit-scrollbar {
  width: 6px;
}
.subject-peek-list::-webkit-scrollbar-track {
  background: color-mix(in srgb, var(--text) 5%, transparent);
  border-radius: 999px;
}
.subject-peek-list::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--text) 28%, transparent);
  border-radius: 999px;
}
html[data-theme="dark"] .subject-peek-list::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--text) 26%, transparent);
}
