/* ============================================================
   ZX SYSTEM — the site-wide "physics layer" reverse-engineered from
   design/ANGA-REFERENCE-SPEC-2026-09-02.md §2 (tokens), §3.1 (carousel),
   §3.2 (sticky tabs). SITE-REDIRECTION-TASKLIST-2026-09-02.md slices 4/5/6.

   SAFETY CONTRACT — every rule in this file is scoped under `body.zx`.
   A page that links this stylesheet but has NOT added `class="zx"` to
   <body> gets ZERO changed pixels. Verified 2026-09-02: SC16 anchor
   (medical-tourism.html) loaded this file WITHOUT body.zx and reported
   0px moved — see ops/shane_check.py run in the ship report for this slice.

   Do NOT touch css/shared.css, css/glow3.css, css/premium.css from here.
   Do NOT write any bare `.lit` rule — only `.zx .lit` (see §6).

   HERO PHOTO CONTRACT (§8, added 2026-09-02, ROLLOUT-REVIEW-2026-09-02.md
   §5#4): pages must drop their page-local `.hero-duo img{filter:...
   url(#duotone-inkmint)}` rule and the inline `<filter id="duotone-
   inkmint">` SVG def, and use the kit classes `.zx-hero-photo` /
   `.zx-hero-scrim` / `.zx-hero-photo--soft` instead — see §8 below for
   the full per-class contract. Not yet applied to any page; a sweep
   agent does that pass.
   ============================================================ */

body.zx{
  /* -------------------------------------------------------------
     §1 SPACING LADDER — ANGA §2.2: 96 / 100 / 192 / 200 rhythm,
     48 on strips. We standardise on the 4-tier ladder the tasklist
     names explicitly: 48 / 96 / 144 / 192.
     ------------------------------------------------------------- */
  --zx-s1: 48px;   /* strip / logo-band / footer padding */
  --zx-s2: 96px;   /* normal section padding — the ANGA default */
  --zx-s3: 144px;  /* major section (between 96 normal and 192 dark-break) */
  --zx-s4: 192px;  /* dark-break / hero-scale section */

  /* -------------------------------------------------------------
     §2 RADIUS TIERS — ANGA §2.3 census: pill (36 uses) / 20px
     (dominant container radius, 28 uses) / 10px (small covers) /
     4px (chips/tags). Collapsed from the reference's 10-tier chaos.
     ------------------------------------------------------------- */
  /* 999px, not ANGA's literal `calc(infinity*1px)`. Reason, measured: Chromium
     resolves calc(infinity*1px) to 33554400px in getComputedStyle, so a radius
     census (the discipline this tier system exists to enforce) reports it as a
     FIFTH distinct value that no human can match against "pill". 999px renders
     pixel-identical on every box narrower than 1998px — every pill on this site. */
  --zx-r-pill: 999px;
  --zx-r-1: 20px;   /* card / panel / container */
  --zx-r-2: 10px;   /* small media / cover */
  --zx-r-3: 4px;    /* chip / tiny tag */

  /* -------------------------------------------------------------
     §3 SHADOW TIERS — exact values handed down in the tasklist,
     matching ANGA §2.4's card-lift / large-panel / ambient-bloom
     three-tier census.
     ------------------------------------------------------------- */
  --zx-sh-1: 0 4px 20px -10px rgba(11,31,42,.25);   /* default card lift */
  --zx-sh-2: 0 4px 50px rgba(11,31,42,.07);          /* large soft panel */
  --zx-sh-3: 0 4px 200px rgba(62,142,247,.17);       /* ambient bloom (brand-recoloured) */

  /* -------------------------------------------------------------
     §3b STICKY RAIL OFFSET — ROLLOUT-REVIEW-2026-09-02.md §4/§5#1:
     18 of 19 rail-bearing pages hid `.zx-tabs` behind the fixed
     header on mobile scroll (7 different page-local `top:` values,
     none sharing a token). Measured 2026-09-02, Playwright, scroll
     to 1500px then settle:
       mobile   (390x844) header bottom = 61px on every page  -> +1px safety = 62px
       desktop (1440x900) header bottom = 79px on google-maps/
                services/case-mdcc (NOT the naively-assumed 64px —
                google-maps.html and case-mdcc.html's own desktop
                `top:64px` overrides sit 15px short and do not
                actually clear; only services.html's `top:79px`
                exactly touches) -> +1px safety = 80px
     These are the kit's shared DEFAULTS (consumed below by the base
     `.zx-tabs` rule and the mobile override). See §6c for the
     mobile `!important` enforcement rule and why a default alone
     is not enough.
     ------------------------------------------------------------- */
  --zx-rail-top: 80px;
  --zx-rail-top-m: 62px;

  /* -------------------------------------------------------------
     §4 MOTION — ANGA §2.6: 0.3s cubic-bezier(.4,0,.2,1) is the
     dominant interaction transition (84+63 elements); anything
     ENTERING the viewport (reveals) gets >=0.9s so it still reads
     as deliberate, not twitchy.
     ------------------------------------------------------------- */
  --zx-ease: cubic-bezier(.4,0,.2,1);
  --zx-dur-interact: .3s;   /* hover, active, click-driven state changes */
  --zx-dur-enter: .9s;      /* scroll reveals / entrance animation floor */

  /* -------------------------------------------------------------
     §5 LAYOUT + TYPE SCALE — ANGA §2.2 container 1410 / §2.1 type.
     Letter-spacing normal on every heading (ANGA §2.1: tracking-tight
     is NOT what ships today, despite older specs claiming it).
     ------------------------------------------------------------- */
  --zx-container: 1410px;

  --zx-h1-size: 70px;   --zx-h1-lh: 87.5px;
  --zx-h1-size-m: 36px; --zx-h1-lh-m: 45px;
  --zx-h2-size: 50px;   --zx-h2-lh: 63.75px;
  --zx-h2-size-m: 26px; --zx-h2-lh-m: 33.15px; /* ANGA §2.1's own eyebrow-h2 variant (26px/33.15px)
     is the same 0.52 ratio a mobile h1 (36/70=.514) already uses on this site — ku-mud
     shipped 26px independently. ku-mud rollout gap 2026-09-02: h2 had no mobile tier at
     all, so a 50px Thai h2 overflowed at 390px and needed a page-local !important. */
  --zx-h3-size: 24px;   --zx-h3-lh: 32px;
  --zx-body-size: 16px; --zx-body-lh: 1.6; /* Thai tone marks need >=1.6, ANGA body is 24/16=1.5 floor-only */

  --zx-font-display: "Bai Jamjuree", "Anuphan", system-ui, sans-serif;
  --zx-font-body: "Anuphan", "IBM Plex Sans Thai", system-ui, sans-serif;

  /* -------------------------------------------------------------
     §5c CONTRAST TOKENS — PILOT-REVIEW-google-maps-2026-09-02.md §2:
     5 AA failures, all muted/secondary text under 4.5:1. Two variants
     because one grey cannot pass against both a light AND a dark
     surface (the math is mutually exclusive — light needs L<=.17,
     dark needs L>=.23): --zx-muted for text on cream/card surfaces,
     --zx-muted-on-ink for text on dark-break (ink) surfaces.
     Measured (WCAG relative-luminance formula):
       --zx-muted        #4E5B55 on #FFFDF8 cream = 7.00:1, on #F4F5F4 card = 6.51:1
       --zx-muted-on-ink #9FB0AA on #0c1f1a dark-break = 7.55:1
     ------------------------------------------------------------- */
  --zx-muted: #4E5B55;
  --zx-muted-on-ink: #9FB0AA;

  font-family: var(--zx-font-body);
  /* ku-mud rollout gap 2026-09-02: `html{overflow-x:clip}` + a page-local
     `body{overflow-x:hidden}` makes BODY the scroll container in some
     engines, which silently breaks `position:sticky` on any descendant
     (the .zx-tabs rail) because sticky positions against the nearest
     scrolling ancestor, not always the viewport. Matching html's `clip`
     here removes body from that role again. Grepped all 22 pages that
     carry both declarations; 17 of them also carry body.zx (see report) —
     this fixes those 17 without editing any of them. The other 5
     (ai-agency-thailand, chatbot, medical-tourism, nail-salon, why-zipai)
     have the same pair but no body.zx, so this kit change does not reach
     them — flagged in the report, not silently left broken. */
  overflow-x: clip;
}

/* ================================================================
   §6e MOBILE FAB / HERO GATE — google-ads rollout gap (2026-09-02). The
   round `.maps-fab`/`.fb-fab` buttons are `position:fixed` near the
   bottom-right corner (shared.css's own, intentional "FABs stay visible
   on mobile" rule, 2026-08-18 — NOT the bug). On a page whose hero fits
   inside the first ~844px (google-ads.html, verified: hero CTA rect
   top:615/bottom:767 vs .maps-fab top:626/bottom:682 and .fb-fab
   top:700/bottom:748 — real x/y overlap at scroll 0), that fixed corner
   lands on top of the hero's own CTA row before the user has scrolled.
   google-maps.html itself does NOT reproduce this (its hero buttons are
   left-aligned and narrower, clearing the FABs) but the fix is kit-level
   so every body.zx page gets it without a page-by-page edit.

   REPLACES a first attempt that hid the FABs for a flat 1.4s via a
   zero-duration delayed-reveal animation — rejected on review, correctly:
   a fixed timer just re-creates the same overlap 1.4s later on any user
   who has not scrolled by then. This is the real, scroll-aware fix —
   an IntersectionObserver in zx-tabs.js (Kit D, added 2026-09-02) toggles
   two body classes; see that file's comment for why two, not one:
     `zx-fab-ready`   — the observer has reported at least once (or found
                        nothing to observe / no IO support)
     `zx-hero-inview` — live: the hero is currently on screen
   Base rule below (unconditional at <=860px) is the "no flash before
   the deferred script runs" safety net; the reveal rule only fires once
   `zx-fab-ready` is present AND `zx-hero-inview` is absent, with a real
   0.3s transition (interpolated fade this time — reduced-motion gets an
   instant cut, since an opacity fade is exactly the kind of motion that
   preference exists to remove).
   ================================================================ */
@media (max-width: 860px){
  body.zx .maps-fab,
  body.zx .fb-fab{
    opacity: 0;
    pointer-events: none;
  }
  body.zx.zx-fab-ready:not(.zx-hero-inview) .maps-fab,
  body.zx.zx-fab-ready:not(.zx-hero-inview) .fb-fab{
    opacity: 1;
    pointer-events: auto;
    transition: opacity .3s var(--zx-ease);
  }
  @media (prefers-reduced-motion: reduce){
    body.zx.zx-fab-ready:not(.zx-hero-inview) .maps-fab,
    body.zx.zx-fab-ready:not(.zx-hero-inview) .fb-fab{
      transition: none;
    }
  }
}

/* ================================================================
   §5b TYPE SCALE APPLICATION — scoped, weight-700 display font,
   tracking normal, body line-height floor 1.6 for Thai tone marks.
   ================================================================ */
body.zx h1,
body.zx h2,
body.zx h3,
body.zx .zx-h1,
body.zx .zx-h2,
body.zx .zx-h3{
  font-family: var(--zx-font-display);
  font-weight: 700;
  letter-spacing: normal;
}
body.zx h1, body.zx .zx-h1{
  font-size: var(--zx-h1-size-m);
  line-height: var(--zx-h1-lh-m);
}
body.zx h2, body.zx .zx-h2{ font-size: var(--zx-h2-size); line-height: var(--zx-h2-lh); }
body.zx h3, body.zx .zx-h3{ font-size: var(--zx-h3-size); line-height: var(--zx-h3-lh); }
@media (min-width: 900px){
  body.zx h1, body.zx .zx-h1{
    font-size: var(--zx-h1-size);
    line-height: var(--zx-h1-lh);
  }
}
@media (max-width: 860px){
  body.zx h2, body.zx .zx-h2{
    font-size: var(--zx-h2-size-m);
    line-height: var(--zx-h2-lh-m);
  }
}
body.zx p, body.zx li, body.zx .zx-body{
  font-family: var(--zx-font-body);
  font-size: var(--zx-body-size);
  line-height: var(--zx-body-lh); /* >=1.6 — Thai tone marks (สระบน/สระล่าง) need the extra headroom ANGA's 1.5 doesn't give */
  letter-spacing: normal;
}

/* §5c HEADING TRACKING — ROBUSTNESS FIX, PILOT-REVIEW-google-maps-2026-09-02.md §1:
   h3 measured -0.24px despite `body.zx h3{letter-spacing:normal}` above. Root cause:
   type.css:69 `body.type-v2 :is(article,section) h3{letter-spacing:-.01em}` is
   (0,1,3) — its `:is(article,section)` ancestor adds a THIRD type-selector slot that
   `body.zx h3` (0,1,2) does not have, so type.css wins on raw specificity regardless
   of source order. Matching the same ancestor shape ties it at (0,1,3); zx-system.css
   loads after type.css, so the tie goes to us. */
body.zx :is(article,section) :is(h1,h2,h3,h4){
  letter-spacing: normal;
}

/* ================================================================
   §6 CONTAINER + SECTION PADDING LADDER (utility classes)
   ================================================================ */
body.zx .zx-container{
  max-width: var(--zx-container);
  margin-left: auto;
  margin-right: auto;
  padding-left: 30px;
  padding-right: 30px;
}
@media (max-width: 480px){
  body.zx .zx-container{ padding-left: 20px; padding-right: 20px; }
}
body.zx .zx-pad-1{ padding-top: var(--zx-s1); padding-bottom: var(--zx-s1); }
body.zx .zx-pad-2{ padding-top: var(--zx-s2); padding-bottom: var(--zx-s2); }
body.zx .zx-pad-3{ padding-top: var(--zx-s3); padding-bottom: var(--zx-s3); }
body.zx .zx-pad-4{ padding-top: var(--zx-s4); padding-bottom: var(--zx-s4); }

/* ================================================================
   §6b REAL PAGE CONTAINER — PILOT-REVIEW-google-maps-2026-09-02.md §1:
   every page's own inline `.wrap{max-width:1200px}` (or 1080/1320) was
   the only rule in play — the kit never targeted `.wrap` itself, only
   the unused `.zx-container` utility. `body.zx .wrap` is (0,2,1),
   always higher than a page-local plain `.wrap{...}` (0,1,0), so this
   wins unconditionally, no source-order dependency.
   Two exclusions, both deliberate, not defects:
     :not(.nav)     — the compact header bar is 1080px by design
                       (`.nav{max-width:1080px}`, a different rule);
                       the kit only owns the page-body container.
     :not(article)  — `article.wrap` is a narrower reading-width
                       override used on long-form pages (line-oa-guide,
                       pin-google-maps); not the defect this fixes.
   ================================================================ */
body.zx .wrap:not(.nav):not(article){
  max-width: var(--zx-container);
}

/* ================================================================
   §6c SECTION PADDING LADDER — PILOT-REVIEW-google-maps-2026-09-02.md
   §1/§5: `.premium section{padding-block:var(--sec-y)}` (premium.css)
   resolves its clamp to one flat ~129.6px on every section because no
   kit rule ever set a real default. `body.zx section` is (0,1,2),
   which beats `.premium section` (0,1,1) unconditionally (compares
   type-selector count after classes tie), so this is the real base —
   normal (96) unless a modifier says otherwise.

   GATE EXCLUSION — same 12 components ops/shane_check.py SC19 pins to
   the /medical-tourism anchor (SC19_COMPONENTS). Several of them ARE
   `<section>` elements on this page (`.cmpbox` is one) and must keep
   whatever padding their own component CSS gives them — the kit does
   not touch a gate-checked class, ever.
   ================================================================ */
body.zx section:not(.cmpgrid):not(.cmpbox):not(.cmp-us):not(.cmp-lbl):not(.cmp-vs):not(.vs):not(.vs-col):not(.proofgrid):not(.answer-box):not(.mt-stat):not(.faq-item):not(.blk){
  padding-top: var(--zx-s2);
  padding-bottom: var(--zx-s2);
}
body.zx section.zx-strip:not(.cmpgrid):not(.cmpbox):not(.cmp-us):not(.cmp-lbl):not(.cmp-vs):not(.vs):not(.vs-col):not(.proofgrid):not(.answer-box):not(.mt-stat):not(.faq-item):not(.blk){
  padding-top: var(--zx-s1);
  padding-bottom: var(--zx-s1);
}
body.zx section.zx-major:not(.cmpgrid):not(.cmpbox):not(.cmp-us):not(.cmp-lbl):not(.cmp-vs):not(.vs):not(.vs-col):not(.proofgrid):not(.answer-box):not(.mt-stat):not(.faq-item):not(.blk){
  padding-top: var(--zx-s3);
  padding-bottom: var(--zx-s3);
}
body.zx section.zx-break:not(.cmpgrid):not(.cmpbox):not(.cmp-us):not(.cmp-lbl):not(.cmp-vs):not(.vs):not(.vs-col):not(.proofgrid):not(.answer-box):not(.mt-stat):not(.faq-item):not(.blk){
  padding-top: var(--zx-s4);
  padding-bottom: var(--zx-s4);
}

/* ================================================================
   §6d EYEBROW — PILOT-REVIEW-google-maps-2026-09-02.md §1: an eyebrow
   label reused the `<h2>`/`.h2` tag+class for its 12px uppercase look
   instead of a real heading. Retag the markup to `<p class="zx-eyebrow">`
   (SC10: same words, no copy change) and give it the exact visual
   treatment `.clients-wall h2,.clients-wall .h2` already ships
   (shared.css:299), so nothing repaints — only the semantics fix.

   2026-09-02 UPDATE (coordinator, about.html rollout gap): this block is
   shared across every page's `.clients-wall`, so retagging is a per-page
   decision, not a kit one — CSS-only route preferred instead of a
   broadcast markup change. The kit now owns BOTH markup states directly:
   a page that has NOT retagged still renders `.clients-wall h2`/`.h2`
   (shared.css:299's own values, duplicated here so the kit does not
   depend on that file); a page that HAS retagged (google-maps.html) gets
   the identical look from `.zx-eyebrow` below.

   font-weight IS 600, not shared.css:299's stated 700 — verified live:
   shared.css:77's global `h2,.h2{font-weight:600!important}` outranks
   shared.css:299's non-important 700 on any element that is an <h2> or
   carries `.h2`, so the un-retagged eyebrow has ALWAYS rendered at 600
   in practice. A bare `<p class="zx-eyebrow">` does not match `h2,.h2`,
   so without this line it would render at whatever weight came before
   this rule instead, breaking the "identical either way" goal this fix
   exists for. `!important` on 4 properties now (font-size, weight,
   color, family) — all 4 matching an `!important` shared.css already
   uses somewhere in this cascade for this exact element, not new ones. */
body.zx .clients-wall h2,
body.zx .clients-wall .h2,
body.zx .zx-eyebrow{
  font-family: "Sarabun", system-ui, sans-serif !important;
  font-size: 12px !important;
  font-weight: 600 !important;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted, #5D6B84) !important;
  margin-bottom: 0;
}
/* DARK-GROUND VARIANT — homepage evaluator gap (2026-09-02, index-v2.html):
   the light-ground `--muted`/`#5D6B84` above measures 2.83:1 / 3.10:1 against
   `--zx-ink` / `--zx-ink-deep` (index-v2's dark `.zx-sec.zx-ink-deep.zx-break`
   section) — fails AA and is genuinely hard to read. `--zx-muted-on-ink`
   (defined above in `body.zx{}`, §5c) was already sized for exactly this:
   7.45:1 on --zx-ink, 8.16:1 on --zx-ink-deep. Two hooks so any page can opt
   in without a markup change on pages that already use one of these
   conventions: `.zx-break` (the dark-section modifier this kit ships) and
   `[data-ground="dark"]` (an attribute hook for pages/components that mark
   dark context that way instead). Specificity (0,3,1) beats the base
   `body.zx .zx-eyebrow` (0,2,1) above unconditionally, and both are
   `!important` on `color` so this cannot lose to a load-order accident.
   DOCUMENTED CONTRACT: a page-local override of eyebrow colour on a dark
   section must be `body.zx`-prefixed (>= this rule's specificity) to win —
   a bare `.some-dark-section .zx-eyebrow{color:...}` will lose to this. */
body.zx .zx-break .zx-eyebrow,
body.zx [data-ground="dark"] .zx-eyebrow{
  color: var(--zx-muted-on-ink) !important;
}

/* ================================================================
   §7 RADIUS + SHADOW UTILITIES
   ================================================================ */
body.zx .zx-radius-pill{ border-radius: var(--zx-r-pill); }
body.zx .zx-radius-1{ border-radius: var(--zx-r-1); }
body.zx .zx-radius-2{ border-radius: var(--zx-r-2); }
body.zx .zx-radius-3{ border-radius: var(--zx-r-3); }
body.zx .zx-shadow-1{ box-shadow: var(--zx-sh-1); }
body.zx .zx-shadow-2{ box-shadow: var(--zx-sh-2); }
body.zx .zx-shadow-3{ box-shadow: var(--zx-sh-3); }

/* ================================================================
   §8 THE .lit RING REPLACEMENT — scoped to `.zx .lit` ONLY.
   Never a bare `.lit` rule (that would edit every page that ships
   the amber ring outside this kit). Reference gap #5 in
   design/ANGA-REFERENCE-SPEC-2026-09-02.md §8(a): replace the amber
   `0 0 0 1.5px rgba(255,184,37,.22)` ring with a real shadow —
   here, shadow tier 2 (large soft panel).

   WHY THIS SELECTOR SHAPE — PILOT-REVIEW-google-maps-2026-09-02.md §3
   found the ring still on 9 elements despite the rule below existing.
   Two independent causes, both fixed here:

   1. SPECIFICITY LOSS ON SPECIFIC COMPONENTS. glow3.css's ring rule is
      one comma-joined selector list; the browser scores each selector
      in it separately per matching element. `body .lit:not(.glow-card)`
      is (0,2,1), tied with the old `body.zx .lit` (0,2,1) — a tie
      zx-system.css should win by loading last. But the SAME glow3 rule
      also lists `.premium .step:not(.glow-card)`, `.premium .stat-tile
      :not(.glow-card)`, `.premium .tcard:not(.glow-card)`, `.premium
      .ba-card:not(.glow-card)` etc — each (0,3,0) — which beats (0,2,1)
      outright regardless of source order. `.lit.lit` (repeating the
      class) plus the 12-class gate exclusion below pushes this rule's
      own specificity past every one of those, so it wins unconditionally.

   2. THE is-lit BLOOM COMES BACK AFTER SCROLL-IN. glow3.css's entrance
      bloom `body .lit.is-lit:not(.glow-card)` is (0,3,1) — also higher
      than the old (0,2,1) rule, so the ring reappeared the moment
      glow3.js added `.is-lit` on intersection, even on elements the
      base rule had already fixed at rest. Because THIS rule's selector
      does not mention `.is-lit` at all, its (very high, see below)
      specificity applies in every state — rest, is-lit and hover alike
      — so there is nothing left for glow3's is-lit rule to re-win.

   GATE EXCLUSION — ops/shane_check.py SC19_COMPONENTS, the exact 12
   classes SC19 pins to the /medical-tourism anchor for both
   borderRadius (4 corners) and boxShadow. A gate-checked element must
   render pixel-identical to the anchor; this kit must never restyle
   one, which is why the whole gate list is `:not()`-excluded from both
   properties below and glow3.css is left to own them, unopposed, in
   every state (rest / is-lit / hover).
   ================================================================ */
body.zx .lit.lit:not(.cmpgrid):not(.cmpbox):not(.cmp-us):not(.cmp-lbl):not(.cmp-vs):not(.vs):not(.vs-col):not(.proofgrid):not(.answer-box):not(.mt-stat):not(.faq-item):not(.blk){
  box-shadow: var(--zx-sh-2);
  border-radius: var(--zx-r-1);
  transition: box-shadow var(--zx-dur-interact) var(--zx-ease);
}
body.zx .lit.lit:not(.cmpgrid):not(.cmpbox):not(.cmp-us):not(.cmp-lbl):not(.cmp-vs):not(.vs):not(.vs-col):not(.proofgrid):not(.answer-box):not(.mt-stat):not(.faq-item):not(.blk):hover{
  box-shadow: var(--zx-sh-3);
}
@media (prefers-reduced-motion: reduce){
  body.zx .lit.lit:not(.cmpgrid):not(.cmpbox):not(.cmp-us):not(.cmp-lbl):not(.cmp-vs):not(.vs):not(.vs-col):not(.proofgrid):not(.answer-box):not(.mt-stat):not(.faq-item):not(.blk){ transition: none; }
}

/* ================================================================
   §9 ENTRANCE MOTION — anything using `.zx-reveal` gets the >=0.9s
   entrance floor; interaction (buttons, tabs, carousel controls)
   stays on the 0.3s interaction duration (applied per-component
   below and in zx-carousel.js / zx-tabs.js).
   ================================================================ */
body.zx .zx-reveal{
  opacity: 0;
  transform: translateY(16px);
  transition: opacity var(--zx-dur-enter) var(--zx-ease),
              transform var(--zx-dur-enter) var(--zx-ease);
}
body.zx .zx-reveal.is-in{
  opacity: 1;
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce){
  body.zx .zx-reveal{ transition: none; opacity: 1; transform: none; }
}

/* ================================================================
   KIT B — .zx-carousel — SITE-REDIRECTION slice 5.
   Mechanism per ANGA §3.1: transform-based track (translate3d),
   arrow buttons, pointer drag, NO autoplay, NO dots, NO CSS
   scroll-snap (scroll-snap-type stays `none` throughout — the
   snap-to-card behaviour lives in JS as a velocity-based settle,
   never as `scroll-snap-type`).
   ================================================================ */
body.zx .zx-carousel{
  position: relative;
  --zx-gap: 20px;
}
body.zx .zx-carousel-viewport{
  overflow: hidden;      /* NOT overflow-x:auto — this is not a scroll container */
  scroll-snap-type: none; /* explicit: no CSS scroll-snap, ANGA §3.1 / §3.8 */
  touch-action: pan-y;
  cursor: grab;
}
body.zx .zx-carousel-viewport.is-dragging{ cursor: grabbing; }
body.zx .zx-carousel-track{
  display: flex;
  gap: var(--zx-gap);
  will-change: transform;
  transition: transform var(--zx-dur-interact) var(--zx-ease);
}
body.zx .zx-carousel-track.is-dragging{ transition: none; }
@media (prefers-reduced-motion: reduce){
  body.zx .zx-carousel-track{ transition: none; }
}
body.zx .zx-card{
  flex: 0 0 clamp(240px, 78%, 320px);
  background: #fff;
  border-radius: var(--zx-r-1);
  box-shadow: var(--zx-sh-1);
  padding: 28px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
@media (min-width: 640px){
  body.zx .zx-card{ flex-basis: clamp(280px, 40%, 340px); }
}
body.zx .zx-card-num{
  font-family: var(--zx-font-display);
  font-weight: 700;
  font-size: 44px;
  line-height: 1.1;
  color: inherit;
}
body.zx .zx-card-num small{ font-size: 18px; font-weight: 700; }
body.zx .zx-card-label{ font-size: 16px; line-height: 1.5; }

body.zx .zx-carousel-controls{
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  margin-top: 16px;
}
body.zx .zx-arrow{
  width: 44px; height: 44px;
  border-radius: var(--zx-r-pill);
  background: #fff;
  box-shadow: var(--zx-sh-1);
  border: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
  cursor: pointer;
  transition: box-shadow var(--zx-dur-interact) var(--zx-ease),
              background var(--zx-dur-interact) var(--zx-ease);
}
body.zx .zx-arrow:hover{ box-shadow: var(--zx-sh-2); }
body.zx .zx-arrow:focus-visible{ outline: 2px solid currentColor; outline-offset: 2px; }
body.zx .zx-arrow[disabled]{ opacity: .35; cursor: default; box-shadow: none; }

/* mobile: cards + arrows never force page-width overflow (tasklist req: no
   390 overflow) — the viewport clips, the track is free to extend past it */
body.zx .zx-carousel-viewport{ max-width: 100%; }

/* ================================================================
   KIT C — .zx-tabs — SITE-REDIRECTION slice 6.
   Mechanism per ANGA §3.2: CSS position:sticky; top:-20px; z-index:20,
   white rounded-full rail, JS-applied .active on click + hash (our
   version also runs a working IntersectionObserver scroll-spy — the
   reference's does not, per ANGA §3.2 "There is no working scroll-spy").

   STICKY RULE — READ BEFORE PLACING THIS COMPONENT:
   `position:sticky` breaks (silently, no console warning) when ANY
   ancestor between `.zx-tabs` and the scrolling viewport has:
     - overflow != visible (hidden / auto / scroll) — clips the sticky
       travel range to that ancestor's box instead of the viewport, OR
     - a `transform`, `filter`, `perspective`, or `will-change` value
       that establishes a new containing block — sticky then sticks
       relative to THAT ancestor, not the viewport.
   `position:relative` on an ancestor does NOT break sticky by itself.
   What DOES break it is a same-specificity rule that sets `position`
   on the sticky element ITSELF later in source order — this is the
   documented services.html defect (ANGA spec §7 DEFECT row):
   `.svc-tabs{position:sticky}` (premium.css:419) loses to
   `.lit{position:relative}` (glow3.css:45) because `.lit` is also
   applied to the same <nav> element and both rules share (0,1,0)
   specificity, so source order decides and glow3.css loads after
   premium.css. FIX: never put `.lit` (or any other class carrying a
   bare `position:` declaration) on the `.zx-tabs` element itself, and
   never nest `.zx-tabs` inside a `.zx-carousel-viewport`,
   `.svc-rail-wrap`, or any other overflow:hidden/auto ancestor.
   ================================================================ */
body.zx .zx-tabs{
  position: sticky;
  /* ANGA's literal `top:-20px` (see block comment above) parks the rail
     20px ABOVE the viewport top before any header is accounted for —
     the direct root cause of the sitewide occlusion in §3b. Every one
     of the 19 rail-bearing pages already overrides this with its own
     page-local `body.zx .zx-tabs{top:...}` rule, but those overrides
     are same-specificity (0,1,1) as this one, so whichever stylesheet
     loads LAST wins the cascade — some pages link zx-system.css BEFORE
     their own <style> block (page wins, fine), others link it AFTER
     (kit wins, e.g. factory.html/dorm.html link order — those pages'
     own `top:` intent is silently discarded and this broken -20px
     default was what actually rendered). Defaulting to the measured,
     header-clearing token here fixes every page that was losing to
     the kit by load order, without touching any page file. Pages that
     already set their own clearing value keep it (this rule carries
     no !important — see §6c below for the enforcement pass that does,
     scoped to mobile only, per ROLLOUT-REVIEW-2026-09-02.md §5#1). */
  top: var(--zx-rail-top, 80px);
  z-index: 20;
  display: flex;
  justify-content: center;
  padding: 20px 0;
}
body.zx .zx-tabs-rail{
  display: inline-flex;
  gap: 4px;
  background: #fff;
  border-radius: var(--zx-r-pill);
  box-shadow: var(--zx-sh-1);
  padding: 9px;
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}
body.zx .zx-tabs-rail::-webkit-scrollbar{ display: none; }

/* ================================================================
   §6c STICKY RAIL — MOBILE ENFORCEMENT (ROLLOUT-REVIEW-2026-09-02.md
   §4/§5#1). The default above is not enough: 18 of 19 pages carry
   their OWN page-local `top:` override (`-20px`/`-1px`/`41px`/`48px`/
   `60px`/`61px`, none of them clearing the 61px mobile header — see
   the measured table in §3b's referenced review) at the SAME (0,1,1)
   specificity as the base rule, so cascade order (not correctness)
   decides which value renders. This rule wins unconditionally:
     - doubled class (`.zx-tabs.zx-tabs`) pushes specificity to
       (0,2,0)/(0,2,1) — strictly higher than any page's (0,1,1) or
       (0,1,0) override, `!important` or not;
     - `!important` also beats every page rule verified NOT to use
       `!important` today (all 19 — grepped 2026-09-02) and future-
       proofs against one that does, since `!important` vs
       `!important` still resolves by specificity, where this rule
       already wins.
   Verified against the two explicit distrust cases named in the
   review: case-mdcc.html (`top:48px`, still 154px/61px = occluded
   pre-fix) and check.html (`top:61px`, the one page that already
   cleared, by ~1.2px, pre-fix) — this rule supersedes both to the
   same shared 62px value; check.html loses nothing (61px was never
   a deliberate token, just an accidental clear).
   ================================================================ */
@media (max-width: 860px){
  body.zx nav.zx-tabs.zx-tabs,
  body.zx .zx-tabs.zx-tabs{
    top: var(--zx-rail-top-m, 62px) !important;
  }
}
@media (min-width: 861px){
  body.zx nav.zx-tabs.zx-tabs,
  body.zx .zx-tabs.zx-tabs{
    top: var(--zx-rail-top, 80px) !important;
  }
}
body.zx .zx-tab{
  border: 0;
  background: transparent;
  color: inherit;
  font-family: var(--zx-font-body);
  font-weight: 700;
  font-size: 16px;
  white-space: nowrap;
  border-radius: var(--zx-r-pill);
  padding: 8px 16px;
  cursor: pointer;
  transition: color var(--zx-dur-interact) var(--zx-ease),
              background-color var(--zx-dur-interact) var(--zx-ease);
}
body.zx .zx-tab:hover{ background: rgba(11,31,42,.06); }
body.zx .zx-tab.active,
body.zx .zx-tab.active:hover{
  background: #000;
  color: #fff;
}
/* about.html rollout gap (2026-09-02): pages that build the tab rail with <a>
   instead of <button> rendered the pills underlined — `.zx-tab` never set
   text-decoration, which only matters on an anchor. Scoped to `a.zx-tab`
   only so the <button> variant (which needs no such reset) is untouched. */
body.zx a.zx-tab{ text-decoration: none; }
body.zx a.zx-tab:focus-visible{ outline: 2px solid currentColor; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce){
  body.zx .zx-tab{ transition: none; }
}

/* ================================================================
   §8 HERO PHOTO TREATMENT — ROLLOUT-REVIEW-2026-09-02.md §5#4:
   "Every hero photo ... is crushed into a flat green-monochrome
   slab" by the page-local `.hero-duo img{filter:url(#duotone-inkmint)}`
   pattern (20 pages, each carrying its own duplicate <svg><filter
   id="duotone-inkmint">> def + `.hero-duo` rule — google-maps.html,
   factory.html, clinic.html, restaurant.html, etc; about.html is the
   one deliberate, considered use, not a copy-paste default).

   CONTRACT for pages (kit-only change — no page has been edited by
   this revision; a sweep agent applies this to the 20 pages):
     1. Drop the page-local `.hero-duo img{filter:...url(#duotone-
        inkmint)}` rule AND the inline `<filter id="duotone-inkmint">`
        SVG def entirely.
     2. Add `zx-hero-photo` to the hero <img> — natural colour,
        the new site default (replaces the flat duotone wash with a
        light saturation/contrast/brightness grade that keeps faces
        and product detail legible).
     3. Give the hero media wrapper `position:relative` and add an
        empty element (e.g. `<div class="zx-hero-scrim"></div>`)
        immediately after the <img>, inside that wrapper, for the
        text-legibility darkening the duotone used to provide
        incidentally.
     4. ONLY when a page's specific source photo is too busy for
        plain colour-correction to read cleanly under real headline
        text — add `zx-hero-photo--soft` alongside `zx-hero-photo`.
        This is about.html's own existing treatment
        (`brightness(.6) contrast(1.25)` + the duotone matrix), kept
        verbatim as an explicit per-page opt-in, not the default.
   ================================================================ */
body.zx .zx-hero-photo{
  filter: saturate(.9) contrast(1.04) brightness(.96);
}
body.zx .zx-hero-photo--soft{
  filter: brightness(.6) contrast(1.25) url(#duotone-inkmint);
}
body.zx .zx-hero-scrim{
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(11,31,42,.12) 0%, rgba(11,31,42,.28) 55%, rgba(11,31,42,.55) 100%);
  pointer-events: none;
}
