/* ==========================================================================
   JamFix — site styles
   Built entirely on the tokens in tokens.css. No literal colour appears below
   except where a value is genuinely not a brand decision (a scrim opacity).

   Every directional property is logical — margin-inline, padding-inline,
   inset-inline, border-inline — so this one file serves /en/ (LTR) and /ar/
   (RTL) with no mirrored copy and no [dir="rtl"] override block.
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

/* overflow-x:hidden is set on both html and body: body's own value is
   normally propagated to the viewport by the UA, but a couple of older
   mobile WebViews skip that propagation, so this is a safety net belonging
   on the root too, not a fix for any specific overflowing element below. */
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; overflow-x: hidden; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background: var(--jf-surface);
  color: var(--jf-ink);
  font-size: var(--jf-text-base);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  /* Guards against a long unbroken string — a URL or an email pasted into
     dashboard-authored content — forcing its container, and with it the
     page, wider than the viewport. */
  overflow-wrap: break-word;
}

/* --- ambient background wash ------------------------------------------------
   Fixed behind everything (z-index 0, painted right after body's own white
   background, before any section). A plain .section has no background of
   its own, so these three huge, barely-there blobs read through it; .section
   --tint and .section--dark paint over them as normal, since those already
   have their own colour. This is the one thing that makes a long run of
   white sections feel like one continuous page instead of stacked brochure
   panels, without tinting every section by hand. */
.bg-ambient { position: fixed; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.bg-ambient-blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(120px);
  opacity: .07;
}
.bg-ambient-blob--1 { top: -10%; inset-inline-start: -8%; width: 42vw; height: 42vw; background: var(--jf-teal); }
.bg-ambient-blob--2 { top: 38%; inset-inline-end: -10%; width: 38vw; height: 38vw; background: var(--jf-gold); opacity: .05; }
.bg-ambient-blob--3 { bottom: -12%; inset-inline-start: 18%; width: 46vw; height: 46vw; background: var(--jf-teal-light); }

@media (prefers-reduced-motion: no-preference) {
  .bg-ambient-blob--1 { animation: jf-drift-1 34s ease-in-out infinite; }
  .bg-ambient-blob--2 { animation: jf-drift-2 42s ease-in-out infinite; }
  .bg-ambient-blob--3 { animation: jf-drift-3 38s ease-in-out infinite; }
}
@keyframes jf-drift-1 { 0%, 100% { transform: translate(0, 0) scale(1); } 50% { transform: translate(4%, 3%) scale(1.08); } }
@keyframes jf-drift-2 { 0%, 100% { transform: translate(0, 0) scale(1); } 50% { transform: translate(-3%, 4%) scale(0.94); } }
@keyframes jf-drift-3 { 0%, 100% { transform: translate(0, 0) scale(1); } 50% { transform: translate(3%, -4%) scale(1.06); } }

/* Below 700px the three blobs can overlap awkwardly on a short viewport and
   cost more paint than they are worth on a small screen — one is plenty. */
@media (max-width: 700px) {
  .bg-ambient-blob--2, .bg-ambient-blob--3 { display: none; }
}

img, svg, video { max-width: 100%; height: auto; display: block; }

h1, h2, h3, h4 {
  margin: 0;
  line-height: var(--jf-leading-tight);
  text-wrap: balance;
  color: var(--jf-navy);
}

p { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }

a { color: var(--jf-ink-accent); text-decoration-thickness: 1px; text-underline-offset: 0.15em; }
a:hover { color: var(--jf-teal-dark); }

/* A visible focus state on every interactive element. The double ring reads on
   both light and dark surfaces, which matters because the same buttons appear
   on white and on navy. */
:focus-visible {
  outline: none;
  box-shadow: var(--jf-focus-ring);
  border-radius: var(--jf-radius-sm);
}

.skip-link {
  position: absolute;
  inset-inline-start: var(--jf-space-4);
  top: calc(var(--jf-space-4) * -4);
  z-index: 999;
  padding: var(--jf-space-3) var(--jf-space-4);
  background: var(--jf-navy);
  color: #fff;
  border-radius: var(--jf-radius-sm);
  text-decoration: none;
  transition: top var(--jf-duration) var(--jf-ease);
}
.skip-link:focus { top: var(--jf-space-4); color: #fff; }

/* Visually hidden but still reachable by assistive tech — the standard clip
   technique (display:none would remove it from the accessibility tree too).
   Used for the page's real <h1> on the home page, where the visible heading
   work is split across two equally-weighted <h2> panels instead. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --- layout --------------------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--jf-container);
  margin-inline: auto;
  padding-inline: var(--jf-gutter);
}
.container--narrow { max-width: 760px; }

.section { padding-block: var(--jf-section-y); }
.section--tint { background: var(--jf-surface-tint); }
.section--dark { background: var(--jf-surface-dark); }

/* Section heads carry a hairline above the eyebrow. It gives the page a
   repeating horizontal rhythm that a stack of centred headings does not, and
   it costs one border. */
.section-head {
  margin-bottom: var(--jf-space-8);
  padding-top: var(--jf-space-4);
  border-top: 1px solid var(--jf-line);
}
.section--dark .section-head { border-top-color: rgba(255, 255, 255, 0.14); }

/* margin-bottom here is a floor, not a design choice every section has to
   repeat: a bare <h2 class="section-title"> with nothing else (no .section-
   head wrapper, no .section-sub) used to sit flush against whatever came
   next, which is the "heading touching the card" bug the testimonials
   section had on the service/category pages. A wrapping .section-head or a
   following .section-sub already carries its own spacing, and margins
   between adjacent siblings collapse to the larger one rather than stack,
   so this never doubles up there — it only fills the gap where nothing else
   was providing one. */
.section-title { margin-bottom: var(--jf-space-5); font-size: var(--jf-text-3xl); font-weight: var(--jf-weight-bold); letter-spacing: -0.015em; }
.section-title--invert { color: #fff; }
.section-sub {
  margin-top: var(--jf-space-3);
  color: var(--jf-ink-soft);
  font-size: var(--jf-text-lg);
  max-width: var(--jf-measure);
}
.section--dark .section-sub { color: #c8d3de; }
.section-more { margin-top: var(--jf-space-8); }

.eyebrow {
  display: block;
  margin-bottom: var(--jf-space-2);
  font-family: var(--jf-font-mono);
  font-size: var(--jf-text-xs);
  font-weight: var(--jf-weight-semibold);
  letter-spacing: var(--jf-tracking-label);
  text-transform: uppercase;
  color: var(--jf-ink-accent);
}
/* Cairo is the Arabic face; a monospace Latin stack would override it and
   break the joins, so Arabic keeps its own face for the eyebrow too. */
html[lang^="ar"] .eyebrow { font-family: var(--jf-font-ar); text-transform: none; }
.section--dark .eyebrow, .hero .eyebrow { color: var(--jf-teal-light); }

.page-head { padding-block: var(--jf-space-16) var(--jf-space-8); }
.page-title { font-size: var(--jf-text-4xl); font-weight: var(--jf-weight-bold); }
.page-lede {
  margin-top: var(--jf-space-4);
  font-size: var(--jf-text-lg);
  color: var(--jf-ink-soft);
  max-width: var(--jf-measure);
}

/* --- page hero: cover photo variant -----------------------------------------
   The same idea as .svc-hero--cover, generalised: .page-head is shared by
   before-after.php, page.php (About/Privacy/Terms) and services.php, so this
   modifier (plus an inline background-image per page) is what lets one of
   them show a cover photo without the others changing. */
.page-head--cover {
  min-height: 18rem;
  display: flex;
  align-items: flex-end;
  background-size: cover;
  background-position: center;
  color: #fff;
}
.page-head--cover .container { width: 100%; }
.page-head--cover .eyebrow { color: var(--jf-teal-light); }
.page-head--cover .page-title { color: #fff; }
.page-head--cover .page-lede,
.page-head--cover .ba-stat { color: rgba(255, 255, 255, 0.85); }

.empty-note {
  padding: var(--jf-space-6);
  border: 1px dashed var(--jf-line-strong);
  border-radius: var(--jf-radius-md);
  color: var(--jf-ink-soft);
  max-width: var(--jf-measure);
}

.link-arrow { font-weight: var(--jf-weight-semibold); text-decoration: none; }
.link-arrow::after { content: " \2192"; }
html[dir="rtl"] .link-arrow::after { content: " \2190"; }

/* --- ticker ----------------------------------------------------------------
   A slow horizontal scroll of real service names, not an invented keyword
   list. Two identical groups sit side by side; the track scrolls from 0% to
   -50% and the loop is seamless because the second half is pixel-identical
   to the first. The second group is aria-hidden so a screen reader hears the
   list once instead of twice. */

.ticker {
  background: var(--jf-surface-tint);
  border-bottom: 1px solid var(--jf-line);
  overflow: hidden;
}
.ticker-track {
  display: flex;
  width: max-content;
  animation: jf-marquee 38s linear infinite;
}
html[dir="rtl"] .ticker-track { animation-name: jf-marquee-rtl; }
.ticker:hover .ticker-track,
.ticker:focus-within .ticker-track { animation-play-state: paused; }

.ticker-group { display: flex; flex: none; padding-block: var(--jf-space-3); }
.ticker-item {
  display: inline-flex;
  align-items: center;
  gap: var(--jf-space-3);
  padding-inline-end: var(--jf-space-6);
  white-space: nowrap;
}
.ticker-item a {
  font-size: var(--jf-text-sm);
  font-weight: var(--jf-weight-semibold);
  color: var(--jf-ink-soft);
  text-decoration: none;
}
.ticker-item a:hover { color: var(--jf-ink-accent); }
.ticker-dot { width: 4px; height: 4px; border-radius: 999px; background: var(--jf-teal); flex: none; }

@keyframes jf-marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@keyframes jf-marquee-rtl { from { transform: translateX(0); } to { transform: translateX(50%); } }

@media (prefers-reduced-motion: reduce) {
  .ticker-track { animation: none; flex-wrap: wrap; width: 100%; }
  .ticker-group:last-child { display: none; } /* the duplicate is only needed for the loop */
}

/* --- buttons -------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--jf-space-2);
  padding: var(--jf-space-3) var(--jf-space-6);
  border: 1.5px solid transparent;
  border-radius: var(--jf-radius-pill);
  font: inherit;
  font-size: var(--jf-text-base);
  font-weight: var(--jf-weight-semibold);
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--jf-duration) var(--jf-ease),
              transform var(--jf-duration-fast) var(--jf-ease),
              box-shadow var(--jf-duration) var(--jf-ease);
}
.btn:active { transform: translateY(1px); }
.btn--sm { padding: var(--jf-space-2) var(--jf-space-4); font-size: var(--jf-text-sm); }
.btn--lg { padding: var(--jf-space-4) var(--jf-space-8); font-size: var(--jf-text-lg); }

/* Primary uses teal-dark, not teal: white on teal is 3.33:1 and fails AA. */
.btn--primary { background: var(--jf-teal-dark); color: #fff; }
/* A touch darker on hover, which only improves the 6.23:1 contrast. */
.btn--primary:hover { background: #0a584c; color: #fff; }

.btn--secondary {
  background: transparent;
  color: var(--jf-ink-accent);
  border-color: var(--jf-line-strong);
}
.btn--secondary:hover { border-color: var(--jf-teal-dark); background: var(--jf-surface-tint); }

/* WhatsApp green with a navy label: white on #25d366 is 1.98:1. */
.btn--wa { background: var(--jf-whatsapp); color: var(--jf-navy); }
.btn--wa:hover { background: #1fbd5a; color: var(--jf-navy); }

.btn--ghost { background: transparent; color: var(--jf-ink-accent); padding-inline: var(--jf-space-2); }
.btn--ghost:hover { text-decoration: underline; }

.section--dark .btn--secondary { color: var(--jf-teal-light); border-color: rgba(255, 255, 255, 0.3); }

/* Invert pair, for a button sitting directly on the navy CTA panel: a solid
   white primary (navy text — 18.13:1) and an outlined secondary in white. */
.btn--primary-invert { background: #fff; color: var(--jf-navy); }
.btn--primary-invert:hover { background: var(--jf-surface-tint); color: var(--jf-navy); }
.btn--outline-invert { background: transparent; color: #fff; border-color: rgba(255, 255, 255, 0.45); }
.btn--outline-invert:hover { background: rgba(255, 255, 255, 0.12); border-color: #fff; color: #fff; }

/* --- header -----------------------------------------------------------------
   A floating rounded pill, fixed over the page and the same on every page —
   by client request, matching a reference screenshot — rather than a
   full-bleed bar of any kind. <header> itself is just a fixed, invisible
   positioning wrapper spanning the viewport; .header-inner (which already
   carries .container, so it inherits the site's normal 1200px cap and
   gutter) is the actual visible card: background, radius, shadow all live
   there, so the pill sits inset from both edges at the same width every
   other section's content uses. */

.site-header {
  position: fixed;
  inset-inline: 0;
  top: 0;
  z-index: var(--jf-z-header-fixed);
  padding-block-start: var(--jf-space-4);
  padding-inline: var(--jf-gutter);
}

.header-inner {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--jf-space-6);
  min-height: 72px;
  padding-inline: var(--jf-space-6);
  border: 1px solid rgba(10, 22, 40, 0.06);
  border-radius: var(--jf-radius-pill);
  box-shadow: var(--jf-shadow-lg);
  transition: box-shadow var(--jf-duration) var(--jf-ease);
}
.site-header.is-scrolled .header-inner { box-shadow: var(--jf-shadow-xl); }
/* The frosted-glass fill lives on a ::before, not on .header-inner itself,
   because backdrop-filter on an element makes it a "containing block" for
   any position: fixed descendant — which .primary-nav's mobile slide-out
   drawer is. With the filter on .header-inner directly, the drawer was
   being positioned relative to this 72px bar instead of the viewport, so
   it "opened" squeezed invisibly inside the header instead of sliding in
   as a full-height panel. Same visual result, just on a layer that has no
   fixed-position descendants to trap. */
.header-inner::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: rgba(255, 255, 255, 0.96);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-radius: inherit;
  pointer-events: none;
}

.logo {
  display: flex;
  align-items: center;
  text-decoration: none;
  white-space: nowrap;
}

/* assets/img/logo-mark.png: logo.png with its white background made really
   transparent, keeping the original dark wordmark (a PowerShell +
   System.Drawing pass — no GD or Imagick available in this environment —
   see docs/status.md for the full method). logo-transparent.png is the same
   but with the wordmark recoloured white, for the dark footer and admin
   sidebar instead. Both are a stacked lockup (icon above the wordmark in one
   square-ish image), not a wide horizontal one, so even cropped tight to
   their content they read best given real height rather than being squeezed
   into a short bar. */
.logo-img { display: block; height: 44px; width: auto; }
.logo-img--footer { height: 68px; }

.primary-nav {
  display: flex;
  align-items: center;
  gap: var(--jf-space-6);
  margin-inline-start: auto;
}
/* Centered on the bar regardless of how wide the logo or the actions cluster
   end up being — absolute positioning against .header-inner rather than a
   flex-centering trick, since the two sides are different widths and don't
   mirror each other. Reset back to a normal stacked block inside the mobile
   slide-out drawer (see the max-width: 1024px block below). */
.nav-list {
  display: flex;
  align-items: center;
  gap: var(--jf-space-5);
  position: absolute;
  inset-inline-start: 50%;
  transform: translateX(-50%);
}
html[dir="rtl"] .nav-list { transform: translateX(50%); }
.nav-list a {
  color: var(--jf-ink-soft);
  text-decoration: none;
  font-size: var(--jf-text-sm);
  font-weight: var(--jf-weight-semibold);
  padding-block: var(--jf-space-2);
  white-space: nowrap;
}
.nav-list a:hover { color: var(--jf-ink-accent); }

/* 7 links (added: "Partner with us") is tight for the absolutely-centered
   desktop bar in the gap between 1024px, where it becomes the mobile
   drawer instead, and roughly 1300px, where the logo and nav-actions on
   either side leave the least room — tighten the gap and type size just in
   that range rather than for every desktop width. */
@media (min-width: 1025px) and (max-width: 1300px) {
  .nav-list { gap: var(--jf-space-3); }
  .nav-list a { font-size: var(--jf-text-xs); }
}
.nav-list a[aria-current="page"] {
  color: var(--jf-ink-accent);
  border-block-end: 2px solid var(--jf-teal);
}

.nav-actions { display: flex; align-items: center; gap: var(--jf-space-4); }

.lang-switch {
  font-size: var(--jf-text-sm);
  font-weight: var(--jf-weight-semibold);
  text-decoration: none;
  color: var(--jf-ink-soft);
  padding: var(--jf-space-2) var(--jf-space-3);
  border: 1px solid var(--jf-line-strong);
  border-radius: var(--jf-radius-pill);
}
.lang-switch:hover { border-color: var(--jf-teal-dark); color: var(--jf-ink-accent); }

.nav-phone {
  display: inline-flex;
  align-items: center;
  gap: var(--jf-space-2);
  /* .nav-actions already gives every item a 16px gap; this adds the extra
     8px that makes the gap specifically before the buttons that follow 24px,
     without needing a non-uniform gap the CSS gap property can't express. */
  margin-inline-end: var(--jf-space-2);
  font-size: var(--jf-text-sm);
  font-weight: var(--jf-weight-semibold);
  text-decoration: none;
  color: var(--jf-ink);
  white-space: nowrap;
}
.nav-phone-icon { color: var(--jf-teal-dark); }

.nav-toggle {
  display: none;
  margin-inline-start: auto;
  width: 44px;
  height: 44px;
  background: transparent;
  border: 1px solid var(--jf-line-strong);
  border-radius: var(--jf-radius-sm);
  cursor: pointer;
  padding: 0;
  place-items: center;
}
.nav-toggle-bar {
  display: block;
  width: 20px;
  height: 2px;
  margin: 3px auto;
  background: var(--jf-ink);
  transition: transform var(--jf-duration) var(--jf-ease), opacity var(--jf-duration) var(--jf-ease);
}
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(1) { transform: translateY(5px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(3) { transform: translateY(-5px) rotate(-45deg); }

/* Explicit close button inside the mobile drawer itself — hidden outside it
   (the drawer markup is reused as-is on desktop, where .primary-nav is a
   normal inline nav and this control has nothing to do). */
.primary-nav-close { display: none; }

/* The active page's nav item gets a solid dark pill rather than the teal
   underline a plain inline link would otherwise get — matches the reference
   screenshot, and doubles as the one deliberate accent of brand navy inside
   an otherwise light, neutral bar. */
.nav-list a[aria-current="page"] {
  color: #fff;
  background: var(--jf-navy);
  padding-inline: var(--jf-space-4);
  border-radius: var(--jf-radius-pill);
  border-block-end: none;
}
.nav-list a[aria-current="page"]:hover { color: #fff; }

/* A circle, not a pill with horizontal padding — equal width and height. */
.lang-switch {
  width: 40px;
  height: 40px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  flex: none;
}

/* .site-header is fixed and out of normal flow, so without this the first
   thing inside <main> would render underneath the floating pill. header and
   main are siblings in views/layouts/base.php, so a general sibling selector
   reaches this with no PHP branching needed in the layout. The value is the
   header's own padding-block-start plus .header-inner's min-height plus a
   little clearance before content starts. */
.site-header ~ #main { padding-top: 112px; }
@media (max-width: 767px) {
  .site-header { padding-block-start: var(--jf-space-3); }
  .header-inner { min-height: 60px; padding-inline: var(--jf-space-4); }
  .logo-img { height: 36px; }
  .site-header ~ #main { padding-top: 88px; }
}

/* --- shared button-row utility ---------------------------------------------
   Used under a headline wherever a row of CTA buttons sits: the service page
   hero, the 404 page, and the final CTA panel. Not hero-exclusive despite the
   name history — kept rather than renamed, since renaming it would mean
   touching every page that uses it for no functional reason. */

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--jf-space-3);
  margin-top: var(--jf-space-8);
}
.hero-actions--center { justify-content: center; }

/* A colour accent on one word of a heading, set from the lang file as
   pre/accent/post parts rather than baking a <span> into translated copy.
   Used on the sr-only page <h1> composition and on each entry-card heading. */
.accent { color: var(--jf-teal-light); }

/* --- entry hero -------------------------------------------------------------
   The hero IS these two panels — indoor and outdoor — full-bleed and edge to
   edge, matching jamfix.ae's own layout exactly: no container, no gap, no
   rounded corners, no card shadow, no hover animation. The whole panel is one
   click target: a single link gets an absolutely-positioned ::after covering
   the panel, so there is one focusable element and one accessible name per
   panel, the same trick .svc-card-title a::after uses elsewhere. */

.entry-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0;
  width: 100%;
}

.entry-card {
  position: relative;
  isolation: isolate;
  height: 100vh;
  min-height: 600px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding-inline: 5%;
  padding-block-start: var(--jf-space-8);
  padding-block-end: 12vh;
  border-radius: 0;
  color: #fff;
  background-size: cover;
  background-position: center;
  box-shadow: none;
}
/* Real photography (Pexels, free commercial-use license, no attribution
   required — see docs/status.md), not AI-generated or stock imagery
   presented as JamFix's own completed work: purely atmospheric background,
   never captioned or claimed as a photo of this company's staff or projects.
   Swap for real company photography when it exists; only these two url()
   paths need to change. The gradient is a brand-colour duotone over the
   photo, not a plain black scrim, and does the job a text-legibility overlay
   needs to: enough contrast at the bottom for the white text sitting there,
   fading out toward the top so the photo still reads. */
/* ?v= on the photo itself, bumped by hand: .htaccess caches .jpg for a full
   year with "immutable", which only ever reads this URL once per visitor —
   swapping the file in place (same filename) leaves everyone who already
   has it cached looking at the old photo indefinitely. Bump the number any
   time this file's actual image content changes. */
.entry-card--indoor {
  background-image: linear-gradient(180deg, rgba(10, 22, 40, 0.20) 0%, rgba(10, 22, 40, 0.55) 55%, rgba(10, 22, 40, 0.92) 100%),
                     url('../img/hero/indoor.jpg?v=2');
}
.entry-card--outdoor {
  background-image: linear-gradient(180deg, rgba(12, 52, 44, 0.20) 0%, rgba(12, 52, 44, 0.55) 55%, rgba(12, 52, 44, 0.92) 100%),
                     url('../img/hero/outdoor.jpg');
}

.entry-badge {
  position: absolute;
  inset-inline-start: var(--jf-space-6);
  top: var(--jf-space-6);
  padding: var(--jf-space-1) var(--jf-space-4);
  border-radius: var(--jf-radius-pill);
  /* Teal-dark, not the brand's base teal: white text on #1a9e8c measures
     3.33:1 and fails AA (see docs/design-decisions.md); teal-dark is 6.23:1,
     the same substitution the primary button uses everywhere on the site. */
  background: var(--jf-teal-dark);
  color: #fff;
  font-size: var(--jf-text-xs);
  font-weight: var(--jf-weight-bold);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
/* Arabic never takes letter-spacing or an uppercase transform — a cursive
   script has no case, and tracking pulls the joins apart. Same rule the rest
   of the site's eyebrows and labels already follow (see tokens.css). */
html[lang^="ar"] .entry-badge { text-transform: none; letter-spacing: normal; }

.entry-card-copy { max-width: 30rem; }
.entry-card-title {
  font-size: clamp(3rem, 7vw, 6rem);
  font-weight: var(--jf-weight-bold);
  line-height: 1;
  color: #fff;
  letter-spacing: -0.02em;
}
/* Cairo's Arabic glyphs read visually larger than its Latin ones at an
   identical size (documented in tokens.css), so the Arabic headline gets its
   own, slightly smaller clamp to keep the two languages visually matched. */
html[lang^="ar"] .entry-card-title { font-size: clamp(2.6rem, 6vw, 5rem); }

.entry-card-title .accent { color: var(--jf-teal-light); font-style: italic; }
/* Arabic is a connected script — italics have no real convention in it and
   just look like a rendering error, the same reasoning tokens.css already
   applies to Arabic letter-spacing. */
html[lang^="ar"] .entry-card-title .accent { font-style: normal; }

.entry-card-desc {
  margin-top: var(--jf-space-4);
  color: rgba(255, 255, 255, 0.8);
  font-size: clamp(1rem, 1.4vw, 1.25rem);
  max-width: 30rem;
}
.entry-card-cta {
  /* No position set here on purpose: that is what lets its own ::after
     escape to the nearest positioned ancestor — .entry-card — and cover the
     whole panel. */
  margin-top: var(--jf-space-6);
  display: inline-flex;
  align-items: center;
  gap: var(--jf-space-2);
  padding: var(--jf-space-3) var(--jf-space-6);
  border-radius: var(--jf-radius-pill);
  background: var(--jf-teal-dark);
  color: #fff;
  font-size: var(--jf-text-base);
  font-weight: var(--jf-weight-bold);
  text-decoration: none;
}
.entry-card-cta:hover { background: #0a584c; color: #fff; }
/* The whole panel is clickable through this one link; everything else in the
   panel (badge, heading, description) is decorative and not separately
   linked. */
.entry-card-cta::after { content: ""; position: absolute; inset: 0; }

/* --- trust bar ------------------------------------------------------------ */

.trust { background: var(--jf-surface-tint); border-bottom: 1px solid var(--jf-line); }
.trust-inner {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-around;
  gap: var(--jf-space-6);
  padding-block: var(--jf-space-8);
}
.trust-item { text-align: center; min-width: 7rem; }
.trust-value {
  display: block;
  font-size: var(--jf-text-2xl);
  font-weight: var(--jf-weight-bold);
  color: var(--jf-ink-accent);
  font-variant-numeric: tabular-nums;
}
.trust-label {
  font-size: var(--jf-text-sm);
  /* On the tint, #6b7280 is 4.39:1 and fails; this token is 5.00:1. */
  color: var(--jf-ink-muted-tint);
}

/* --- service cards -------------------------------------------------------- */

/* A fixed column count, not auto-fit. auto-fit packs as many columns as the
   container width allows, which for 6 cards at desktop width produced an
   uneven 4-then-2 split instead of two even rows. A fixed count degrades
   predictably instead.

   The default is 3 columns, for the 6-item indoor grid (two even rows) and
   the 3-item "related services" grid on a service page (one full row). The
   4-item outdoor grid gets its own modifier, .svc-grid--outdoor, set to 2
   columns so it reads as two even rows of 2 rather than 3-then-1. */
.svc-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--jf-space-6);
}
.svc-grid.svc-grid--outdoor {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (max-width: 860px) {
  .svc-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .svc-grid { grid-template-columns: 1fr; }
  /* .svc-grid--outdoor's own rule above is two classes, more specific than
     .svc-grid alone, so without this it keeps 2 columns at phone width
     instead of collapsing like every other grid here. */
  .svc-grid.svc-grid--outdoor { grid-template-columns: 1fr; }
}

.svc-card {
  position: relative;
  padding: var(--jf-space-6);
  background: var(--jf-surface);
  border: 1px solid var(--jf-line);
  border-radius: var(--jf-radius-lg);
  box-shadow: var(--jf-shadow-sm);
  min-width: 0;
  /* The tilt is driven by --tilt-x/--tilt-y set by site.js, bounded by
     --jf-tilt-max, which reduced motion sets to 0deg. */
  transform: perspective(var(--jf-perspective))
             rotateX(var(--tilt-x, 0deg))
             rotateY(var(--tilt-y, 0deg));
  transition: transform var(--jf-duration) var(--jf-ease),
              box-shadow var(--jf-duration) var(--jf-ease),
              border-color var(--jf-duration) var(--jf-ease);
}
.svc-card:hover, .svc-card:focus-within {
  box-shadow: var(--jf-shadow-lg);
  border-color: var(--jf-teal);
}

.svc-card-icon {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--jf-radius-pill);
  background: var(--jf-teal);
  color: #fff;
}
.svc-card-title { margin-top: var(--jf-space-4); font-size: var(--jf-text-xl); }
.svc-card-title a { color: var(--jf-navy); text-decoration: none; }
/* The whole card is the hit area, built from the title link so there is still
   exactly one focusable element and one accessible name. */
.svc-card-title a::after { content: ""; position: absolute; inset: 0; }
.svc-card-title a:hover { color: var(--jf-ink-accent); }

.svc-card-desc { margin-top: var(--jf-space-2); color: var(--jf-ink-soft); font-size: var(--jf-text-sm); }
.svc-card-price { margin-top: var(--jf-space-4); font-size: var(--jf-text-sm); color: var(--jf-ink-soft); }
.svc-card-price strong { color: var(--jf-ink); }
.svc-card-link {
  display: inline-block;
  margin-top: var(--jf-space-4);
  font-size: var(--jf-text-sm);
  font-weight: var(--jf-weight-semibold);
  color: var(--jf-ink-accent);
}

/* --- service page --------------------------------------------------------- */

.svc-hero { padding-block: var(--jf-space-16) var(--jf-space-12); border-bottom: 1px solid var(--jf-line); }
.svc-hero-inner {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: var(--jf-space-12);
  align-items: start;
}

/* --- category hero: cover photo variant -------------------------------------
   The indoor/outdoor landing pages (services-category.php) get the same
   full-bleed cover-photo treatment as a blog article (.post-hero) and the
   home page's entry cards, instead of the plain white .svc-hero a single
   service page uses — this page's whole job is "show what this category
   looks like", so the photo belongs in the hero here. A single service page
   keeps the plain two-column price/SLA layout, untouched by this variant. */
.svc-hero--cover {
  min-height: 20rem;
  display: flex;
  align-items: flex-end;
  background-size: cover;
  background-position: center;
  border-bottom: none;
  color: #fff;
}
.svc-hero--cover .svc-hero-inner { display: block; width: 100%; }
.svc-hero--cover .svc-hero-inner > div { max-width: 42rem; }
.svc-hero--cover .eyebrow { color: var(--jf-teal-light); }
.svc-hero--cover .page-title { color: #fff; }
.svc-hero--cover .page-lede { color: rgba(255, 255, 255, 0.85); }
.svc-aside {
  padding: var(--jf-space-6);
  background: var(--jf-surface-tint);
  border-radius: var(--jf-radius-lg);
}
.svc-price { display: flex; flex-direction: column; gap: var(--jf-space-1); }
.svc-price span { font-size: var(--jf-text-sm); color: var(--jf-ink-muted-tint); }
.svc-price strong { font-size: var(--jf-text-2xl); color: var(--jf-navy); font-weight: var(--jf-weight-bold); }
.svc-price--ask strong { font-size: var(--jf-text-xl); }
.svc-price-note { margin-top: var(--jf-space-3); font-size: var(--jf-text-sm); color: var(--jf-ink-muted-tint); }
.svc-sla {
  margin-top: var(--jf-space-4);
  padding-top: var(--jf-space-4);
  border-top: 1px solid var(--jf-line-strong);
  font-size: var(--jf-text-sm);
}

.check-list { display: grid; gap: var(--jf-space-3); }
.check-list li {
  position: relative;
  padding-inline-start: var(--jf-space-8);
  color: var(--jf-ink-soft);
}
.check-list li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  top: 0.45em;
  width: 0.85rem;
  height: 0.5rem;
  border-inline-start: 2px solid var(--jf-state-success);
  border-block-end: 2px solid var(--jf-state-success);
  transform: rotate(-45deg);
}

/* --- "what the job covers" with a photo ------------------------------------
   The image sits first in markup, which CSS Grid places at the inline-start
   edge — physically left in English, right in Arabic — the same
   direction-aware ordering pattern used throughout the site, so this needs
   no [dir="rtl"] override. A service with no cover_image (not expected once
   every real service has one, but still handled) falls back to a single
   column via .svc-includes--no-media, so the checklist is never left
   stranded in a half-built two-column layout. */
.svc-includes {
  display: grid;
  grid-template-columns: minmax(260px, 360px) minmax(0, 1fr);
  gap: var(--jf-space-12);
  align-items: center;
}
.svc-includes--no-media { grid-template-columns: 1fr; max-width: 760px; margin-inline: auto; }

.svc-includes-media {
  border-radius: var(--jf-radius-xl);
  overflow: hidden;
  box-shadow: var(--jf-shadow-lg);
  aspect-ratio: 4 / 5;
}
.svc-includes-img { display: block; width: 100%; height: 100%; object-fit: cover; }

.svc-includes-body .section-title { margin-bottom: var(--jf-space-2); }

@media (max-width: 860px) {
  .svc-includes { grid-template-columns: 1fr; }
  .svc-includes-media { aspect-ratio: 16 / 9; max-width: 480px; margin-inline: auto; }
}

.note-panel {
  padding: var(--jf-space-6);
  border-inline-start: 3px solid var(--jf-teal);
  background: var(--jf-surface-tint);
  border-radius: var(--jf-radius-md);
}

/* --- why grid ------------------------------------------------------------- */

/* Same fixed-3-column reasoning as .svc-grid above: 6 commitments read as two
   even rows of 3, not an uneven 4-then-2. */
.why-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--jf-space-6);
}
@media (max-width: 860px) {
  .why-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .why-grid { grid-template-columns: 1fr; }
}
.why-item {
  padding: var(--jf-space-6);
  background: var(--jf-surface);
  border: 1px solid var(--jf-line);
  border-radius: var(--jf-radius-lg);
  box-shadow: var(--jf-shadow-sm);
  transition: box-shadow var(--jf-duration) var(--jf-ease), transform var(--jf-duration) var(--jf-ease);
}
.why-item:hover { box-shadow: var(--jf-shadow-md); transform: translateY(-2px); }
.why-icon {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  margin-bottom: var(--jf-space-4);
  border-radius: var(--jf-radius-pill);
  background: var(--jf-surface-tint);
  color: var(--jf-teal-dark);
}
.why-title { font-size: var(--jf-text-lg); }
.why-text { margin-top: var(--jf-space-2); color: var(--jf-ink-soft); font-size: var(--jf-text-sm); }

/* --- partner-with-us benefit cards ------------------------------------------
   The same "quiet depth" device as .mv-panel and .post-cta-card: a gradient
   accent bar, an icon medallion, and a soft corner blob, rather than the
   plainer .why-item treatment — this page is a cold pitch to a business
   reader, so it gets the more deliberate version of the card. */
.partner-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--jf-space-6);
  margin-top: var(--jf-space-8);
}
@media (max-width: 1023px) {
  .partner-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .partner-grid { grid-template-columns: 1fr; }
}
.partner-card {
  position: relative;
  overflow: hidden;
  padding: var(--jf-space-6);
  background: var(--jf-surface);
  border-radius: var(--jf-radius-lg);
  box-shadow: var(--jf-shadow-sm);
  transition: transform var(--jf-duration) var(--jf-ease), box-shadow var(--jf-duration) var(--jf-ease);
}
.partner-card::before {
  content: '';
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 4px;
  background: linear-gradient(90deg, var(--jf-teal), var(--jf-teal-dark));
  opacity: .7;
  transition: opacity var(--jf-duration) var(--jf-ease);
}
.partner-card::after {
  content: '';
  position: absolute;
  z-index: 0;
  inset-inline-end: -1.5rem;
  inset-block-end: -1.5rem;
  width: 7rem;
  height: 7rem;
  border-radius: 50%;
  background: var(--jf-teal);
  opacity: .07;
  pointer-events: none;
}
.partner-card:hover { transform: translateY(-4px); box-shadow: var(--jf-shadow-lg); }
.partner-card:hover::before { opacity: 1; }
.partner-card-icon {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  margin-bottom: var(--jf-space-4);
  border-radius: var(--jf-radius-pill);
  background: linear-gradient(135deg, var(--jf-teal), var(--jf-teal-dark));
  color: #fff;
}
.partner-card h3 { position: relative; z-index: 1; font-size: var(--jf-text-lg); font-weight: var(--jf-weight-bold); }
.partner-card p { position: relative; z-index: 1; margin-top: var(--jf-space-2); color: var(--jf-ink-soft); font-size: var(--jf-text-sm); }

/* --- steps ---------------------------------------------------------------- */

.steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--jf-space-6);
  margin-top: var(--jf-space-8);
}
.step { padding-top: var(--jf-space-4); border-top: 2px solid var(--jf-teal); }
.step-num {
  display: block;
  font-family: var(--jf-font-mono);
  font-size: var(--jf-text-sm);
  font-weight: var(--jf-weight-bold);
  color: var(--jf-teal-light);
  font-variant-numeric: tabular-nums;
}
/* Arabic-indic digits (٠١٢) are passed as plain text, not numerals the
   monospace stack has glyphs for, so they fall back to the Arabic face. */
html[lang^="ar"] .step-num { font-family: var(--jf-font-ar); }
.step-label { display: block; margin-top: var(--jf-space-2); color: #fff; font-weight: var(--jf-weight-semibold); }

/* --- mission & vision ------------------------------------------------------
   Two contrasting panels — now a light/dark contrast rather than a
   tint/outline one, which read as two shades of the same thing. The copy
   inside is the company's stated way of working restated as intent, not a
   new factual claim, so it needs no evidence gate the way a licence number
   would. */

.mv-eyebrow { text-align: center; margin-bottom: var(--jf-space-2); }

.mv-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--jf-space-6);
  margin-top: var(--jf-space-8);
}
.mv-panel {
  position: relative;
  overflow: hidden;
  padding: var(--jf-space-8);
  border-radius: var(--jf-radius-xl);
  transition: transform var(--jf-duration) var(--jf-ease), box-shadow var(--jf-duration) var(--jf-ease);
}
.mv-panel:hover { transform: translateY(-4px); }

/* A soft, oversized circle bleeding off the corner — the same "quiet depth"
   device as the testimonial cards' watermark quote mark, here giving each
   panel a focal point without adding another line of text. */
.mv-panel::after {
  content: '';
  position: absolute;
  z-index: 0;
  inset-inline-end: -2rem;
  inset-block-end: -2rem;
  width: 9rem;
  height: 9rem;
  border-radius: 50%;
  pointer-events: none;
}

.mv-panel--light { background: var(--jf-surface-tint); box-shadow: var(--jf-shadow-sm); }
.mv-panel--light:hover { box-shadow: var(--jf-shadow-lg); }
.mv-panel--light::after { background: var(--jf-teal); opacity: .08; }
.mv-panel--light .mv-icon { background: var(--jf-surface); color: var(--jf-teal-dark); box-shadow: var(--jf-shadow-sm); }
.mv-panel--light p { color: var(--jf-ink-soft); }

.mv-panel--dark { background: var(--jf-navy); color: #fff; box-shadow: var(--jf-shadow-md); }
.mv-panel--dark:hover { box-shadow: var(--jf-shadow-xl); }
.mv-panel--dark::after { background: var(--jf-teal-light); opacity: .12; }
.mv-panel--dark .mv-icon { background: rgba(255, 255, 255, 0.12); color: var(--jf-teal-light); }
.mv-panel--dark h3 { color: #fff; }
.mv-panel--dark p { color: rgba(255, 255, 255, 0.78); }

.mv-icon {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3.25rem;
  height: 3.25rem;
  margin-bottom: var(--jf-space-4);
  border-radius: var(--jf-radius-pill);
}
.mv-panel h3 { position: relative; z-index: 1; font-size: var(--jf-text-xl); font-weight: var(--jf-weight-bold); }
.mv-panel p { position: relative; z-index: 1; margin-top: var(--jf-space-3); line-height: 1.7; }

/* --- before / after ---------------------------------------------------------
   A portfolio-grade treatment: a larger drag-to-compare frame with real
   elevation, an optional "during" photo pinned to the corner like a second
   print rather than squeezed into a side column, a hover lift, and a brief
   one-time auto-demo sweep (site.js) that shows a first-time visitor the
   slider is draggable instead of relying on them to guess. */

.ba-stat {
  margin-top: var(--jf-space-3);
  font-size: var(--jf-text-sm);
  font-weight: var(--jf-weight-semibold);
  color: var(--jf-ink-accent);
}

/* The column max is a fixed size, not 1fr: with 1fr, a single card (e.g. a
   filtered view with only one matching job) stretches to fill the entire
   row on its own and ends up oversized. A fixed max caps every card at a
   sensible width regardless of how many there are, and justify-content
   centers the row when it has fewer cards than would fill it. */
.ba-grid {
  display: grid;
  /* min(300px, 100%): the track's floor shrinks to the container's own
     width on a narrow phone instead of forcing it, so this never needs a
     matching @media rule — the same reasoning as .ba-grid--full below,
     where a flat 420px floor was wider than most phones and forced the
     whole page into horizontal scroll. */
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 420px));
  justify-content: center;
  gap: var(--jf-space-12) var(--jf-space-8);
}
/* The dedicated gallery page gets fewer, larger cards than the home-page
   preview strip — this is the page a visitor lands on specifically to look
   closely at the work, so the cards are sized to reward that. */
.ba-grid--full {
  grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 480px));
  justify-content: center;
  gap: var(--jf-space-12) var(--jf-space-8);
  max-width: 1040px;
  margin-inline: auto;
}

.ba-case {
  margin: 0;
  padding-bottom: var(--jf-space-2);
}
.ba-media { position: relative; }

.ba {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: var(--jf-radius-xl);
  overflow: hidden;
  border: 1px solid var(--jf-line);
  box-shadow: var(--jf-shadow-md);
  cursor: ew-resize;
  /* Stops the browser scrolling the page while dragging on a touch screen. */
  touch-action: none;
  user-select: none;
  background: var(--jf-surface-tint);
  transition: box-shadow var(--jf-duration) var(--jf-ease),
              transform var(--jf-duration) var(--jf-ease);
}
.ba-case:hover .ba { box-shadow: var(--jf-shadow-xl); transform: translateY(-3px); }
@media (prefers-reduced-motion: reduce) {
  .ba-case:hover .ba { transform: none; }
}

.ba-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* The after image is clipped rather than resized, so dragging never reflows
   or rescales either picture. The auto-demo sweep (site.js adds .is-demo for
   its duration only) is the one time this clip is allowed to transition —
   while a visitor is actually dragging it must track the pointer 1:1 with
   no lag, so the transition is absent by default. */
.ba-after-wrap {
  position: absolute;
  inset: 0;
  clip-path: inset(0 0 0 var(--reveal, 50%));
}
html[dir="rtl"] .ba-after-wrap { clip-path: inset(0 var(--reveal, 50%) 0 0); }
.ba.is-demo .ba-after-wrap { transition: clip-path 900ms var(--jf-ease); }
.ba.is-demo .ba-handle { transition: inset-inline-start 900ms var(--jf-ease); }

.ba-handle {
  position: absolute;
  top: 0;
  bottom: 0;
  inset-inline-start: var(--reveal, 50%);
  width: 3px;
  background: #fff;
  box-shadow: 0 0 0 1px rgba(10, 22, 40, 0.3);
}
.ba-knob {
  position: absolute;
  top: 50%;
  inset-inline-start: 50%;
  transform: translate(-50%, -50%);
  width: 2.75rem;
  height: 2.75rem;
  display: grid;
  place-items: center;
  border-radius: var(--jf-radius-pill);
  background: #fff;
  color: var(--jf-navy);
  box-shadow: var(--jf-shadow-lg);
  transition: transform var(--jf-duration-fast) var(--jf-ease);
}
html[dir="rtl"] .ba-knob { transform: translate(50%, -50%); }
.ba-case:hover .ba-knob { transform: translate(-50%, -50%) scale(1.08); }
html[dir="rtl"] .ba-case:hover .ba-knob { transform: translate(50%, -50%) scale(1.08); }
@media (prefers-reduced-motion: reduce) {
  .ba-case:hover .ba-knob, html[dir="rtl"] .ba-case:hover .ba-knob { transform: translate(-50%, -50%); }
}

/* A brief glow once, the moment the auto-demo sweep finishes, to leave the
   handle looking like the obvious thing to grab next — not a looping
   animation, so it never becomes background noise on a page with several
   cases. */
@keyframes ba-knob-invite {
  0%, 100% { box-shadow: var(--jf-shadow-lg); }
  50% { box-shadow: var(--jf-shadow-lg), 0 0 0 8px rgba(13, 109, 95, 0.18); }
}
.ba-knob.is-inviting { animation: ba-knob-invite 1100ms var(--jf-ease) 1; }

.ba-tag {
  position: absolute;
  top: var(--jf-space-3);
  padding: var(--jf-space-1) var(--jf-space-3);
  border-radius: var(--jf-radius-pill);
  background: rgba(10, 22, 40, 0.78);
  color: #fff;
  font-size: var(--jf-text-xs);
  font-weight: var(--jf-weight-semibold);
}
.ba-tag--before { inset-inline-start: var(--jf-space-3); }
.ba-tag--after { inset-inline-end: var(--jf-space-3); }

/* An optional third "during" photo, pinned over the slider's corner like a
   second print rather than squeezed into a side column — a fixed size keeps
   it reading as a deliberate detail at any card width, instead of a
   narrow, stretched sliver. A case with no during_image adds nothing here. */
.ba-during {
  position: absolute;
  inset-block-end: calc(-1 * var(--jf-space-4));
  inset-inline-end: calc(-1 * var(--jf-space-4));
  width: clamp(84px, 26%, 136px);
  aspect-ratio: 1 / 1;
  border-radius: var(--jf-radius-md);
  overflow: hidden;
  border: 3px solid var(--jf-surface);
  box-shadow: var(--jf-shadow-lg);
  transition: transform var(--jf-duration) var(--jf-ease), box-shadow var(--jf-duration) var(--jf-ease);
}
.ba-case:hover .ba-during { transform: scale(1.06); box-shadow: var(--jf-shadow-xl); }
@media (prefers-reduced-motion: reduce) {
  .ba-case:hover .ba-during { transform: none; }
}
.ba-during-img { display: block; width: 100%; height: 100%; object-fit: cover; }
.ba-during .ba-tag--during {
  top: auto;
  inset-inline: var(--jf-space-1);
  inset-block-end: var(--jf-space-1);
  padding: 2px var(--jf-space-2);
  font-size: 0.6875rem;
  text-align: center;
}

.ba-caption { margin-top: var(--jf-space-5); font-size: var(--jf-text-sm); color: var(--jf-ink-soft); }
.ba-caption strong { display: block; margin-bottom: var(--jf-space-1); font-size: var(--jf-text-base); color: var(--jf-ink); }
.ba-area { color: var(--jf-ink-muted); }

@media (max-width: 640px) {
  .ba-during { width: clamp(72px, 30%, 104px); inset-block-end: calc(-1 * var(--jf-space-3)); inset-inline-end: calc(-1 * var(--jf-space-3)); }
}

/* --- zoom-to-lightbox ------------------------------------------------------
   A sibling of .ba, not a child of it — a click on this button must never
   reach .ba's own pointerdown handler (which would otherwise jump the
   slider to wherever the button happens to sit). */
.ba-zoom {
  position: absolute;
  inset-block-end: var(--jf-space-3);
  inset-inline-start: var(--jf-space-3);
  width: 2.25rem;
  height: 2.25rem;
  display: grid;
  place-items: center;
  border-radius: var(--jf-radius-pill);
  background: rgba(10, 22, 40, 0.78);
  color: #fff;
  border: none;
  cursor: pointer;
  transition: transform var(--jf-duration-fast) var(--jf-ease), background var(--jf-duration-fast) var(--jf-ease);
}
.ba-zoom:hover, .ba-zoom:focus-visible { background: var(--jf-teal-dark); transform: scale(1.08); }

.ba-lightbox {
  position: relative;
  border: none;
  border-radius: var(--jf-radius-xl);
  padding: var(--jf-space-6);
  max-width: 58rem;
  width: calc(100% - 2rem);
  max-height: calc(100% - 3rem);
  overflow: auto;
  box-shadow: var(--jf-shadow-xl);
  background: var(--jf-surface);
  color: var(--jf-ink);
}
.ba-lightbox::backdrop { background: rgba(10, 22, 40, 0.72); }
.ba-lightbox-close {
  position: absolute;
  top: var(--jf-space-4);
  inset-inline-end: var(--jf-space-4);
  width: 2.5rem;
  height: 2.5rem;
  display: grid;
  place-items: center;
  border-radius: var(--jf-radius-pill);
  background: var(--jf-surface-tint);
  border: none;
  cursor: pointer;
  color: var(--jf-ink);
  z-index: 1;
}
.ba-lightbox-close:hover { background: var(--jf-line); }

.ba-lightbox-body { display: grid; gap: var(--jf-space-6); margin-top: var(--jf-space-2); }
.ba-lightbox .ba--lg { aspect-ratio: 16 / 10; border-radius: var(--jf-radius-lg); box-shadow: none; }

.ba-lightbox-during { margin: 0; }
.ba-lightbox-during img {
  display: block;
  width: 100%;
  max-height: 16rem;
  object-fit: cover;
  border-radius: var(--jf-radius-lg);
}
.ba-lightbox-during figcaption {
  margin-top: var(--jf-space-2);
  font-size: var(--jf-text-sm);
  font-weight: var(--jf-weight-semibold);
  color: var(--jf-ink-soft);
}

.ba-lightbox-caption { margin-top: var(--jf-space-2); padding-top: var(--jf-space-4); border-top: 1px solid var(--jf-line); }
.ba-lightbox-caption strong { display: block; font-size: var(--jf-text-lg); color: var(--jf-ink); }
.ba-lightbox-caption .ba-area { display: block; margin-top: var(--jf-space-1); }
.ba-lightbox-caption p { margin-top: var(--jf-space-2); color: var(--jf-ink-soft); }

@media (min-width: 640px) {
  .ba-lightbox-body { grid-template-columns: minmax(0, 1fr) 14rem; align-items: start; }
}

/* --- testimonials -----------------------------------------------------------
   A horizontally scrolling row with scroll-snap, operable by dragging,
   trackpad, the two nav buttons, or the keyboard (the track is a focusable,
   native-scrolling region, so arrow keys and Tab both work with no JS). The
   buttons are a progressive enhancement on top of that, not the only way
   through — see the matching JS in assets/js/site.js. */

.tst-carousel {
  position: relative;
  margin-top: var(--jf-space-8);
  padding-block: var(--jf-space-4);
}
/* Two soft brand-colour blobs behind the cards — the only thing that makes
   a plain card row read as "designed" instead of a default carousel. They
   sit behind .tst-track (z-index 0 vs auto) and never intercept clicks. */
.tst-carousel::before,
.tst-carousel::after {
  content: '';
  position: absolute;
  z-index: 0;
  width: 18rem;
  height: 18rem;
  border-radius: 50%;
  filter: blur(60px);
  opacity: .16;
  pointer-events: none;
}
.tst-carousel::before { top: -4rem; inset-inline-start: -4rem; background: var(--jf-teal); }
.tst-carousel::after { bottom: -6rem; inset-inline-end: -2rem; background: var(--jf-navy); }

.tst-track {
  position: relative;
  z-index: 1;
  display: flex;
  gap: var(--jf-space-6);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding: var(--jf-space-2);
  scrollbar-width: none;
}
.tst-track::-webkit-scrollbar { display: none; }

.tst-card {
  position: relative;
  display: flex;
  flex-direction: column;
  flex: 0 0 min(360px, 85vw);
  scroll-snap-align: start;
  margin: 0;
  padding: var(--jf-space-8) var(--jf-space-6) var(--jf-space-6);
  background: var(--jf-surface);
  border-radius: var(--jf-radius-xl);
  box-shadow: var(--jf-shadow-sm);
  overflow: hidden;
  transition: transform var(--jf-duration) var(--jf-ease), box-shadow var(--jf-duration) var(--jf-ease);
}
.tst-card::before {
  content: '';
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 4px;
  background: linear-gradient(90deg, var(--jf-teal), var(--jf-teal-dark));
  opacity: .7;
  transition: opacity var(--jf-duration) var(--jf-ease);
}
.tst-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--jf-shadow-lg);
}
.tst-card:hover::before { opacity: 1; }

.tst-quote-mark {
  position: absolute;
  top: -.5rem;
  inset-inline-end: var(--jf-space-5);
  font-family: Georgia, 'Times New Roman', serif;
  font-size: 5rem;
  line-height: 1;
  color: var(--jf-teal);
  opacity: .14;
  pointer-events: none;
}

.tst-stars { position: relative; z-index: 1; display: inline-flex; gap: 2px; margin-bottom: var(--jf-space-3); color: var(--jf-line-strong); }
.tst-stars svg.is-filled { color: #F5A623; }

.tst-text {
  position: relative;
  z-index: 1;
  color: var(--jf-ink-soft);
  font-size: var(--jf-text-base);
  line-height: 1.6;
}

/* margin-top: auto, not a fixed gap — pushes the name/avatar to the bottom
   of the card regardless of how long the quote above it runs, so every
   card's footer lines up with its neighbours' instead of trailing the
   text at whatever height it happens to end. */
.tst-card footer { position: relative; z-index: 1; margin-top: auto; padding-top: var(--jf-space-5); display: flex; align-items: center; gap: var(--jf-space-3); }
.tst-avatar {
  position: relative;
  flex: none;
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--jf-radius-pill);
  background: linear-gradient(135deg, var(--jf-teal), var(--jf-teal-dark));
  color: #fff;
  font-size: var(--jf-text-sm);
  font-weight: var(--jf-weight-bold);
}
.tst-src-badge {
  position: absolute;
  inset-block-end: -3px;
  inset-inline-end: -3px;
  display: grid;
  place-items: center;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  background: var(--jf-surface);
  box-shadow: 0 0 0 2px var(--jf-surface);
}
.tst-foot-text { display: flex; flex-direction: column; min-width: 0; }
.tst-card cite { font-style: normal; font-weight: var(--jf-weight-semibold); color: var(--jf-ink); }
.tst-area { font-size: var(--jf-text-sm); color: var(--jf-ink-muted); }
.tst-date { font-size: var(--jf-text-xs); color: var(--jf-ink-muted); }
.tst-service-pill {
  position: relative;
  z-index: 1;
  display: inline-block;
  margin-top: var(--jf-space-4);
  padding: var(--jf-space-1) var(--jf-space-3);
  border-radius: var(--jf-radius-pill);
  background: var(--jf-surface-tint);
  color: var(--jf-ink-muted-tint);
  font-size: var(--jf-text-xs);
  font-weight: var(--jf-weight-semibold);
}

.tst-controls { position: relative; z-index: 1; display: flex; justify-content: flex-end; gap: var(--jf-space-2); margin-top: var(--jf-space-4); }
.tst-nav-btn {
  width: 2.5rem;
  height: 2.5rem;
  display: grid;
  place-items: center;
  border-radius: var(--jf-radius-pill);
  border: 1px solid var(--jf-line-strong);
  background: var(--jf-surface);
  color: var(--jf-ink-soft);
  cursor: pointer;
  transition: border-color var(--jf-duration) var(--jf-ease), color var(--jf-duration) var(--jf-ease), background var(--jf-duration) var(--jf-ease);
}
.tst-nav-btn:hover { border-color: var(--jf-teal-dark); color: #fff; background: var(--jf-teal-dark); }

/* A plain grid, for the service and category pages, which show a handful of
   relevant reviews rather than the home page's full carousel — same .tst-
   card as the carousel, just laid out without a track or nav buttons. */
.tst-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--jf-space-6); }

/* A livelier entrance than the generic [data-reveal] fade-up (site.css's
   "reveal on scroll" block, further down): cards pop in with a slight scale
   as well as a rise, scoped above the generic rule by selector specificity
   so the shared stagger timing and reduced-motion guard still apply. */
@media (prefers-reduced-motion: no-preference) {
  .tst-track[data-reveal].is-armed > .tst-card { opacity: 0; transform: translateY(20px) scale(0.94); }
  .tst-track[data-reveal].is-revealed > .tst-card { opacity: 1; transform: none; }
}

/* --- clients who trust us ---------------------------------------------------
   Same duplicated-track CSS loop as the services ticker at the top of the
   page (jf-marquee / jf-marquee-rtl, defined there) — reused rather than a
   second keyframe pair for the same motion. */

.partners-section .section-head { text-align: center; }
.partners-section .section-sub { margin-inline: auto; }

.partners-ticker { margin-top: var(--jf-space-8); overflow: hidden; }
.partners-track {
  display: flex;
  width: max-content;
  animation: jf-marquee 32s linear infinite;
}
html[dir="rtl"] .partners-track { animation-name: jf-marquee-rtl; }
.partners-ticker:hover .partners-track,
.partners-ticker:focus-within .partners-track { animation-play-state: paused; }

.partners-group { display: flex; flex: none; gap: var(--jf-space-8); padding-inline: var(--jf-space-5); }

.partner-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--jf-space-2);
  width: 11rem;
  height: 6rem;
  flex: none;
  border-radius: var(--jf-radius-lg);
  background: var(--jf-surface);
  border: 1px solid var(--jf-line);
  color: var(--jf-ink-muted);
  text-decoration: none;
  filter: grayscale(1);
  opacity: .7;
  transition: opacity var(--jf-duration) var(--jf-ease), filter var(--jf-duration) var(--jf-ease), box-shadow var(--jf-duration) var(--jf-ease);
}
.partner-tile:hover { opacity: 1; filter: grayscale(0); box-shadow: var(--jf-shadow-md); }

.partner-logo-img { max-width: 75%; max-height: 2.5rem; object-fit: contain; }
.partner-placeholder-icon { color: var(--jf-line-strong); }
.partner-placeholder-name { font-size: var(--jf-text-xs); font-weight: var(--jf-weight-semibold); letter-spacing: .02em; }

@media (prefers-reduced-motion: reduce) {
  .partners-track { animation: none; flex-wrap: wrap; width: 100%; }
  .partners-group:last-child { display: none; }
}

/* --- blog listing head ------------------------------------------------------ */

.blog-head {
  padding-block: clamp(3rem, 2rem + 5vw, 6rem) clamp(2rem, 1.5rem + 2vw, 4rem);
  background-size: cover;
  background-position: center;
  color: #fff;
}
.blog-head .eyebrow { color: var(--jf-teal-light); }
.blog-head-title {
  margin-top: var(--jf-space-2);
  font-size: clamp(2.25rem, 1.6rem + 3vw, 4.5rem);
  font-weight: var(--jf-weight-bold);
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: #fff;
}
.blog-head-sub { margin-top: var(--jf-space-4); font-size: var(--jf-text-lg); color: rgba(255, 255, 255, 0.85); }

/* --- category filter chips -------------------------------------------------- */

.blog-filters { border-bottom: 1px solid var(--jf-line); }
.blog-filters-track {
  display: flex;
  gap: var(--jf-space-3);
  padding-block: var(--jf-space-4);
  overflow-x: auto;
  scrollbar-width: none;
}
.blog-filters-track::-webkit-scrollbar { display: none; }
.chip {
  flex: none;
  padding: var(--jf-space-2) var(--jf-space-5, 1.25rem);
  border-radius: var(--jf-radius-pill);
  border: 1.5px solid var(--jf-line-strong);
  background: var(--jf-surface);
  color: var(--jf-ink-soft);
  font-size: var(--jf-text-sm);
  font-weight: var(--jf-weight-semibold);
  text-decoration: none;
  white-space: nowrap;
}
.chip:hover { border-color: var(--jf-teal-dark); color: var(--jf-ink-accent); }
.chip.is-active { background: var(--jf-teal-dark); border-color: var(--jf-teal-dark); color: #fff; }

/* --- featured article -------------------------------------------------------
   One wide two-column card ahead of the grid: image at roughly 60% of the
   width, the title and excerpt at 40%, so the newest guide reads as the
   obvious first thing to open rather than one tile among equals. */

.post-featured {
  display: grid;
  grid-template-columns: 1.5fr 1fr;
  gap: var(--jf-space-8);
  align-items: center;
  margin-bottom: var(--jf-space-16);
  padding-bottom: var(--jf-space-16);
  border-bottom: 1px solid var(--jf-line);
}
.post-featured-media {
  position: relative;
  display: block;
  border-radius: var(--jf-radius-xl);
  overflow: hidden;
  box-shadow: var(--jf-shadow-lg);
}
.post-featured-img {
  aspect-ratio: 16 / 10;
  object-fit: cover;
  width: 100%;
  display: block;
  transition: transform var(--jf-duration-slow) var(--jf-ease);
}
.post-featured-media:hover .post-featured-img { transform: scale(1.04); }
.post-featured-badge {
  position: absolute;
  top: var(--jf-space-4);
  inset-inline-start: var(--jf-space-4);
  padding: var(--jf-space-1) var(--jf-space-4);
  border-radius: var(--jf-radius-pill);
  background: var(--jf-teal-dark);
  color: #fff;
  font-size: var(--jf-text-xs);
  font-weight: var(--jf-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--jf-tracking-label);
}
html[lang^="ar"] .post-featured-badge { text-transform: none; }
.post-featured-title { margin-top: var(--jf-space-3); font-size: var(--jf-text-3xl); font-weight: var(--jf-weight-bold); }
.post-featured-title a { color: var(--jf-navy); text-decoration: none; }
.post-featured-title a:hover { color: var(--jf-ink-accent); }
.post-featured-excerpt { margin-top: var(--jf-space-3); color: var(--jf-ink-soft); font-size: var(--jf-text-base); }
.post-featured-link { margin-top: var(--jf-space-4); display: inline-block; color: var(--jf-ink-accent); }

/* --- post grid + card -------------------------------------------------------- */

.post-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--jf-space-8);
}
.post-grid--related { margin-top: var(--jf-space-8); }
@media (max-width: 1023px) {
  .post-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
  .post-grid { grid-template-columns: 1fr; }
}

.post-card {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--jf-line);
  border-radius: var(--jf-radius-lg);
  overflow: hidden;
  background: var(--jf-surface);
  box-shadow: var(--jf-shadow-sm);
  min-width: 0;
  transition: transform var(--jf-duration) var(--jf-ease), box-shadow var(--jf-duration) var(--jf-ease);
}
.post-card:hover, .post-card:focus-within {
  transform: translateY(-6px);
  box-shadow: var(--jf-shadow-xl);
}
.post-card-media { display: block; }
.post-card-img {
  aspect-ratio: 4 / 3;
  object-fit: cover;
  width: 100%;
  display: block;
  transition: transform var(--jf-duration-slow) var(--jf-ease);
}
/* .post-card itself clips overflow, so the scaled image never breaks past
   the card's rounded corners. */
.post-card:hover .post-card-img { transform: scale(1.06); }
/* No position:relative here on purpose — .post-card-title a::after needs to
   escape past this element to its positioned ancestor, .post-card, to cover
   the whole card including the image above; stopping it here would shrink
   the click target to just this text block. */
.post-card-body { padding: var(--jf-space-5, 1.25rem); }

.post-card-pill {
  display: inline-block;
  padding: var(--jf-space-1) var(--jf-space-3);
  border-radius: var(--jf-radius-pill);
  background: rgba(13, 109, 95, 0.1);
  color: var(--jf-teal-dark);
  font-size: var(--jf-text-xs);
  font-weight: var(--jf-weight-semibold);
}

.post-card-title {
  margin-top: var(--jf-space-3);
  font-size: 1.25rem;
  font-weight: var(--jf-weight-semibold);
  line-height: 1.3;
  /* Two-line clamp: a title longer than that is truncated with an ellipsis
     rather than pushing every card in the row to a different height. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.post-card-title a {
  color: var(--jf-navy);
  text-decoration: none;
}
/* The whole card is the hit area: this link's own ::after is absolutely
   positioned with inset:0, which — because .post-card-title itself has no
   position set — escapes to the nearest positioned ancestor, .post-card,
   and covers it entirely. Same trick .svc-card-title a::after uses. */
.post-card-title a::after { content: ""; position: absolute; inset: 0; }
.post-card-title a:hover { color: var(--jf-ink-accent); }

.post-card-excerpt {
  margin-top: var(--jf-space-2);
  font-size: 0.9375rem;
  color: rgba(17, 24, 39, 0.65);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.post-card-meta {
  margin-top: var(--jf-space-4);
  display: flex;
  align-items: center;
  gap: var(--jf-space-2);
  font-size: var(--jf-text-xs);
  color: var(--jf-ink-muted);
}
.post-card-dot { opacity: 0.6; }
.post-card-readtime { display: inline-flex; align-items: center; gap: var(--jf-space-1); }
.post-card-readtime svg { flex: none; color: var(--jf-ink-muted); }

.blog-load-more { margin-top: var(--jf-space-16); text-align: center; }
.blog-load-more .btn[disabled] { opacity: 0.6; cursor: default; }

/* --- prose ------------------------------------------------------------------
   Shared by service pages and blog articles, so the reading-comfort sizing
   here (18px/1.75, 17px on phones) benefits both rather than being a
   blog-only rule duplicated elsewhere. */

.prose {
  max-width: var(--jf-measure);
  color: var(--jf-ink-soft);
  font-size: 1.125rem;
  line-height: 1.75;
}
@media (max-width: 640px) {
  .prose { font-size: 1.0625rem; }
}
/* Arabic reads more comfortably with extra leading in a long-form article
   than the site's general 1.7 body leading — scoped to .prose specifically
   rather than changing the shared html[lang^="ar"] leading everywhere. */
html[lang^="ar"] .prose { line-height: 1.9; }

.prose > * + * { margin-top: 1.4em; }
.prose h2 { margin-top: 4rem; margin-bottom: 1.5rem; font-size: 2rem; }
.prose h3 { margin-top: var(--jf-space-8); font-size: 1.5rem; }
.prose ul { list-style: none; padding-inline-start: var(--jf-space-5, 1.25rem); }
.prose ol { list-style: decimal; padding-inline-start: var(--jf-space-6); }
.prose li { position: relative; padding-inline-start: var(--jf-space-4); }
.prose ul > li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  top: 0.65em;
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--jf-teal);
}
.prose li + li { margin-top: 12px; }
.prose strong { color: var(--jf-ink); }

/* A pull quote — not currently used by any seeded content, but available to
   an editor writing in the dashboard: wrap a paragraph in <blockquote>. */
.prose blockquote {
  border-inline-start: 4px solid var(--jf-teal);
  padding-inline-start: var(--jf-space-6);
  font-size: 1.375rem;
  font-style: italic;
  color: var(--jf-ink);
}
html[lang^="ar"] .prose blockquote { font-style: normal; } /* Arabic takes no italics — see tokens.css */

.prose figure { margin-inline: auto; max-width: 1100px; }
.prose img { border-radius: var(--jf-radius-md); width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
.prose figcaption { margin-top: var(--jf-space-2); text-align: center; font-size: var(--jf-text-sm); color: var(--jf-ink-muted); }

/* --- reading progress -------------------------------------------------------
   A 2px bar fixed to the very top of the viewport, above the sticky header.
   Width is driven by data-reading-progress's own custom property, set by
   progress() in site.js from scroll position — see that function for why a
   CSS variable rather than a direct style.width write. */

.reading-progress {
  position: fixed;
  inset-inline: 0;
  top: 0;
  z-index: calc(var(--jf-z-header) + 1);
  height: 2px;
  background: transparent;
}
.reading-progress-bar {
  height: 100%;
  width: var(--progress, 0%);
  background: var(--jf-teal);
  transition: width 80ms linear;
}
@media (prefers-reduced-motion: reduce) {
  .reading-progress-bar { transition: none; }
}

/* --- article hero ------------------------------------------------------------
   Full-bleed, like the home page's entry-hero — no .container, edge to edge.
   The gradient is written inline per-post (it has to reference that post's
   own cover image), so only the non-image-dependent rules live here. */

.post-hero {
  position: relative;
  min-height: 22rem;
  max-height: 70vh;
  display: flex;
  align-items: flex-end;
  background-color: var(--jf-navy);
  background-size: cover;
  background-position: center;
  color: #fff;
}
.post-hero-inner { padding-block: var(--jf-space-12) var(--jf-space-8); }
.post-hero-pill { background: rgba(255, 255, 255, 0.92); }
.post-hero-readtime {
  margin-inline-start: var(--jf-space-3);
  font-size: var(--jf-text-sm);
  color: rgba(255, 255, 255, 0.85);
}
.post-hero-title {
  margin-top: var(--jf-space-4);
  font-size: clamp(2rem, 1.5rem + 2.5vw, 4rem);
  font-weight: var(--jf-weight-bold);
  line-height: 1.1;
  color: #fff;
  max-width: 44rem;
}

/* --- article layout: sticky TOC + body --------------------------------------
   TOC is first in document order, so in a 2-column grid it lands at the
   inline-start side — left in English, right in Arabic — automatically,
   the same logical-order pattern the home hero's entry-grid uses. Below
   1280px the grid collapses to one column and the TOC simply appears above
   the body instead of being hidden outright. */

.post-layout {
  padding-block: var(--jf-section-y);
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--jf-space-12);
}
@media (min-width: 1280px) {
  .post-layout { grid-template-columns: 220px minmax(0, 680px); justify-content: center; }
}

.post-toc {
  display: none;
  font-size: var(--jf-text-sm);
}
@media (min-width: 1280px) {
  .post-toc {
    display: block;
    align-self: start;
    position: sticky;
    top: calc(80px + var(--jf-space-6));
    padding: var(--jf-space-5, 1.25rem);
    background: var(--jf-surface-tint);
    border-radius: var(--jf-radius-lg);
  }
}
.post-toc-title {
  font-size: var(--jf-text-xs);
  font-weight: var(--jf-weight-bold);
  text-transform: uppercase;
  letter-spacing: var(--jf-tracking-label);
  color: var(--jf-ink-muted-tint);
  margin-bottom: var(--jf-space-3);
}
html[lang^="ar"] .post-toc-title { text-transform: none; }
.post-toc ul { display: grid; gap: var(--jf-space-1); }
.post-toc a {
  display: block;
  padding: 0.3em 0 0.3em var(--jf-space-3);
  border-inline-start: 2px solid var(--jf-line);
  color: var(--jf-ink-muted);
  text-decoration: none;
  line-height: 1.4;
  transition: border-color var(--jf-duration-fast) var(--jf-ease), color var(--jf-duration-fast) var(--jf-ease);
}
.post-toc a:hover { color: var(--jf-ink-accent); }
.post-toc a.is-active { border-inline-start-color: var(--jf-teal-dark); color: var(--jf-ink-accent); font-weight: var(--jf-weight-semibold); }

.post-body { max-width: 680px; min-width: 0; }

.post-byline {
  display: flex;
  align-items: center;
  gap: var(--jf-space-3);
  padding-bottom: var(--jf-space-6);
  margin-bottom: var(--jf-space-6);
  border-bottom: 1px solid var(--jf-line);
}
.post-avatar {
  flex: none;
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--jf-radius-pill);
  background: var(--jf-navy);
  color: var(--jf-teal-light);
  font-size: var(--jf-text-sm);
  font-weight: var(--jf-weight-bold);
}
.post-byline-name { display: block; font-weight: var(--jf-weight-semibold); color: var(--jf-ink); font-size: var(--jf-text-sm); }
.post-byline-date { display: block; font-size: var(--jf-text-xs); color: var(--jf-ink-muted); }

/* --- in-article CTA ----------------------------------------------------------
   Teal-tinted rather than the solid-navy .cta-panel used at page level — a
   lighter, inline touch that fits inside the reading column instead of
   breaking out to a full-width band. */

.post-cta-card {
  position: relative;
  overflow: hidden;
  margin-top: var(--jf-space-16);
  padding: var(--jf-space-8);
  background: var(--jf-surface-tint);
  border: 1px solid var(--jf-line);
  border-radius: var(--jf-radius-lg);
}
.post-cta-card::before {
  content: '';
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 4px;
  background: linear-gradient(90deg, var(--jf-teal), var(--jf-teal-dark));
}
.post-cta-card::after {
  content: '';
  position: absolute;
  z-index: 0;
  inset-inline-end: -2rem;
  inset-block-end: -2rem;
  width: 9rem;
  height: 9rem;
  border-radius: 50%;
  background: var(--jf-teal);
  opacity: .08;
  pointer-events: none;
}
.post-cta-icon {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  margin-bottom: var(--jf-space-3);
  border-radius: var(--jf-radius-pill);
  background: var(--jf-surface);
  color: var(--jf-teal-dark);
  box-shadow: var(--jf-shadow-sm);
}
.post-cta-card h2 { position: relative; z-index: 1; font-size: var(--jf-text-xl); }
.post-cta-card p { position: relative; z-index: 1; margin-top: var(--jf-space-2); color: var(--jf-ink-soft); }
.post-cta-card .hero-actions { position: relative; z-index: 1; margin-top: var(--jf-space-5); }

/* --- faq ------------------------------------------------------------------ */

.faq-list { display: grid; gap: var(--jf-space-3); margin-top: var(--jf-space-8); }
.faq-item {
  border: 1px solid var(--jf-line);
  border-radius: var(--jf-radius-lg);
  background: var(--jf-surface);
  padding: var(--jf-space-4) var(--jf-space-6);
}
.faq-item summary {
  cursor: pointer;
  font-weight: var(--jf-weight-semibold);
  color: var(--jf-navy);
  list-style: none;
  display: flex;
  justify-content: space-between;
  gap: var(--jf-space-4);
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: "+";
  color: var(--jf-ink-accent);
  font-weight: var(--jf-weight-bold);
  flex: none;
}
.faq-item[open] summary::after { content: "\2212"; }
.faq-answer { margin-top: var(--jf-space-3); color: var(--jf-ink-soft); }

/* --- forms ---------------------------------------------------------------- */

.booking { margin-top: var(--jf-space-8); }

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--jf-space-5, 1.25rem);
}
.field { display: flex; flex-direction: column; gap: var(--jf-space-2); min-width: 0; }
.field--wide { grid-column: 1 / -1; }
.field label { font-size: var(--jf-text-sm); font-weight: var(--jf-weight-semibold); color: var(--jf-ink); }
.req { color: var(--jf-state-error); }

.field input, .field select, .field textarea {
  width: 100%;
  padding: var(--jf-space-3) var(--jf-space-4);
  font: inherit;
  color: var(--jf-ink);
  background: var(--jf-surface);
  /* 1.4.11 wants 3:1 for a control boundary; the live site's #d1d5db is 1.47:1. */
  border: 1.5px solid var(--jf-line-strong);
  border-radius: var(--jf-radius-sm);
}
.field textarea { resize: vertical; min-height: 7rem; }
.field input:focus-visible, .field select:focus-visible, .field textarea:focus-visible {
  border-color: var(--jf-teal-dark);
}
.field--error input, .field--error select, .field--error textarea { border-color: var(--jf-state-error); }
.field-err {
  display: flex;
  align-items: center;
  gap: var(--jf-space-1);
  font-size: var(--jf-text-sm);
  color: var(--jf-state-error);
  font-weight: 500;
}
.field-err-icon { flex: none; }

/* --- phone field: static "+971" affix, not part of the submitted value ---
   Validator::normalisePhone() already accepts a bare local number, so the
   prefix here is purely visual; nothing server-side needs it typed. */
.phone-input { position: relative; }
.phone-prefix {
  position: absolute;
  inset-inline-start: var(--jf-space-4);
  top: 50%;
  transform: translateY(-50%);
  color: var(--jf-ink-muted);
  font-weight: var(--jf-weight-semibold);
  pointer-events: none;
}
.phone-input input { padding-inline-start: calc(var(--jf-space-4) * 2 + 1.6em); }

/* --- segmented pill service selector -------------------------------------
   Real radio inputs, visually hidden but still in the tab order and still
   submitted with no JavaScript — the pill look is styling on the <label>
   that each hidden radio is paired with, not a button-driven illusion. */
.pill-group { display: flex; flex-wrap: wrap; gap: var(--jf-space-2); margin-top: var(--jf-space-1); }
.pill-radio {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.pill {
  cursor: pointer;
  padding: var(--jf-space-2) var(--jf-space-4);
  border: 1.5px solid var(--jf-line-strong);
  border-radius: var(--jf-radius-pill);
  background: var(--jf-surface);
  color: var(--jf-ink-soft);
  font-size: var(--jf-text-sm);
  font-weight: var(--jf-weight-semibold);
  transition: background var(--jf-duration-fast) var(--jf-ease), color var(--jf-duration-fast) var(--jf-ease),
    border-color var(--jf-duration-fast) var(--jf-ease);
}
.pill-radio:checked + .pill { background: var(--jf-teal-dark); color: #fff; border-color: var(--jf-teal-dark); }
.pill-radio:focus-visible + .pill { box-shadow: var(--jf-focus-ring); }
.pill-radio:disabled + .pill { opacity: 0.5; cursor: not-allowed; }

/* --- consent checkbox ---------------------------------------------------- */
.field--consent { padding-top: var(--jf-space-1); }
.check { display: flex; align-items: flex-start; gap: var(--jf-space-3); cursor: pointer; font-size: var(--jf-text-sm); color: var(--jf-ink-soft); }
.check input { margin: 0.15em 0 0; flex: none; width: 1.1em; height: 1.1em; accent-color: var(--jf-teal-dark); }

/* --- submit button loading state ----------------------------------------
   The spinner is in the DOM on first paint, 0 width via the icon's own
   viewBox collapsing to display:none, so a no-JS visitor never sees it
   appear or spin — .is-loading is only ever added by site.js. */
.btn-spinner { display: none; animation: jf-spin 0.8s linear infinite; }
.btn.is-loading .btn-spinner { display: inline-block; }
.btn.is-loading { pointer-events: none; opacity: 0.85; }
@keyframes jf-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .btn-spinner { animation-duration: 1ms; }
}

/* --- success panel --------------------------------------------------------
   Replaces the form outright (both on the no-JS redirect-after-POST render
   and after a successful fetch()), rather than leaving empty fields visible
   under a one-line status message. */
.form-success {
  display: grid;
  justify-items: center;
  gap: var(--jf-space-2);
  text-align: center;
  padding: var(--jf-space-12) var(--jf-space-6);
  background: var(--jf-surface-tint);
  border-radius: var(--jf-radius-lg);
}
/* An author-stylesheet display value always beats the UA stylesheet's
   [hidden]{display:none}, origin before specificity — so without this, the
   panel's own "display: grid" above wins and it shows up on every page load,
   success or not. This is the bug in the screenshot: the "request received"
   panel sitting permanently above an empty form. */
.form-success[hidden] { display: none; }
.form-success-icon {
  display: grid;
  place-items: center;
  width: 3.5rem;
  height: 3.5rem;
  border-radius: 50%;
  background: var(--jf-state-success);
  color: #fff;
  margin-bottom: var(--jf-space-2);
}
.form-success-title { font-size: var(--jf-text-2xl); color: var(--jf-navy); }
.form-success-body { color: var(--jf-ink-soft); max-width: 36ch; }
.form-success .btn { margin-top: var(--jf-space-4); }

/* The honeypot. Moved off-screen AND faded out, rather than display:none,
   because some bots skip fields that are not rendered at all, and others skip
   only the specific technique they've been tuned to detect. */
.hp {
  position: absolute;
  inset-inline-start: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
}

.form-privacy { margin-top: var(--jf-space-4); font-size: var(--jf-text-sm); color: var(--jf-ink-muted); }
.form-actions { display: flex; flex-wrap: wrap; gap: var(--jf-space-3); margin-top: var(--jf-space-6); }
.form-status { margin-top: var(--jf-space-4); font-size: var(--jf-text-sm); }

.alert {
  padding: var(--jf-space-4) var(--jf-space-5, 1.25rem);
  border-radius: var(--jf-radius-md);
  margin-bottom: var(--jf-space-6);
  font-weight: 500;
}
.alert--success { background: #e3f3ee; color: var(--jf-state-success); border-inline-start: 3px solid var(--jf-state-success); }
.alert--error { background: #fdecea; color: var(--jf-state-error); border-inline-start: 3px solid var(--jf-state-error); }

/* --- contact page ---------------------------------------------------------
   45/55 two-column split: the info column (channels, hours, map) first at
   45%, the booking form second at 55%. Info-first in markup also means the
   phone/WhatsApp numbers are the first thing a screen reader or a no-CSS
   client encounters on this page, which matches how a visitor actually uses
   a contact page — many are here to copy a number, not to fill a form. */

.contact-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.82fr) minmax(0, 1fr);
  gap: var(--jf-space-12);
  align-items: start;
}
.contact-aside { display: grid; gap: var(--jf-space-6); }
.contact-card {
  padding: var(--jf-space-6);
  background: var(--jf-surface-tint);
  border-radius: var(--jf-radius-lg);
}
.contact-aside-head {
  font-size: var(--jf-text-lg);
  margin-bottom: var(--jf-space-4);
}
.contact-list { display: grid; gap: var(--jf-space-5); }
.contact-item { display: flex; align-items: flex-start; gap: var(--jf-space-4); }
.contact-icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  background: var(--jf-surface);
  color: var(--jf-teal-dark);
}
.contact-item-body { display: flex; flex-direction: column; gap: var(--jf-space-1); min-width: 0; }
.contact-label {
  font-size: var(--jf-text-xs);
  text-transform: uppercase;
  letter-spacing: var(--jf-tracking-label);
  color: var(--jf-ink-muted-tint);
}
.contact-hours { display: grid; gap: var(--jf-space-2); font-size: var(--jf-text-sm); color: var(--jf-ink-soft); }

/* The map is a link, not an embed — this site has no Maps API key, and an
   iframe embed would need one. The grayscale-to-colour hover on the icon
   signals "this opens somewhere else" without pretending to be a live map. */
.contact-map-card {
  display: flex;
  align-items: center;
  gap: var(--jf-space-4);
  text-decoration: none;
  transition: background var(--jf-duration-fast) var(--jf-ease);
}
.contact-map-card:hover { background: #e9ebee; }
.contact-map-card .contact-icon { filter: grayscale(1); transition: filter var(--jf-duration-fast) var(--jf-ease); }
.contact-map-card:hover .contact-icon { filter: none; }
.contact-map-card-title { display: block; font-weight: var(--jf-weight-semibold); color: var(--jf-ink); }
.contact-map-card-sub { display: block; font-size: var(--jf-text-sm); color: var(--jf-ink-muted); }

/* --- home page: find us ----------------------------------------------------
   A link-out card, not an embed — this site has no Maps API key, and an
   iframe embed would need one (and is blocked by the CSP regardless). The
   link itself is the client's own Google Maps share link, not coordinates
   re-typed by hand. */
/* Smaller than the site's usual section rhythm on purpose — this is a quick
   "here's where we are" aside near the bottom of the home page, not a
   full-weight section, so it does not need the standard --jf-section-y
   padding or a map as large as a dedicated contact page would use. */
.map-section { text-align: center; padding-block: var(--jf-space-12); }
.map-section-intro { max-width: 520px; margin-inline: auto; }
.map-section-icon {
  display: inline-grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  background: var(--jf-surface);
  color: var(--jf-teal-dark);
  box-shadow: var(--jf-shadow-sm);
  margin-bottom: var(--jf-space-3);
}
.map-section .section-title { margin-top: 0; font-size: var(--jf-text-2xl); }

.map-embed-wrap {
  margin-top: var(--jf-space-6);
  max-width: 560px;
  margin-inline: auto;
  aspect-ratio: 16 / 10;
  border-radius: var(--jf-radius-lg);
  overflow: hidden;
  box-shadow: var(--jf-shadow-md);
}
.map-embed { display: block; width: 100%; height: 100%; border: 0; }

.map-section-cta { margin-top: var(--jf-space-6); }

/* The map's own pin popup — Leaflet wraps whatever HTML bindPopup() is given
   in its own chrome (.leaflet-popup-content-wrapper), so the card look
   (radius, shadow) is set there and the inner spacing on .leaflet-popup-content,
   rather than needing a second nested card inside .map-popup. */
.leaflet-popup-content-wrapper { border-radius: var(--jf-radius-lg); box-shadow: var(--jf-shadow-lg); }
.leaflet-popup-content { margin: var(--jf-space-5); text-align: start; }
.map-popup-name { display: block; font-size: var(--jf-text-lg); font-weight: var(--jf-weight-bold); color: var(--jf-navy); margin-bottom: var(--jf-space-2); }
.map-popup-row {
  display: flex;
  align-items: flex-start;
  gap: var(--jf-space-2);
  margin-top: var(--jf-space-2);
  font-size: var(--jf-text-sm);
  color: var(--jf-ink-soft);
}
.map-popup-row svg { flex: none; margin-top: 0.15em; color: var(--jf-teal-dark); }
.map-popup-row a { color: var(--jf-ink-soft); text-decoration: none; }
.map-popup-row a:hover { color: var(--jf-ink-accent); }
.map-popup-link {
  display: inline-flex;
  align-items: center;
  gap: var(--jf-space-2);
  margin-top: var(--jf-space-3);
  padding-top: var(--jf-space-3);
  border-top: 1px solid var(--jf-line);
  font-size: var(--jf-text-sm);
  font-weight: var(--jf-weight-semibold);
  color: var(--jf-ink-accent);
  text-decoration: none;
}
.map-popup-link:hover { color: var(--jf-teal-dark); }

/* --- breadcrumbs ---------------------------------------------------------- */

.crumbs { padding-block: var(--jf-space-4); border-bottom: 1px solid var(--jf-line); }
.crumbs ol { display: flex; flex-wrap: wrap; gap: var(--jf-space-2); font-size: var(--jf-text-sm); }
.crumbs li + li::before { content: "/"; color: var(--jf-ink-muted); margin-inline-end: var(--jf-space-2); }
.crumbs a { color: var(--jf-ink-muted); text-decoration: none; }
.crumbs a:hover { color: var(--jf-ink-accent); }
.crumbs [aria-current="page"] { color: var(--jf-ink); font-weight: var(--jf-weight-semibold); }

/* --- cta band ---------------------------------------------------------------
   The navy background sits on an inset, rounded panel rather than running
   full-bleed edge to edge — the "rounded container in the primary colour"
   treatment reads calmer than a hard-edged band. */

.cta-panel {
  padding: var(--jf-space-16) var(--jf-space-8);
  background: var(--jf-navy);
  border-radius: var(--jf-radius-xl);
  box-shadow: var(--jf-shadow-xl);
  text-align: center;
}
.cta-lede { margin-top: var(--jf-space-3); color: #c8d3de; font-size: var(--jf-text-lg); max-width: 50ch; margin-inline: auto; }
.cta-panel .hero-actions { justify-content: center; }
/* Button spacing, not .cta-lede's: some pages go straight from the title to
   the button with no lede paragraph between them (page.php, before-after.php),
   and without this the button sits flush under the heading. Targeting the
   direct child rather than an adjacent-sibling selector means this applies
   the same whether the button follows the title directly or follows
   .cta-lede, with no specificity fight against .cta-lede's own margin. */
.cta-panel > .btn,
.cta-panel > .hero-actions {
  margin-top: var(--jf-space-6);
}

/* --- footer --------------------------------------------------------------- */

.site-footer { background: var(--jf-navy); color: #c8d3de; padding-block: var(--jf-space-16) var(--jf-space-8); }
.footer-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--jf-space-8);
}
.footer-tag { margin-top: var(--jf-space-3); font-size: var(--jf-text-sm); }
.footer-head { font-size: var(--jf-text-base); color: #fff; margin-bottom: var(--jf-space-4); }
.footer-head--sub { margin-top: var(--jf-space-6); }
.footer-col ul { display: grid; gap: var(--jf-space-2); font-size: var(--jf-text-sm); }
.footer-col a, .footer-social a { color: #c8d3de; text-decoration: none; }
.footer-col a:hover, .footer-social a:hover { color: var(--jf-teal-light); text-decoration: underline; }
.footer-social { display: flex; gap: var(--jf-space-3); margin-top: var(--jf-space-4); }
.footer-social-badge {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.3);
  color: #fff;
  transition: background var(--jf-duration-fast) var(--jf-ease), border-color var(--jf-duration-fast) var(--jf-ease);
}
.footer-social-badge:hover { background: var(--jf-teal-dark); border-color: var(--jf-teal-dark); }
.footer-contact { display: grid; gap: var(--jf-space-2); font-size: var(--jf-text-sm); }
.footer-hours { display: grid; gap: var(--jf-space-1); font-size: var(--jf-text-sm); }
.footer-bottom {
  margin-top: var(--jf-space-12);
  padding-top: var(--jf-space-6);
  border-top: 1px solid var(--jf-navy-light);
  display: flex;
  flex-wrap: wrap;
  gap: var(--jf-space-2) var(--jf-space-6);
  justify-content: space-between;
  font-size: var(--jf-text-sm);
}
.footer-licence { display: block; margin-top: var(--jf-space-2); color: #9fb0c0; }
.footer-credit { display: inline-flex; align-items: center; gap: var(--jf-space-2); color: #9fb0c0; }
.footer-credit a { display: inline-flex; align-items: center; color: #c8d3de; font-weight: var(--jf-weight-semibold); }
.footer-credit a:hover { color: var(--jf-teal-light); }

/* --- floats and admin ----------------------------------------------------- */

.floats {
  position: fixed;
  inset-inline-end: var(--jf-space-4);
  bottom: var(--jf-space-4);
  /* One above .mobile-cta-bar's z-index, not the same value: the two sit at
     different heights in normal flow (see the 640px block below), but
     explicit stacking means WhatsApp always wins if that gap is ever lost on
     a very short viewport, rather than leaving it to DOM order. */
  z-index: calc(var(--jf-z-float) + 1);
  display: flex;
  flex-direction: column;
  gap: var(--jf-space-3);
  align-items: center;
}

.float-btn {
  display: grid;
  place-items: center;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: var(--jf-radius-pill);
  box-shadow: var(--jf-shadow-lg);
  border: none;
  cursor: pointer;
  text-decoration: none;
  transition: transform var(--jf-duration) var(--jf-ease);
}
.float-btn:hover { transform: translateY(-2px); }
.float-btn--wa {
  background: var(--jf-whatsapp);
  color: var(--jf-navy);
  animation: jf-pulse 2.8s ease-out infinite;
}
.float-btn--wa:hover { animation-play-state: paused; }

/* A soft, slow ring — not an aggressive attention-grabber. Opacity never
   exceeds 0.4, and the whole thing stops under reduced motion. */
@keyframes jf-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(37, 211, 102, 0.40), var(--jf-shadow-lg); }
  70%  { box-shadow: 0 0 0 12px rgba(37, 211, 102, 0), var(--jf-shadow-lg); }
  100% { box-shadow: 0 0 0 0 rgba(37, 211, 102, 0), var(--jf-shadow-lg); }
}
@media (prefers-reduced-motion: reduce) {
  .float-btn--wa { animation: none; }
}

/* The admin key: present but quiet. Its small size is not the security
   control — Auth is. */
.float-btn--key {
  width: 2.5rem;
  height: 2.5rem;
  background: var(--jf-surface);
  color: var(--jf-ink-muted);
  border: 1px solid var(--jf-line-strong);
  z-index: var(--jf-z-admin);
}
.float-btn--key:hover { color: var(--jf-ink-accent); border-color: var(--jf-teal-dark); }

.admin-dialog {
  border: none;
  border-radius: var(--jf-radius-lg);
  padding: 0;
  max-width: 24rem;
  width: calc(100% - 2rem);
  box-shadow: var(--jf-shadow-lg);
  color: var(--jf-ink);
  background: var(--jf-surface);
}
.admin-dialog::backdrop { background: rgba(10, 22, 40, 0.55); }
.admin-form { padding: var(--jf-space-6); display: grid; gap: var(--jf-space-4); }
.admin-form h2 { font-size: var(--jf-text-xl); }
.admin-form-actions { display: flex; gap: var(--jf-space-3); align-items: center; }

/* --- cookie consent ------------------------------------------------------- */
/* Built by script and only when a GA4 ID is configured, so this bar never
   appears while analytics is dormant. */

.consent-bar {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: var(--jf-z-modal);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--jf-space-4);
  padding: var(--jf-space-4) var(--jf-gutter);
  background: var(--jf-navy);
  color: #c8d3de;
  font-size: var(--jf-text-sm);
  box-shadow: var(--jf-shadow-lg);
}
.consent-bar p { max-width: 60ch; margin: 0; }
.consent-actions { display: flex; gap: var(--jf-space-3); flex: none; }
.consent-bar .btn--secondary { color: #fff; border-color: rgba(255, 255, 255, 0.35); }
.consent-bar .btn--secondary:hover { background: var(--jf-navy-light); }

/* --- error page ----------------------------------------------------------- */

.error-page { padding-block: var(--jf-space-24); }
.error-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--jf-space-4);
  margin-top: var(--jf-space-12);
  padding-top: var(--jf-space-6);
  border-top: 1px solid var(--jf-line);
  font-size: var(--jf-text-sm);
}

/* --- reveal on scroll ----------------------------------------------------- */
/* The resting state is visible. Only the transform is animated, and only when
   the browser supports the observer and motion is not reduced — so the page is
   complete with JavaScript off and nothing is ever stuck invisible. */
@media (prefers-reduced-motion: no-preference) {
  [data-reveal].is-armed > * {
    opacity: 0;
    transform: translateY(12px);
  }
  [data-reveal].is-revealed > * {
    opacity: 1;
    transform: none;
    transition: opacity var(--jf-duration-slow) var(--jf-ease),
                transform var(--jf-duration-slow) var(--jf-ease);
  }
  [data-reveal].is-revealed > *:nth-child(2) { transition-delay: 60ms; }
  [data-reveal].is-revealed > *:nth-child(3) { transition-delay: 120ms; }
  [data-reveal].is-revealed > *:nth-child(4) { transition-delay: 180ms; }
  [data-reveal].is-revealed > *:nth-child(5) { transition-delay: 240ms; }
  [data-reveal].is-revealed > *:nth-child(6) { transition-delay: 300ms; }
}

/* --- responsive ----------------------------------------------------------- */

@media (max-width: 900px) {
  .svc-hero-inner, .contact-grid { grid-template-columns: 1fr; gap: var(--jf-space-8); }
  .post-featured { grid-template-columns: 1fr; }
}

@media (max-width: 700px) {
  .mv-grid { grid-template-columns: 1fr; }
}

/* The brief's own breakpoint (<768px) for the hero specifically, kept
   separate from the 700px one above so the two can move independently. */
@media (max-width: 767px) {
  .entry-grid { grid-template-columns: 1fr; }
  .entry-card {
    /* 50vh first as the widely-supported fallback; 50svh second so browsers
       that understand it get the dynamic-viewport-safe version instead — an
       unrecognised unit drops only that one declaration and leaves the 50vh
       just above in place, so older browsers still get a sane height. */
    height: 50vh;
    height: 50svh;
    min-height: 280px;
    /* 12vh of a full 100vh desktop panel is generous; 12vh of a 50vh mobile
       panel eats nearly a quarter of it, and sits close enough to the
       viewport bottom to crowd the floating WhatsApp button on the lower
       panel. A fixed, more modest value reads better at this height. */
    padding-block-end: var(--jf-space-8);
  }
}

@media (max-width: 1024px) {
  .nav-toggle { display: grid; }

  .primary-nav {
    position: fixed;
    inset-block: 0;
    inset-inline-end: 0;
    width: min(20rem, 85vw);
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: var(--jf-space-6);
    /* 88px matches .site-header ~ #main's mobile padding-top just above —
       enough to clear the floating pill header at this width. */
    padding: 88px var(--jf-space-6) var(--jf-space-6);
    background: var(--jf-surface);
    border-inline-start: 1px solid var(--jf-line);
    box-shadow: var(--jf-shadow-lg);
    transform: translateX(100%);
    transition: transform var(--jf-duration) var(--jf-ease);
    overflow-y: auto;
  }
  html[dir="rtl"] .primary-nav { transform: translateX(-100%); }
  .primary-nav.is-open { transform: translateX(0); }

  .primary-nav-close {
    display: grid;
    place-items: center;
    align-self: flex-end;
    flex: none;
    width: 36px;
    height: 36px;
    margin-block-end: var(--jf-space-2);
    background: transparent;
    border: 1px solid var(--jf-line-strong);
    border-radius: var(--jf-radius-pill);
    color: var(--jf-ink-soft);
    cursor: pointer;
  }
  .primary-nav-close:hover { color: var(--jf-ink-accent); border-color: var(--jf-teal-dark); }
  /* html[dir="rtl"] .primary-nav above is more specific (0,2,1) than
     .primary-nav.is-open (0,2,0), so in Arabic it kept winning and holding
     the drawer off-screen even after JS added .is-open — this is the actual
     bug: not the JS (which was always toggling the class correctly, as
     confirmed by adding it manually in the console and still seeing
     nothing), not the earlier backdrop-filter containing-block issue
     (a real bug too, worth having fixed, just not this one) — just this one
     selector never being specific enough to override the RTL rule it needs
     to beat. Matching dir="rtl" + .is-open together here outranks it. */
  html[dir="rtl"] .primary-nav.is-open { transform: translateX(0); }

  /* The absolute centering above is a desktop-bar trick only — reset to a
     normal stacked block inside the mobile slide-out drawer. */
  .nav-list {
    position: static;
    transform: none;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
  }
  html[dir="rtl"] .nav-list { transform: none; }
  .nav-list li + li { border-top: 1px solid var(--jf-line); }
  .nav-list a { display: block; padding-block: var(--jf-space-4); font-size: var(--jf-text-base); }
  .nav-list a[aria-current="page"] { border-block-end: none; }

  .nav-actions { flex-wrap: wrap; }
  .nav-phone { width: 100%; }
}

@media (max-width: 560px) {
  .trust-inner { gap: var(--jf-space-6) var(--jf-space-4); }
  .trust-item { min-width: 6rem; flex: 1 1 40%; }
  .form-actions .btn { flex: 1 1 100%; }
  .floats { inset-inline-end: var(--jf-space-3); bottom: var(--jf-space-3); }
}

/* --- mobile sticky CTA bar --------------------------------------------------
   Call and Book, fixed to the bottom of the viewport on phone widths only.
   Hidden entirely above 640px — on a desktop or tablet layout the header and
   the float buttons already cover the same two actions, and a second copy
   fixed to the bottom would just be clutter. */

.mobile-cta-bar {
  display: none;
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: var(--jf-z-float);
  gap: var(--jf-space-3);
  padding: var(--jf-space-3) var(--jf-gutter);
  padding-bottom: calc(var(--jf-space-3) + env(safe-area-inset-bottom, 0px));
  background: var(--jf-surface);
  border-top: 1px solid var(--jf-line);
  box-shadow: 0 -4px 16px rgba(10, 22, 40, 0.08);
}
.mobile-cta-bar .btn { flex: 1; }

@media (max-width: 640px) {
  .mobile-cta-bar { display: flex; }
  /* The floating WhatsApp/admin stack would otherwise sit under the bar. */
  .floats { bottom: calc(68px + env(safe-area-inset-bottom, 0px)); }
  /* Keeps the final section of a short page from being covered by the bar. */
  body { padding-bottom: calc(60px + env(safe-area-inset-bottom, 0px)); }
}

@media print {
  .site-header, .floats, .cta-band, .nav-toggle, .mobile-cta-bar, .ticker { display: none; }
  body { color: #000; background: #fff; }
}
