/* ============================================================================
   IBIX base — the non-negotiables, pre-solved.
   Start every new client site with this file, then layer the site's own theme
   on top. Every rule here exists because its absence shipped a real bug on a
   real client site; the comment names which one. Don't delete a rule without
   reading its comment.

   Pair it with the guardrail check:   python "IBIX Studio/qa/qa.py" <url>
   ========================================================================= */

/* --- reset ---------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box }
* { margin: 0; padding: 0 }

/* `height:auto` is load-bearing. An <img> carrying width/height attributes
   (good practice for CLS) whose only CSS is max-width:100% renders VERTICALLY
   STRETCHED below its natural width — object-fit defaults to `fill`, so a
   1280x720 photo draws as 350x720 on a phone. Desktop looks perfect, which is
   why it hides for weeks. Ajyal fleet photo + East Hope, both owner-caught. */
img, video, svg { max-width: 100%; height: auto; display: block }

/* Scoped rules that set an explicit height (cover-fit heroes, logo tiles) are
   more specific and still win — that is intentional. */

input, button, textarea, select { font: inherit; color: inherit }
a { color: inherit; text-decoration: none }

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

/* No horizontal scroll at ANY width. Wide things scroll inside their own box.

   `clip`, NOT `hidden` — this matters more than it looks. `overflow-x:hidden` on
   the root makes html/body a SCROLL CONTAINER, and a `position:sticky` header
   then sticks to that container instead of the viewport, which means it scrolls
   away and never comes back. No error, no warning: the rule applies and simply
   does nothing.

   Found 2026-08-04 on RANCO. Its header was declared `position:sticky; top:0`
   and measured at `top:-1200px` after scrolling 1200px, in BOTH languages, on a
   site that had already passed a full qa.py sweep. `overflow:clip` prevents the
   same horizontal scroll without establishing a scroll container, so sticky
   works again. ~93% support; the remainder fall back to today's behaviour, so
   nothing regresses for them.

   Do NOT "fix" a sticky header by switching it to `position:fixed` — that takes
   it out of flow and every following section slides underneath it. Fix the
   ancestor's overflow, which is here. */
html { overflow-x: clip }
body { overflow-x: clip; -webkit-font-smoothing: antialiased }
@supports not (overflow: clip) {
  html { overflow-x: hidden }
  body { overflow-x: hidden }
}
table, pre { max-width: 100% }
.scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch }

/* NEVER style bare landmarks — a page has more than one <nav>, <header>,
   <footer>, <section>. `nav{position:fixed}` on IBIX's own site also hit the
   FOOTER's <nav class="footer-links"> and pinned it to the top of the viewport.
   Scope to an id/class instead. This comment is the rule; there is deliberately
   no `nav{}` block here. */

/* A portable component must reset its own landmarks so it survives being
   dropped into a page whose CSS you haven't audited. */
#site-footer nav { max-width: none; margin: 0; padding: 0; display: block }

/* --- controls ------------------------------------------------------------- */

/* A header CTA that wraps to two lines grows taller than the fixed nav row, so
   it stops matching its neighbours and reads as "misaligned and a different
   size" — the owner's exact words on East Hope. The cause is the wrap, not the
   alignment. nowrap is the fix, and it belongs in the BASE rule. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  white-space: nowrap; line-height: 1.15; cursor: pointer;
  border: 1.5px solid transparent; transition: .18s;
}

/* Full-screen hero headline: size it by the SMALLER of viewport width AND height.
   A width-only headline (e.g. font-size:12vw) grows so tall on a wide-but-short
   laptop that it pushes the sub-head + CTA below the fold — the hero looks broken
   and the primary button disappears (Ajyal immersive demo; qa.py `hero-fit` catches
   it). The min(_vw,_vh) makes it stay dramatic on tall screens but fit short ones.
   Use .hero-display on the headline, or copy the min() pattern with your own values. */
.hero-display { font-size: clamp(2.5rem, min(11vw, 15vh), 8rem); line-height: .98 }
/* and trim a 100vh hero's vertical padding on short laptops so the CTA stays visible */
@media (max-height: 820px) {
  .hero-fit-pad { padding-top: clamp(80px, 12vh, 120px); padding-bottom: clamp(28px, 5vh, 60px) }
}

/* WCAG 2.2 AA (2.5.8) target size. Cheap to honour, awkward to retrofit. */
@media (hover: none) and (pointer: coarse) {
  a, button, [role="button"], input[type="submit"] { min-height: 24px }
  /* Only ever set `cursor` in a blanket a,button rule. Setting
     `pointer-events:auto` here re-enables links inside CLOSED overlays
     (opacity:0 + pointer-events:none), creating invisible tap targets that
     open random pages. IBIX shipped exactly that for weeks. */
  * { -webkit-tap-highlight-color: transparent }
}

/* Note the SPACES in the media query above: writing the combinator "and" with
   NO space before the next "(" makes the whole query DEAD in every browser —
   the parser reads it as a function token. IBIX's tablet layout silently never
   applied for months. (Described in words so this comment can never
   false-positive qa.py's dead-query scan when the file is inlined.) */

/* --- degradation: never ship a blank page --------------------------------- */

/* Reveal-on-scroll hides content until JS runs. Gate it on html.js (set by a
   pre-paint inline script) so that if JS is off, blocked, or main.js 404s, the
   content is simply visible instead of the whole page being blank.
   Put this in <head> of every page, BEFORE the stylesheet:
       <script>document.documentElement.classList.add('js');</script> */
html.js .reveal { opacity: 0; transform: translateY(18px); transition: .6s ease }
html.js .reveal.in { opacity: 1; transform: none }

/* Reduced-motion users must SEE the content, not a blank page. Both selectors
   are listed because `html.js .reveal` (0,2,0) outranks a bare `.reveal`
   (0,1,0) — gating the hide silently disabled this override on East Hope. */
@media (prefers-reduced-motion: reduce) {
  html.js .reveal, .reveal { opacity: 1; transform: none; transition: none }
  html { scroll-behavior: auto }
  *, *::before, *::after { animation-duration: .001ms !important;
    animation-iteration-count: 1 !important; transition-duration: .001ms !important }
}

/* Belt and braces for the no-JS case: in every page's <head>, wrap the rule
   .reveal{opacity:1!important;transform:none!important} in noscript+style tags.
   (Spelled out in WORDS on purpose — a literal closing style tag inside this
   comment TERMINATES the <style> element whenever this file is inlined into a
   single-file build, spilling the rest of the stylesheet onto the page as
   visible text. Found live on the ACME demo build 2026-07-26.) */

/* --- mobile menu scroll lock ---------------------------------------------- */

/* `body{overflow:hidden}` is a NO-OP on any site with `html{overflow-x:hidden}`
   — per CSS overflow propagation the viewport takes its scrolling from the root,
   and body's overflow is only propagated when html computes to `visible`. The
   class applies, and nothing happens. This position-fixed lock is the only one
   that works here AND on iOS Safari. Drive it from JS: store scrollY, set
   body.style.top = -y, restore with scrollTo(0, y) on release. Use an
   owner-keyed lock (lock('menu') / lock('about')) — one overlay opening from
   another otherwise strands the lock forever. */
body.scroll-locked { position: fixed; left: 0; right: 0; width: 100%; overflow: hidden }

/* --- cross-browser -------------------------------------------------------- */

/* Safari needs the -webkit- twin or the effect silently does nothing. */
.glass {
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
          backdrop-filter: blur(14px) saturate(1.2);
}
/* Blur is expensive: a grid of many blurred panels janks mid-range Android,
   which is what Gulf clients actually browse on. Drop it at phone widths. */
@media (max-width: 720px) {
  .glass { -webkit-backdrop-filter: none; backdrop-filter: none }
}
@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  .glass { background-color: rgba(20, 52, 79, .82) }
}

/* Firefox ignores ::-webkit-scrollbar entirely — ship both. On a light design a
   default scrollbar reads as a "dark strip" bug to clients, so style it. */
html { scrollbar-width: thin; scrollbar-color: #CBC4B8 transparent }
::-webkit-scrollbar { width: 11px; height: 8px }
::-webkit-scrollbar-thumb { background: #CBC4B8; border-radius: 9px }

/* Decorative fixed canvases NEVER take pointer events — a fixed canvas never
   scrolls away, so it floats over every section and swallows taps in dead
   zones, sending users to random pages. */
canvas[data-decorative] { pointer-events: none }

/* --- RTL / bilingual ------------------------------------------------------ */

/* letter-spacing breaks Arabic letter joining outright. */
[dir="rtl"] * { letter-spacing: normal !important }
/* Latin display faces (Bebas, Archivo, Saira) have NO Arabic glyphs, so Arabic
   headings silently fall back to an ugly default. Same trap for Cyrillic. */
[dir="rtl"] h1, [dir="rtl"] h2, [dir="rtl"] h3 { font-family: var(--body) }
/* Carousel tracks need LTR geometry with RTL content inside. */
[dir="rtl"] .carousel-track { direction: ltr }

/* Hiding something off-screen with a PHYSICAL offset breaks RTL. The classic
   `left:-9999px` is invisible and harmless in LTR — and in RTL the browser
   scrolls the other way, so the same trick extends the scrollable canvas
   instead of hiding anything.

   RANCO, 2026-08-04: a form honeypot using `left:-9999px` blew the Arabic
   contact and capability pages out to a 10,374px-wide canvas at a 375px
   viewport — content crushed into a sliver, effectively broken on a phone —
   while the English mirrors measured 0px overflow. It reached QA because the
   bug cannot exist in the language it was built in.

   `inset-inline-start:-9999px` is NOT the fix: in RTL that resolves to
   `right:-9999px` and overflows the other way. Use a zero-offset clip instead —
   nothing is ever positioned outside the viewport, in either direction.

   .visually-hidden — for content screen readers SHOULD read (skip links, form
   labels, headings that exist for the outline).
   .decoy-field   — for form honeypots, which nobody should reach. Pair it in
   the markup with aria-hidden="true" AND tabindex="-1"; qa.py's
   invisible-with-no-JS check skips exactly that pair, so a correctly-built
   decoy no longer produces a permanent FAIL. */
.visually-hidden, .decoy-field {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.visually-hidden:focus-within, .visually-hidden:active {
  position: static; width: auto; height: auto;
  margin: 0; overflow: visible; clip-path: none; white-space: normal;
}
.decoy-field { opacity: 0; pointer-events: none }

/* --- print ---------------------------------------------------------------- */
@media print {
  .no-print, .wa-float, .cookie-banner { display: none !important }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: .8em }
}
