/* ============================================================================
   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. Don't delete a rule without reading its comment.

   ⚠ NEVER NAME A CLIENT IN THIS FILE. It is copied verbatim into every build,
   so anything written here is served on every site we run — and a note about
   client A is then published to client B, who may be their competitor.
   Describe the incident, date it, and keep the name in that client's own
   STATUS.md. Same rule for any theme stylesheet or script that ships.
   (Enforced 2026-08-07; see PLAYBOOK-ARCHIVE.md for the incident.)

   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. Shipped twice on live client sites, both times
   spotted by the client's own owner rather than by us. */
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 a bilingual client build: the 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" — a client's exact words on a live build. 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 (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 a live site. */
@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.

   2026-08-04: a form honeypot using `left:-9999px` blew two Arabic
   pages of a live build 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 }
}
