/* Bundled Tajawal + Amiri fonts, fetched from Google Fonts once
   and shipped locally with the app. No CDN call at runtime.
   ──────────────────────────────────────────────────────────────
   `font-display: block` (not `swap`) is intentional. With local
   fonts the load is tens of milliseconds; `block` keeps text
   invisible during that window and paints once with the correct
   face. `swap` rendered a system fallback first and then
   "swapped" to Amiri/Tajawal a moment later, producing a visible
   reflow on every fresh app launch: the user-reported "text
   shift" (subtle but noticeable difference between system Naskh
   and the Amiri Naskh used in the reader). The brief block
   period is imperceptible in WKWebView; the swap reflow was not.
   The two most-used Arabic faces are also preloaded from
   alkafi.html so the fetch begins before this stylesheet is
   even parsed, which keeps the block period as short as possible. */

@font-face {
  font-family: 'Amiri';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('fonts/amiri-400-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
/* Scheherazade New full font (NOT the previous subset).
   ──────────────────────────────────────────────────────
   The bundled Amiri build (pre-2021) doesn't ship glyphs for the
   Shia-specific Arabic Presentation Forms-A honorifics added to
   Unicode 14 (U+FD46/FD47/FD49/FD4A/FD4C-FD4E). When those codepoints
   appear in our matn, iOS falls back to the system font, which also
   lacks them, and renders [] tofu boxes.

   Why we use the FULL font (~100 KB) instead of a 7 KB text= subset:
   the user reported that codepoints stored in the DOM (verified via
   copy/paste returning the correct expanded Arabic phrase, e.g.
   "عليه السلام") were rendering visually as a different honorific
   form (e.g. dual "عليهما السلام"). The most likely cause was that
   Google Fonts' `text=` subsetting elided OpenType lookup tables
   (GSUB/GPOS substitutions, contextual variants, etc.) needed for
   correct glyph selection at FD40-FD4F. Subsets strip everything
   the requested codepoints don't directly reference, including
   ligature variants and disambiguating shapes. The full font keeps
   the original SIL glyph design intact.

   unicode-range still limits this declaration to honorific
   codepoints; the font won't be activated for ordinary matn text,
   which stays in Amiri.

   100 KB is bigger than the previous 7 KB subset but still small
   relative to our Amiri (~140 KB) and Tajawal (~280 KB) bundle. */
@font-face {
  font-family: 'Scheherazade Honorifics';
  font-style: normal;
  font-weight: 400;
  /* block (not swap) to match the matn faces: with swap, every honorific
     glyph flashed a fallback/tofu and then swapped to Scheherazade on cold
     launch. Paired with the preload in alkafi.html, block keeps the honorific
     invisible for only the few ms it takes the bundled woff2 to load, then
     paints the correct calligraphy once. */
  font-display: block;
  src: url('fonts/scheherazade-full.woff2') format('woff2');
  unicode-range: U+FD40-FD4F, U+FDFA-FDFB;
}
@font-face {
  font-family: 'Amiri';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('fonts/amiri-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Amiri';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('fonts/amiri-400-latin.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: 'Amiri';
  font-style: normal;
  font-weight: 700;
  font-display: block;
  src: url('fonts/amiri-700-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
@font-face {
  font-family: 'Amiri';
  font-style: normal;
  font-weight: 700;
  font-display: block;
  src: url('fonts/amiri-700-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Amiri';
  font-style: normal;
  font-weight: 700;
  font-display: block;
  src: url('fonts/amiri-700-latin.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;
}
/* Noto Naskh Arabic (variable 400-700, SIL OFL 1.1): the reader font.
   Wider letterforms + more generous tashkeel clearance than Amiri, which
   crowded diacritics on dense words. Amiri stays bundled as fallback. */
@font-face {
  font-family: 'Noto Naskh Arabic';
  font-style: normal;
  font-weight: 400 700;
  font-display: block;
  src: url('fonts/noto-naskh-arabic-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}

@font-face {
  font-family: 'Noto Naskh Arabic';
  font-style: normal;
  font-weight: 400 700;
  font-display: block;
  src: url('fonts/noto-naskh-arabic-latin.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;
}

/* ── Reader-font roster (user-selectable via «خط القراءة») ── */
@font-face {
  font-family: 'IBM Plex Sans Arabic';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('fonts/ibm-plex-sans-arabic-400-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}

@font-face {
  font-family: 'IBM Plex Sans Arabic';
  font-style: normal;
  font-weight: 700;
  font-display: block;
  src: url('fonts/ibm-plex-sans-arabic-700-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}

@font-face {
  font-family: 'Readex Pro';
  font-style: normal;
  font-weight: 400 700;
  font-display: block;
  src: url('fonts/readex-pro-400-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}

@font-face {
  font-family: 'Scheherazade New';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('fonts/scheherazade-new-400-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}

@font-face {
  font-family: 'Scheherazade New';
  font-style: normal;
  font-weight: 700;
  font-display: block;
  src: url('fonts/scheherazade-new-700-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}

@font-face {
  font-family: 'Harmattan';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('fonts/harmattan-400-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}

@font-face {
  font-family: 'Harmattan';
  font-style: normal;
  font-weight: 700;
  font-display: block;
  src: url('fonts/harmattan-700-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}

@font-face {
  font-family: 'Markazi Text';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('fonts/markazi-text-400-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}

@font-face {
  font-family: 'Tajawal';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('fonts/tajawal-400-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
@font-face {
  font-family: 'Tajawal';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('fonts/tajawal-400-latin.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: 'Tajawal';
  font-style: normal;
  font-weight: 500;
  font-display: block;
  src: url('fonts/tajawal-500-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
@font-face {
  font-family: 'Tajawal';
  font-style: normal;
  font-weight: 500;
  font-display: block;
  src: url('fonts/tajawal-500-latin.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: 'Tajawal';
  font-style: normal;
  font-weight: 700;
  font-display: block;
  src: url('fonts/tajawal-700-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
@font-face {
  font-family: 'Tajawal';
  font-style: normal;
  font-weight: 700;
  font-display: block;
  src: url('fonts/tajawal-700-latin.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: 'Tajawal';
  font-style: normal;
  font-weight: 800;
  font-display: block;
  src: url('fonts/tajawal-800-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
@font-face {
  font-family: 'Tajawal';
  font-style: normal;
  font-weight: 800;
  font-display: block;
  src: url('fonts/tajawal-800-latin.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;
}
/* ============================================================
   الكافي: Stylesheet
   Cross-platform (mobile / tablet / desktop)
   ------------------------------------------------------------
   Sections:
     1. Theme variables (light + dark)
     2. Base / reset
     3. Layout scaffolding (responsive breakpoints)
     4. Header & navigation
     5. Bottom tab bar
     6. Cards, lists, badges
     7. Hadith detail view
     8. Search box & chips
     9. Selection popup / color picker
    10. Modals & toasts
    11. Loading & empty states
    12. Animations & misc
   ============================================================ */


/* ─── 1. Theme variables ─── */

:root {
  /* colors */
  --bg:         #F7F3EA;
  --card:       #ffffff;
  --card-h:     #f5f0e6;
  --surface:    #EFE9DC;
  --text:       #1c1917;
  --muted:      #6B655F;
  --accent:     #1E6B52;
  --accent-s:   rgba(30, 107, 82, 0.08);
  --border:     #E2DBCB;
  --header:     #F7F3EA;
  --overlay:    rgba(0, 0, 0, 0.3);
  --shadow-sm:  0 1px 3px rgba(0, 0, 0, 0.06);
  --shadow-md:  0 4px 16px rgba(0, 0, 0, 0.12);
  --shadow-lg:  0 8px 32px rgba(0, 0, 0, 0.2);

  /* sizing: used by reader / modals */
  --reader-max-width:  640px;   /* comfortable Arabic reading column */
  --menu-width:        280px;
  --menu-width-wide:   320px;
  --modal-max-width:   480px;
  --toast-max-width:   320px;

  /* Sidebar-clear right offset. Used by any popup that pins to the
     bottom of the viewport (selection toolbar, mobile FAB, color
     picker) so its right edge doesn't extend into the permanent
     sidebar on iPad-class landscape. The base value is 20 px (a
     viewport breathing margin when no sidebar is visible); the
     @media block at ≥ 1150 px overrides it with the sidebar width
     plus the breathing margin. Using a CSS variable instead of JS
     measurement removes any chance of a DOM-read-timing bug;
     this resolves at paint time, always correct. */
  --popup-right:  20px;

  /* typography */
  --font-ui:       'Tajawal', 'Noto Sans Arabic', sans-serif;
  --font-arabic:   'Amiri', 'Noto Naskh Arabic', serif;

  /* radii */
  --radius-sm:  6px;
  --radius-md:  10px;
  --radius-lg:  14px;

  /* spacing scale (for future use) */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;

  /* z-index scale */
  --z-header:     10;
  --z-menu:       100;
  --z-overlay:    98;
  --z-popup:      150;
  --z-modal:      160;
  --z-toast:      200;
}

/* ورقي: the sepia reading theme. Kindle-style: the paper warms AND
   the ink warms with it (#FBF0D9 paper / #5F4B32 ink); tinting only
   the background reads as a filter, not a material. Accent stays the
   2.0 forest green, slightly quieter via a warmer -s tint. The six
   .dark-only selectors (mark, ::selection, shimmer…) keep their light
   defaults, which hold up on sepia paper. */
.sepia {
  --bg:         #FBF0D9;
  --card:       #FFF8E7;
  --card-h:     #F6E9CB;
  --surface:    #F4E7C8;
  --text:       #5F4B32;
  --muted:      #97815F;
  --accent:     #1E6B52;
  --accent-s:   rgba(30, 107, 82, 0.09);
  --border:     #E9DBB8;
  --header:     #FBF0D9;
  --overlay:    rgba(63, 48, 28, 0.32);
  --shadow-sm:  0 1px 3px rgba(95, 75, 50, 0.08);
  --shadow-md:  0 4px 16px rgba(95, 75, 50, 0.14);
  --shadow-lg:  0 8px 32px rgba(95, 75, 50, 0.22);
}

.dark {
  --bg:         #141312;
  --card:       #1D1B17;
  --card-h:     #22262e;
  --surface:    #191813;
  --text:       #E4DFD3;
  --muted:      #9A9385;
  --accent:     #4E9C7C;
  --accent-s:   rgba(78, 156, 124, 0.14);
  --border:     #2B2922;
  --header:     #141312;
  --overlay:    rgba(0, 0, 0, 0.6);
  --shadow-sm:  0 1px 3px rgba(0, 0, 0, 0.3);
  --shadow-md:  0 4px 16px rgba(0, 0, 0, 0.4);
  --shadow-lg:  0 8px 32px rgba(0, 0, 0, 0.5);
}


/* ─── 2. Base / reset ─── */

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

/* Prevent double-tap zoom on iOS */
html {
  touch-action: manipulation;
  -webkit-text-size-adjust: 100%;
  -ms-text-size-adjust: 100%;
  background: var(--bg);
  overscroll-behavior: none;
  height: 100%;
}

body {
  font-family: var(--font-ui);
  background: var(--bg);
  color: var(--text);
  overflow-x: hidden;
  position: relative;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overscroll-behavior: none;
  margin: 0;
}

button {
  font-family: inherit;
  cursor: pointer;
  color: inherit;
  background: none;
  border: none;
}

input, textarea, select {
  font-family: inherit;
  color: inherit;
}

a {
  color: var(--accent);
  text-decoration: none;
}

mark {
  border-radius: 3px;
  padding: 1px 3px;
  background: rgba(255, 235, 59, 0.45);
  color: inherit;
}

.dark mark {
  background: rgba(250, 204, 21, 0.35);
}

/* ::selection paints during a live drag-to-select. Previously this
   was the app's accent green at 25% alpha, which competed visually
   with the saved-highlight colour on iOS WKWebView; the user
   reported "the highlight is always green" because the green
   ::selection band lingered briefly after tapping a colour and
   covered the actual ::highlight() background. Switched to a
   theme-neutral gray so the highlight's chosen colour clearly
   dominates after the selection is cleared. */
::selection {
  background: rgba(0, 0, 0, 0.18);
  color: inherit;
}
::-moz-selection {
  background: rgba(0, 0, 0, 0.18);
  color: inherit;
}
.dark ::selection {
  background: rgba(255, 255, 255, 0.22);
  color: inherit;
}
.dark ::-moz-selection {
  background: rgba(255, 255, 255, 0.22);
  color: inherit;
}
/* iOS WKWebView selection tint, also de-greened. */
.arabic-text {
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0.08);
}
.dark .arabic-text {
  -webkit-tap-highlight-color: rgba(255, 255, 255, 0.1);
}

/* ─── Pressed-state (touch) feedback ───
   Native iOS acknowledges a touch on finger-DOWN; until now the app
   gave no visual response before the tap's action completed, which
   reads as lag even when the work is instant. Two tiers, matching
   platform conventions: controls DIM while pressed; navigational
   cards/rows COMPRESS slightly. transform+opacity only, so the
   feedback runs on the compositor.
   Selector notes: the app's string-template HTML wires taps through
   inline onclick ATTRIBUTES, so `[onclick]` matches exactly the
   tappable content elements; overlays and backdrops are wired via
   the .onclick DOM *property* and are naturally excluded. :where()
   keeps specificity at zero so any component's own transition or
   opacity rules still win. The press is instant (transition: none);
   the release eases out on the shared iOS curve. WebKit only applies
   :active while a touch listener exists; app.js registers a no-op
   passive touchstart to unlock these. */
:where(button, [onclick]) {
  transition: opacity 0.22s cubic-bezier(0.32, 0.72, 0, 1),
              transform 0.22s cubic-bezier(0.32, 0.72, 0, 1);
}
:where(button, [onclick]):active {
  opacity: 0.55;
  transition: none;
}
:where(.card[onclick]):active {
  opacity: 0.78;
  transform: scale(0.98);
}
/* When the pressed element is a control INSIDE the card (fav star,
   delete button…), only the control should react, so suppress the
   card-level compress. On engines without :has() this override is
   dropped and both levels animate, which is harmless. */
:where(.card[onclick]):active:has(button:active, [onclick]:active) {
  opacity: 1;
  transform: none;
  transition: none;
}

/* Honorifics keep their decorative green inline-filter colour even
   inside a user-highlight. The ::highlight() background paints
   behind the glyph, so the user-chosen colour shows around the
   calligraphy while the green glyph itself stays unchanged.
   Earlier iterations attempted to recolour the glyph in various
   ways (filter:brightness(0) → grey block; mix-blend-mode multiply
   /screen → yellow tint in dark; alt-text replacement → glyph
   disappears; mask-image with highlight colour → glyph changed
   colour, which the user explicitly didn't want). User's preference:
   only the highlight background changes, the glyph stays green. */

/* Extend the painted-background band of inline elements whose natural
   inline-box height is shorter than the matn's line-height; without
   this they show as visible gaps inside a user highlight.

   Two cases:

   .honorific-wrap: span around a calligraphic honorific PNG. Image
   is height:1.4em with vertical-align:middle; the span's inline box
   is determined by the image's vertical extent, ~1.4em tall. Matn's
   line-height is 2.2-2.4em.

   .fn-marker: footnote-number sup. font-size:calc(11px * var(--ts, 1)) versus matn's
   16-18px, so its natural inline-box paint band is ~11px tall, well
   below the matn's painted band.

   First attempt was `padding-block: 0.5em`. That worked SOMETIMES:
   the half-em extension matched the matn's half-leading exactly on
   the iPad tier (2.4em line-height − 1.4em image = 1.0em total
   shortfall, 0.5em on each side). But it was wrong by sub-pixels on
   other tiers / for some font metrics, producing:
     • Tiny gap above some honorifics (extension fell short of line
       boundary by 1-2px).
     • Visible *double-painted* band above other honorifics
       (extension overshot into the previous line's painted area;
        alpha-blending of two ~21% paints produced a darker stripe).

   New approach: use a zero-width inline-block strut sized at 1lh
   (one line-height). `lh` is a CSS unit equal to the COMPUTED line-
   height of the element, so the strut is always EXACTLY as tall as
   the surrounding text's line-box: no sub-pixel mismatch, no
   theme/breakpoint tuning. Setting it as a ::before pseudo means it
   contributes to the host element's inline-box height without taking
   horizontal space (width:0). The host's background paints across
   its full inline box, which now matches the matn's line-height.

   `1lh` is supported in Safari 16.4+ (iOS 16.4+, Mar 2023). For
   older browsers we fall back to a hardcoded 2.4em (iPad-tier
   line-height), slightly off on the mobile 2.2em tier but better
   than no extension at all.

   For .fn-marker, `1lh` reads the SUP's own line-height (driven by
   its 11px font), which is too short. We override the sup's line-
   height to inherit from the parent matn instead, so the strut sees
   the matn's real line-height. */
/* Hidden-text-pseudo strut + matn-line-height inheritance.
   ──────────────────────────────────────────────────────
   This is the approach production annotation apps use when the
   ::highlight() pseudo's painting can't be matched on its own.
   Every previous attempt failed because:

   • padding-block was em-based → sub-pixel slop
   • inline-block ::before strut + vertical-align:middle → aligned to
     the parent's x-height middle, which doesn't equal the line-box
     centre in fonts like Amiri (where ascent ≠ descent + leading) →
     asymmetric gaps
   • display:inline-flex + height:1lh → broke ::highlight() painting
     entirely on the .fn-marker sup (CSS Custom Highlights API does
     not paint flex containers)

   The pattern that works:
     a) Force the inline-box of each problem element to be exactly
        the matn's line-height tall using a TEXT-bearing pseudo-
        element (\200B zero-width space). The pseudo inherits the
        parent's font-size and line-height, so its strut is exactly
        one matn line-box tall, and that strut extends the parent
        element's inline-box.
     b) For elements with a smaller font-size (the sup .fn-marker
        at 11px vs matn's 18px), CSS unit `em` and `1lh` resolve to
        the SMALLER value. We store matn's line-height as an
        absolute-pixel custom property and inherit that, so the
        sup's inline-box matches matn's line-height in actual px.
     c) ::highlight() paints across these (now-tall-enough) inline
        boxes naturally. No DOM rewriting, no JS work, no breaking
        the standard painting path.

   --matn-lh is set on .arabic-text where 1lh resolves to that
   container's computed line-height value (e.g. 43.2 px at the iPad
   18 px / 2.4 line-height tier). Descendant elements inherit this
   absolute value via CSS custom property cascade. */
.arabic-text {
  --matn-lh: 1lh;
}
.fn-marker {
  /* Use the matn font (Amiri) rather than the UI font: Tajawal renders
     the Arabic-Indic ١ as a bare vertical stroke that reads as a stray
     line next to the parentheses. Amiri gives ١ its proper top-flag and
     harmonises the marker with the surrounding text. */
  font-family: var(--font-arabic);
  /* NOTE: the old `line-height: var(--matn-lh)` hack (which forced the
     sup's ::highlight box to full line-height to avoid a gap) is gone:
     highlights are now painted as merged per-line overlay rects
     (highlights.js), so the footnote marker is covered seamlessly by the
     line's rect with no per-element box tricks. */
}

/* Honorific image: the overlay-strip approach.
   ───────────────────────────────────────────
   The honorific is a <img> (replaced element). Its inline-box height
   is dictated by intrinsic dimensions (1.4em), not by line-height.
   Every in-element CSS trick we tried (padding-block, ::before strut,
   inline-flex, vertical-align variants) failed in some specific way
   on WebKit: replaced elements simply don't honour text-flow inline-
   box extensions reliably.

   Production annotation apps (Hypothesis for PDFs, Foliate, Apple
   Books' TextKit) sidestep this entirely: they don't try to extend the
   element's painted area; they draw a SEPARATE overlay LAYER behind
   the element at the line-height geometry. We mirror that for the
   honorific. Two zero-impact strip <div>s are appended as wrapper
   children when the wrapper falls inside a user highlight (one
   strip above, one below), sized to exactly fill the gap between
   the wrapper's natural inline-box (image height) and the matn's
   line-height. The strips paint the same color at the same alpha as
   the rest of the highlight, so the visible result is one continuous
   band the same height as the surrounding matn line.

   Geometry: each strip's height = (matn line-height − wrapper height)
   / 2. The wrapper itself is position:relative (set inline in the JS
   template). The strips are position:absolute children, anchored to
   the wrapper's top edge (top-strip with bottom:100%) or bottom edge
   (bottom-strip with top:100%), so they sit exactly outside the
   wrapper's natural inline-box: no overlap with the wrapper's own
   JS-painted background (which paints the image area), no overlap
   with adjacent lines' ::highlight() paint. Single alpha everywhere
   → consistent visual band, no darker double-painted stripes.

   z-index:-1 puts the strips behind the honorific image so the green
   calligraphy renders on top. pointer-events:none keeps them inert
   for selection / tap. */
/* Unicode-honorific span. New path enabled by USE_UNICODE_HONORIFICS
   in screens.js. The Unicode codepoints we use (U+FD40-FD4F + U+FDFB)
   ship in Amiri's Arabic Presentation Forms-A coverage, i.e. the
   font we already load for matn text renders them as the calligraphic
   ligature glyph natively.
     • color matches the green tone we used on the PNG calligraphy
       via the SVG filter
     • size bumped slightly so the ligature has comparable visual
       weight to the previous PNGs (1.4em image height)
     • no extra layout glue: these are text characters, so they flow
       in the matn line-box and ::highlight() paints over them
       naturally. No replaced-element gap problem. The whole reason
       for this migration is to eliminate the overlay-strip code path
       below (kept for the legacy PNG fallback under the flag). */
.honorific {
  color: var(--accent);
  /* Scheherazade Honorifics first: that's the font that actually
     contains glyphs for U+FD40-FD4F (Amiri's older build doesn't,
     so listing it first means honorifics get the Scheherazade
     calligraphic glyph while everything else in the matn stays in
     Amiri via the unicode-range declarations above). Amiri is the
     fallback in case the subset woff2 fails to load; it'll render
     the codepoint as composed Arabic letters rather than tofu. */
  font-family: 'Scheherazade Honorifics', var(--font-arabic);
  font-size: 1.15em;
  /* Keep the glyph from breaking off the line when copying / sharing
     plain text; the text is normal Arabic content that should remain
     part of the surrounding selection. */
  white-space: nowrap;
}
.dark .honorific {
  /* Same accent tone in dark mode (--accent is theme-aware in :root
     and .dark blocks above). This rule is only here as a guard against
     future overrides; it's a no-op today. */
  color: var(--accent);
}

/* Search highlight inside the رواية matn is painted by the geometry OVERLAY
   (.hl-rect, see applyDomHighlights/paintRangeRects): a rectangle drawn OVER
   the matched letters without splitting the DOM, so it highlights only the
   searched term and never breaks Arabic letter-joining. The inline
   <mark class="search-mark"> still wraps the match (extended to the whole word
   in highlightText so its boundaries fall on spaces/punctuation, never
   mid-word) and stays in the DOM as the scroll-to-match anchor, but it must be
   visually invisible here so it doesn't double-paint or tint the whole word.
   Search-RESULT snippets render their <mark> outside .arabic-text, so they keep
   the yellow background. */
.arabic-text mark.search-mark { background: transparent !important; }


/* .honorific-hl-strip used to be sized purely via CSS:
     height: calc((var(--matn-lh) - 100%) / 2);
   plus `bottom: 100%` / `top: 100%` for the two positions. That broke
   in production: the wrapper is display:inline (so we don't disturb
   the image's vertical-align:middle alignment), and CSS percentages on
   absolute children of inline parents resolve to `auto` (effectively
   0), so both `height: 100%` and `bottom: 100%` collapsed silently and
   the strips painted nothing.

   The fix moved all geometry into the JS in highlights.js: read the
   wrapper's actual offsetHeight and the matn's computed line-height
   in pixels, set strip dimensions inline. The strip class now only
   carries the properties that don't depend on layout context. */
.honorific-hl-strip {
  /* Layout properties set inline by highlights.js (position, top,
     height, left, right, z-index, background-color, pointer-events).
     Keeping the class here for cleanup querySelector targeting and
     for potential future styling that doesn't depend on dimensions. */
}

/* Continuous-reader card virtualization.
   ───────────────────────────────────────
   Continuous mode renders EVERY رواية of the current باب as a card in
   one pass. Most أبواب are ~20-40 روايات, but الروضة is a single باب of
   ~598 روايات, and laying out + painting all of them on entry froze the
   page for many seconds.

   content-visibility:auto lets the browser skip layout, paint, AND
   style for cards that are off-screen, rendering them only as they
   approach the viewport. The DOM still holds all cards (so the
   IntersectionObserver, highlights, and anchors keep working), but the
   expensive work is deferred to scroll time. This is the standard
   long-list virtualization primitive and is what the card comments in
   screens.js already assumed was here; it had been dropped.

   contain-intrinsic-size gives a placeholder height for not-yet-
   rendered cards so the scrollbar + document height are sane:
     • iOS 16: a fixed 500px estimate (close to the average rendered
       رواية card). Over/under-estimates cause minor scroll drift as
       you pass cards, but the page is usable instead of frozen.
     • iOS 17+ (@supports the `auto` keyword): the browser remembers
       each card's REAL height after it's been rendered once, so
       re-scrolling past it doesn't shift. */
.continuous-card {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px;
}
@supports (contain-intrinsic-size: auto 1px) {
  .continuous-card {
    contain-intrinsic-size: auto 500px;
  }
}


/* ─── 3. Layout scaffolding (responsive) ─── */

/*
   Breakpoints (revised 2026-05 for iPad-native polish):

     < 640px      →  Phone: drawer menu, content fills the viewport.
     640-819px    →  iPad mini portrait (744 px): drawer menu,
                     content centred with shadow (still feels phone-y
                     because the screen is too narrow for a sidebar
                     to pay rent).
     ≥ 820px      →  Viewport-utilisation tier. Lists / search / home
                     tiles fill the screen up to 1000 px instead of
                     floating in a 640/820 px column with empty space
                     on each side. Matn stays at 820 px for Arabic
                     reading column. Menu is STILL a drawer at this
                     tier; sidebar only kicks in at landscape (see
                     next tier). Covers iPad 10.9"/11"/12.9"/13" in
                     PORTRAIT plus iPad mini in landscape.
     ≥ 1150px     →  Permanent sidebar tier: iPad-class landscape
                     only. iPad 10.9"/11" landscape is the narrowest
                     viewport that hits this (1180 px). iPad mini
                     landscape (1133 px) deliberately falls below the
                     threshold so it stays drawer per user spec:
                     "the portrait view should not have a permanent
                     side menu only landscape" + "iPads/tablets that
                     are 11 inch upwards" get the permanent sidebar.
     ≥ 1100px     →  Roomy enough to bump the matn font-size + padding
                     for comfortable reading.
     ≥ 1600px     →  Ultra-wide displays: give the matn a bit more
                     column room (900 px instead of 820 px).

   Why two thresholds (820 + 1150) instead of one:
     • 820 px is the narrowest "real iPad" viewport (iPad 10.9"/11"
       portrait). Above this, content max-width:1000 makes the layout
       feel iPad-native instead of phone-app-stretched. This applies
       in any orientation.
     • 1150 px is where iPad 11"+ in LANDSCAPE starts (iPad 10.9"
       landscape = 1180 px). At that width AND landscape, there's
       genuinely enough horizontal room for a 280 px sidebar + content
       without crowding. In portrait orientations (820-1032 px range)
       a sidebar would leave only 540-752 px of content area, too
       cramped for comfortable Arabic reading, per user feedback.

   Why content max-width:1000 px (and matn max-width:820 px):
     • At iPad Pro 12.9" landscape (1366 px viewport - 280 px sidebar
       = 1086 px content area), letting lists/cards stretch the full
       1086 px makes the screen feel empty/over-scaled.
     • 1000 px is the sweet spot: it fills enough of the iPad to feel
       "iPad-native" without becoming a wall of stretched content.
     • The matn itself stays at 820 px because Arabic reading at
       Amiri-size needs a comfortable column; wider lines force
       eyes to track too far.
*/

#app {
  width: 100%;
  max-width: var(--reader-max-width);
  margin: 0 auto;
  background: var(--bg);
  position: relative;
}

/* iPad mini portrait (640-819 px): centred card with shadow.
   The drawer menu still applies; at this width neither a sidebar
   nor full-bleed-content would help much. Phone-style with a hint
   of card chrome to suggest "this is a tablet". */
@media (min-width: 640px) and (max-width: 819px) {
  #app {
    box-shadow: var(--shadow-md);
  }
}

/* ─── iPad-class viewport (≥ 820 px): content fills the screen ───
   No sidebar here yet (drawer still active); sidebar lives in
   the ≥ 1150 px landscape block below. This tier handles the
   "stretch the screen" side of iPad polish for any iPad orientation
   except iPad mini portrait. */
@media (min-width: 820px) {
  #app {
    max-width: none;
    margin: 0;
    border-radius: 0;
    box-shadow: none;
  }

  /* No gradient backdrop: the app fills all space */
  body { background: var(--bg); }

  /* ── Content max-width: fills the screen, capped at 1000 px ──
     Applies to all immediate children of #app EXCEPT the header
     (which stays edge-to-edge for that iPad-native top-bar feel).
     Lists, search, settings, home tiles all centre at 1000 px
     within the content area, so on iPad Pro 12.9" landscape
     (1086 px content area) there's a comfortable 43 px gutter on
     each side instead of content stretched the full 1086 px. */
  #app > *:not(.header) {
    max-width: 1000px;
    margin-left: auto;
    margin-right: auto;
  }

  /* ── Reading text constraint ──
     Arabic body text stays at 820 px for readability, nested
     INSIDE the 1000 px content wrapper. Centres relative to its
     parent (the screen wrapper), so the matn floats nicely in
     the middle of the content area. */
  .arabic-text {
    max-width: 820px;
    margin-left: auto;
    margin-right: auto;
  }
  .footnotes-panel {
    max-width: 820px;
    margin-left: auto;
    margin-right: auto;
  }
}

/* ─── iPad-class landscape (≥ 1150 px): permanent sidebar ───
   Per user spec: "portrait view should not have a permanent side
   menu, only landscape." 1150 px catches iPad 10.9" landscape
   (1180 px) and above while excluding iPad mini landscape
   (1133 px), so iPad mini stays drawer in every orientation,
   matching "iPads/tablets 11 inch upwards" intent.

   No orientation:landscape filter here; width alone is enough.
   No portrait iPad viewport reaches 1150 px (iPad Pro 13"
   portrait is 1032 px, the widest portrait iPad), so width is a
   reliable proxy for "landscape on an 11+ inch device". Stage
   Manager / Split View resize windows below 1150 → drawer kicks
   back in naturally. */
/* The permanent-sidebar tier died with the drawer; the bottom tab
   bar is the navigation at every width now. --popup-right stays at
   its phone default so bottom-pinned popups keep full width. */

/* Roomy reading (≥ 1100 px): bump the matn font + padding now
   that there's enough content area for it (iPad Air/Pro 11" in
   landscape, iPad Pro 12.9"+ in any orientation). At 820 px
   viewport the content area would be only ~540 px, so we don't
   apply this bigger-font treatment there. */
@media (min-width: 1100px) {
  .arabic-text {
    font-size:calc(18px * var(--ts, 1));
    line-height: 2.4;
    padding: 20px 32px;
  }
}

/* Ultra-wide (≥ 1600px): more generous reading column */
@media (min-width: 1600px) {
  .arabic-text,
  .footnotes-panel {
    max-width: 900px;
  }
}

/* Make use of safe areas on notched mobile devices */
@supports (padding: max(0px)) {
  #app {
    padding-left: max(0px, env(safe-area-inset-left));
    padding-right: env(safe-area-inset-right);
  }
  body {
    padding-bottom: env(safe-area-inset-bottom);
  }
}


/* ─── 4. Header ─── */

.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 13px 16px;
  padding-top: calc(env(safe-area-inset-top, 0px) + 13px);
  /* Uniform chrome height whether or not the row holds buttons:
     the home header lost its last icon when search moved to the tab
     bar, and without this the title would sit 15px higher there than
     on drill-down screens (and jump during push transitions). 66px
     is the with-button height. */
  min-height: calc(env(safe-area-inset-top, 0px) + 66px);
  background: var(--header);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  backdrop-filter: saturate(1.5) blur(8px);
  -webkit-backdrop-filter: saturate(1.5) blur(8px);
  flex-shrink: 0;
}

.header h2 {
  font-size:calc(16px * var(--ts, 1));
  font-weight: 700;
  margin: 0;
  flex: 1;
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding: 0 8px;
}

.header button {
  color: var(--accent);
  font-size:calc(24px * var(--ts, 1));
  padding: 4px;
  border-radius: var(--radius-sm);
  transition: background 0.15s;
}

.header button:hover {
  background: var(--accent-s);
}


/* ─── 5. Bottom tab bar ─── */

/* Category-standard bottom navigation (القراءة · البحث · مكتبتي ·
   المزيد). Replaced the hamburger drawer. Stable element outside
   #app; render() syncs active state via renderTabBar(). Hidden on
   immersive reading surfaces (hadithDetail, continuousReader, khutba,
   bio, composers). body.has-tabbar reserves space so content and
   toasts clear the bar. */
.tab-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 60;
  display: flex;
  direction: rtl;
  background: var(--header);
  border-top: 1px solid var(--border);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.tab-bar.is-hidden {
  display: none;
}
.tab-btn {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 7px 0 5px;
  background: none;
  border: none;
  color: var(--muted);
  font-size:calc(10.5px * var(--ts, 1));
  font-family: inherit;
  cursor: pointer;
}
.tab-btn.is-active {
  color: var(--accent);
  font-weight: 700;
}
body.has-tabbar {
  padding-bottom: calc(52px + env(safe-area-inset-bottom, 0px));
}
body.has-tabbar .toast {
  bottom: calc(62px + env(safe-area-inset-bottom, 0px));
}

.menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 16px;
  background: none;
  border: none;
  border-bottom: 1px solid var(--border);
  color: var(--text);
  font-size:calc(15px * var(--ts, 1));
  width: 100%;
  direction: rtl;
  text-align: right;
  transition: background 0.15s;
}

.menu-item:hover {
  background: var(--card-h);
}


/* ─── 6. Cards, lists, badges ─── */

.card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  background: var(--card);
  border: none;
  border-radius: var(--radius-lg);
  width: 100%;
  text-align: right;
  direction: rtl;
  cursor: pointer;
  color: var(--text);
  -webkit-tap-highlight-color: transparent;
}

/* Only apply hover on devices with a real pointer (not touch) */
@media (hover: hover) {
  .card:hover {
    background: var(--card-h);
    transform: translateY(-1px);
    box-shadow: var(--shadow-sm);
  }
}

.num-badge {
  width: 30px;
  height: 30px;
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size:calc(12px * var(--ts, 1));
  background: var(--accent-s);
  color: var(--accent);
  font-weight: 700;
  flex-shrink: 0;
}

.hadith-num {
  width: 26px;
  height: 26px;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size:calc(12px * var(--ts, 1));
  font-weight: 700;
}

.grading {
  display: inline-flex;
  font-size:calc(10px * var(--ts, 1));
  padding: 2px 7px;
  border-radius: 5px;
  font-weight: 600;
}


/* ─── 7. Hadith detail view ─── */

.hadith-card {
  margin-bottom: 16px;
  background: var(--card);
  border-radius: 13px;
  border: 1px solid var(--border);
  overflow: hidden;
}

.hadith-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 9px 12px;
  background: var(--accent-s);
  border-bottom: 1px solid var(--border);
  direction: rtl;
  flex-wrap: wrap;
  gap: 6px;
}

.arabic-text {
  font-family: var(--font-arabic);
  padding: 14px 16px;
  font-size:calc(16px * var(--ts, 1));
  line-height: 2.2;
  direction: rtl;
  /* Justified matn: Arabic's long, connected words fill the measure
     edge-to-edge cleanly, the look testers preferred.
     Justify was *previously* removed because the CSS Custom Highlight API
     can't paint a band across the stretched inter-word spaces (it rendered
     yellow-gap-yellow-gap). That constraint is gone: highlights are now
     painted as merged PER-LINE overlay rectangles (paintRangeRects /
     repaintOverlay in highlights.js), which cover the stretched spaces
     (and honorifics + footnote markers) seamlessly.
     word-spacing nudges the inter-word rhythm so justification spreads
     evenly. NOTE: plain justify only. Do NOT add `text-wrap: pretty`;
     pretty + justify is a known WebKit bug (spacing blows out on iOS). */
  text-align: justify;
  word-spacing: 0.05em;
  user-select: text;
  -webkit-user-select: text;
  /* Disable native iOS callout; our JS menu handles copy + highlight */
  -webkit-touch-callout: none;
}

/* Matn sizing across iPad tiers is now centralised in the layout
   block (Section 3): no font bump between 820-1099 px (content area
   is narrow there), then a single bump at ≥ 1100 px to 18 px /
   2.4 line-height / wider padding. The pre-revision 17 px middle
   tier between 900 and 1279 px is gone; it was redundant once we
   merged the tablet-landscape and desktop tiers. */

.footnotes-panel {
  direction: rtl;
  margin: 0 20px 100px;
  border-top: 1px solid var(--border);
  padding-top: 12px;
  user-select: text;
  -webkit-user-select: text;
  /* Suppress iOS native callout; our JS selection menu owns the
     copy interaction here (same as .arabic-text). Without this,
     iOS shows its own copy/paste bubble on long-press, which races
     with and visually overlaps our floating menu. */
  -webkit-touch-callout: none;
}


/* ─── 8. Search box & chips ─── */

/* Sticky search header: pins the search box + filter bar just under the title
   header while results scroll. top ≈ title-header height (safe-area + ~50px). */
.search-sticky {
  position: sticky;
  top: calc(env(safe-area-inset-top, 0px) + 56px);  /* fallback; JS sets the exact header height */
  z-index: 9;                 /* under .header (--z-header: 10), above content */
  background: var(--bg);
  padding: 12px 16px 8px;
  border-bottom: 1px solid var(--border);
}

/* iOS-style sticky section headers: each tier's header pins just under the
   search bar and is pushed out by the next section's header as you scroll, so the
   pinned label always names the section you're viewing. --section-sticky-top is
   set in JS to (title-header + search-sticky) height; the calc() is a fallback. */
.sec-sticky {
  position: sticky;
  top: var(--section-sticky-top, calc(env(safe-area-inset-top, 0px) + 150px));
  z-index: 8;                 /* under .search-sticky (9) and .header (10) */
  background: var(--bg);
}

.search-box {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 12px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 11px;
  direction: rtl;
  transition: border-color 0.15s, background 0.15s;
}

.search-box:focus-within {
  border-color: var(--accent);
  background: var(--card-h);
}

.search-box input {
  flex: 1;
  border: none;
  background: none;
  outline: none;
  font-size:calc(16px * var(--ts, 1)); /* Must be ≥16px to prevent iOS Safari zoom on focus */
  color: var(--text);
  font-family: inherit;
  direction: rtl;
  user-select: text;
  -webkit-user-select: text;
  /* No edit menu at all on search bars, neither custom nor native.
     Users just type what they need. Suppresses iOS callout. */
  -webkit-touch-callout: none;
}

.search-box input::placeholder {
  color: var(--muted);
}

/* Textareas: suppress the iOS native "Paste" tooltip / edit menu so it
   doesn't double up with our own نسخ / لصق pill that fires from
   search.js showPasteBar. Without this, both menus appear at once
   (the custom pill on top, the native one below); see screenshot
   in the comment editor. We keep the custom toolbar because users
   need access to clipboard actions, just not two of them. */
textarea {
  -webkit-touch-callout: none;
}

.chip {
  padding: 4px 12px;
  border-radius: 16px;
  font-size:calc(11px * var(--ts, 1));
  font-family: inherit;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--muted);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}

.chip:hover {
  background: var(--card-h);
}

.chip.active {
  border-color: var(--accent);
  background: var(--accent-s);
  color: var(--accent);
}


/* ─── 9. Selection popup / color picker ─── */

/* Used for BOTH popups that surface action buttons on text:
   - selection toolbar (fresh text selection, defined in screens.js)
   - highlight-tap menu (tapping an existing highlight, built in highlights.js)
   Both share this class so they look identical. The iOS-native feel
   comes from three things added in this rule:
     1. backdrop-filter saturate+blur → the frosted-glass look that
        UIKit context menus use. Falls back gracefully on browsers that
        don't support it (older Android): just a solid card bg there.
     2. semi-transparent card background → so the blur actually has
        something to blur through. Without the alpha tweak the blur is
        invisible.
     3. tighter, more pronounced drop shadow → matches the elevation
        UIKit gives to a presented context menu over content.
   These align the popup with the iOS share-sheet / context-menu
   visual language. The popIn keyframe (defined further down) gives
   the spring-y scale-in that completes the native feel. */
.sel-popup {
  position: fixed;
  z-index: var(--z-popup);
  background: color-mix(in srgb, var(--card) 86%, transparent);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  border: 1px solid var(--border);
  border-radius: 13px;
  box-shadow: 0 10px 32px rgba(0, 0, 0, 0.22), 0 2px 6px rgba(0, 0, 0, 0.08);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  min-width: 168px;
  animation: popIn 0.18s cubic-bezier(0.32, 0.72, 0, 1);
}

/* Sidebar-aware right edge enforced via CSS variable, with !important
   to defeat any code path that still ships a hardcoded `right: 20px`
   inline style. This rule applies only when the popup uses bottom-pin
   positioning (selector below targets popups that have `bottom` and
   `right` set, which is how every JS path positions the bottom-bar
   variant). On non-bottom-pinned popups (anchored-to-tap variants in
   highlights.js for tapping a highlight) the popup uses `top:Xpx;
   left:Ypx` with no `right`, so this rule has no effect there. */
.sel-popup[style*="bottom"][style*="right"] {
  right: var(--popup-right) !important;
}

.sel-popup button {
  padding: 10px 16px;
  background: none;
  border: none;
  border-bottom: 1px solid var(--border);
  font-size:calc(13px * var(--ts, 1));
  color: var(--text);
  font-family: inherit;
  direction: rtl;
  text-align: right;
  display: flex;
  align-items: center;
  gap: 8px;
  transition: background 0.15s;
}

.sel-popup button:last-child { border-bottom: none; }
.sel-popup button:hover { background: var(--card-h); }

.color-picker {
  display: flex;
  gap: 8px;
  padding: 10px 16px;
  justify-content: center;
  background: var(--surface);
}

.color-picker .cdot {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  cursor: pointer;
  border: 2px solid transparent;
  transition: transform 0.15s, border-color 0.15s;
}

.color-picker .cdot:hover {
  transform: scale(1.15);
}

/* The note-attached variant of the highlight-tap menu: a bottom-sheet
   that shows the saved note + action icons. Built imperatively in
   highlights.js (showHighlightCommentPopup) so it can't be styled
   purely through a class. We layer on the same iOS frosted-glass feel
   as .sel-popup via an ID selector so the inline cssText in the JS
   stays minimal. !important is needed because the JS sets bg/shadow
   inline; without it the inline declarations would win and the frost
   effect would never render. */
#hl-comment-popup {
  background: color-mix(in srgb, var(--card) 88%, transparent) !important;
  -webkit-backdrop-filter: saturate(180%) blur(22px);
  backdrop-filter: saturate(180%) blur(22px);
  box-shadow: 0 12px 38px rgba(0, 0, 0, 0.28), 0 2px 8px rgba(0, 0, 0, 0.10) !important;
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent) !important;
}


/* ─── 10. Modals & toasts ─── */

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  animation: fadeOverlayIn 0.2s ease-out;
  -webkit-tap-highlight-color: transparent;
}

.modal {
  animation: scaleIn 0.30s cubic-bezier(0.32, 0.72, 0, 1);
  background: var(--card);
  border-radius: 16px;
  width: 100%;
  max-width: var(--modal-max-width);
  padding: 20px;
  direction: rtl;
  box-shadow: var(--shadow-lg);
  max-height: 90vh;
  overflow-y: auto;
}

.modal h3 {
  font-size:calc(16px * var(--ts, 1));
  margin-bottom: 12px;
  color: var(--text);
}

.modal textarea {
  width: 100%;
  min-height: 100px;
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px;
  font-size:calc(14px * var(--ts, 1));
  font-family: inherit;
  direction: rtl;
  background: var(--bg);
  color: var(--text);
  outline: none;
  resize: vertical;
}

.modal textarea:focus {
  border-color: var(--accent);
}

.modal .btns {
  display: flex;
  gap: 8px;
  margin-top: 12px;
  justify-content: flex-start;
}

.modal .btns button {
  padding: 8px 20px;
  border-radius: 8px;
  font-size:calc(13px * var(--ts, 1));
  font-family: inherit;
  border: none;
  transition: opacity 0.15s;
}

.modal .btns button:hover { opacity: 0.9; }

.modal .btn-primary {
  background: var(--accent);
  color: white;
}

.modal .btn-secondary {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border) !important;
}

.toast {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--accent);
  color: white;
  padding: 8px 20px;
  border-radius: 20px;
  font-size:calc(12px * var(--ts, 1));
  z-index: var(--z-toast);
  box-shadow: var(--shadow-md);
  max-width: var(--toast-max-width);
  text-align: center;
  animation: fadeIn 0.2s;
}

/* Keep toast above iOS safe area */
@supports (bottom: max(0px)) {
  .toast {
    bottom: max(24px, calc(env(safe-area-inset-bottom) + 16px));
  }
}


/* ─── 11. Loading & empty states ─── */

.loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 80vh;
  gap: 16px;
}

.loading .spinner {
  width: 40px;
  height: 40px;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

/* Boot skeleton (alkafi.html). While the corpus loads (~1-2 s on
   device) the boot screen shows the HOME SCREEN'S OWN SHAPES (real
   header, resume-card block, daily-رواية block, bookshelf tiles), so
   first paint reads as "the app, still settling" rather than a
   generic spinner, and the swap to the real home is a morph instead
   of a scene change (HIG Launching: the ideal launch sequence is
   effectively invisible). Shimmer is a slow steady wave (perceived
   as shorter than pulsing), swept in reading direction (RTL), on
   transform only so it stays on the compositor. The global
   prefers-reduced-motion rule flattens it automatically. */
.skel {
  background: var(--card);
  position: relative;
  overflow: hidden;
}
.skel::after {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  transform: translateX(100%);
  background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.045), transparent);
  animation: skelWave 1.8s ease-in-out infinite;
}
.dark .skel::after {
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), transparent);
}
@keyframes skelWave {
  to { transform: translateX(-100%); }
}

.empty {
  text-align: center;
  padding: 36px;
  color: var(--muted);
}


/* ─── 12. Animations & misc ─── */

/* Easing curves shared across the app. iOS-feel entry vs sharper exit. */
/* Entry:  cubic-bezier(0.32, 0.72, 0, 1)   gentle settle, used by Apple */
/* Exit:   cubic-bezier(0.4, 0, 1, 1)       quicker pull-away */

@keyframes fadeIn {
  from { opacity: 0; transform: translateX(-50%) translateY(10px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}

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

/* ش popover: expands and drops open out of its anchor (the ش button);
   transform-origin is set inline at the arrow tip. transform+opacity
   only: composited, no layout work. */
@keyframes tpPopIn {
  from { opacity: 0; transform: translateY(-10px) scale(0.35); }
  55%  { opacity: 1; }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes tpPopOut {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to   { opacity: 0; transform: translateY(-8px) scale(0.5); }
}

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

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

@keyframes fadeOverlayIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes fadeOut {
  from { opacity: 1; }
  to { opacity: 0; }
}

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

/* Mirror of scaleIn, used by dismissOverlay() to retract a modal
   back into where it scaled in from, with a slightly faster curve
   so the dismiss feels snappier than the entrance (Apple pattern). */
@keyframes scaleOut {
  from { transform: scale(1); opacity: 1; }
  to { transform: scale(0.92); opacity: 0; }
}

/* iOS-style screen push/pop transitions. Direction is from the
   user's reading-flow point of view in RTL: a "push" (drilling
   deeper into the hierarchy) brings the new screen in from the
   leading edge (left in RTL Arabic), sliding rightward into place.
   "Pop" (going back) brings the previous screen back from the
   trailing edge (right). Subtle ~24 px slide + fade so it reads as
   a transition without delaying the user. */
@keyframes screenPushIn {
  from { transform: translateX(-24px); opacity: 0.55; }
  to   { transform: translateX(0);     opacity: 1; }
}
@keyframes screenPopIn {
  from { transform: translateX(24px);  opacity: 0.55; }
  to   { transform: translateX(0);     opacity: 1; }
}
.screen-push-in {
  animation: screenPushIn 0.26s cubic-bezier(0.32, 0.72, 0, 1) backwards;
}
.screen-pop-in {
  animation: screenPopIn 0.26s cubic-bezier(0.32, 0.72, 0, 1) backwards;
}

/* Title settles in slightly after the screen body, Apple's staggered
   entry. Body slides at 0.26 s with translateX; the <h2> in the
   header drops in at 0.45 s with a small translateY + fade, starting
   100 ms after the screen begins. The user reads the title arriving
   *into* a settled context rather than racing past as the screen
   slides. `backwards` keeps the title invisible during the 100 ms
   delay so it doesn't peek out before its turn. */
@keyframes titleEnter {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* The header element itself receives the screen-push/pop class (it's
   one of the direct children of #app), so the selector is
   `.screen-push-in h2` (h2 inside an element with the push class),
   not `.screen-push-in .header h2`. */
.screen-push-in h2,
.screen-pop-in  h2 {
  animation: titleEnter 0.45s cubic-bezier(0.32, 0.72, 0, 1) 0.1s backwards;
}

/* Re-target every animation utility class to use the iOS curves. The
   keyframes above describe the visual change; the curve here describes
   the FEEL of how that change unfolds; flat `ease` is what makes
   web apps feel like web apps. */
.toast { animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1) !important; }

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

/* Search-landing flash: after the continuous reader centers a matched
   term, its marks pulse briefly so the eye finds them (JS removes the
   class after ~2.4s; the global reduced-motion rule flattens it). */
@keyframes markFlash {
  0%, 100% { box-shadow: 0 0 0 0 rgba(250, 204, 21, 0); }
  40% { box-shadow: 0 0 0 5px rgba(250, 204, 21, 0.4); }
}
mark.mark-flash {
  animation: markFlash 1.1s ease-in-out 2;
}

.caret {
  transition: transform 0.2s;
}

.cm-body {
  /* comment body container in detail view */
}

.chk {
  /* checkbox-like toggle used in menu */
}


/* ─── Utilities ─── */

.scroll-area {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/* Prevent iOS from zooming on input focus: all inputs must be ≥16px */
input, textarea, select {
  font-size:calc(16px * var(--ts, 1)) !important;
}

/* Hide scrollbars where they look messy (still scrollable) */
.hide-scrollbar::-webkit-scrollbar { display: none; }
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }


/* ─── Print (optional, for sharing hadith pages) ─── */

@media print {
  body { background: white; color: black; }
  #app { max-width: none; box-shadow: none; }
  .header, .tab-bar, .toast, .sel-popup, .modal-overlay { display: none !important; }
  .hadith-card { break-inside: avoid; border: 1px solid #ccc; }
}


/* ─── Reduced-motion preference ─── */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
