/* ============================================================
   SIPADAN.COM â€” UNIFIED RESPONSIVE STYLESHEET (mobile-first)
   Polish pass: Revision 1b folded into base (no trailing
   overrides), modern 2026 finish on the preserved skeleton.
   Requires: tokens.css loaded first.
   Desktop breakpoint: 1024px (tablets use the hamburger).
   ============================================================ */

/* ---------- 0. Base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { transition: none !important; animation: none !important; }
}
body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--fs-base);
  line-height: var(--lh-body);
  color: var(--c-ink);
  background: var(--c-white);   /* white base; sand appears as section bands */
}
img, svg, video { max-width: 100%; height: auto; }
a { color: var(--c-navy); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--c-teal-dark); }
:focus-visible { outline: 3px solid var(--c-teal); outline-offset: 2px; border-radius: var(--radius-sm); }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  color: var(--c-navy);
  line-height: var(--lh-tight);
  font-weight: 570;                 /* Fraunces variable axis: elegant, not heavy */
  font-variation-settings: "opsz" 40;
  letter-spacing: -0.01em;
  margin: var(--sp-6) 0 var(--sp-3);
  padding: 0;
}
h1 { font-size: var(--fs-2xl); margin-top: 0; letter-spacing: -0.015em; font-variation-settings: "opsz" 72; }
h2 { font-size: var(--fs-xl); }
h3 { font-size: var(--fs-lg); }
h2, h3 { scroll-margin-top: calc(var(--header-h) + var(--sp-5)); } /* sticky-header safe anchors */
h1, h2, h3, h4, h1 a, h2 a, h3 a, h4 a { text-decoration: none; }
h1 a:hover, h2 a:hover, h3 a:hover, h4 a:hover { text-decoration: none; color: var(--c-teal-dark); }
.content-card > h2 { margin-top: var(--sp-7); }
.content-card > h2::after {
  content: ""; display: block; width: 56px; height: 2px;
  background: var(--c-gold);
  margin-top: var(--sp-3); opacity: 0.8;
}
.content-card > h2.text-center::after,
.content-card > h1.text-center::after { margin-left: auto; margin-right: auto; }
p  { margin: 0 0 var(--sp-4); }
ul, ol { margin: 0 0 var(--sp-4); padding-left: 1.4em; }
li { margin-bottom: var(--sp-1); }
hr { border: 0; border-top: 1px solid var(--c-hairline-gold); margin: var(--sp-7) 0; }

.page-plain {
  /* info/article/form/thank-you/contact pages: cleaner modern headings.
     Plus Jakarta Sans is Latin-scoped; CN/RU glyphs fall back per-glyph
     to Source Sans Pro -> system, so non-Latin builds never break. */
  --font-display: 'Plus Jakarta Sans', 'Source Sans Pro', -apple-system, 'Segoe UI', Arial, sans-serif;
}
.page-plain h1, .page-plain h2, .page-plain h3, .page-plain h4 {
  font-weight: var(--fw-bold);
  font-variation-settings: normal;
  letter-spacing: -0.01em;
}

.skip-link {
  position: absolute; left: -999px; top: 0; z-index: 200;
  background: var(--c-navy); color: #fff; padding: var(--sp-2) var(--sp-4);
  border-radius: 0 0 var(--radius-sm) 0;
}
.skip-link:focus { left: 0; }

/* Inline SVG icon primitive (license-clean, multilingual-neutral) */
.icon {
  width: 1.25em; height: 1.25em;
  display: inline-block; vertical-align: -0.22em;
  fill: none; stroke: currentColor;
  stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
  flex: 0 0 auto;
}
.icon--fill { fill: currentColor; stroke: none; }

/* ---------- 1. Header (watery band + native transparent logo, NO glass) ---------- */
.site-header {
  background: var(--c-band-fall) url('/images/header/header-band.webp') center/cover no-repeat;
  border-bottom: 1px solid rgba(10,31,51,0.25);
  box-shadow: inset 0 -1px 0 rgba(255,255,255,0.25), var(--shadow-sm);
  position: sticky;            /* sticky on desktop AND mobile */
  top: 0;
  z-index: 80;                 /* above content + fly-outs; drawer (100) still covers it */
}
.site-header__inner {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 2px var(--gutter);         /* tight padding around the logo */
  min-height: var(--header-h);
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--sp-3);
}
.site-header__logo { display: inline-flex; align-items: center; flex: 0 0 auto; }
@media (min-width: 1024px) {
  .site-header__logo img { transition: filter var(--t-fast) var(--ease); }
  .site-header__logo:hover img { filter: brightness(1.05); }
}
.site-header__logo img { display: block; width: auto; height: 74px; }     /* mobile: bigger */
@media (min-width: 1024px) { .site-header__logo img { width: 250px; height: 129px; } } /* native, never resized */

/* right cluster: CTA (+ mobile mini flags + hamburger) */
.site-header__actions { margin-left: auto; display: flex; align-items: center; gap: var(--sp-3); }
/* header CTA inherits the enriched .btn--cta gradient/border/shadow */
@media (max-width: 1023px) {
  /* intentional exception to the 17px floor: compact single-line header CTA */
  .site-header .site-header__cta { display: none; }   /* removed on mobile (beats .btn) */
}

/* mobile-only EN+CN mini flags, side by side, centred with the menu icon */
.header-mini-flags { display: none; flex-direction: row; gap: 5px; align-items: center; align-self: center; }
@media (max-width: 1023px) { .header-mini-flags { display: flex; } }

/* ============ Build 14.4: mobile language MENU (all 6 languages, no flags) ============
   A frosted pill labeled "EN" opens a small language card. <details> = works
   without JS; site.js closes it on outside taps. Desktop keeps its flag rows. */
.lang-menu { position: relative; display: none; align-self: center; z-index: 95; }
.lang-menu[open]::before {           /* page dims 5% while the menu is open */
  content: ""; position: fixed; inset: 0; z-index: -1;
  background: rgba(0,0,0,.05); pointer-events: none;
}
@media (max-width: 1023px) { .lang-menu { display: block; } }
.lang-menu { margin-right: 10px; }   /* clear separation from the menu icon */
.lang-menu__btn {
  display: inline-flex; align-items: center; gap: 6px;
  list-style: none; cursor: pointer; user-select: none;
  font-size: 14px; font-weight: 500; letter-spacing: .06em; line-height: 1;
  color: var(--c-deep); opacity: .85;               /* secondary to the menu icon */
  padding: 6px 12px; border-radius: 999px;
  background: rgba(255,255,255,.13);                /* barely lighter than the sea */
  border: 1px solid rgba(255,255,255,.32);
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
}
.lang-menu__btn::-webkit-details-marker { display: none; }
.lang-menu__chev { width: 11px; height: 8px; transition: transform var(--t-fast) var(--ease); }
.lang-menu[open] .lang-menu__chev { transform: rotate(180deg); }
.lang-menu__list {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 90;
  margin: 0; padding: var(--sp-2); list-style: none;
  min-width: 158px;
  background: var(--c-white);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius);
  box-shadow: 0 14px 38px rgba(6, 18, 30, .22);
}
.lang-menu__list a {
  display: block; padding: 9px 12px; border-radius: var(--radius-sm);
  font-size: var(--fs-base); text-decoration: none; color: var(--c-ink);
}
.lang-menu__list a:hover, .lang-menu__list a:focus-visible { background: var(--c-sand); color: var(--c-navy); }

/* form pages: the 6-flag row under the CTA is desktop-only (mobile uses the menu) */
@media (max-width: 1023px) { .header-flags { display: none; } }

/* Hamburger: frameless, dark ink on the light watery band */
.nav-toggle {
  display: inline-flex; align-items: center; justify-content: center; align-self: center;
  background: transparent;
  border: 0;                                   /* no frame (deliberate) */
  color: var(--c-deep);
  padding: 9px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background var(--t-fast) var(--ease);
}
.nav-toggle:hover { background: rgba(255,255,255,0.35); }
.nav-toggle__bars { display: inline-block; width: 22px; }
.nav-toggle__bars span {
  display: block; height: 2.5px; background: var(--c-deep); border-radius: 2px;
  margin: 4px 0; transition: transform var(--t-fast) var(--ease), opacity var(--t-fast);
}
[aria-expanded="true"] .nav-toggle__bars span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
[aria-expanded="true"] .nav-toggle__bars span:nth-child(2) { opacity: 0; }
[aria-expanded="true"] .nav-toggle__bars span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }
@media (min-width: 1024px) { .nav-toggle { display: none; } }

/* Language flags: rectangles; hover lightens only â€” never moves */
.lang-flag {
  width: 30px; height: 20px; display: inline-block; vertical-align: middle;
  border-radius: 3px; overflow: hidden; line-height: 0;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.08);          /* thin edge so light flags read on any bg */
  transition: filter var(--t-fast) var(--ease);
}
.lang-flag svg { display: block; width: 100%; height: 100%; }
.lang-flag:hover, .lang-flag:focus-visible { filter: brightness(1.08); transform: none; }

/* ---------- 2. Permit bar (254 pill + bold lead + flags right on desktop) ---------- */
.permit-bar {
  background: var(--c-white);
  border-bottom: 1px solid var(--c-hairline-gold);
}
.permit-bar__inner {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: var(--sp-3) var(--gutter);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
}
.permit-bar__pill {
  flex: 0 0 auto;
  background: linear-gradient(180deg, #f7b165, var(--c-btn-book)); color: var(--c-deep);
  border: 1px solid var(--c-btn-border);
  font-weight: var(--fw-bold); font-size: var(--fs-sm); line-height: 1.7;
  border-radius: var(--radius-pill); padding: 2px 11px;
  box-shadow: 0 1px 3px rgba(207,127,40,0.3), inset 0 1px 0 rgba(255,255,255,0.42);
}
.permit-bar__hook { flex: 1 1 260px; margin: 0; font-size: var(--fs-sm); color: var(--c-ink); }
.permit-bar__hook strong { color: var(--c-navy); }
/* mobile: tighten spacing so the bold line fits on ONE line (same font size) */
@media (max-width: 559px) {
  .permit-bar__inner { padding: var(--sp-3) var(--sp-2); gap: 5px; }
  .permit-bar__pill { padding: 2px 8px; }
  .permit-bar__hook { flex: 1 1 auto; letter-spacing: -0.01em; }
}
.permit-bar__flags { flex: 0 0 auto; display: flex; gap: 6px; margin-left: auto; }
.permit-bar__flags .lang-flag { width: 30px; height: 20px; }   /* Build 14.5: +4px height (owner) */
.permit-bar__flags .lang-flag:hover, .permit-bar__flags .lang-flag:focus-visible { filter: brightness(1.25); }
@media (max-width: 1023px) { .permit-bar__flags { display: none; } }
@media (max-width: 767px)  { .permit-bar__more  { display: none; } } /* mobile: lead line only */

/* ---------- 3. Shell: one fluid container, two columns at >=1024px ---------- */
.shell {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: var(--sp-6) var(--gutter) var(--sp-8);
}
.shell--with-sidebar { display: block; }
.shell__main { min-width: 0; }
@media (min-width: 1024px) {
  .shell--with-sidebar {
    display: grid;
    grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
    gap: var(--sp-6);
    align-items: start;
  }
  /* sticky, no overflow clipping â€” the fly-out must stay in the foreground */
  .shell__sidebar { position: sticky; top: var(--sp-4); align-self: start; z-index: 85  /* WALK-R1 â‘¡: was 20 â€” the sidebar is a stacking context, so the
      .site-nav__sub flyout (z:90 INSIDE it) was capped at 20 and clipped under the
      header banner (z:80). 85 floats the whole sidebar context above the banner,
      below the lang-menu (95) and the mobile drawer (100). */; }
}

/* ---------- 4. Navigation: one markup â€” drawer (<1024) / sidebar + fly-out (>=1024) ---------- */
.site-nav { font-size: 17px; }                      /* 17px floor applies to menus too */
.site-nav__list { list-style: none; margin: 0; padding: 0; }
.site-nav__item + .site-nav__item { border-top: 1px solid var(--c-hairline); }
.site-nav__row { display: flex; align-items: stretch; }
.site-nav__link {
  flex: 1 1 auto;
  display: block;
  padding: var(--sp-2) var(--sp-4);
  color: var(--c-navy);
  text-decoration: none;
  font-weight: var(--fw-semi);
  text-transform: uppercase;
  letter-spacing: 0.045em;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.site-nav__link:hover { background: var(--c-sand); color: var(--c-navy); }
/* Two-line nav item: bold label + a muted, sentence-case tagline underneath, both in the
   same <a>. The tagline is visibly a subtitle (smaller, lighter, not uppercase) so it never
   reads as a second menu entry. */
.site-nav__label { display: block; }
.site-nav__tagline {
  display: block;
  margin-top: 2px;
  /* Sized to the legacy nav's sub-text proportion (12px sub / 16px label â‰ˆ 0.78), scaled to
     the new nav â€” the old 0.68em read as unreadably small. Readable slate, not pale grey. */
  font-size: 0.78em;
  font-weight: var(--fw-reg);
  text-transform: none;
  letter-spacing: 0;
  color: #4a5b6d;
  line-height: 1.25;
}
.site-nav__link--cta .site-nav__tagline { color: inherit; opacity: 0.8; }
.site-nav__item.active > .site-nav__row .site-nav__link,
.site-nav__item.active-parent > .site-nav__row .site-nav__link {
  color: var(--c-navy);
  box-shadow: inset 2px 0 0 var(--c-gold);
  background: var(--c-surface-2);
}
.site-nav__link--cta { color: var(--c-gold-text); }
.site-nav__sub { font-size: 17px; }
.site-nav__sub li.active > a { color: var(--c-teal-dark); font-weight: var(--fw-semi); }

/* --- <1024px: off-canvas drawer with accordions --- */
@media (max-width: 1023px) {
  .site-nav {
    position: fixed;
    inset: 0 0 0 auto;
    width: min(320px, 86vw);
    background: var(--c-white);
    box-shadow: var(--shadow-lg);
    transform: translateX(105%);
    transition: transform var(--t-med) var(--ease);
    z-index: 100;
    overflow-y: auto;
    padding-top: var(--sp-7);
  }
  .site-nav.active { transform: translateX(0); }
  .site-nav__close {
    position: absolute; top: var(--sp-3); right: var(--sp-3);
    background: transparent; border: 0; cursor: pointer;
    font-size: 1.6rem; line-height: 1; color: var(--c-navy);
    padding: var(--sp-2); border-radius: var(--radius-sm);
  }
  .site-nav__link { padding: var(--sp-3) var(--sp-4); } /* roomier tap targets in the drawer */
  .site-nav__disclosure {
    flex: 0 0 48px;
    display: inline-flex; align-items: center; justify-content: center;
    background: transparent; border: 0;
    border-left: 1px solid var(--c-hairline);
    cursor: pointer; color: var(--c-navy);
  }
  .site-nav__disclosure::before {
    content: "";
    width: 8px; height: 8px;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg) translateY(-2px);
    transition: transform var(--t-fast) var(--ease);
  }
  .site-nav__disclosure[aria-expanded="true"]::before { transform: rotate(-135deg) translateY(-2px); }
  .site-nav__sub {
    list-style: none; margin: 0;
    padding: var(--sp-1) 0 var(--sp-2);
    background: var(--c-surface-2);
    display: none;
  }
  .site-nav__item.open > .site-nav__sub { display: block; }
  .site-nav__sub a {
    display: block;
    padding: var(--sp-2) var(--sp-4) var(--sp-2) var(--sp-5);
    color: var(--c-ink);
    text-decoration: none;
    font-weight: var(--fw-reg);
    text-transform: none;
    letter-spacing: 0;
  }
  .site-nav__sub a:hover { color: var(--c-teal-dark); }

  .nav-overlay {
    position: fixed; inset: 0; z-index: 90;
    display: block;                       /* fix: overlay actually renders */
    background: rgba(2, 33, 63, 0.45);
    opacity: 0; pointer-events: none;
    transition: opacity var(--t-med) var(--ease);
  }
  .nav-overlay.active { opacity: 1; pointer-events: auto; }
}

/* --- >=1024px: persistent sidebar card; submenus FLY OUT right on hover, no arrows --- */
@media (min-width: 1024px) {
  .site-nav {
    background: var(--c-white);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    border: 1px solid var(--c-hairline);
    overflow: visible;                    /* let the fly-out escape the card */
  }
  .site-nav__list { border-radius: inherit; overflow: visible; }  /* NEVER hidden â€” it clips the fly-out */
  .site-nav__item { position: relative; }
  .site-nav__item:first-child .site-nav__link { border-radius: var(--radius) var(--radius) 0 0; }
  .site-nav__item:last-child  .site-nav__link { border-radius: 0 0 var(--radius) var(--radius); }
  /* Desktop first-level cells were too tall â€” halve the vertical padding (0.5remâ†’0.25rem) so
     the whole first-level block is ~15% shorter (~550px â†’ ~470px). Roomy drawer taps (Â§ mobile)
     are unaffected; the fly-out panels keep their own padding. Verify no clip at 1024â€“1280px. */
  .site-nav__link { padding-top: var(--sp-1); padding-bottom: var(--sp-1); }
  .site-nav__close, .site-nav__disclosure, .nav-overlay { display: none; }

  .site-nav__sub {
    position: absolute; left: 100%; top: 0; min-width: 230px; margin: 0;
    list-style: none;
    background: var(--c-white);
    border: 1px solid var(--c-hairline);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-lift);
    padding: var(--sp-2);
    z-index: 90;
    /* subtle fade/slide (killed globally by prefers-reduced-motion) */
    display: block;
    opacity: 0; visibility: hidden; pointer-events: none;
    transform: none;               /* fade only, no sideways slide */
    transition: opacity var(--t-fast) var(--ease),
                transform var(--t-fast) var(--ease),
                visibility 0s linear var(--t-fast);
  }
  /* Reveal on mouse HOVER, or on KEYBOARD focus only. Using :has(:focus-visible) instead of
     :focus-within is the fix for the two-panels-open bug: a right-click / mouse-click focuses
     the link (contextmenu path) but is NOT :focus-visible, so it no longer PINS the panel open
     while the mouse hovers another item. Keyboard Tab sets :focus-visible, so Tab still opens
     the panel. At most one panel is ever revealed. (JS in site.js blurs a mouse-focused child
     on mouseleave as a belt-and-braces fallback for browsers without :has()/:focus-visible.) */
  .site-nav__item:hover > .site-nav__sub,
  .site-nav__item:has(:focus-visible) > .site-nav__sub {
    opacity: 1; visibility: visible; pointer-events: auto;
    transition-delay: 0s;
  }
  .site-nav__item:hover > .site-nav__row .site-nav__link { background: var(--c-surface-2); color: var(--c-teal-dark); }
  .site-nav__sub li { margin: 0; }
  .site-nav__sub li + li { border-top: 1px solid var(--c-hairline); }   /* thin separators */
  .site-nav__sub a {
    display: block; padding: 7px 16px; line-height: 1.3;
    white-space: nowrap; text-decoration: none;
    color: var(--c-ink); font-weight: var(--fw-reg);
    text-transform: none; letter-spacing: 0;
    border-radius: 6px;
  }
  .site-nav__sub a:hover { text-decoration: none; color: var(--c-navy); background: var(--c-sand); }
  .site-nav__sub--dense a { padding-top: 5px; padding-bottom: 5px; }     /* long menus only */
}

/* Sidebar secondary module: Top 5 dive sites */
.top10-card {
  background: var(--c-white);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  margin-top: var(--sp-4);
  padding: var(--sp-4);
}
.top10-card h2 {
  /* TYPOGRAPHY unified with .content-card > h2 (A9): same display family + weight (inherited
     from the h1â€“h4 rule) and the same gold accent-bar underline, scaled to the sidebar card.
     The card width is unchanged â€” only the heading treatment is harmonised. */
  font-size: var(--fs-md);
  margin: 0 0 var(--sp-3);
}
.top10-card h2::after {
  content: ""; display: block; width: 40px; height: 2px;
  background: var(--c-gold);
  margin-top: var(--sp-2); opacity: 0.8;
}
.top10-card ul { list-style: none; margin: 0; padding: 0; font-size: var(--fs-sm); }
.top10-card li { padding: var(--sp-1) 0; border-bottom: 1px dashed var(--c-hairline); margin: 0; }
.top10-card li:last-child { border-bottom: 0; }
.top10-card a { text-decoration: none; transition: color var(--t-fast) var(--ease); }
.top10-card a:hover { text-decoration: none; color: #3f8fb8; }
@media (max-width: 1023px) { .shell__sidebar .top10-card { display: none; } }

/* ---------- 5. Content card (scuba depth) ---------- */
.content-card {
  position: relative;
  background: var(--c-white);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--sp-6) var(--gutter) var(--sp-7);
}
@media (min-width: 1024px) { .content-card { padding: var(--sp-6) var(--sp-7) var(--sp-7); } }
.content-card > h1:first-child { margin-top: 0; }

/* Z3-3 (owner): on the inquiry forms the eyebrow carries the FUNCTION label ("Booking &
   enquiries") now that the h1 is the resort name alone, so it is stepped up one size to
   make the page's purpose read clearly. Scoped to the form hero â€” the site-wide eyebrow
   (course pages, resort pages) is unchanged. */
.eyebrow--form {
  font-size: var(--fs-sm);
  letter-spacing: 0.10em;
}

.eyebrow {
  display: block;
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--c-gold-text);
  margin-bottom: var(--sp-3);
}
.wave-rule {
  border: 0; height: 14px; margin: var(--sp-6) 0;
  background: left/24px 14px repeat-x
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='14'%3E%3Cpath d='M0 7 q6 -7 12 0 t12 0' fill='none' stroke='%2312a3b8' stroke-width='1.6' opacity='0.4'/%3E%3C/svg%3E");
}
.section-tint {
  background: var(--c-sand);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-lg);
  padding: var(--sp-6) var(--gutter);
  margin: var(--sp-7) calc(-1 * var(--sp-2));
}
@media (min-width: 1024px) { .section-tint { padding: var(--sp-6); } }

/* ---------- 6. Buttons (semantic colours; hover = background only, text fixed) ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: 48px;
  padding: var(--sp-2) var(--sp-5);
  border: 0;
  border-radius: var(--radius-btn);
  font: var(--fw-bold) var(--fs-base)/1.25 var(--font-sans);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: background var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease),
              transform var(--t-fast) var(--ease);
}
.btn:hover { box-shadow: var(--shadow); transform: translateY(-1px); }
.btn:active { transform: translateY(0); box-shadow: var(--shadow-sm); }
@media (max-width: 1023px) { .btn { font-size: var(--fs-btn-m); } }  /* mobile buttons 18px */

.btn .nw { white-space: nowrap; }                 /* controlled 2-line wrap */
.btn-sub { display: inline; }
@media (max-width: 767px) { .btn-sub { display: none; } }   /* hide "â€” online discount" suffix on mobile */
/* gold = book / enquiry */
.btn--cta {
  background: linear-gradient(180deg, #f7b165, var(--c-btn-book));
  color: var(--c-deep);
  border: 1px solid var(--c-btn-border);
  box-shadow: 0 2px 5px rgba(207,127,40,0.32), inset 0 1px 0 rgba(255,255,255,0.45);
}
.btn--cta:hover {
  background: linear-gradient(180deg, #f3a552, var(--c-btn-book-hover));
  color: var(--c-deep);
  box-shadow: 0 5px 12px rgba(207,127,40,0.42), inset 0 1px 0 rgba(255,255,255,0.45);
  transform: translateY(-1px);
}
/* blue = call */
.btn--tel { background: var(--c-btn-call); color: var(--c-deep); }
.btn--tel:hover { background: var(--c-btn-call-hover); color: var(--c-deep); }
/* green = WhatsApp */
.btn--whatsapp { background: var(--c-btn-wa); color: #fff; }
.btn--whatsapp:hover { background: var(--c-btn-wa-hover); color: #fff; }
/* utility variants */
.btn--primary { background: var(--c-teal); color: #fff; }
.btn--primary:hover { background: var(--c-teal-dark); color: #fff; }
.btn--ghost { background: transparent; color: var(--c-navy); border: 2px solid var(--c-navy); box-shadow: none; }
.btn--ghost:hover { background: var(--c-navy); color: #fff; }
.btn--block { display: flex; width: 100%; }

.contact-strip { display: grid; gap: var(--sp-3); margin: var(--sp-5) 0; }
@media (min-width: 600px) { .contact-strip { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); } }

/* ---------- 7. Hero (resort pages) ---------- */
.page-hero { margin-bottom: var(--sp-6); }
.page-hero h1 { margin-bottom: var(--sp-2); }
.page-hero__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-4);
  margin-bottom: var(--sp-4);
}
.rating { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-sm); color: var(--c-ink-soft); }
.rating__stars { color: var(--c-star); letter-spacing: 2.5px; font-size: 1.3rem; line-height: 1; }
.badge {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  background: var(--c-white); color: var(--c-navy);
  border: 1px solid var(--c-hairline-gold);
  border-radius: var(--radius-pill);
  padding: 3px 14px;
  font-size: var(--fs-sm); font-weight: var(--fw-semi);
}
.page-hero__media {
  /* HERO RULE v3 (owner): NEVER crop a hero. The image renders FULL (natural aspect) at
     the COLUMN width â€” upscaling to column width is ACCEPTED for heroes. No aspect box,
     no dark backing, no truncation. */
  position: relative;
  margin: 0 0 var(--sp-4);
  max-width: 100%;
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow);
}
.page-hero__media img {
  width: 100%; height: auto; object-fit: unset; display: block;
  transition: transform 600ms var(--ease);
}
.page-hero__media:hover img { transform: scale(1.025); }
/* NATIVE hero mode (owner rule, site-wide): display at native size, NEVER upscale â€” fill the
   column only if the native width allows. Small sources render left-aligned at true pixel
   size with no aspect crop (cropping a small source caused the pixelation and the mobile
   dark-block: the aspect box's --c-deep backing showed beside the crop). */
.page-hero__media--native { /* obsolete under hero rule v3 */ }


.page-hero__media::after { display: none; }
.page-hero__lead { font-size: var(--fs-md); }
.page-hero__actions { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-top: var(--sp-4); }
@media (max-width: 599px) { .page-hero__actions .btn { flex: 1 1 100%; } } /* Book btn may wrap to 2 lines */
@media (min-width: 600px) {
  .page-hero__actions { flex-wrap: nowrap; }
  .page-hero__actions .btn { flex: 1 1 auto; white-space: nowrap; }
}
/* the booking CTA is THE action on a resort page: prominent gold, soft glow */
.page-hero__actions .btn--cta { box-shadow: var(--glow-gold); }
.page-hero__actions .btn--cta:hover { box-shadow: var(--glow-gold); }
@media (min-width: 1024px) { .page-hero__actions .btn, .btn--lg { font-size: var(--fs-md); } }  /* Make-a-booking matches the hero Book button */

/* ---------- 8. Images & galleries (uniform tiles, gentle hover zoom, never upscale) ---------- */
.figure { margin: var(--sp-5) 0; }
/* Facing image pair (layout restoration): reproduces the original site's side-by-side
   left/right image placement (its <div class="imgvilla*"><div class="section" float> wrapper
   mechanism) with the new template's figure markup. Two figures per row, EQUAL HEIGHTS. */
.figure-pair { display: flex; flex-wrap: nowrap; gap: var(--sp-3); align-items: stretch; margin: var(--sp-5) 0; max-width: var(--pair-max, none); }
.figure-pair .figure { flex: var(--ar, 1) 1 0%; margin: 0; min-width: 0; }
.figure-pair .figure img { width: 100%; height: auto; display: block; }
@media (max-width: 767px) { .figure-pair { flex-direction: column; max-width: none; } }  /* W2C mobile release */
/* MOBILE IMAGE RULES (owner rulings, site-wide):
   1. ALL in-flow content images render CENTERED.
   2. EQUAL-SIZE for consecutive images (supersedes width:auto for pairs/runs): stacked
      figure-pair members and run members (figure--half) render at the SAME width â€” the
      COLUMN width â€” so a small-native member (e.g. the 279px rig shot) never renders
      smaller than the image above it. Lone in-prose singles keep natural centered size. */
@media (max-width: 1023px) {
  .content-card .figure--left, .content-card .figure--right, .content-card .figure--half {
    float: none; width: 100%; text-align: center;
  }
  .content-card .figure--left img, .content-card .figure--right img {
    width: 100%; height: auto; margin: 0 auto; display: block;   /* F2: column width, no exceptions */
  }
  .figure-pair .figure { text-align: center; }
  .figure-pair .figure img, .content-card .figure--half img {
    width: 100%; height: auto; margin: 0 auto; display: block;   /* column width, equal */
  }
}
.figure img {
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  display: block;
  margin: 0 auto;
  width: auto; max-width: 100%; height: auto;  /* HTML width attr = native cap; never upscale */
  transition: transform var(--t-med) var(--ease), box-shadow var(--t-med) var(--ease);
}
.figure:hover img { transform: scale(1.03); box-shadow: var(--shadow-lift); }  /* gentle zoom on all content images */
.figure figcaption { font-size: var(--fs-xs); color: var(--c-ink-soft); margin-top: var(--sp-2); text-align: center; }
/* pair of small legacy images shown at native size, side by side */
.figure-row {
  display: flex; flex-wrap: wrap; justify-content: center; align-items: flex-start;
  gap: var(--sp-4); margin: var(--sp-5) 0;
}
.figure-row img {
  border-radius: var(--radius); box-shadow: var(--shadow-sm);
  display: block; width: auto; max-width: 100%; height: auto;
}

.gallery-grid { display: grid; gap: var(--sp-4); margin: var(--sp-4) 0 var(--sp-5); }
@media (min-width: 600px)  { .gallery-grid { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1024px) { .gallery-grid--3 { grid-template-columns: 1fr 1fr 1fr; } }
.gallery-grid figure {
  margin: 0;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  aspect-ratio: 3 / 2;               /* uniform tiles: crop, never upscale */
  background: var(--c-tint-teal);
}
.gallery-grid img {
  display: block; width: 100%; height: 100%; object-fit: cover;
  transition: transform 500ms var(--ease);
}
.gallery-grid figure:hover img { transform: scale(1.04); }

/* One slider sitewide: CSS scroll-snap (touch + mouse + keyboard) */
.snap-gallery {
  display: flex;
  gap: var(--sp-4);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  padding-bottom: var(--sp-2);
  margin: var(--sp-4) 0 var(--sp-5);
  scrollbar-color: var(--c-teal) transparent;
}
.snap-gallery > * {
  flex: 0 0 min(86%, 540px);
  scroll-snap-align: center;
  margin: 0;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}
.snap-gallery img { display: block; width: 100%; aspect-ratio: 3 / 2; object-fit: cover; }

[data-full] { cursor: zoom-in; }     /* lightbox-ready hook (no JS yet) */

/* ---------- 9. Feature / spec / room cards ---------- */
.feature-grid {
  display: grid; gap: var(--sp-3);
  grid-template-columns: 1fr;                      /* mobile: 1 column */
  margin: var(--sp-4) 0 var(--sp-5);
  list-style: none; padding-left: 0;   /* usable as <ul> */
}
.feature-grid li { margin: 0; }
@media (min-width: 1024px) { .feature-grid { grid-template-columns: 1fr 1fr; } }  /* desktop: 2 */
.feature {
  display: flex; align-items: center; gap: var(--sp-3);
  background: var(--c-white);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius);
  padding: var(--sp-3);
  box-shadow: var(--shadow-sm);
  font-weight: var(--fw-semi); color: var(--c-navy); font-size: var(--fs-sm);
  transition: box-shadow var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.feature:hover { box-shadow: var(--shadow); transform: translateY(-2px); }
.feature__icon {
  flex: 0 0 auto; width: 38px; height: 38px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(181,146,78,0.10); color: var(--c-gold-text);
}
.feature__icon .icon { width: 20px; height: 20px; vertical-align: 0; }

.spec-cards {
  display: grid; gap: var(--sp-3);
  grid-template-columns: 1fr;          /* single column, desktop AND mobile */
  margin: var(--sp-4) 0 var(--sp-5);
}
.spec {
  background: var(--c-surface-2);
  border: 1px solid var(--c-hairline);
  border-left: 2px solid var(--c-gold);
  border-radius: var(--radius);
  padding: var(--sp-3) var(--sp-4);
}
.spec__label {
  font-size: var(--fs-xs); text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--c-ink-soft);
}
.spec__value {
  font-family: var(--font-display);
  font-size: var(--fs-md); font-weight: var(--fw-bold);
  color: var(--c-navy); line-height: 1.25;
}

.room-grid { display: grid; gap: var(--sp-4); grid-template-columns: 1fr; margin: var(--sp-4) 0 var(--sp-5); }
@media (min-width: 600px) { .room-grid { grid-template-columns: 1fr 1fr; } }
.room {
  background: var(--c-white);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  display: flex; flex-direction: column;
  transition: box-shadow var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.room:hover { box-shadow: var(--shadow-lift); transform: translateY(-2px); }
.room__media { aspect-ratio: 4 / 3; background: var(--c-deep); overflow: hidden; }
.room__media img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 500ms var(--ease); }
.room:hover .room__media img { transform: scale(1.04); }
.room__body { padding: var(--sp-3) var(--sp-4) var(--sp-4); }
.room__body h3 { margin: 0 0 var(--sp-2); font-size: var(--fs-md); }
.room__body p { margin: 0; font-size: var(--fs-sm); color: var(--c-ink-soft); }

/* ---------- 10. Reviews & discounts ---------- */
.reviews { margin-top: var(--sp-7); }
.reviews__head {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2) var(--sp-4);
  border-top: 1px solid var(--c-hairline-gold);
  padding-top: var(--sp-7);
  margin-bottom: var(--sp-4);
}
.reviews__head h2 { margin: 0; }
.review {
  background: var(--c-surface-2);
  border: 1px solid var(--c-hairline);
  border-left: 2px solid var(--c-gold);
  border-radius: var(--radius);
  padding: var(--sp-4) var(--sp-5);
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-base);           /* reviews readable >=17px */
}
.review p { margin: 0; }
.review cite { display: block; margin-top: var(--sp-2); font-style: normal; font-weight: var(--fw-semi); color: var(--c-navy); font-size: var(--fs-sm); }

.discount-box {
  background: linear-gradient(165deg, var(--c-navy), var(--c-deep));
  color: #fff;
  border: 1px solid var(--c-hairline-gold);
  border-radius: var(--radius-lg);
  padding: var(--sp-7) var(--sp-5);
  text-align: center;
  margin: var(--sp-8) 0 var(--sp-5);
  box-shadow: var(--shadow);
}
.discount-box h2::after {
  content: ""; display: block; width: 56px; height: 2px;
  background: var(--c-gold); margin: var(--sp-3) auto 0; opacity: 0.9;
}
.discount-box h2 { color: #fff; margin-top: 0; }
.discount-box p { color: rgba(255,255,255,0.85); max-width: 62ch; margin-left: auto; margin-right: auto; }  /* measure wide enough for the enquiry line in <=2 comfortable lines, no font tricks */
/* discount-box primary enquiry stays GOLD (calls blue, whatsapp green) */
/* discount-box CTA inherits the enriched .btn--cta gradient/border/shadow */

/* ---------- 11. Tables (keeps .tg + .spot_list compat) ---------- */
.table-scroll { overflow-x: auto; margin: var(--sp-4) 0; border-radius: var(--radius-sm); }
table, .tg { border-collapse: collapse; width: 100%; font-size: var(--fs-sm); }
th, td, .tg th, .tg td {
  border: 1px solid var(--c-hairline);
  padding: var(--sp-2) var(--sp-3);
  text-align: left;
  vertical-align: top;
  font-family: var(--font-sans);
}
th, .tg th { background: var(--c-sand); color: var(--c-navy); font-weight: var(--fw-bold); }
tr:nth-child(even) td { background: #fbf9f4; }
/* PRICE-TABLE-LINKS-01 (owner ruling, EN's _REQUEST-CSS-rt-link-no-underline.md).
   `.tg` carries no link rule at all, so a resort link in a price-range cell fell through to
   the base `a` at main.css:25 - which sets thickness and offset only - and rendered
   underlined. EN checked the estate's non-underlined link classes first; every one is
   CONTAINER-scoped (.content-card li a, .link-run a, .pk-resort a, .pk-toc a,
   .ds-index__group a) and none reaches a <td>, which is why a class was minted.

   PATTERN MATCHED: `.content-card li a` (main.css:1715-1716) - suppress the underline,
   restore it on hover/focus, and DECLARE NO COLOUR, so navy at rest and teal-dark on hover
   come from the base `a` / `a:hover` rules. That is the table's own text convention: `td`
   sets no colour and `th` is var(--c-navy), which is what the base `a` already gives.
   `.pk-toc a` and `.ds-index__group a:hover` restate those colours; copying that here would
   pin the table link against future token changes, so it is deliberately not copied.

   SCOPED BY CLASS, NOT BY CONTAINER, on purpose: eleven trees carry this table and the
   class lands per tree over time, so a class-only selector covers each one the moment its
   markup ships, with no further CSS. (0,1,0) beats the base `a` at (0,0,1); no !important
   is needed and the position is not load-bearing - every `text-decoration: underline` in
   this file is scoped to a container that cannot contain a table cell. */
.rt-link { text-decoration: none; }
.rt-link:hover, .rt-link:focus-visible { text-decoration: underline; }
.spot_list, .spot_list_label, .spot_list-left, .spot_list_number { font-family: var(--font-sans); }
.spot_list_label { background: var(--c-sand); color: var(--c-navy); font-weight: var(--fw-bold); }
.spot_list-left strong { color: var(--c-navy); }
.spot_list_number { text-align: center; color: var(--c-ink-soft); }
.spot_list font { color: var(--c-ink-soft) !important; }

/* ---------- 12. Forms (Template C) ---------- */
.form-card { max-width: 560px; }
.form-field { margin-bottom: var(--sp-4); }
.form-field label {
  display: block;
  font-weight: var(--fw-semi);
  color: var(--c-navy);
  margin-bottom: var(--sp-1);
}
.form-field .hint { font-size: var(--fs-xs); color: var(--c-ink-soft); margin-top: var(--sp-1); }
.form-field input[type="text"],
.form-field input[type="email"],
.form-field input[type="tel"],
.form-field input[type="date"],
.form-field input[type="number"],
.form-field select,
.form-field textarea {
  width: 100%;
  font: var(--fw-reg) var(--fs-base)/1.4 var(--font-sans);
  color: var(--c-ink);
  background: var(--c-white);
  border: 1.5px solid var(--c-hairline);
  border-radius: var(--radius-sm);
  padding: var(--sp-3);
  min-height: 48px;
  appearance: none;
  -webkit-appearance: none;
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.form-field select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23003466' stroke-width='2'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--sp-3) center;
  padding-right: var(--sp-6);
}
.form-field textarea { min-height: 120px; resize: vertical; }
.form-field input:focus, .form-field select:focus, .form-field textarea:focus {
  border-color: var(--c-teal);
  box-shadow: 0 0 0 3px rgba(12, 143, 160, 0.16);
  outline: none;
}
.form-field input:user-invalid { border-color: var(--c-error); }
.form-row { display: grid; gap: var(--sp-4); }
@media (min-width: 600px) { .form-row { grid-template-columns: 1fr 1fr; } }
/* Build 14.4: long <option> text must never widen a grid column (page blowout
   on phones) â€” and every control, placeholder and option stays 17px. */
.form-row > .form-field, .form-field { min-width: 0; }
.form-field select { max-width: 100%; text-overflow: ellipsis; }
.form-field select option { font-size: 15px; }
@media (max-width: 1023px) {
  .form-field select { font-size: 16px; }          /* mobile: dropdown stays compact */
  .form-field select option { font-size: 15px; }
}
.form-field input::placeholder, .form-field textarea::placeholder { font-size: var(--fs-base); }

/* ---------- 13. Footer ---------- */
.site-footer {
  background: var(--c-deep);
  color: rgba(255,255,255,0.85);
  border-top: 1px solid var(--c-hairline-gold);
  margin-top: var(--sp-7);
}
.site-footer__inner {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: var(--sp-6) var(--gutter);
  text-align: center;
}
.site-footer__inner > p:first-child { font-size: 16px; }  /* ONLY the copyright line may be 16px */
.site-footer a { color: #fff; }
.site-footer__reg { font-size: var(--fs-xs); color: rgba(255,255,255,0.6); margin-top: var(--sp-2); }
.site-footer__social {
  display: flex; justify-content: center; gap: var(--sp-3);
  margin: var(--sp-4) 0 var(--sp-2);
}
.site-footer__social a {
  display: inline-flex; align-items: center; justify-content: center;
  width: 42px; height: 42px;
  border-radius: 50%;
  background: rgba(255,255,255,0.08);
  border: 1px solid rgba(255,255,255,0.18);
  color: #fff;
  transition: background var(--t-fast) var(--ease);
}
.site-footer__social a:hover { background: var(--c-gold); }
.site-footer__social .icon { width: 20px; height: 20px; vertical-align: 0; }
.site-footer__lang { display: flex; justify-content: center; gap: var(--sp-2); margin-top: var(--sp-3); }
.footer-tawk-spacer { display: none; height: 88px; background: var(--c-sand); }
@media (max-width: 1023px) { .site-footer { margin-top: 0; } .footer-tawk-spacer { display: block; } }  /* beige clearance for the Tawk bubble; tighter top gap */

/* ---------- 14. Legacy compatibility layer ----------
   Existing page bodies use these classes; mapping here lets content
   pour into the new templates with minimal markup edits. */
.clr { clear: both; }
center { display: block; text-align: center; }
/* In-prose images get a larger BOTTOM margin (owner A7: too close to the following text) â€”
   consistent --sp-6 across every in-prose image wrapper, site-wide. */
.imgvilla, .imgvilla2, .imgvilla3 { float: none; margin: var(--sp-4) 0 var(--sp-6); }
.imgvilla img, .imgvilla2 img, .imgvilla3 img {
  width: 100%; height: auto; max-width: 540px;
  border: 0; border-radius: var(--radius); box-shadow: var(--shadow-sm);
  margin: 0;
}
.textandimg .text { padding: 0; color: inherit; font: inherit; line-height: inherit; }
.li-1-8 { line-height: 1.8; }
.sub_head h2 { margin-top: 0; }

.navigationWrapperResort { display: grid; gap: var(--sp-2); margin: var(--sp-3) 0 var(--sp-5); }
@media (min-width: 600px) { .navigationWrapperResort { grid-template-columns: 1fr 1fr; } }
.navigationWrapperResort a {
  display: flex; align-items: center; min-height: 48px;
  padding: var(--sp-2) var(--sp-4);
  background: var(--c-surface-2);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-sm);
  color: var(--c-navy);
  font-weight: var(--fw-semi);
  text-decoration: none;
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.navigationWrapperResort a:hover { border-color: var(--c-hairline-gold); background: var(--c-sand); box-shadow: var(--shadow-sm); }
.navigationWrapperResort a::after { content: "\2192"; margin-left: auto; color: var(--c-gold-text); }

.innerContactUsButton, a.makeBookingBtn {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 48px; padding: var(--sp-2) var(--sp-5);
  background: var(--c-btn-book); color: var(--c-deep);
  border-radius: var(--radius-btn);
  font-weight: var(--fw-bold);
  text-decoration: none;
  box-shadow: var(--shadow-sm);
  margin: var(--sp-3) 0;
  border: 0; float: none;
  transition: background var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.innerContactUsButton:hover, a.makeBookingBtn:hover { background: var(--c-btn-book-hover); color: var(--c-deep); box-shadow: var(--shadow); }
a.makeBookingBtn img { display: none; }   /* sprite image replaced by real text */

.socialButtonWrapper { display: grid; gap: var(--sp-3); margin: var(--sp-5) 0; }
@media (min-width: 600px) { .socialButtonWrapper { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); } }
.socialButtonWrapper a {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  min-height: 48px; padding: var(--sp-2) var(--sp-4);
  border-radius: var(--radius-btn);
  color: var(--c-deep); font-weight: var(--fw-bold); text-decoration: none;
  white-space: normal;
  box-shadow: var(--shadow-sm);
  transition: background var(--t-fast) var(--ease);
}
.socialButtonWrapper a.phoneIcon { background: var(--c-btn-call); }
.socialButtonWrapper a.phoneIcon:hover { background: var(--c-btn-call-hover); color: var(--c-deep); }
.socialButtonWrapper a.phoneIcon[href^="https://wa.me"] { background: var(--c-btn-wa); color: #fff; }
.socialButtonWrapper a.phoneIcon[href^="https://wa.me"]:hover { background: var(--c-btn-wa-hover); color: #fff; }
.socialButtonWrapper a.emailIcon { background: var(--c-teal); color: #fff; }
.socialButtonWrapper a.emailIcon:hover { background: var(--c-teal-dark); color: #fff; }
.socialButtonWrapper a::before { content: none; }   /* drop sprite icon */

.innerPagesWrapper { border: 0; background: transparent; box-shadow: none; padding: 0; margin: 0; overflow: visible; }
.singleBannerWrapper { display: block; margin: var(--sp-4) auto; border-radius: var(--radius); box-shadow: var(--shadow-sm); }

/* ---------- 15. Utilities ---------- */
.visually-hidden {
  position: absolute !important; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.text-center { text-align: center; }
.mt-0 { margin-top: 0; }
.back-to-top { display: block; text-align: right; font-size: var(--fs-sm); margin-top: var(--sp-5); }

/* ---------- Vertical rhythm (Part 1.6) ---------- */
.content-card > ul, .content-card > ol { margin-bottom: var(--sp-5); }
.content-card > .btn,
.content-card > .innerContactUsButton,
.content-card > a.makeBookingBtn { margin: var(--sp-2) 0 var(--sp-6); }
.content-card > h2 + .btn, .content-card > h3 + .btn { margin-top: var(--sp-3); }
.content-card > .snap-gallery, .content-card > .island-circles,
.content-card > .contact-card { margin-top: var(--sp-5); margin-bottom: var(--sp-6); }

/* ---------- Dive-stats: compact 2-card panel (count + conditions) ---------- */
.dive-stats {
  display: grid; grid-template-columns: 1fr; gap: var(--sp-4);
  margin: var(--sp-4) 0 var(--sp-5);
}
@media (min-width: 1024px) { .dive-stats { grid-template-columns: 1fr; } }  /* always ONE column, stacked */
.dive-stats__count {
  display: flex; flex-direction: row; align-items: center; justify-content: center; gap: var(--sp-3);
  background: #d8ecf3; color: var(--c-navy);  /* light blue */
  border-radius: var(--radius); padding: var(--sp-4) var(--sp-5); text-align: center;
}
.dive-stats__num {
  font-family: var(--font-display); font-weight: 600;
  font-size: clamp(2.2rem, 1.8rem + 2vw, 2.9rem); line-height: 1; color: var(--c-navy);
}
.dive-stats__cap { font-size: var(--fs-base); letter-spacing: 0.04em; text-transform: uppercase; color: var(--c-navy); }
.dive-stats__list {
  margin: 0; background: var(--c-white); border: 1px solid var(--c-hairline);
  border-radius: var(--radius); padding: var(--sp-2) var(--sp-4);
}
.dive-stats__list > div {
  display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-4);
  padding: var(--sp-3) 0; border-bottom: 1px solid var(--c-hairline);
}
.dive-stats__list > div:last-child { border-bottom: 0; }
.dive-stats__list dt { margin: 0; color: var(--c-ink-soft); font-size: var(--fs-base); }
.dive-stats__list dd { margin: 0; color: var(--c-navy); font-weight: var(--fw-semi); font-size: var(--fs-base); text-align: right; }

/* ---------- Mobile: permit bar moves inline, just above the Special Online Discounts card ---------- */
@media (max-width: 1023px) {
  .permit-bar:not(.permit-bar--inline) { display: none; }   /* hide the top permit bar on mobile */
}
.permit-bar--inline { display: none; border-bottom: 0; }    /* inline copy shows on mobile only */
@media (max-width: 1023px) {
  .permit-bar--inline { display: block; margin: calc(var(--sp-3) + 30px) 0 calc(var(--sp-4) - 30px); border-radius: var(--radius); border: 1px solid var(--c-hairline-gold); }
  .permit-bar--inline .permit-bar__inner { padding: var(--sp-3) var(--sp-4); }
}

/* ---------- Template C additions (Build 2) ---------- */
.inquiry-form { max-width: none; width: 100%; }   /* match the contact-card width (Build 13) */
.hp-field {                     /* honeypot: visually hidden, still in the DOM */
  position: absolute !important; left: -9999px !important;
  width: 1px; height: 1px; overflow: hidden; margin: 0;
}
.form-actions { margin-top: var(--sp-5); }
.form-actions .hint { text-align: center; margin-top: var(--sp-3); }
.form-field input[type="date"] { appearance: auto; -webkit-appearance: auto; } /* native picker UI */
.cf-turnstile { margin-bottom: var(--sp-4); }

/* ---------- Template D additions (Build 3) ---------- */
.confirm-card { text-align: initial; }
.confirm-mark {
  width: 72px; height: 72px; margin: var(--sp-2) auto var(--sp-4);
  border-radius: 50%;
  background: var(--c-tint-teal);
  border: 1px solid var(--c-hairline-gold);
  display: flex; align-items: center; justify-content: center;
  color: var(--c-teal-dark);
}
.confirm-mark .icon { width: 34px; height: 34px; stroke-width: 2.4; }
.confirm-emails {
  list-style: none; padding: 0; max-width: 420px;
  margin: var(--sp-4) auto var(--sp-5);
  text-align: center;
}
.confirm-emails li {
  margin: 0; padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--c-hairline);
  font-weight: var(--fw-semi);
}
.confirm-emails li:last-child { border-bottom: 0; }
.confirm-back { margin-top: var(--sp-7); }

/* ---------- Template A additions (homepage) ---------- */
/* A2 â€” intro line: centered kicker in the display font, gold middots + hairline */
.intro-line {
  text-align: center;
  font-family: 'Plus Jakarta Sans', var(--font-display), sans-serif;   /* Build 14.3: matches the resort-page H1 */
  font-size: var(--fs-md);
  font-weight: 700;
  /* TOPBAR-LETTERSPACING (owner ruling): the 0.18em tracking is REMOVED outright, all
     trees, Latin and CJK alike - 'normal text spacing everywhere'. It measured 3.42px
     at 19px and is what rendered ko as broken syllable blocks and my as spaced capitals.
     `text-transform: uppercase` is NOT part of the ruling and is left alone. */
  text-transform: uppercase;
  color: var(--c-navy);
  opacity: 0.92;
  margin: var(--sp-2) 0 var(--sp-5);
}
.intro-line__dot { color: var(--c-gold); margin: 0 0.45em; }
.intro-line__place { white-space: nowrap; }
.intro-line::after {
  content: ""; display: block;
  width: 72px; height: 1px;
  background: var(--c-hairline-gold);
  margin: var(--sp-3) auto 0;
}

/* A3 â€” hero FADE slider: cross-fade, dots only, zero scroll mechanics */
.hero-fade { max-width: 655px; margin: 0 auto var(--sp-5); }
.hero-fade__frame {
  position: relative;
  aspect-ratio: 655 / 333;
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow);
  background: var(--c-deep);
}
.hero-fade__slide {
  position: absolute; inset: 0; margin: 0;
  opacity: 0;
  transition: opacity 700ms var(--ease);
}
.hero-fade__slide img { width: 100%; height: 100%; object-fit: cover; display: block; }
.hero-fade__slide { pointer-events: none; }            /* stacked slides: only the...*/
.hero-fade__slide.is-active { opacity: 1; pointer-events: auto; }  /* ...visible one is clickable */
.hero-fade__slide.is-active img { cursor: zoom-in; }
.hero-fade__slide:first-child { opacity: 1; }             /* no-JS + reduced-motion baseline */
.hero-fade__slide.is-active ~ .hero-fade__slide:first-child { opacity: 0; }
.hero-fade__dots { display: flex; justify-content: center; gap: var(--sp-2); margin-top: var(--sp-3); }
.hero-fade__dot {
  width: 9px; height: 9px; border-radius: 50%;
  border: 0; padding: 0; cursor: pointer;
  background: var(--c-hairline);
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.hero-fade__dot[aria-current="true"] { background: var(--c-gold); transform: scale(1.25); }

/* A4 â€” Circular island PHOTO DISKS (brand signature).
   Assets: /images/circles/<island>-11.png â€” EXACTLY 180x360 = two stacked
   180x180 cells (top = resting disk, bottom = hover disk). The navy ring is
   part of the artwork; CSS adds the thin gold hairline, shadow and motion.
   Labels are live text below the disk â€” never baked into the image.
   (cycle_layer.png/webp sprite: retired â€” add to cleanup list.) */
.island-circles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 180px));
  gap: var(--sp-6) var(--sp-4);
  justify-content: center;
  margin: var(--sp-6) 0;
}
@media (min-width: 1024px) { .island-circles { grid-template-columns: repeat(4, 180px); } }
.island-circle { text-decoration: none; text-align: center; display: block;
  -webkit-tap-highlight-color: transparent; }   /* no blue tap rectangle on mobile */
.island-circle__disk {
  position: relative;
  display: block;
  width: 180px; max-width: 100%;
  aspect-ratio: 1;
  margin: 0 auto;
  border-radius: 50%;
  background-repeat: no-repeat;
  background-size: 100% 200%;
  background-position: top;                 /* cell 1: resting state */
  outline: 1px solid var(--c-hairline-gold);/* thin gold hairline around the navy ring */
  outline-offset: 2px;
  box-shadow: var(--shadow);
  transition: transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.island-circle__disk::after {               /* cell 2: hover state, cross-fades in */
  content: ""; position: absolute; inset: 0;
  border-radius: 50%;
  background: inherit;
  background-position: bottom;
  opacity: 0;
  transition: opacity 350ms var(--ease);
}
/* image swap + lift on devices with a real pointer only â€” on touch the disk
   is a plain link (no swap before navigating) */
@media (hover: hover) {
  .island-circle:hover .island-circle__disk,
  .island-circle:focus-visible .island-circle__disk {
    transform: translateY(-4px) scale(1.02);
    box-shadow: var(--shadow-lift);
  }
  .island-circle:hover .island-circle__disk::after,
  .island-circle:focus-visible .island-circle__disk::after { opacity: 1; }
}
.island-circle--sipadan  .island-circle__disk { background-image: url('/images/circles/sipadan-11.png'); }
.island-circle--mabul    .island-circle__disk { background-image: url('/images/circles/mabul-11.png'); }
.island-circle--kapalai  .island-circle__disk { background-image: url('/images/circles/kapalai-11.png'); }
.island-circle--mataking .island-circle__disk { background-image: url('/images/circles/mataking-11.png'); }
.island-circle__label {
  display: block; margin-top: var(--sp-3);
  font-family: var(--font-display);
  font-size: var(--fs-md); font-weight: 570; font-variation-settings: "opsz" 24;
  text-transform: uppercase;   /* TOPBAR-LETTERSPACING: 0.08em tracking removed, all trees */
  color: var(--c-navy);
  line-height: 1.2;
}
.island-circle:hover .island-circle__label { color: var(--c-gold-text); }

/* ---------- Contact card (Parts 1.5, 1.7, 3) ---------- */
.contact-card {
  background: var(--c-surface-2);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-lg);
  padding: var(--sp-5);
  margin: var(--sp-6) 0;
}
.contact-card__title { margin: 0 0 var(--sp-4); text-align: center; }
.contact-card__grid {
  display: grid; gap: var(--sp-3);
  grid-template-columns: 1fr;
}
@media (min-width: 600px)  { .contact-card__grid { grid-template-columns: repeat(2, minmax(0,1fr)); } } /* two columns everywhere >=600 (owner request) */

/* icon button: ONE line always â€” icon carries the channel, label carries the number */
.cbtn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  min-height: 48px; padding: var(--sp-2) var(--sp-4);
  border-radius: var(--radius-btn);
  font: var(--fw-bold) var(--fs-base)/1.2 var(--font-sans);
  text-decoration: none;
  white-space: nowrap;
  box-shadow: var(--shadow-sm);
  transition: background var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.cbtn:hover { box-shadow: var(--shadow); }
.cbtn { min-width: 0; }
@media (max-width: 420px) { .cbtn { white-space: normal; } }  /* never clip numbers on tiny screens */
.cbtn .icon { width: 20px; height: 20px; flex: 0 0 auto; vertical-align: 0; }
@media (max-width: 1023px) { .cbtn { font-size: var(--fs-btn-m); } }
/* semantic colours â€” consistent on every row */
.cbtn--wa     { background: var(--c-btn-wa);   color: #fff; }
.cbtn--wa:hover     { background: var(--c-btn-wa-hover);   color: #fff; }
.cbtn--wechat { background: #07c160;           color: #fff; }
.cbtn--wechat:hover { background: #06a552;                 color: #fff; }
.cbtn--call, .cbtn--mobile { background: var(--c-btn-call); color: var(--c-deep); }
.cbtn--call:hover, .cbtn--mobile:hover { background: var(--c-btn-call-hover); color: var(--c-deep); }
.cbtn--email  { background: #275b86;  color: #fff; }   /* medium blue â€” lighter than navy, not pale */
.cbtn--email:hover  { background: #1f4a6e;  color: #fff; }


/* ---------- B1: responsive dive-site comparison table ----------
   Replaces the legacy fixed .spot_list layout (and the fixed-height
   .shortpage container): a real table >=1024px, stacked cards below.
   Cross-links inside cells are ordinary anchors (SEO preserved). */
.spot-table-wrap { margin: var(--sp-5) 0 var(--sp-6); }
.spot-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.spot-table caption {
  caption-side: top; text-align: left;
  font-weight: var(--fw-bold); color: var(--c-navy);
  padding-bottom: var(--sp-3);
}
.spot-table th, .spot-table td {
  border: 1px solid var(--c-hairline);
  padding: var(--sp-2) var(--sp-3);
  text-align: left; vertical-align: top;
}
.spot-table thead th { background: var(--c-sand); color: var(--c-navy); }
.spot-table tbody tr:nth-child(even) td { background: var(--c-surface-2); }
.spot-table tbody tr.is-current td { background: var(--c-tint-teal); }
.spot-table td[data-label="Dive site"] { font-weight: var(--fw-semi); }
@media (max-width: 1023px) {
  /* stacked cards: header row hidden, each cell labelled via data-label */
  .spot-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .spot-table, .spot-table tbody, .spot-table tr, .spot-table td { display: block; width: 100%; }
  .spot-table tr {
    border: 1px solid var(--c-hairline);
    border-radius: var(--radius);
    background: var(--c-white);
    box-shadow: var(--shadow-sm);
    margin-bottom: var(--sp-3);
    overflow: hidden;
  }
  .spot-table td { border: 0; border-bottom: 1px solid var(--c-hairline); display: flex; gap: var(--sp-3); }
  .spot-table td:last-child { border-bottom: 0; }
  .spot-table td::before {
    content: attr(data-label);
    flex: 0 0 42%;
    font-weight: var(--fw-semi);
    color: var(--c-ink-soft);
  }
  .spot-table tbody tr:nth-child(even) td { background: var(--c-white); }
}

/* ---------- B2: numbered course steps ---------- */
.steps { counter-reset: step; list-style: none; padding-left: 0; margin: var(--sp-4) 0 var(--sp-6); }
.steps > li {
  counter-increment: step;
  position: relative;
  padding: var(--sp-1) 0 var(--sp-4) 52px;
  margin: 0;
}
.steps > li::before {
  content: counter(step);
  position: absolute; left: 0; top: 0;
  width: 36px; height: 36px;
  border-radius: 50%;
  background: var(--c-tint-teal); color: var(--c-teal-dark);
  border: 1px solid var(--c-hairline);
  display: flex; align-items: center; justify-content: center;
  font-weight: var(--fw-bold);
}
.steps > li:not(:last-child)::after {
  content: ""; position: absolute; left: 17.5px; top: 40px; bottom: 0;
  width: 1px; background: var(--c-hairline);
}

/* ---------- B4: uniform photo grid (2 -> 3 -> 4 columns) ----------
   Fixed-aspect cover tiles so rows align; gentle hover zoom;
   [data-full] hook stays available for a future lightbox. */
.photo-grid {
  display: grid; gap: var(--sp-3);
  grid-template-columns: repeat(2, 1fr);
  margin: var(--sp-5) 0 var(--sp-6);
}
@media (min-width: 600px)  { .photo-grid { grid-template-columns: repeat(3, 1fr); gap: var(--sp-4); } }
@media (min-width: 1024px) { .photo-grid { grid-template-columns: repeat(4, 1fr); } }
.photo-grid figure {
  margin: 0;
  aspect-ratio: 1;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--c-tint-teal);
  box-shadow: var(--shadow-sm);
}
.photo-grid img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform 500ms var(--ease);
}
.photo-grid figure:hover img { transform: scale(1.05); }

/* ---------- B3 inclusions checklist + small utilities (Build 8) ---------- */
.checklist { list-style: none; padding-left: 0; margin: var(--sp-4) 0 var(--sp-6); display: grid; gap: var(--sp-2) var(--sp-5); }
@media (min-width: 600px) { .checklist { grid-template-columns: 1fr 1fr; } }
.checklist li { position: relative; padding-left: 30px; margin: 0; }
.checklist li::before {
  content: ""; position: absolute; left: 0; top: 0.35em;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--c-tint-teal);
  border: 1px solid var(--c-hairline);
}
.checklist li::after {
  content: ""; position: absolute; left: 4.5px; top: calc(0.35em + 3.5px);
  width: 7px; height: 4px;
  border-left: 2px solid var(--c-teal-dark);
  border-bottom: 2px solid var(--c-teal-dark);
  transform: rotate(-45deg);
}
.u-soft { color: var(--c-ink-soft); font-size: 0.9em; }
.room__body h2 { margin: 0 0 var(--sp-2); font-size: var(--fs-md); }  /* lodge cards keep verbatim heading levels */

/* ---------- Error pages 403/404 (Template A2) ---------- */
.error-hero { text-align: center; padding: var(--sp-5) 0 var(--sp-2); }
.error-hero__code {
  font-family: var(--font-display);
  font-size: clamp(4.5rem, 12vw, 7rem);
  font-weight: var(--fw-bold);
  line-height: 1;
  color: var(--c-navy);
  opacity: 0.14;
  letter-spacing: 0.02em;
}
.error-hero h1 { margin-top: calc(-0.6em); }
.error-hero .page-hero__lead { max-width: 52ch; margin-left: auto; margin-right: auto; }
.error-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--sp-3); margin: var(--sp-5) 0 var(--sp-6); }
.error-links { text-align: center; font-size: var(--fs-sm); color: var(--c-ink-soft); }

/* ---------- Blog (Part B) ---------- */
.post-meta {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
  font-size: var(--fs-sm); color: var(--c-ink-soft);
  margin: calc(-1 * var(--sp-2)) 0 var(--sp-5);
}
.post-meta strong { color: var(--c-navy); font-weight: var(--fw-semi); }
.blog-card { text-decoration: none; }
.blog-card .room__body h2 { font-size: var(--fs-md); margin: 0 0 var(--sp-2); }
.blog-card .room__body p { color: var(--c-ink-soft); }
.blog-card .room__body .post-meta { margin: 0 0 var(--sp-2); font-size: var(--fs-xs); }
.blog-card .room__more { color: var(--c-gold-text); font-weight: var(--fw-semi); margin-top: var(--sp-3); display: inline-block; }

.post-toc { list-style: none; padding: var(--sp-3) var(--sp-4); margin: 0 0 var(--sp-5);
  background: var(--c-surface-2); border: 1px solid var(--c-hairline); border-radius: var(--radius); }
@media (min-width: 600px) { .post-toc { columns: 2; column-gap: var(--sp-6); } }
.post-toc li { margin: 0 0 var(--sp-1); break-inside: avoid; padding-left: 14px; position: relative; }
.post-toc li::before { content: ""; position: absolute; left: 0; top: 0.55em; width: 6px; height: 6px;
  border-radius: 50%; background: var(--c-gold); }
.post-toc a { text-decoration: none; }
.post-toc a:hover { color: var(--c-teal-dark); }

/* ---------- Gallery masonry (Build 11) ----------
   Column masonry per the reference look: equal-width columns, natural image
   heights, thin even gaps, CSS multi-column (no JS layout, no layout cost).
   Column counts map the directive to our sidebar shell (~830px main col):
   2 phone / 3 tablet / 5 desktop / 6 wide. */
.masonry { columns: 2; column-gap: 8px; margin: var(--sp-5) 0; column-fill: balance; }
@media (min-width: 600px)  { .masonry { columns: 2; } }
@media (min-width: 1024px) { .masonry { columns: 3; } }             /* ~265px columns; images fill for clean alignment */
/* desktop stays at 3 columns (owner request) */
.masonry figure {
  margin: 0 0 8px;
  break-inside: avoid;
  border-radius: var(--radius-sm, 6px);
  overflow: hidden;
  background: var(--c-tint-teal);
}
.masonry a.lb-item { display: block; outline-offset: 2px; }
.masonry img {
  width: 100%; height: auto; display: block;   /* fill column: equal widths, natural heights (reference look) */
  transition: transform 400ms var(--ease), filter 400ms var(--ease);
}
.masonry a.lb-item:hover img,
.masonry a.lb-item:focus-visible img { transform: scale(1.03); filter: brightness(1.06); }
@media (prefers-reduced-motion: reduce) {
  .masonry img { transition: none; }
  .masonry a.lb-item:hover img { transform: none; }
}
/* progressive reveal: JS adds is-collapsed + marks extras; no JS = all visible */
.masonry.is-collapsed .masonry--extra { display: none; }
.masonry-more {
  display: block; margin: var(--sp-2) auto var(--sp-6);
}

/* ---------- Lightbox (shared, vanilla) ---------- */
.lb {
  position: fixed; inset: 0; z-index: 200;
  /* transparent BLUR â€” the page stays visible behind, no blue wash */
  background: rgba(18, 22, 26, 0.28);
  -webkit-backdrop-filter: blur(14px) saturate(0.85);
  backdrop-filter: blur(14px) saturate(0.85);
  display: flex; align-items: center; justify-content: center;
  padding: 4vh 4vw;                     /* guaranteed click-outside space */
  opacity: 0; pointer-events: none;
  transition: opacity 200ms var(--ease);
}
.lb.is-open { opacity: 1; pointer-events: auto; }
@media (prefers-reduced-motion: reduce) { .lb { transition: none; } }
/* 2026 popup: DARK-BLUE panel that shrink-wraps the image â€” the caption's
   left/right edges line up exactly with the image edges. Never full width. */
.lb__panel {
  position: relative;
  background: linear-gradient(170deg, #12314e, var(--c-deep));
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--radius);
  box-shadow: 0 30px 80px rgba(4,12,20,0.55);
  padding: var(--sp-3);
  width: fit-content;                  /* panel hugs the (native-capped) image */
  max-width: min(82vw, 1040px);
  max-height: 86vh;
  display: flex; flex-direction: column;
}
@media (max-width: 599px) { .lb__panel { max-width: 92vw; } }
.lb__img {
  /* never upscale in the popup: cap at the image's own pixel size, so small
     images sit at natural size with dark-blue padding around them (no blur/
     pixelation). Large images are bounded by the viewport instead. */
  max-width: min(100%, var(--lb-natw, 100%));
  max-height: calc(86vh - 110px);
  width: auto; height: auto;
  display: block; margin: 0 auto;        /* centered when panel is wider (small image) */
  border-radius: calc(var(--radius) - 6px);
}
/* give small images a comfortable minimum panel so the dark-blue frame reads
   as intentional padding, not a cramped border */
/* caption/panel width: hug the image, but never below a readable 400px floor
   (small images => caption wraps to several lines instead of a wide strip) */
.lb__panel { min-width: min(400px, 90vw); align-items: stretch; }
.lb__caption {
  padding: var(--sp-3) var(--sp-1) var(--sp-1);
  color: #dfe9f2;
  font-size: var(--fs-sm); line-height: 1.55;
  text-align: left;         /* flush with the image edges */
  /* KEY: takes the panel width (image-driven) WITHOUT widening it â€”
     without this, a long one-line caption stretches the whole panel. */
  width: 0;
  min-width: 100%;
}
/* content images anywhere open the same lightbox */
.content-card figure img { cursor: zoom-in; }
.lb__btn {
  position: absolute;
  width: 46px; height: 46px; border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.45);   /* thin outline only */
  background: transparent; color: #fff;        /* fully transparent disk */
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease);
}
.lb__btn:hover, .lb__btn:focus-visible { background: transparent; border-color: rgba(255,255,255,0.9); }
.lb__btn svg { width: 20px; height: 20px; }
/* Build 14.6/14.7: buttons anchor to the PANEL (dark card), never the
   viewport. Desktop: the card gets a generous frame â€” a top band for the X
   and side gutters for the arrows â€” so every control rests comfortably on
   the dark blue, clear of both the photo and the blurred page behind.
   Mobile: tighter frame, controls at 50% size. */
.lb__btn { z-index: 2; }
.lb__close { top: 10px; right: 10px; }
.lb__prev { left: 10px; top: calc(50% - 20px); transform: translateY(-50%); }
.lb__next { right: 10px; top: calc(50% - 20px); transform: translateY(-50%); }
@media (min-width: 1024px) {
  .lb__panel { padding: 64px 78px 14px; }        /* room above for X, gutters for arrows */
  .lb__img { max-height: calc(86vh - 170px); }   /* keep the taller frame inside 86vh */
  .lb__close { top: 12px; right: 14px; width: 24px; height: 24px; }
  .lb__close svg { width: 12px; height: 12px; }
  .lb__prev { left: 16px; top: 50%; }
  .lb__next { right: 16px; top: 50%; }
  .lb__caption { padding-top: var(--sp-3); }
}
@media (max-width: 1023px) {
  .lb__btn { width: 24px; height: 24px; }
  .lb__btn svg { width: 12px; height: 12px; }
  .lb__close { top: 8px; right: 8px; }
  .lb__prev { left: 8px; }
  .lb__next { right: 8px; }
}

/* ---------- Video gallery: 16:9 matrix + YouTube facade ---------- */
.video-grid {
  display: grid; gap: 8px;
  grid-template-columns: repeat(2, 1fr);
  margin: var(--sp-5) 0;
}
@media (min-width: 600px)  { .video-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .video-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1440px) { .video-grid { grid-template-columns: repeat(4, 1fr); } }
@media (max-width: 479px)  { .video-grid { grid-template-columns: 1fr; } }
.video-cell {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-sm, 6px);
  overflow: hidden;
  background: var(--c-deep);
}
.video-cell iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.yt-facade {
  position: absolute; inset: 0; width: 100%; height: 100%;
  border: 0; padding: 0; cursor: pointer; background: var(--c-deep);
}
.yt-facade img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 400ms var(--ease); }
.yt-facade:hover img, .yt-facade:focus-visible img { transform: scale(1.04); }
@media (prefers-reduced-motion: reduce) { .yt-facade img { transition: none; } .yt-facade:hover img { transform: none; } }
.yt-facade::after {  /* darken for play-button contrast */
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(circle at center, rgba(10,31,51,0.15), rgba(10,31,51,0.35));
}
.yt-facade__play {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%);
  width: 58px; height: 42px; border-radius: 10px;
  background: rgba(14,42,68,0.85);
  display: flex; align-items: center; justify-content: center;
  z-index: 1;
  transition: background var(--t-fast) var(--ease);
}
.yt-facade:hover .yt-facade__play, .yt-facade:focus-visible .yt-facade__play { background: var(--c-cta); }
.yt-facade__play svg { width: 20px; height: 20px; fill: #fff; margin-left: 2px; }
.video-cell__label {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  padding: var(--sp-2) var(--sp-3);
  background: linear-gradient(transparent, rgba(10,31,51,0.8));
  color: #fff; font-size: var(--fs-sm); text-align: left;
  pointer-events: none;
}

/* ---------- Inquiry form v2 (Build 12) ---------- */
.form-row { display: grid; gap: var(--sp-3) var(--sp-4); grid-template-columns: 1fr; }
@media (min-width: 600px) { .form-row { grid-template-columns: 1fr 1fr; } }
.form-field .req { color: var(--c-error); }
.form-hint { font-size: var(--fs-sm); color: var(--c-ink-soft); margin: var(--sp-1) 0 0; }
.form-alert {
  background: #fbf3f1;
  border: 1px solid #e5b8b2;      /* softer than the full error red */
  border-radius: var(--radius-sm);
  color: #7c1d17;
  padding: var(--sp-3) var(--sp-4);
  margin-bottom: var(--sp-5);
}
.form-alert__line1 { display: block; font-weight: var(--fw-semi); font-size: var(--fs-base); }  /* matches the field labels */
.form-alert__line2 { display: block; margin-top: var(--sp-1); font-size: var(--fs-base); }
.form-field.is-invalid label { color: var(--c-error); }
.form-field.is-invalid input, .form-field.is-invalid select, .form-field.is-invalid textarea {
  border-color: var(--c-error);
  box-shadow: 0 0 0 3px rgba(179,38,30,0.12);
}
.form-submit { width: 100%; margin-top: var(--sp-4); }
.form-page .contact-card { max-width: none; }   /* full main-column width on form pages */
/* form matches the contact card width: both span the full main column */
.form-page .inquiry-form,
.form-page .form-card { max-width: none; width: 100%; }
/* native selects: full width from the shared input rule; open direction is
   the browser's (downward whenever there's room below). */

/* ---------- Spec cards (Build 12): label on top, value below, left-aligned ---------- */
.spec-cards { display: grid; gap: var(--sp-3); grid-template-columns: 1fr; margin: var(--sp-4) 0 var(--sp-6); }
@media (min-width: 600px) { .spec-cards { grid-template-columns: 1fr 1fr; } }
.spec-card {
  background: var(--c-surface-2);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius);
  padding: var(--sp-4);
}
.spec-card dt {
  font-family: var(--font-display);
  font-weight: var(--fw-bold);
  color: var(--c-navy);
  margin: 0 0 var(--sp-2);
}
.spec-card dd { margin: 0; text-align: left; color: var(--c-ink); }
/* B: bordered inclusions card */
.checklist--card {
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius);
  background: var(--c-surface-2);
  padding: var(--sp-4) var(--sp-5);
}

/* ---------- Rooms: carousel cards + details modal (Build 13) ---------- */
.rooms-grid { display: grid; gap: var(--sp-5); grid-template-columns: 1fr; margin: var(--sp-5) 0; align-items: stretch; }
@media (min-width: 700px) { .rooms-grid { grid-template-columns: repeat(var(--rooms-cols, 2), minmax(0,1fr)); } }
.room-card {
  background: var(--c-white);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  display: flex; flex-direction: column;
  height: 100%;
}
.room-card__body { padding: var(--sp-4) var(--sp-4) var(--sp-5); display: flex; flex-direction: column; flex: 1; }
.room-card__name { font-family: var(--font-display); font-size: var(--fs-lg); color: var(--c-navy); margin: 0 0 var(--sp-2); }
.room-card__meta { color: var(--c-ink-soft); font-size: var(--fs-sm); margin: 0 0 var(--sp-3); }

/* carousel: track of full-width slides, arrows + dots, swipe via JS */
.carousel { position: relative; overflow: hidden; background: var(--c-tint-teal); }
.carousel__track { display: flex; transition: transform 380ms var(--ease); }
@media (prefers-reduced-motion: reduce) { .carousel__track { transition: none; } }
.carousel__slide { flex: 0 0 100%; }
.carousel__slide img { width: 100%; height: auto; aspect-ratio: 4 / 3; object-fit: cover; display: block; cursor: zoom-in; }
.carousel__btn {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 38px; height: 38px; border-radius: 50%;
  border: 0; cursor: pointer;
  background: rgba(14,42,68,0.55); color: #fff;
  display: flex; align-items: center; justify-content: center;
  backdrop-filter: blur(4px);
  transition: background var(--t-fast) var(--ease);
}
.carousel__btn:hover, .carousel__btn:focus-visible { background: var(--c-teal-dark); }
.carousel__btn svg { width: 17px; height: 17px; }
.carousel__btn--prev { left: 10px; }
.carousel__btn--next { right: 10px; }
.carousel__dots { position: absolute; left: 0; right: 0; bottom: 10px; display: flex; gap: 7px; justify-content: center; }
.carousel__dot { width: 8px; height: 8px; border-radius: 50%; border: 0; padding: 0; cursor: pointer; background: rgba(255,255,255,0.55); }
.carousel__dot[aria-current="true"] { background: #fff; width: 22px; border-radius: 99px; }
.carousel--single .carousel__btn, .carousel--single .carousel__dots { display: none; }

/* details modal: same transparent-blur backdrop language as the lightbox */
.modal {
  position: fixed; inset: 0; z-index: 210;
  background: rgba(18,22,26,0.28);
  -webkit-backdrop-filter: blur(14px) saturate(0.85);
  backdrop-filter: blur(14px) saturate(0.85);
  display: flex; align-items: center; justify-content: center;
  padding: 4vh 4vw;
  opacity: 0; pointer-events: none;
  transition: opacity 200ms var(--ease);
}
.modal.is-open { opacity: 1; pointer-events: auto; }
@media (prefers-reduced-motion: reduce) { .modal { transition: none; } }
.modal__panel {
  background: var(--c-white);
  border-radius: var(--radius);
  box-shadow: 0 30px 80px rgba(4,12,20,0.45);
  max-width: min(92vw, 560px);
  max-height: 86vh; overflow-y: auto;
  padding: var(--sp-5) var(--sp-5) var(--sp-6);
  position: relative;
}
.modal__close {
  position: absolute; top: 12px; right: 12px;
  width: 38px; height: 38px; border-radius: 50%;
  border: 1px solid var(--c-hairline); background: var(--c-white);
  color: var(--c-navy); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.modal__close:hover { background: var(--c-sand); }
.modal__title { font-family: var(--font-display); color: var(--c-navy); margin: 0 var(--sp-6) var(--sp-3) 0; }

/* amenity / facility / stat icon lines */
.iconline { width: 21px; height: 21px; flex: 0 0 21px; color: var(--c-teal-dark); }
.amenities { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: grid; gap: var(--sp-2); }
@media (min-width: 480px) { .amenities--2col { grid-template-columns: 1fr 1fr; } }
.amenities li { display: flex; align-items: center; gap: var(--sp-2); }
/* Room-modal amenity list, ONE column with real bullets (owner feedback d â€” the icon-less
   2-column grid read as two empty columns). */
.amenities--bullets { display: block; list-style: disc; padding-left: 1.3em; }
.amenities--bullets li { display: list-item; margin: 0 0 var(--sp-1); }
/* Photo-less room types render as SMALL cards until their photos arrive (owner feedback c). */
.room-card--compact .room-card__body { padding: var(--sp-3) var(--sp-3) var(--sp-4); }
.room-card--compact .room-card__name { font-size: var(--fs-md); }
.rooms-grid--compact { gap: var(--sp-3); }
@media (max-width: 767px) { .rooms-grid, .rooms-grid--compact { grid-template-columns: 1fr !important; } }
/* Run-trailing single image sized to MATCH its pair row above (owner feedback b â€”
   seaventures-diving-sabah aligns at the pair members' width, not its small native size). */
.figure--half { width: calc(50% - var(--sp-3) / 2); }
.figure--half img { width: 100%; height: auto; }
@media (max-width: 1023px) { .figure--half { width: 100%; } }
.iconlist { list-style: none; padding: 0; }
.iconlist li { display: flex; align-items: center; gap: var(--sp-2); margin: 0 0 var(--sp-2); }
.dive-stats__list dt { display: flex; align-items: center; gap: var(--sp-2); }

/* fully-clickable cards (stretched link) â€” add .room--link to the card;
   the FIRST .stretch link inside covers the whole card. Cards without a
   link yet: add data-href later or just leave â€” nothing breaks. */
.room--link { position: relative; transition: box-shadow var(--t-fast) var(--ease), transform var(--t-fast) var(--ease); }
.room--link:hover { box-shadow: var(--shadow-lift); transform: translateY(-2px); }
.room--link { cursor: pointer; }
.room--link a.stretch::after { content: ""; position: absolute; inset: 0; z-index: 1; border-radius: inherit; }
.room--link a.stretch { position: static; }  /* the ::after (not the inline box) is the hit area */

/* Build 14.3: link lists (Table of Contents, resort lists) read cleaner
   without underlines; underline returns on hover/focus. Paragraph links
   keep their underline. */
/* LI-1-8-LINK-STYLE-01 (ES + DE + FR concurring; landed as an ACCESSIBILITY fix, not a list fix).
   A link inside a content-card list had NO cue at rest: `text-decoration: none` stripped the
   underline, leaving colour alone to carry it - and the link navy rgb(14,42,68) against the body
   text measures **1.10:1**, where WCAG 1.4.1 wants 3:1 for a colour-only link. The underline came
   back on hover/focus only, which is no help to a reader who is not already pointing at it.

   PRE-EXISTING AND ESTATE-WIDE, not a conversion defect: EN's own /index.php "Big 5" list has
   rendered this way all along. The twelve-tree conversions only made it visible at once. DE did
   not reproduce it because that page has no side article.content-card.

   The estate's own convention decides the remedy: prose links are underlined (the base `a` rule),
   and the un-underlined families are MENUS - a ToC, a link run, a resort index - where underlining
   every row is noise. So the default flips to underline and the menus are named as exceptions,
   rather than the reverse.

   EXCEPTIONS ARE BY ANCESTOR, and each was measured before being listed:
     .pk-toc / .ds-index__group / .pk-resort  - menus, stay clean
     .link-run:not(.link-run--u)              - the `--u` variant deliberately DOES underline
                                                (main.css: "a short run reads as prose"), so it
                                                must not be caught by the exception
     a.rt-link / a.btn                        - a price-table link (AD-134) and a button
   Exceptions compute (0,3,1) against the (0,2,1) default, so they win on specificity, not order. */
.content-card li a { text-decoration: underline; }
.content-card .pk-toc li a,
.content-card .ds-index__group li a,
.content-card .pk-resort a,
.content-card .link-run:not(.link-run--u) li a,
.content-card li a.rt-link,
.content-card li a.btn { text-decoration: none; }
.content-card li a:hover, .content-card li a:focus-visible { text-decoration: underline; }

/* Build 14.3: breathing room below content tables (e.g. the permit-stay table) */
.content-card table { margin-bottom: var(--sp-6); }

/* footer: reg line last + breathing room above the flags */
.site-footer__lang { margin-top: var(--sp-5); }
@media (max-width: 1023px) {
  .site-footer__reg .reg-sep { display: none; }
  .site-footer__reg .reg-lic { display: block; margin-top: 2px; }
}
.site-footer__reg { margin-top: var(--sp-4); font-size: 16px; opacity: 0.85; }
/* header flags: more separation from the CTA */
.header-flags { margin-top: var(--sp-3); }
/* junk-folder note under the send button (smaller by owner request) */
.form-hint--small { font-size: 16px; }


/* ============================================================
   Build 14 â€” animated homepage header "Reef Channel" (FR5).
   Pages opt in with  $HEADER_SCENE = true;  before header.php.
   Pure CSS/SVG, transform/opacity only, reduced-motion safe.
   ============================================================ */
.site-header--scene {
  position: relative;   /* overflow moved to .hscene so dropdowns can escape */
  /* daylight sea: light enough for the navy logo to pop, deep enough for the
     silhouettes to read (Build 14.2, owner request) */
  background: linear-gradient(180deg,#8ecbdd 0%,#3f9fb7 62%,#136f86 150%);
}
.site-header--scene .site-header__inner { position: relative; z-index: 6; }
.site-header--scene .hscene { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
/* CTA hover (scene pages): the button brightens slightly and casts a soft
   diffuse glow into the water around it â€” no sweep (Build 14.3). */
.site-header--scene .site-header__cta {
  transition: background var(--t-fast) var(--ease), box-shadow .4s var(--ease), filter .4s var(--ease);
}
.site-header--scene .site-header__cta:hover {
  filter: brightness(1.04);
  box-shadow: 0 0 18px 4px rgba(255,214,150,.28), 0 0 46px 18px rgba(240,160,74,.18);
}
/* sun rays */
.hscene .ray { position:absolute; top:-30%; width:130px; height:160%;
  background:linear-gradient(180deg,rgba(255,255,255,.5),rgba(255,255,255,0) 75%);
  transform:skewX(-18deg); filter:blur(6px); animation:hs-ray 16s ease-in-out infinite; z-index:1; }
.hscene .r1 { left:14%; }
.hscene .r2 { left:38%; width:90px;  animation-duration:19s; animation-delay:-6s; }
.hscene .r3 { left:63%; width:150px; animation-duration:23s; animation-delay:-11s; }
@keyframes hs-ray { 0%,100% { opacity:.25; transform:skewX(-18deg) translateX(0) scaleX(1); }
  50% { opacity:.6; transform:skewX(-15deg) translateX(26px) scaleX(1.18); } }
/* bubbles */
.hscene .bub { position:absolute; bottom:-12px; border-radius:50%;
  border:1px solid rgba(255,255,255,.85); background:rgba(255,255,255,.3);
  animation:hs-rise linear infinite; z-index:2; }
@keyframes hs-rise { 0% { transform:translateY(0); } 12% { opacity:.85; }
  100% { transform:translateY(-235px) translateX(14px); opacity:0; } }
/* reef band */
.hscene .reef { position:absolute; left:0; right:0; bottom:0; width:100%; height:84px; z-index:4;
  opacity:.55;   /* corals stay atmospheric â€” never compete with the CTA */ }
/* pages without the permit bar (forms etc): the flags sit under the CTA, so
   the corals drop to a whisper (owner: ~90% transparent) */
.site-header--no-permit .hscene .reef { opacity: .1; }
/* creatures */
.hscene .creature { position:absolute; z-index:3; opacity:0; }
.hscene .fl { transform-origin:60px 42px; }
.hscene .fA { animation:hs-sweep 3.1s ease-in-out infinite; }
.hscene .fB { animation:hs-sweep 3.1s ease-in-out infinite reverse; }
@keyframes hs-sweep { 0%,100% { transform:rotate(-9deg); } 50% { transform:rotate(11deg); } }
.hscene .tail { transform-origin:32px 52px; animation:hs-beat 2.3s ease-in-out infinite; }
.hscene .pec  { transform-origin:92px 56px; animation:hs-beat 2.3s ease-in-out infinite reverse; }
@keyframes hs-beat { 0%,100% { transform:rotate(-6deg); } 50% { transform:rotate(7deg); } }
@keyframes hs-swimR { 0% { transform:translateX(-120px) translateY(0); opacity:0; } 5% { opacity:1; }
  45% { transform:translateX(44vw) translateY(6px); } 82% { opacity:1; }
  94% { transform:translateX(104vw) translateY(-2px); opacity:0; } 100% { transform:translateX(104vw); opacity:0; } }
@keyframes hs-swimL { 0% { transform:translateX(104vw) scaleX(-1) translateY(0); opacity:0; } 5% { opacity:1; }
  50% { transform:translateX(46vw) scaleX(-1) translateY(-6px); } 86% { opacity:1; }
  96% { transform:translateX(-130px) scaleX(-1); opacity:0; } 100% { transform:translateX(-130px) scaleX(-1); opacity:0; } }
/* barracudas: half-transparent silhouettes cruising left -> right */
.hscene .barr-lane { position:absolute; inset:0; pointer-events:none; }
.hscene .barrs { position:absolute; width:220px; height:84px; bottom:84px; opacity:0;
  animation:hs-swimR 34s linear infinite; animation-delay:-6s; }
.hscene .barrs .sf { opacity:.5; animation-name:hs-wig; }
.hscene .fish { position:absolute; background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 10'%3E%3Cpath fill='%23081a2c' d='M32 5 L25 1 q-13 -2 -20 3 l-3 -3 v8 l3 -3 q7 5 20 3z'/%3E%3C/svg%3E") center/contain no-repeat;
  offset-rotate:auto; offset-distance:0%;
  animation-name:hs-orbit; animation-timing-function:linear; animation-iteration-count:infinite;
  opacity:.9; z-index:3; }
@keyframes hs-orbit { from { offset-distance:0%; } to { offset-distance:100%; } }
@supports not (offset-path:path('M0 0 h1')) { .hscene .fish { display:none; } }
.hscene .shoal { position:absolute; width:200px; height:66px; z-index:3; opacity:0; }
.hscene .sf { position:absolute; background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 10'%3E%3Cpath fill='%23081a2c' d='M32 5 L25 1 q-13 -2 -20 3 l-3 -3 v8 l3 -3 q7 5 20 3z'/%3E%3C/svg%3E") center/contain no-repeat;
  animation:hs-wig 2.6s ease-in-out infinite; }
@keyframes hs-wig { 0%,100% { transform:translateY(0); } 50% { transform:translateY(-4px); } }
/* FR5 placement: traffic through the channel, tornado beyond the right bommie */
.hscene .shA { bottom:80px;  animation:hs-swimR 32s linear infinite; }
.hscene .shB { bottom:108px; animation:hs-swimR 40s linear infinite; animation-delay:-16s; filter:opacity(.65); }
.hscene .tA  { bottom:70px;  animation:hs-swimL 46s linear infinite; animation-delay:-12s; filter:opacity(.7); }
.hscene .bA  { bottom:100px; animation:hs-swimL 50s linear infinite; animation-delay:-26s; filter:opacity(.4); }
.hscene .bB  { bottom:116px; animation:hs-swimL 50s linear infinite; animation-delay:-29s; filter:opacity(.3); }
.hscene .bC  { bottom:84px;  animation:hs-swimL 50s linear infinite; animation-delay:-31.4s; filter:opacity(.25); }
/* MOBILE (<1024px, where the hamburger + mini flags live): the full scene is
   desktop-only. Phones get a bright DAYTIME sea â€” light enough that the navy
   menu icon and the language flags read clearly â€” with just the bubbles. */
@media (max-width: 1023px) {
  .site-header--scene {
    background: linear-gradient(180deg,#8fc9db 0%,#48a3ba 55%,#1c7f97 140%);
  }
  .hscene .reef,
  .hscene .shoal,
  .hscene .creature { display: none; }
  /* sun rays stay on mobile â€” thinner than desktop */
  .hscene .ray { width: 58px; }
  .hscene .r2 { width: 40px; }
  .hscene .r3 { width: 68px; }
  /* no bubbles near the menu icon (right side) */
  .hscene .bub:nth-of-type(5), .hscene .bub:nth-of-type(6) { display: none; }
  .hscene .bub {
    border-color: rgba(255,255,255,.85);
    background: rgba(255,255,255,.3);
  }
  /* a few barracudas cruising right -> left, heavily transparent; the static
     lane's mask fades them further (70-90%) under the logo and the menu icon */
  .hscene .barrs {
    display: block; height: 56px; bottom: auto; top: 30px; width: 170px;
    animation: hs-swimL-m 15s linear infinite;   /* ~12s crossing + ~3s pause */
  }
  .hscene .barrs .sf { opacity: .35; }
  /* fish sprite faces right; flip so heads lead the right->left cruise */
  .hscene .barrs .sf:nth-child(n+6) { display: none; }   /* just a few */
  .hscene .barr-lane {
    -webkit-mask-image: linear-gradient(90deg,
      rgba(0,0,0,.10) 0%, rgba(0,0,0,.10) 22%,
      rgba(0,0,0,.55) 42%, rgba(0,0,0,.55) 58%,
      rgba(0,0,0,.10) 80%, rgba(0,0,0,.10) 100%);
    mask-image: linear-gradient(90deg,
      rgba(0,0,0,.10) 0%, rgba(0,0,0,.10) 22%,
      rgba(0,0,0,.55) 42%, rgba(0,0,0,.55) 58%,
      rgba(0,0,0,.10) 80%, rgba(0,0,0,.10) 100%);
  }
}
@keyframes hs-swimL-m {
  0%   { transform: translateX(104vw) scaleX(-1); opacity: 0; }
  3%   { opacity: 1; }
  76%  { opacity: 1; }
  80%  { transform: translateX(-190px) scaleX(-1); opacity: 0; }
  100% { transform: translateX(-190px) scaleX(-1); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .hscene .ray, .hscene .bub, .hscene .fish, .hscene .sf,
  .hscene .creature, .hscene .fl, .hscene .fA, .hscene .fB,
  .hscene .tail, .hscene .pec { animation:none !important; }
  .hscene .creature, .hscene .shoal { opacity:.85; }
}

/* Build 14.5: the form submit uses the IDENTICAL font shorthand as .cbtn */
.inquiry-form .form-submit { font: var(--fw-bold) var(--fs-base)/1.2 var(--font-sans); }

/* Build 14.7: left-aligned figure variant (contact page QR code) */
.figure--left { text-align: left; }
.figure--left img { margin-left: 0; }

/* Build 14.8: free-standing contact button rows (contact-details page).
   Flex-wrap with gap on BOTH axes so buttons never touch, desktop or mobile. */
.contact-card__btns {
  display: flex; flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-3);
  margin: var(--sp-2) 0 var(--sp-4);
}

/* Build 14.8: license sub-sub-title under Our office (contact page) */
.office-license { font-size: var(--fs-md); margin-top: var(--sp-4); margin-bottom: var(--sp-1); }

/* 7a â€” CTA button row: CTAs sit on their OWN line below the sentence, never inline.
   WhatsApp button sits beside the first CTA (resort pages). Wrap/stack on mobile. */
.cta-row { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; margin: var(--sp-4) 0; }
@media (max-width: 559px) { .cta-row .btn { flex: 1 1 100%; } }
/* MOBILE-CTA-WIDTH-01 (owner ruling from /ko/ on mobile): the legacy CTA buttons take a
   COMMON width, and the estate-consistent way to do that is FULL WIDTH IN THE CONTENT
   COLUMN - the same thing `.cta-row .btn` does on the line above, at the same 559px
   breakpoint, which is where the estate already decided CTAs stop sitting side by side.

   WHY NOT A MIN-WIDTH, measured rather than argued: the labels differ by a factor of
   nearly three. At 390 the widest single label is de's 306.4px and the narrowest is
   ko/zh/cn's 116px. A min-width that fits Korean leaves German shrink-wrapped, so it is
   not a common width at all; a min-width that fits German (>=307) leaves only 52px of
   headroom in the 358px column and ALREADY FAILS at 320px, where the column is 288px -
   measured, de's two long buttons are pinned to 254px there while its third sits at
   174.1px. Full width is immune to label length, which is the whole point when eleven
   trees write the same button.

   `.innerContactUsButton` and `a.makeBookingBtn` are the legacy camelCase pair (main.css:969),
   not the `.btn` family - 20 pages across 10 trees (fr carries none). Desktop is untouched:
   above 559px they keep shrink-to-fit exactly as now. */
@media (max-width: 559px) {
  .content-card .innerContactUsButton,
  .content-card a.makeBookingBtn { display: flex; width: 100%; }
}

/* ---------- 7c: resort cards on packages / resort-list pages ----------
   A main image under each resort title, clickable through to the resort page.
   Card box is fixed so the cards align; object-fit does the fitting.          */
.resort-thumb {
  display: block;
  width: 300px; max-width: 100%;
  margin: var(--sp-2) 0 var(--sp-4);
  border-radius: var(--radius-sm, 6px);
  overflow: hidden;
  background: var(--c-tint-teal);
}
.resort-thumb img {
  width: 100%; height: 170px; display: block;
  object-fit: cover;                 /* consistent card size across every resort */
  transition: transform 400ms var(--ease), filter 400ms var(--ease);
}
/* Sources smaller than the 300x170 card: cover would ENLARGE them past their
   natural size. scale-down never upscales and keeps the card box identical.  */
.resort-thumb img.is-small { object-fit: scale-down; }
.resort-thumb:hover img,
.resort-thumb:focus-visible img { transform: scale(1.03); filter: brightness(1.06); }
@media (prefers-reduced-motion: reduce) {
  .resort-thumb img { transition: none; }
  .resort-thumb:hover img { transform: none; }
}
/* Link-run underline rule: a short run (1-3) reads as prose and is underlined;
   a long run (>=4) reads as a menu, where underlining every item is noise.    */
.link-run a { text-decoration: none; }
.link-run--u a:not(.resort-thumb) { text-decoration: underline; }
.link-run .resort-thumb { text-decoration: none; }
/* .navigationWrapper (some FR pages) had NO styling at all â€” bare inline anchors.
   Keep it in flow so each title stacks above its own thumb (a grid would split
   the title and its image into separate cells).                               */
.navigationWrapper a:not(.resort-thumb) {
  display: block;
  font-weight: var(--fw-semi);
  margin-top: var(--sp-3);
}


/* ================= ITEM 7d / 7e / 7g â€” legacy imgvilla* content images =================
   PAIR-DETECTION (owner's rule, encoded as CSS so no markup changes and no churn in the
   translated twins): a PAIR is an .imgvilla3 whose .section carries float:left immediately
   followed by an .imgvilla3 whose .section does NOT. .imgvilla wraps both sections in one
   div instead. An UNPAIRED imgvilla3 is a single image and is treated like imgvilla2.   */

/* ---- 7d: paired images render at EQUAL height on desktop (widths may differ) ----
   The old markup gave both images of a pair height="195" â€” that is the parity to restore.
   The generic rule below (width:100%; height:auto) is exactly what destroyed it: each
   image resolved to its own aspect-ratio height. Crop with object-fit instead of editing
   any file; anchor BOTTOM so the crop takes from the top (owner's call).             */
@media (min-width: 900px) {
  .imgvilla3:has(> .section[style*="float:left"]):has(+ .imgvilla3:not(:has(> .section[style*="float:left"]))) img,
  .imgvilla3:has(> .section[style*="float:left"]) + .imgvilla3:not(:has(> .section[style*="float:left"])) img,
  .imgvilla > .section[style*="float:left"] img,
  .imgvilla > .section[style*="float:left"] + .section img {
    height: 195px;
    width: auto;
    max-width: 100%;
    object-fit: cover;
    object-position: 50% 100%;   /* anchor bottom, crop off the top */
  }
}

/* ---- 7e: single (unpaired) images align LEFT in the text column, never centred ---- */
.imgvilla2,
.imgvilla3:not(:has(> .section[style*="float:left"])):not(:has(+ .imgvilla3)) {
  margin-left: 0;
  margin-right: auto;
}
.imgvilla2 > .section,
.imgvilla2 img { margin-left: 0; margin-right: auto; }
/* Preserved exception (owner): sipadan.php's img1 stays RIGHT-aligned. */
.content-card img[src$="sipadan-turtle.png"] {
  float: right;
  margin: 0 0 var(--sp-3) var(--sp-4);
}

/* ---- 7g: on mobile a content image fills the paragraph width ----
   ROOT CAUSE: .section is float:left, so it shrink-wraps to the image's NATIVE width and
   the img's width:100% then resolves to 100% OF NATIVE â€” a 300px image in a ~380px column
   renders at ~79%, which is the reported "~80% of text width". Unfloat the section on
   mobile so the image can fill; the per-image caps below stop that from upscaling any
   image more than 10% past its native width (owner's 5-10% ceiling).                 */
@media (max-width: 899px) {
  .imgvilla > .section,
  .imgvilla2 > .section,
  .imgvilla3 > .section {
    float: none !important;
    width: 100%;
  }
  .imgvilla img, .imgvilla2 img, .imgvilla3 img {
    width: 100%;
    height: auto;          /* undo the 7d desktop crop */
    object-fit: fill;
    max-width: 100%;
    margin-left: 0;        /* anything capped below stays LEFT-aligned, never centred */
    margin-right: auto;
  }
  .content-card img[src$="sipadan-turtle.png"] { float: none; margin: var(--sp-3) 0; }

  }

/* ---------- A (owner ruling): SINGLE content images render at NATURAL width ----------
   A single (non-paired) content image must show at its own natural width, limited only
   by the text column â€” never visibly shrunk when it fits. The blanket max-width:540px
   above was reducing 110 single images across 62 pages (Dive-site-maps' 898px maps ->
   540px, Semporna.webp, Tawau, ...). Pairs are EXCLUDED: they keep the 7d height parity.
   Desktop only â€” on mobile 7g's fill + per-image native caps already govern.          */
@media (min-width: 900px) {
  .imgvilla2 img,
  .imgvilla3:not(:has(> .section[style*="float:left"])):not(:has(+ .imgvilla3)) img {
    width: auto;
    max-width: 100%;      /* the text column is the only cap */
    height: auto;
  }
  /* Re-assert 7d AFTER the singles rule so a PAIR always wins: the second image of a
     pair is not followed by an .imgvilla3, so it would otherwise match "single" above. */
  .imgvilla3:has(> .section[style*="float:left"]):has(+ .imgvilla3:not(:has(> .section[style*="float:left"]))) img,
  .imgvilla3:has(> .section[style*="float:left"]) + .imgvilla3:not(:has(> .section[style*="float:left"])) img,
  .imgvilla > .section[style*="float:left"] img,
  .imgvilla > .section[style*="float:left"] + .section img {
    height: 195px;
    width: auto;
    max-width: 100%;
    object-fit: cover;
    object-position: 50% 100%;
  }
}

/* ---------- B (owner ruling): packages typography + table of contents ----------
   Tier = SUBTITLE (largest below the h1). Resort name = SUB-SUB-TITLE: bigger than
   body, smaller than the tier, and NOT a bullet â€” it is a heading now.            */
.pk-tier {
  font-size: clamp(1.5rem, 1.2rem + 1.2vw, 2.05rem);
  line-height: 1.2;
  margin: var(--sp-6) 0 var(--sp-3);
  color: var(--c-navy);
}
.pk-resort {
  font-size: clamp(1.1rem, 1rem + 0.5vw, 1.35rem);   /* > body, < tier */
  line-height: 1.3;
  margin: var(--sp-4) 0 var(--sp-2);
  font-weight: var(--fw-semi);
}
.pk-resort a { text-decoration: none; }
.pk-resort a:hover, .pk-resort a:focus-visible { text-decoration: underline; }
/* the resort entries are headings now -> no bullet, no list indent */
.pk-list { list-style: none; margin: 0; padding: 0; }
.pk-list > li { list-style: none; margin: 0 0 var(--sp-5); }
.pk-list > li::marker { content: none; }

.pk-toc {
  margin: var(--sp-4) 0 var(--sp-6);
  padding: var(--sp-4) var(--sp-5);
  background: var(--c-surface-2);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-sm, 6px);
}
/* CSS-TOC-CLEAR-01 (supervisor-ticked, TAWAU-CORALFLYER-AUDIT-01 Part C/D).
   A ToC never renders inside a float's shadow.

   MEASURED CAUSE, not a guess: the portrait-image law at main.css:2302 floats a content
   image RIGHT and its float box extends downward past the image. On the "shape A" page
   order - intro <p>, floated image, ONE <p>, then the ToC - the ToC lands in that shadow
   and is squeezed to 310px inside a 726px host (42.7%), with a 400px image beside it.
   That is the only place in the estate where the adjacent image is WIDER than the ToC.
   "Shape B" pages carry two paragraphs, which consume the float height, so their ToC
   already clears at the full 726px - which is why the defect was 6 of 11 languages and
   not all of them.

   BOTH INTERACTING RULES ARE LEFT ALONE, deliberately:
     * main.css:2302 (the (7)(b) portrait float) is the estate image law and correct;
       removing the float would change every portrait image on every page to fix a ToC.
     * main.css:943 `.imgvilla, .imgvilla2, .imgvilla3 { float: none }` is DEAD - later
       rules override it - so it neither causes nor cures this, and reading it alone is
       misleading. It stays as the audit found it.
   Adding `clear` is the minimal change: it moves the ToC below the float without altering
   what the float does.

   Estate reach: fixes the 25-row COMPRESSION class across all trees with no page edit. It
   does NOT address the separate 66-row LONG-LABEL class (full-width ToCs whose items wrap
   on text length) - the audit rules that a content/CSS choice, not a layout bug.
   Harmless where it does not apply: `clear` is ignored on the flex child at main.css:2258
   (`.feat-row--toc > .pk-toc`), and at <=899px the float is already `float:none`, so
   mobile is untouched. */
nav.pk-toc { clear: both; }
/* F7 (supervisor-consolidated): the ToC title genuinely matches .content-card > h2 â€”
   display family + Fraunces variation + --fs-xl + the gold accent underline. */
.pk-toc__title {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 570;
  font-variation-settings: "opsz" 40;
  letter-spacing: -0.01em;
  color: var(--c-navy);
  line-height: var(--lh-tight);
  margin: 0 0 var(--sp-3);
}
.pk-toc__title::after {
  content: ""; display: block; width: 56px; height: 2px;
  background: var(--c-gold); margin-top: var(--sp-3); opacity: 0.8;
}
.pk-toc ul { list-style: none; margin: 0; padding: 0; }
.pk-toc__tier > a { font-weight: var(--fw-semi); }
.pk-toc__tier { margin-top: var(--sp-2); }
.pk-toc__sub { margin: var(--sp-1) 0 0 var(--sp-4) !important; }
.pk-toc__resort a { font-size: 0.95rem; }
.pk-toc a { color: var(--c-navy); text-decoration: none; }
.pk-toc a:hover, .pk-toc a:focus-visible { text-decoration: underline; }
@media (min-width: 900px) {
  .pk-toc { columns: 2; column-gap: var(--sp-6); }
  .pk-toc__title { column-span: all; }
}
/* an anchored heading must not hide under any sticky chrome */
.pk-tier, .pk-resort { scroll-margin-top: 90px; }

/* ---------- F: Special-Offers hero image (owner ruling) ----------
   Desktop: the small vertical nudibranch sits RIGHT, the opening text runs down its
   left. .imgvilla2 is float:none site-wide (and 7e pins singles left), so this page
   needs an explicit hook â€” same pattern as the sipadan-turtle.png exception.
   Mobile: no float, so it lands directly under the H1 in DOM order, which is exactly
   what the original m/Special-Offers.php did.                                      */
@media (min-width: 900px) {
  .imgvilla2.so-hero {
    float: right;
    width: auto;
    max-width: 300px;
    margin: 0 0 var(--sp-4) var(--sp-5);
  }
  .imgvilla2.so-hero img { width: auto; max-width: 100%; height: auto; margin: 0; }
}
@media (max-width: 899px) {
  .imgvilla2.so-hero { float: none; margin: var(--sp-3) 0; }
}

/* F3: "View details" buttons pinned to the card bottom â€” level across each row even when a
   meta line wraps (applies to photo AND compact cards). */
.room-card__body .btn { margin-top: auto; align-self: flex-start; }
/* F12b: neutral info card (e.g. the Bohey-Dulang Boat Transfer Schedule) */
.info-card {
  background: var(--c-white);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: var(--sp-4) var(--sp-5);
  margin: var(--sp-5) 0;
}
.info-card h3 { margin-top: 0; }

/* F13: icon list (BD treatment) â€” one column, sip_icon glyphs as bullets, text verbatim */
.amenities--icons { display: block; list-style: none; padding-left: 0; }
.amenities--icons li { display: flex; align-items: center; gap: var(--sp-2); margin: 0 0 var(--sp-1); }

/* BLOG (BL2/BL4): single-column post ToC + breathing room under the post title before meta */
.post-toc { column-count: 1 !important; columns: 1 !important; }
h1 + .post-meta { margin-top: var(--sp-3); padding-top: var(--sp-1); }

/* ===== OWNER EYEBALL-2 UNIT 1 ===== */
/* R-E: mobile â€” EVERY in-flow content-card image renders full column width, regardless of
   wrapper class (plain .figure, bare <img>, etc.) â€” not just the float/pair modifiers. */
@media (max-width: 1023px) {
  .content-card .figure { float: none; width: 100%; text-align: center; }
  /* EVERY content-card <img> (bare or wrapped) fills the column on mobile â€” sip_icon renders
     <svg>, not <img>, so amenity/line icons are unaffected. Carousels keep their own object-fit. */
  /* V1-CORRECTION: mobile only â€” fill column, but cap consecutive-pair members at their
     shared --pair-w (emitted by the render layer). Desktop never reads --pair-w. */
  .content-card img { width: min(100%, var(--pair-w, 9999px)) !important; height: auto; margin: 0 auto; display: block; }
  .content-card .carousel__slide img { height: 100%; }
}
/* R-F: image-less (compact) room cards render at the SAME width as photo cards â€” the compact
   grid matches the photo grid's 2 columns on desktop (overrides the inline --rooms-cols:3). */
@media (min-width: 700px) {
  .rooms-grid--compact { grid-template-columns: repeat(2, minmax(0,1fr)) !important; }
}
/* F-CD: contact-card button rows render EQUAL/full width on mobile (WhatsApp / Mobile-Office /
   email series + Open-inquiry-form) â€” no single wider button. */
@media (max-width: 599px) {
  .contact-card .cbtn, .contact-card__grid .cbtn, .contact-card__btns .cbtn, .contact-card .btn, .contact-card__btns .btn { width: 100%; }
}

/* F-PV: Sipadan photo matrix + 2x3 YouTube-embed placeholder video grid */
.pv-photo-matrix{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:var(--sp-3);margin:var(--sp-4) 0;}
.pv-photo-matrix a{display:block;overflow:hidden;border-radius:var(--radius-md,8px);}
.pv-photo-matrix img{width:100%;height:100%;aspect-ratio:4/3;object-fit:cover;display:block;transition:transform .3s;}
.pv-photo-matrix a:hover img{transform:scale(1.05);}
.pv-video-grid{display:grid;grid-template-columns:1fr;gap:var(--sp-3);margin:var(--sp-4) 0;}
@media(min-width:640px){.pv-video-grid{grid-template-columns:repeat(2,1fr);}}
.pv-video-card{position:relative;display:block;overflow:hidden;border-radius:var(--radius-md,8px);background:#0b1f33;}
.pv-video-card img{width:100%;aspect-ratio:16/9;object-fit:cover;display:block;opacity:.82;}
.pv-video-card__play{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none;}
.pv-video-card__play svg{width:54px;height:54px;fill:#fff;opacity:.94;filter:drop-shadow(0 2px 6px rgba(0,0,0,.55));}
.pv-video-card__cap{position:absolute;left:0;right:0;bottom:0;padding:var(--sp-2) var(--sp-3);color:#fff;background:linear-gradient(transparent,rgba(0,0,0,.72));font-size:.85rem;}

/* F-DS: comparison caption renders as a real h2-style subtitle (full width above the table). */
.spot-table caption{font-size:var(--fs-h3,1.35rem);line-height:1.25;padding-bottom:var(--sp-3);}
/* F-DS: Depth/Visibility/Location stats as a CARD with the dive-map image on the RIGHT (desktop). */
.dive-stats-card{display:flex;flex-wrap:wrap;gap:var(--sp-4);align-items:flex-start;margin:var(--sp-4) 0;padding:var(--sp-4);background:var(--c-surface-2,#f4f7f9);border-radius:var(--radius-md,8px);}
.dive-stats-card .dive-stats{flex:1 1 260px;min-width:0;margin:0;}
.dive-stats-card .dive-stats-card__map{flex:0 0 auto;margin:0;}
.dive-stats-card .dive-stats-card__map img{display:block;border-radius:var(--radius-md,8px);max-width:100%;height:auto;}
@media(max-width:600px){.dive-stats-card{flex-direction:column;}.dive-stats-card .dive-stats-card__map{align-self:center;}}

/* ============================================================================
   CSS-R28b â€” THE DIVE-SITE STAT CARD IS CENTRED ON MOBILE, IN EVERY TREE
   ----------------------------------------------------------------------------
   Owner screenshot: `/Sipadan/West-Ridge.php` at ~442px shows the stat panel
   hugging the left while the fr twin looks centred. Measured, the cause is not a
   language rule and nothing is missing from the EN markup â€” both trees run the
   same CSS. At <=600px the card becomes `flex-direction: column` with the flex
   default `align-items: flex-start`, so each child is shrink-to-fit: the panel is
   only as wide as its own content. FR's French labels happen to be long enough to
   fill the card (337 of 337px) while EN's shorter labels stop at 283px, leaving
   71px of slack on the right â€” the same rule, two contents, two appearances.
   Stretching the children makes the panel span the card in both, so the slack is
   symmetric whatever the language happens to say. Desktop keeps its row layout.
   ============================================================================ */
@media (max-width: 600px) {
  .dive-stats-card { align-items: stretch; }
  .dive-stats-card > .dive-stats { width: 100%; }
}

/* M6/G7 dive-site contact card: on plain/dive-site pages the page-end contact card renders
   as a BOUNDED card aligned under the right-side text column (not full-bleed). */
.page-plain .content-card > .contact-card{max-width:640px;margin-left:auto;margin-right:auto;}
@media(max-width:700px){.page-plain .content-card > .contact-card{max-width:none;margin-left:0;}}

/* M11 course eyebrow: subtitle promoted to eyebrow above the h1 on course pages â€” breathing padding. */
.content-card > .eyebrow:first-child{display:block;margin-top:var(--sp-2);margin-bottom:var(--sp-2);}

/* M2-FINAL: Chinese header lines render as distinct stacked lines under the EN title/subtitle. */
.hdr-zh{font-weight:600;color:var(--c-navy,#0d2b44);font-size:var(--fs-lg,1.15rem);line-height:1.2;margin:calc(-1*var(--sp-2)) 0 var(--sp-3);letter-spacing:.02em;}

/* V2: listing per-resort image = natural-aspect BLOCK under the resort-name subtitle (no fixed crop, never floated). */
.resort-thumb{display:block;width:auto;max-width:min(100%,360px);height:auto;background:none;overflow:visible;margin:var(--sp-2) 0 var(--sp-4);}
.resort-thumb img{width:auto;max-width:100%;height:auto;object-fit:initial;aspect-ratio:auto;display:block;}
/* V4: listing resort-name lists carry NO bullet (icon removed from the pk-resort title entries in markup). */
.pk-list,.link-run{list-style:none;padding-left:0;}


/* X-RES: m/-only restorations render MOBILE-ONLY â€” the originals never showed them on
   desktop. Owner-explicit both-view placements (B2 Massage-Spa, F-SR Luxury hero) are
   exempt and never carry this class. */
.figure--m-only{display:none;}
@media(max-width:1023px){.figure--m-only{display:block;}}

/* X-ANE: structural two-column feature row â€” list LEFT, narrow figure RIGHT, top-aligned.
   Mobile stacks: image AFTER the list (DOM order), W1 sizing applies. */
.feat-row{display:flex;gap:var(--sp-5);align-items:flex-start;margin:var(--sp-4) 0;}
.feat-row>ul{flex:1 1 auto;min-width:0;margin:0;}
.feat-row>.feat-row__fig{flex:0 0 auto;margin:0;max-width:44%;}
.feat-row>.feat-row__fig img{display:block;max-width:100%;height:auto;}
@media(max-width:767px){.feat-row{flex-direction:column;}.feat-row>.feat-row__fig{max-width:100%;align-self:center;}}

/* X-CUR: listing per-resort images are LINKS to the resort page â€” pointer cursor, no
   zoom affordance. Lightbox zoom-in stays only on genuine gallery figures. */
.content-card figure.resort-thumb img{cursor:pointer;}

/* B1: tablet-band cascade fix (768-1023): while a figure-pair renders as a ROW, its
   member imgs must NOT be width-capped by the W1 --pair-w (flex sizes the figures;
   a cap would unbalance the equal heights). The cap RE-APPLIES at <=767 when pairs
   stack (each member then behaves as a run image). Choice (a): selector exclusion â€”
   the inline --pair-w stays on the tags but is inert while the row is active. */
@media (min-width: 768px) and (max-width: 1023px){
  .content-card .figure-pair .figure img{ width:100% !important; }
}

/* Y10: uniform resort entry card (reusable; currently applied on diving-holiday-packages
   only). Inside stays V2-STRUCTURAL h3.pk-resort -> figure.resort-thumb -> p. */
.pk-card{background:var(--c-surface-2);border:1px solid var(--c-hairline);border-radius:var(--radius-lg);padding:var(--sp-4) var(--sp-5);margin:var(--sp-4) 0;box-shadow:var(--shadow-sm);}
.pk-card>.pk-resort{margin-top:0;}
.pk-card>p:last-child{margin-bottom:0;}

/* Y12-3 (diving-holiday-packages only): ToC row - one-column ToC left, nudibranch right. */
.feat-row--toc>.pk-toc{flex:1 1 auto;min-width:0;margin:0;}
.feat-row--toc .pk-toc ul{columns:1 !important;column-count:1 !important;}

/* ===== WALK-ROUND-3 REPAIRS (owner 2026-07-23) â€” BASE SCOPE, appended last so the cascade
   is unambiguous. Root causes: the pass-1 centering rules and the X01 :has rules had been
   inserted inside mobile @media blocks, so DESKTOP never received them. ===== */
.cta-row--center { justify-content: center; }
.cta-row--center .btn { flex: 0 1 auto; min-width: 260px; }
/* dive-site feature image: RIGHT of text on desktop. The base .imgvilla2 img width rules
   (width:100% / natural-width singles) defeated the align attribute's float. Explicit now. */
.imgvilla2 > img[align="right"] { float: right; width: auto; max-width: min(50%, 540px);
  margin: 0 0 var(--sp-4) var(--sp-4); }
.imgvilla2 > .section:has(> img[align="right"]) { float: right; width: auto;
  margin-left: var(--sp-4); margin-right: 0; }
.imgvilla2 > .section:has(> img[align="right"]) > img { width: auto; max-width: 100%; margin: 0; }
/* hero-title divider (owner round-2 item 2): the Discover-Scuba 56px gold bar, opt-in per page */
/* DIVIDER-SITEWIDE (owner 2026-07-23): the short gold bar renders under EVERY hero h1,
   site-wide â€” ONE rule, generalizing the .content-card>h2::after style. Supersedes the
   per-page hero-rule opt-in (class + build_hero flag stay in markup, now inert: ::after is
   a single pseudo-element, so no double-render is possible). Centered h1s inherit the
   existing h1.text-center::after margin-auto rule. */
.content-card h1::after { content: ""; display: block; width: 56px; height: 2px;
  background: var(--c-gold); margin-top: var(--sp-3); opacity: 0.8; }
@media (max-width: 899px) { /* mobile: centered stack, floats off */
  .imgvilla2 > img[align="right"] { float: none; width: 100%; max-width: 100%; margin: 0 auto; }
  .imgvilla2 > .section:has(> img[align="right"]) { float: none !important; width: 100%; margin: 0; }
}

/* ===== WALK-R1 â‘¢â€“â‘¦ (owner 2026-07-24) ===== */
/* â‘¢ thank-you contact: four titled blocks, each on its own line */
.contact-card__group { margin-bottom: var(--sp-4); }
.contact-card__subtitle { margin: 0 0 var(--sp-2); font-size: 17px; color: var(--c-navy); }
/* â‘¤ blog: ONE column with generous breathing space */
.room-grid:has(> .blog-card) { grid-template-columns: 1fr; gap: var(--sp-6); max-width: 760px; }
.blog-card .room__body { padding: var(--sp-5); }
/* â‘¥ one-column ToC modifier (long link texts; set per page from data) */
.pk-toc--one, .pk-toc--one ul { columns: 1 !important; column-count: 1 !important; }
/* R-ROOT-14 catch: the 2-col layout lives on the NAV element (.pk-toc{columns:2}),
   not the ul â€” the modifier must hit both. */
/* â‘¦(a) IMAGE LAW: every *-Dive-Site-Map.webp (the big area maps â€” case-exact, the small
   card maps are lowercase -dive-map) renders FULL WIDTH */
.content-card img[src$="-Dive-Site-Map.webp"] { width: 100% !important; max-width: 100% !important; height: auto !important; float: none !important; }
/* â‘¦(b) IMAGE LAW: narrow/vertical (portrait) content images float RIGHT, text LEFT â€”
   the render stamps .img--portrait when measured height > width */
.content-card p img.img--portrait, .imgvilla2 > img.img--portrait { float: right; width: auto; max-width: min(42%, 420px); margin: 0 0 var(--sp-4) var(--sp-4); }
@media (max-width: 899px) { .content-card p img.img--portrait, .imgvilla2 > img.img--portrait { float: none; width: auto; max-width: 100%; margin: 0 auto var(--sp-4); display: block; } }

/* ===== WALK-R2 â‘¨â‘ª (owner 2026-07-24) ===== */
/* â‘¨ DIVIDER POSITION LAW (resort pages): the gold bar renders BELOW the stars+pill meta
   row (below stars alone if no pill). When a hero meta row exists, the sitewide h1 bar is
   suppressed and the meta row carries it; heroes without a meta row keep the h1 bar. */
.content-card .page-hero:has(> .page-hero__meta) h1::after { content: none; }
.page-hero__meta { flex-wrap: wrap; }
.page-hero__meta::after { content: ""; flex: 0 0 100%; height: 2px;
  /* FR-WALK-R6 â‘¦ root-cause (2026-07-24): flex line-breaking uses the hypothetical main
     size CLAMPED by max-width â€” 56px fit INLINE beside stars+pill at wide viewports, so
     the bar never wrapped. Basis stays 100% (unclamped -> always wraps to its own line);
     the gold is painted 56px wide via gradient. Rendered proofs on resort heroes. */
  background: linear-gradient(90deg, var(--c-gold) 56px, transparent 0);
  margin-top: var(--sp-3); opacity: 0.8; }
/* â‘ª POPUP STANDARD (Borneo-Divers Deluxe model): 2-col amenity icons on desktop,
   breathing space under the room title, text-only content. */
.modal__panel .modal__title { margin-bottom: var(--sp-4); }
@media (min-width: 900px) {
  .modal__panel .amenities { columns: 2; column-gap: var(--sp-5); }
  .modal__panel .amenities li { break-inside: avoid; }
}
.modal__panel .cta-row { margin-top: var(--sp-4); }

/* ===== FR-WALK-R3 â‘¥ TOC-BULLETS LAW (owner 2026-07-24) â€” fr-scoped: pk-toc entries
   always carry a bullet marker. Scoped to html[lang="fr"] so EN/CN adopt via their
   own owners (cross-terminal announcement _NOTIFY_walk-r3-laws.md). */
html[lang="fr"] .pk-toc ul { list-style: disc; padding-left: 1.25em; }
html[lang="fr"] .pk-toc li { display: list-item; }

/* CORPUS-AUDIT L11, values aligned to the RATIFIED spec (REPORT-FR-017): Send button
   75% desktop / 100% mobile, centered. Supersedes the interim 50%. */
.inquiry-form button[type="submit"] { width: 75%; margin-left: auto; margin-right: auto; display: block; }
@media (max-width: 899px) { .inquiry-form button[type="submit"] { width: 100%; } }

/* CORPUS-AUDIT L8 (owner): narrow-LANDSCAPE content images (w<=360, w>=h â€” the
   barracuda-tornado / hammerhead class) float RIGHT with text LEFT on desktop. */
.content-card p img.img--narrow, .imgvilla2 > img.img--narrow { float: right; width: auto; max-width: min(40%, 360px); margin: 0 0 var(--sp-4) var(--sp-4); }
@media (max-width: 899px) { .content-card p img.img--narrow, .imgvilla2 > img.img--narrow { float: none; display: block; margin: 0 auto var(--sp-4); } }

/* EN-R3 â‘¢: dive-resorts ToC model â€” bulleted, bold links */
.pk-toc--bullets ul { list-style: disc; padding-left: 1.2em; }
.pk-toc--bullets ul ul { list-style: circle; }
.pk-toc--bullets a { font-weight: var(--fw-semi); font-size: 1rem; }
/* EN-R3-TOUCHUP â‘¡: every entry the same size as the tier entry (resort links were 0.95rem) */
.pk-toc--bullets .pk-toc__resort a { font-size: 1rem; }
.pk-toc--bullets ul ul { list-style: disc; }  /* â‘¢ flat look even if a sub ever returns */
/* FR-DIVESITE-STRUCTURAL-PARITY (2026-07-24): FR spot-tables carry French data-labels
   (mobile stacked-card captions), so the EN-keyed semibold selector above misses them.
   fr-scoped like the TOC-BULLETS law â€” EN/CN cascade untouched. */
html[lang="fr"] .spot-table td[data-label="Site de plongÃ©e"] { font-weight: var(--fw-semi); }

/* ===== WALK-R4 (owner 2026-07-24) ===== */
/* â‘  TOC TYPOGRAPHY LAW (sitewide): every ToC entry at the dive-resorts reference size,
   semi-bold, bulleted. Kills the 0.95rem resort-entry downsizing everywhere. */
.pk-toc ul { list-style: disc; padding-left: 1.2em; }
.pk-toc ul ul { list-style: disc; }
.pk-toc a, .pk-toc__resort a { font-size: 1rem; font-weight: var(--fw-semi); }
/* â‘¡ TOC COLUMN LAW: 1-4 entries = ONE column; 5+ may keep two.
   FR-WALK-R7 â‘¢ (owner 2026-07-25) â€” SCOPED AWAY FROM fr. `> li:nth-child(5)` counts only
   TOP-LEVEL <li>, so every tiered fr TOC (3-4 tier rows carrying 9-16 nested resort rows)
   read as "<=4 entries" and was forced to one column with !important â€” the single-column
   regression, on every tiered page at once. CSS cannot count descendants, so on fr the
   counting law is computed over TOTAL entries and stamped as .pk-toc--one at build time
   (scripts/toc_columns_fr.py). EN/CN cascade deliberately unchanged â€” announced, their call. */
html:not([lang="fr"]) .pk-toc:has(> ul:not(:has(> li:nth-child(5)))) { columns: 1 !important; }
/* â‘¦ DIVE-SITE FEATURE IMAGE â€” the real fix. The 7d system computes the WRAPPER as
   display:inline-block at half width, so floating the IMG was invisible (it floated inside
   a left-positioned half-width box; probed computed style, REPORT-EN-009). Float the
   WRAPPER itself right when it holds a single align-right image. */
.content-card .imgvilla2:has(> img[align="right"]):not(:has(> .section)):not(.so-hero) {
  display: block; float: right; width: auto; max-width: min(50%, 540px);
  margin: 0 0 var(--sp-4) var(--sp-4); }
.content-card .imgvilla2:has(> img[align="right"]):not(:has(> .section)) > img {
  float: none; width: auto; max-width: 100%; margin: 0; }
@media (max-width: 899px) {
  .content-card .imgvilla2:has(> img[align="right"]):not(:has(> .section)) {
    float: none; width: 100%; max-width: 100%; margin: 0 0 var(--sp-4); }
  .content-card .imgvilla2:has(> img[align="right"]):not(:has(> .section)) > img {
    display: block; margin: 0 auto; }
}
/* WALK-R5 Bâ‘  (2026-07-24): hero subtitle slot (opt-in via hero data 'sub'). */
.page-hero__sub { font-size: var(--fs-md); font-weight: var(--fw-semi); margin: 0 0 var(--sp-2); }
/* FR-WALK-R6 â‘  (2026-07-24): popup subtitle under the (single) modal title. */
.modal__subtitle { font-size: var(--fs-md); font-weight: var(--fw-bold); margin: 0 0 var(--sp-3); }

/* ===== FR-WALK-R7 (owner 2026-07-25) ===== */
/* â‘¡ TOC TYPOGRAPHY LAW â€” TWO LEVELS (supersedes the WALK-R4 â‘  uniform-size reading).
   FIRST-LEVEL entries (the tier labels: Â«Top luxeÂ», Â«Gamme luxeÂ», Â«Gamme intermÃ©diaireÂ»,
   Â«CroisiÃ¨re SipadanÂ», and their equivalents on every TOC page) render at the LARGER size;
   SECOND-LEVEL entries (resort names nested under a tier) keep the current 1rem.
   Expressed by LEVEL, not by class, so TOCs whose rows carry no pk-toc__tier/__resort class
   (Tawau, Venir-ici, sipadan, Hotels-a-Semporna) obey the same law. fr-scoped like the R3
   bullets law; announced to EN for their adoption. */
html[lang="fr"] .pk-toc > ul > li > a { font-size: var(--fs-md); }
html[lang="fr"] .pk-toc > ul > li > ul > li > a { font-size: 1rem; }

/* ===== FR-WALK-R8 (owner 2026-07-25) ===== */
/* L-CTA-LEN â€” a paragraph CTA button renders on ONE line on desktop. The label sweep caps every
   button at 10 words, so nowrap cannot overflow the content column; mobile keeps the same text and
   is free to wrap (the â‰¤559px rule already gives it the full row). */
@media (min-width: 900px) { .cta-row .btn { white-space: nowrap; } }
/* â‘ª a CTA row the owner wants flush left on desktop (opt-in modifier, never a global change). */
.cta-row--left { justify-content: flex-start; }
.cta-row--left .btn { flex: 0 1 auto; }
/* â‘« one-column resort cards (the Semporna budget water resorts digest). */
.room-grid--one { grid-template-columns: 1fr; max-width: 760px; }
.room-grid--one .resort-thumb { margin: var(--sp-2) 0 0; }
.room-grid--one .resort-thumb img { width: 100%; height: auto; border-radius: var(--radius-sm, 6px); }
/* TOC SIZE HARMONIZATION â€” supersedes the FR-WALK-R7 â‘¡ depth-based rule, which sized by NESTING:
   a flat TOC (Borneo-Dive-Resorts, Hotels-de-luxe-a-Mabul, Tawau, Packages-Sejours, and the five
   loose resort rows on Hotels-de-plongee) is all top level, so every resort name there rendered at
   the tier size. Size now follows the ROLE: a row that HEADS a tier (it carries pk-toc__tier or
   contains a nested list) is large; every other row â€” every resort name, at any depth â€” is the
   level-2 size. */
html[lang="fr"] .pk-toc > ul > li > a { font-size: 1rem; }
html[lang="fr"] .pk-toc > ul > li > ul > li > a { font-size: 1rem; }
html[lang="fr"] .pk-toc > ul > li.pk-toc__tier > a,
html[lang="fr"] .pk-toc > ul > li:has(> ul) > a { font-size: var(--fs-md); }
/* EN-WALK-R8 â‘  â€” the same ROLE-based TOC sizing adopted for EN (owner order: the
   Sipadan-Resorts / Luxury-Diving-Packages / Luxury-Non-Diving-Packages tier rows â€”
   Top Luxury, Luxury Range, Medium Range, Sipadan Liveaboard â€” render at the BIG tier
   size; resort names at any depth stay level-2). Gated on html[lang="en"], so cn/es/de/
   ru/zh output is untouched and each terminal adopts on its own order. */
html[lang="en"] .pk-toc > ul > li > a { font-size: 1rem; }
html[lang="en"] .pk-toc > ul > li > ul > li > a { font-size: 1rem; }
html[lang="en"] .pk-toc > ul > li.pk-toc__tier > a,
html[lang="en"] .pk-toc > ul > li:has(> ul) > a { font-size: var(--fs-md); }
/* L-LONE-IMG â€” a page whose ONLY image is a small one puts it at the TOP of the article, right of
   the text on desktop, centred above the text on mobile. The "only image / small" test cannot be
   expressed in CSS, so scripts/lone_image_fr.py stamps .img--lone at build time. */
.content-card img.img--lone { float: right; width: auto; max-width: min(42%, 420px); height: auto;
  margin: 0 0 var(--sp-4) var(--sp-4); }
.content-card figure.img--lone { float: right; width: auto; max-width: min(42%, 420px);
  margin: 0 0 var(--sp-4) var(--sp-4); }
.content-card figure.img--lone img { width: 100%; height: auto; }
@media (max-width: 899px) {
  .content-card img.img--lone, .content-card figure.img--lone { float: none; display: block;
    margin: 0 auto var(--sp-4); max-width: 100%; }
}
/* â‘° ROOT CAUSE of "barracuda-tornado is still not right-aligned": the CORPUS-AUDIT L8 rule and the
   WALK-R1 â‘¦(b) portrait rule only match `p img` and `.imgvilla2 > img`. The class WAS stamped on
   the image (measured 300x200 -> .img--narrow), but the image lives inside <figure class="figure">,
   which neither selector reaches â€” so the cascade never floated it. The earlier pass checked the
   class, not the resolved cascade. Float the FIGURE when it wraps such an image (same technique the
   WALK-R4 â‘¦ fix used for .imgvilla2). */
.content-card figure.figure:has(> img.img--narrow),
.content-card figure.figure:has(> img.img--portrait),
.content-card figure.figure:has(> a > img.img--narrow),
.content-card figure.figure:has(> a > img.img--portrait) {
  float: right; width: auto; max-width: min(42%, 420px); margin: 0 0 var(--sp-4) var(--sp-4); }
.content-card figure.figure:has(> img.img--narrow) img,
.content-card figure.figure:has(> img.img--portrait) img { width: 100%; height: auto; }
@media (max-width: 899px) {
  .content-card figure.figure:has(> img.img--narrow),
  .content-card figure.figure:has(> img.img--portrait),
  .content-card figure.figure:has(> a > img.img--narrow),
  .content-card figure.figure:has(> a > img.img--portrait) {
    float: none; max-width: 100%; margin: 0 auto var(--sp-4); }
}
/* â‘© package blocks are BULLETED lists (the .amenities icon-list style suppresses markers, and
   these rows carry no icons, so they read as loose lines). */
.content-card ul.pk-bullets { list-style: disc; padding-left: 1.25em; margin: var(--sp-3) 0; }
.content-card ul.pk-bullets li { display: list-item; margin: 0 0 var(--sp-2); }

/* ============================================================================
   CSS-R1 â€” MOBILE STICKY TOP BAR  (<=1023px ONLY)
   ----------------------------------------------------------------------------
   MEASURED CAUSE, not a guess: .site-header already declares
   `position: sticky; top: 0` (Â§1, "sticky on desktop AND mobile"), but EVERY
   page in both trees renders `class="site-header site-header--scene"`, and
   `.site-header--scene { position: relative }` (Â§ Build 14 header scene) has
   equal specificity and comes later in the cascade â€” so the sticky declaration
   has been dead on every page, at every width. Probed on 8 pages x 2 viewports:
   header rectTop == -scrollY on all 16, i.e. it scrolled away everywhere.

   This block restores stickiness AT THE MOBILE BREAKPOINT ONLY. It is a
   `max-width: 1023px` query, so no declaration in it can reach >=1024px: the
   baseline desktop header is untouched, byte for byte.

   IT IS DELIBERATELY NOT THE INVERSE RULE. The reverted WALK MOBILE-R1 block
   did this with `@media (min-width:1024px){ .site-header{ position:static } }`,
   which de-stickied the DESKTOP header and displaced the animated hero. That
   shape is the named anti-pattern: never reintroduce a min-width rule that
   touches .site-header positioning.

   z-index (80) and the stacking context are inherited from Â§1 and unchanged;
   the mobile drawer (.site-nav, z-index 100) is a SIBLING of <header>, not a
   descendant, so it still covers the bar exactly as before.
   ============================================================================ */
@media (max-width: 1023px) {
  .site-header,
  .site-header--scene {
    position: sticky;
    top: 0;
  }
}

/* ============================================================================
   CSS-R3 â€” DATA TABLES BECOME CARDS ON MOBILE  (<=1023px ONLY)
   ----------------------------------------------------------------------------
   Owner ruling from live mobile use: a content data table renders at the mobile
   breakpoint as stacked cards â€” one card per body row, each cell a
   label/value line, header row hidden.

   This is the SAME card language the site already uses for .spot-table
   (Â§ "Dive-spot table", max-width:1023px): same hairline border, same radius,
   same surface, same shadow, same 42% label column, all from the tokens. The
   only difference is where the label comes from â€” .spot-table has data-label
   baked into its markup, everything else is stamped at load by the CSS-R3
   module in js/site.js from the table's own header row, so each language tree
   gets its own translated labels for free.

   .spot-table is NOT retargeted here: it already does this, correctly, on 161
   tables. Touching it would be risk without a gain.

   `.t-cards` is added by the JS only after it has found a usable header row and
   at least one body row, so a table this cannot label keeps its desktop layout
   at every width rather than becoming an unlabelled card stack.

   Everything below is inside `max-width: 1023px`: no declaration can reach
   >=1024px, so the desktop table rendering is untouched, byte for byte. AP-1
   (a min-width rule that reaches back into desktop) is not used.
   ============================================================================ */
@media (max-width: 1023px) {
  .t-cards { display: block; width: 100%; border: 0; }
  .t-cards thead,
  .t-cards .t-cards__head { display: none; }
  .t-cards tbody,
  .t-cards tr,
  .t-cards td { display: block; width: 100%; }

  .t-cards tr {
    border: 1px solid var(--c-hairline);
    border-radius: var(--radius);
    background: var(--c-white);
    box-shadow: var(--shadow-sm);
    margin: 0 0 var(--sp-3);
    overflow: hidden;
  }
  .t-cards tr:last-child { margin-bottom: 0; }

  .t-cards td {
    display: flex;
    gap: var(--sp-3);
    border: 0;
    border-bottom: 1px solid var(--c-hairline);
    padding: var(--sp-2) var(--sp-3);
    /* the pre-rebuild tables carry nowrap="nowrap" on their cells; left alone it
       re-creates the very horizontal overflow this round exists to remove */
    white-space: normal;
    background: transparent;
  }
  /* Â§ Tables stripes with `tr:nth-child(even) td` â€” (0,1,2), MORE specific than
     `.t-cards td` (0,1,1) â€” so a plain override loses and every second card came
     out beige. Same selector shape .spot-table already uses, for the same reason. */
  .t-cards tr:nth-child(even) td,
  .t-cards tr:nth-child(odd) td { background: transparent; }
  .t-cards td:last-child { border-bottom: 0; }
  /* R20 â‘¥ (owner): a card reads TITLE ON TOP, then its lines BELOW â€” never a
     label-left / value-right pair. The label sits ABOVE its value as a small caption,
     and the row's first cell is the card title, which needs no label at all.
     Exemplar: the Mataking snorkelling row on the frontpage table. */
  .t-cards td { display: block; }
  .t-cards td::before {
    content: attr(data-label);
    display: block;
    flex: none;
    margin-bottom: 2px;
    font-size: var(--fs-xs);
    font-weight: var(--fw-semi);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--c-ink-soft);
  }
  .t-cards tr > td:first-child {
    font-size: var(--fs-md);
    font-weight: var(--fw-bold);
    color: var(--c-navy);
    padding-top: var(--sp-3);
  }
  .t-cards tr > td:first-child::before { content: none; }

  /* a colspan cell spans columns, so no single header labels it: full-width line */
  .t-cards td.is-span { display: block; }
  .t-cards td.is-span::before { content: none; }

  /* an empty cell would print a label with nothing after it */
  .t-cards td.is-empty { display: none; }
}

/* ============================================================================
   CSS-R2 â€” ANCHOR LANDINGS CLEAR THE MOBILE STICKY BAR  (<=1023px ONLY)
   ----------------------------------------------------------------------------
   CSS-R1 pinned the header at the mobile breakpoint, so anything scrolled to by
   an in-page link now has to start below it. Measured before this block, at
   414px across 6 pages and 64 in-page anchors:

     h2/h3          landed at 88px  (Â§ line 42, calc(--header-h + --sp-5))
     .pk-tier/.pk-resort  landed at 90px  (Â§ line 2078, a literal)
     #main          landed at   0px  -> UNDER the 79px bar, on every page that
                    has the "Skip to content" link, which is all of them

   So the compensation existed but was partial: it covered headings only, and it
   was expressed as two different constants, neither equal to the bar's real
   79px height.

   ROBUST FORM, not a magic number: --sticky-h is the MEASURED header height,
   published by the CSS-R2 module in js/site.js and re-measured on resize, font
   load and header resize. The fallback is the existing --header-h token, so a
   no-JS render still clears the bar rather than falling back to zero.

   [id] is deliberate: "anchor targets site-wide" includes <main id="main">, the
   accessibility skip-link target, which no heading selector reaches. It is
   (0,1,0), so it also supersedes the .pk-* literal here â€” at mobile only; both
   earlier rules keep their exact desktop behaviour, untouched.

   scroll-margin-top paints nothing. The static gate must read 0.0000% on every
   row, desktop and mobile alike: this is scroll geometry, not pixels.
   ============================================================================ */
@media (max-width: 1023px) {
  [id] { scroll-margin-top: calc(var(--sticky-h, var(--header-h)) + var(--sp-4)); }
}

/* ============================================================================
   CSS-R4 â€” WECHAT QR SIZE CAP  (<=1023px ONLY)
   ----------------------------------------------------------------------------
   Owner ruling, re-confirmed on live mobile: the WeChat QR must not render
   near full width. Measured before this block, on all six contact pages that
   carry it (en, cn, de, es, ru, zh):

     natural   154 x 154
     mobile    346 x 346   = x2.25 upscale  -> the pixelation
     desktop   154 x 154   = x1.00          -> already sane, left as it is

   WHAT WAS INFLATING IT â€” and why this needs `!important`. It is not a stray
   rule: `Â§ OWNER EYEBALL-2 UNIT 1 / R-E` (line ~2130) is an owner ruling that
   deliberately forces EVERY content-card image to the full column on mobile:

       .content-card img { width: min(100%, var(--pair-w, 9999px)) !important; }

   So CSS-R4 is a NARROW, DELIBERATE EXCEPTION to a standing owner ruling, not a
   correction of a mistake. It is scoped to this one asset by src, it is the only
   way to beat an !important declaration, and R-E keeps governing every other
   image on the page. **Two owner rulings meet here â€” flagged in the report.**

   THE CAP ITSELF. `--qr-nat-w` is the image's measured intrinsic width, published
   by the CSS-R4 module in js/site.js (the same trick the lightbox uses for
   --lb-natw). The 154px fallback is the asset's width today and is only used in
   the moment before the lazy image decodes.

     width      fills the column but never past 150% of natural  -> the cap
     min-width  never below 100% of natural  -> stays crisp on a narrow phone
     margin     auto/auto -> centred, as ordered

   Desktop is untouched: this is a max-width:1023px block, and desktop already
   measured x1.00.
   ============================================================================ */
@media (max-width: 1023px) {
  .content-card img[src*="QR-code-ADH"] {
    width: min(100%, calc(var(--qr-nat-w, 154px) * 1.5)) !important;
    min-width: var(--qr-nat-w, 154px);
    max-width: calc(var(--qr-nat-w, 154px) * 1.5);
    margin-left: auto;
    margin-right: auto;
  }
}

/* ============================================================================
   CSS-R5 â€” IMAGE SEQUENCES RENDER AT ONE WIDTH  (>=1024px ONLY)
   ----------------------------------------------------------------------------
   Owner ruling, desktop only: a resort/listing image sequence renders at a
   uniform width equal to the SMALLEST natural width in that sequence, so the run
   reads as a column and no member is ever upscaled.

   Exemplar /dive-resorts.php, Mabul Water Bungalows -> Pom Pom, measured before:
     325, 260, 360, 360, 279, 325, 325, 360   (natural 325,260,640,640,279,325,325,530
     capped by `.resort-thumb{max-width:min(100%,360px)}`) -> ragged.
   Smallest natural in the sequence = 260, so the whole run renders 260 wide.

   --seq-w and the .seq-uniform class are written by the CSS-R5 module in
   js/site.js; the detection rule for "sequence" is stated in full there. CSS
   never guesses: if the module cannot resolve a sequence it emits nothing and
   the existing rules keep governing.

   Alignment is NOT set here â€” the members keep whatever the page already gives
   them (`.resort-thumb` is display:block with its own margins), per "centered
   per the page's existing alignment".

   A min-width query is correct for a desktop-only ruling and is NOT the AP-1
   anti-pattern: AP-1 is specifically a min-width rule that changes
   .site-header POSITIONING. This touches listing figures only, never the header.
   ============================================================================ */
@media (min-width: 1024px) {
  .content-card .seq-uniform {
    width: var(--seq-w);
    max-width: var(--seq-w);
  }
  .content-card .seq-uniform > img,
  .content-card .seq-uniform > a > img {
    width: 100%;
    height: auto;
  }
}

/* ============================================================================
   CSS-R7 â€” MOBILE BODY IMAGES ARE FULL BLOCKS, NEVER FLOATS  (<= 1023px)
   ----------------------------------------------------------------------------
   Owner ruling: at the mobile breakpoint no image floats inside a paragraph â€”
   the image is a full block before or after the paragraph, with a standard
   margin ABOVE and BELOW.

   ROOT CAUSE (exemplar /fr/Mabul-Inn.php, the Â«LeÂ» orphan). Three faults stack:
     1. the corpus writes `align="right"` ON THE IMAGE. The existing mobile rules
        unfloat the FIGURE, never the image, so the UA presentational float
        survived and the figure collapsed to height 0 (measured: figH 0).
     2. every unfloat rule in this file was written at `max-width: 899px`, but the
        canon mobile breakpoint is 1023px â€” so 900â€“1023px kept the desktop float.
     3. the desktop float rules set `margin: 0 0 var(--sp-4) var(--sp-4)`, i.e.
        NO top margin, which the 899px rules never restored.
   Measured on the exemplar before this block, first text line beside the float:
     414px 336px wide (no orphan) Â· 480px **17px â€” the word Â«LeÂ» alone** Â·
     560px 98px Â· 640px 159px Â· 720px 182px Â· 800px 298px Â· 900â€“1023px 400â€“515px.
   So the reported orphan is real and the whole 480â€“1023 band flowed around the
   float.

   SCOPE â€” `> figure` / `> img` / `p img` is the discriminator between prose and
   grid. Measured ancestry: a prose image is `FIGURE|IMG < ARTICLE.content-card`
   (and the legacy corpus wrapper is `.imgvilla* < DIV.text`), while every grid
   image sits one level deeper â€” `FIGURE < DIV.masonry`, `FIGURE < DIV.figure-pair`.
   So pairs, masonry, carousels, listing thumbs and room cards are untouched here.

   `!important` on float, mobile-only: the desktop float rules are written with
   `:has()` (specificity 0,3,2) and outrank any plain breakpoint-scoped selector.
   Same reasoning as CSS-R4/AD-6 â€” a scoped exception, and the desktop cascade is
   not touched by a max-width block.

   Margins are set on the block that occupies the column, as longhands, so the
   `margin: 0 auto` centring from OWNER EYEBALL-2 R-E survives. `.imgvilla*` is
   deliberately NOT in the margin list: it already carries 16px/32px, both at or
   above standard, and a shorthand there would have shrunk its bottom margin.
   ============================================================================ */
@media (max-width: 1023px) {
  .content-card > figure,
  .content-card > figure img,
  .content-card > img,
  .content-card p figure,
  .content-card p img,
  .imgvilla, .imgvilla2, .imgvilla3,
  .imgvilla > .section, .imgvilla2 > .section, .imgvilla3 > .section,
  .imgvilla img, .imgvilla2 img, .imgvilla3 img {
    float: none !important;
  }
  .content-card > figure,
  .content-card > img,
  .content-card p figure,
  .content-card p img {
    margin-top: var(--sp-4) !important;      /* 1rem â€” standard, above AND below */
    margin-bottom: var(--sp-4) !important;
  }
}

/* ============================================================================
   CSS-R8 â€” MOBILE LIGHTBOX FIT: THE CONTROLS NEVER SIT ON THE PHOTO (<= 1023px)
   ----------------------------------------------------------------------------
   Owner ruling: on mobile the popup image renders slightly smaller with side
   padding, so the top-right X never overlaps it.

   Measured before, /Sipadan-Photo-Video.php at 414 (and 480):
     panel  [17, 267, 381, 361]   padding 12px
     image  [30, 280, 355, 214]   -> right edge at panel_right - 13
     close  [364, 276,  24,  24]  -> left edge at panel_right - 34
     X âˆ© image = 400pxÂ²   (and prev âˆ© image = next âˆ© image = 480pxÂ²)

   The panel is `width: fit-content` with the image capped at its content box, so
   widening only the side padding shrinks the image and opens a gutter for the
   controls â€” no change to the panel box, the caption width or the height budget.
   Gutter = 10px control offset + 24px control + 6px clearance = 40px, which puts
   the image's right edge 6px clear of the X's left edge.

   Desktop keeps its own frame (`min-width: 1024px`: padding 64px 78px 14px,
   where the X already clears the photo by 40px) and is not touched here.
   The prev/next arrows, at the same 10px offset on the other axis, stop
   overlapping as a by-product of the same gutter.
   ============================================================================ */
@media (max-width: 1023px) {
  .lb__panel {
    padding-left: 40px;
    padding-right: 40px;
  }
}

/* ============================================================================
   CSS-R9 â€” TOC CARDS TAKE THE FULL AVAILABLE WIDTH ON MOBILE  (<= 1023px)
   ----------------------------------------------------------------------------
   Owner ruling: at the mobile breakpoint a ToC card fills the available width,
   inner text left-aligned. Desktop ToC untouched.

   ROOT CAUSE (exemplar /fr/Packages-Sejours-Plongees.php). `.feat-row` is
   `display:flex; align-items:flex-start`. At <=767px it already switches to
   `flex-direction:column`, but in column mode `align-items` governs the
   HORIZONTAL (cross) axis, so `flex-start` makes each child SHRINK-WRAP to its
   content width â€” and `flex:1 1 auto` cannot help, it only sizes the main axis.
   Measured before, .pk-toc outer width vs the width available in its parent:
     414px  255.6 of 345.8  -> 90.2px short
     480px  255.6 of 401.3  -> 145.7px short
     /diving-holiday-packages.php  316.1 of 345.8 -> 29.7px short
   Every other ToC in the tree already measured shortfall 0.

   Two changes, both scoped to the ToC feature row:
     1. cross-axis stretch, so the card fills the column;
     2. the column direction extended from 767px to the canon 1023px breakpoint,
        because the ruling is "at the mobile breakpoint" and 768â€“1023 was still a
        side-by-side row. The figure keeps the centred, capped treatment the
        <=767px rule already gave it, so the band matches what mobile already did.
   Desktop (>=1024px) never sees this block: the row, the two columns and the
   nudibranch keep their measured geometry.

   text-align is pinned left as the ruling states. It already computes to `start`
   on every ToC measured (card, title, li, a), so this is a no-op today; it exists
   so a later centring rule cannot silently reach ToC prose.
   ============================================================================ */
@media (max-width: 1023px) {
  .feat-row--toc { flex-direction: column; align-items: stretch; }
  .feat-row--toc > .pk-toc { width: 100%; margin: 0 0 var(--sp-4); }
  .feat-row--toc > .feat-row__fig { max-width: 100%; align-self: center; }
  .pk-toc, .pk-toc__title, .pk-toc li, .pk-toc a { text-align: left; }
}

/* ============================================================================
   CSS-R10 â€” CONSECUTIVE IMAGE PAIRS: ONE SIZE PER PAIR
   ----------------------------------------------------------------------------
   Owner rulings: a pair STACKED (mobile) renders both members at one width,
   the SMALLER member's width, centred; a pair SIDE-BY-SIDE renders both members
   at EQUAL HEIGHT. Expressed by direction, not by breakpoint, because the pair's
   own stacking boundary is `max-width: 767px` (`.figure-pair{flex-direction:
   column}`) â€” a row is a row whether it is at 900px or at 1920px.

   ROOT CAUSE of unequal heights in a row. `.figure-pair > .figure` matches the â‘°
   L8 float rule `.content-card figure.figure:has(> img.img--narrow)`, which sets
   `max-width: min(42%, 420px)`. In a pair the percentage resolves against the ROW,
   so BOTH members were clamped to 42% of it â€” measured 259.97px each on
   /fr/Sipadan-Mabul-Resort.php â€” which defeats the `flex: var(--ar) 1 0%`
   proportional sizing that was supposed to produce equal heights. Members then
   rendered 195 vs 172.4, 183.7 vs 213.5, 187.3 vs 208.6.

   Equal height is driven by HEIGHT (--pair-h from js/site.js), not by widths:
   h = min(smallest natural height, height that makes the row fit its column).
   The first term means equal height never upscales a member past its natural
   size (the desktop no-upscale law); the second keeps the row inside the column.
   Measured consequence on the exemplar pair: 260x195 + 294x195 â€” both at natural
   size, equal height, sum 566px inside a 619px row.

   Stacked members read --pair-eq (min natural width) instead. `!important` there
   is required to beat OWNER EYEBALL-2 R-E (`.content-card img{width:min(100%,
   var(--pair-w))!important}`), whose per-member --pair-w is what made stacked
   members unequal (260 vs 286, 325 vs 300); this selector is more specific, so
   the R-E law keeps governing every image that is NOT a pair member.
   ============================================================================ */
.figure-pair { justify-content: center; }

/* ---- pair as a ROW: equal height ---- */
@media (min-width: 768px) {
  .content-card .figure-pair > .figure {
    /* !important is a measured necessity: the L8 rule
       `.content-card figure.figure:has(> img.img--narrow)` sits at specificity (0,3,2)
       and outranks any plain pair selector. Without it the clamp survived and, with the
       height now fixed, squashed the members â€” measured 304.9x210 on a 325x210 image,
       aspect ratio 1.452 against a natural 1.5476. */
    max-width: none !important;       /* undo the 42%-of-row clamp from the L8 rule */
    float: none;
    flex: 0 0 auto;                   /* height drives the size now, not flex-grow */
  }
  .content-card .figure-pair.pair-eq > .figure > img {
    height: var(--pair-h);
    width: auto !important;           /* beats the B1 768â€“1023 `width:100%!important` */
    max-width: 100%;
  }
}

/* ---- pair STACKED: one width, the smaller member's, centred ---- */
@media (max-width: 767px) {
  .content-card .figure-pair.pair-eq > .figure > img {
    width: min(100%, var(--pair-eq)) !important;
    height: auto;
    margin-left: auto;
    margin-right: auto;
  }
}

/* ============================================================================
   CSS-R11 â€” SECONDARY MENU, MOBILE: ITEM PADDING -2px TOP AND -2px BOTTOM
   ----------------------------------------------------------------------------
   Owner ruling, exact. The secondary menu is the drawer's second level,
   `.site-nav__sub` â€” the primary level is `.site-nav__link`, the sidebar's
   secondary module (`.top10-card`) is `display:none` below 1024px, and the
   footer carries no menu. Measured before: `.site-nav__sub a` padding
   `var(--sp-2)` = 8px top and bottom at the mobile breakpoint.
   8 - 2 = 6px, written as an absolute so the token cannot drift it.
   The desktop copy of this rule (inside `min-width: 1024px`, with its own
   `--dense` 5px variant) is untouched.
   ============================================================================ */
@media (max-width: 1023px) {
  .site-nav__sub a {
    padding-top: 6px;
    padding-bottom: 6px;
  }
}

/* ============================================================================
   CSS-R12 â€” FORM CONTROLS ON MOBILE READ AT THE PLACEHOLDER'S DECLARED SIZE
   ----------------------------------------------------------------------------
   Owner ruling: on mobile, the select, the date input and the country list use
   the text-input PLACEHOLDER's declared size. Read from the served stylesheet,
   not guessed:
     .form-field input::placeholder, .form-field textarea::placeholder
       { font-size: var(--fs-base) }        css/main.css:878
     --fs-base: 1.0625rem   (= 17px)        css/tokens.css:59
   So the target is `var(--fs-base)` â€” quoted as the token, so it tracks the
   placeholder if the token ever moves.

   Measured before, at <= 1023px:
     .form-field select                16px   (`mobile: dropdown stays compact`)
     .form-field select option         15px   (this is the country list, #f-country,
                                               and the resort list, #f-resort)
     .form-field input[type=date]      17px   (already at the placeholder size,
                                               inherited from the shared `font:`)
   So this round moves the select from 16 to 17 and its options from 15 to 17;
   the date input is asserted at 17, not changed.

   The NATIVE PICKER is untouched: no `appearance`, no
   `::-webkit-calendar-picker-indicator`, no `::-webkit-datetime-edit` rules here.
   `input[type=date]{appearance:auto}` (main.css:1050) keeps the native UI.

   Applies to both form surfaces because it is class-level on `.form-field`: the
   static `inquiryform*.php` pages and the registry-routed slugs both render the
   same `includes/form.inquiry.php` fields.
   ============================================================================ */
@media (max-width: 1023px) {
  .form-field select,
  .form-field select option,
  .form-field input[type="date"] {
    font-size: var(--fs-base);
  }
}

/* ============================================================================
   CSS-R13 â‘  â€” MEDIA EMBEDS NEVER EXCEED THE CONTENT COLUMN (both viewports)
   ----------------------------------------------------------------------------
   Exemplar /Borneo-From-Below.php. Measured before: the EN page wraps each
   iframe in `.video-cell`, which already sizes it (346x195 mobile, 726x408
   desktop). The FR twin has NO wrapper â€” its iframes keep the markup's
   width="560" and render 214px PAST the 346px column at 414px, and 560px flat
   on desktop. So the fix cannot live on the wrapper class alone; it has to
   catch a bare iframe as well.
   The 16/9 ratio comes from the attributes (560x315) and matches what
   `.video-cell` produces, so the EN and FR twins now render identically.
   ============================================================================ */
.content-card .video-cell { max-width: 100%; }
.content-card > iframe,
.content-card > video,
.content-card :not(.video-cell) > iframe,
.content-card :not(.video-cell) > video {
  display: block;
  width: 100%;
  max-width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  border: 0;
  margin-left: auto;
  margin-right: auto;
}

/* ============================================================================
   CSS-R13 â‘¡ â€” VIDEO FACADE COMPONENT
   ----------------------------------------------------------------------------
   Binds to the markup contract EN shipped in EN-038
   (_REQUEST-CSS-video-facade-component.md): section.video-facades >
   .video-facade-grid > a.video-facade > (img.video-facade__thumb,
   span.video-facade__play, span.video-facade__cap).

   Owner addendum: 2 columns x 2 rows on desktop, 1 column stacked on mobile,
   consistent card sizing. The unstyled link-list state is the defect this
   closes. Caption text gets a standard margin ABOVE and BELOW (exemplar the
   Â«Turtle Rodeo at SipadanÂ» line).

   Card sizing is consistent by construction: the grid gives every card the same
   column width and the thumb is a fixed 16/9 crop, so all cards in a row match
   whatever the thumbnail's own proportions are (the ytimg hqdefault frames are
   4:3, hence object-fit: cover).

   The click-to-load swap is in js/site.js; `.video-facade__embed` below is the
   box it inserts. `.pv-video-card*` (the retired static matrix, EN-038 Â§â‘¢) is
   left in place for now â€” retiring it is a separate, orderable cleanup.
   ============================================================================ */
.video-facades { margin: var(--sp-6) 0; }
.video-facade-grid {
  display: grid;
  grid-template-columns: 1fr;              /* mobile: one column, stacked */
  grid-auto-rows: 1fr;                     /* cards in one row share a height */
  gap: var(--sp-5);
  margin: var(--sp-4) 0;
}
@media (min-width: 1024px) {
  .video-facade-grid { grid-template-columns: 1fr 1fr; }   /* 2 x 2 for four videos */
}
.video-facade {
  display: flex;
  flex-direction: column;                  /* with grid-auto-rows:1fr = consistent card sizing */
  text-decoration: none;
  color: inherit;
  position: relative;
  cursor: pointer;
}
.video-facade__thumb {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--radius);
  background: #0b1f33;
  box-shadow: var(--shadow-sm);
}
/* play badge: a CSS-drawn triangle in a disc â€” no external asset, no icon font */
.video-facade__play {
  position: absolute;
  top: 0; left: 0; right: 0;
  aspect-ratio: 16 / 9;
  display: flex; align-items: center; justify-content: center;
  pointer-events: none;
}
.video-facade__play::before {
  content: "";
  width: 62px; height: 62px;
  border-radius: 50%;
  background: rgba(2, 33, 63, 0.62);
  border: 2px solid rgba(255, 255, 255, 0.9);
  transition: background var(--t-fast) var(--ease);
}
.video-facade__play::after {
  content: "";
  position: absolute;
  width: 0; height: 0;
  border-style: solid;
  border-width: 11px 0 11px 18px;
  border-color: transparent transparent transparent #fff;
  margin-left: 6px;                        /* optical centring of a triangle in a disc */
}
.video-facade:hover .video-facade__play::before,
.video-facade:focus-visible .video-facade__play::before { background: rgba(2, 33, 63, 0.82); }
.video-facade__cap {
  display: block;
  margin-top: var(--sp-4);                 /* standard breathing margin ABOVE ... */
  margin-bottom: var(--sp-4);              /* ... and BELOW (owner addendum) */
  font-size: var(--fs-sm);
  line-height: 1.45;
  color: var(--c-navy);
}
/* the box the click handler swaps in */
.video-facade__embed {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius);
  overflow: hidden;
  background: #0b1f33;
}
.video-facade__embed iframe {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0;
  aspect-ratio: auto;                      /* the wrapper owns the ratio */
}

/* ============================================================================
   CSS-R13b â€” NO-UPSCALE LAW (DESKTOP)
   ----------------------------------------------------------------------------
   Owner ruling: on desktop an image never renders wider than its natural width;
   cap at natural, centred per context. MOBILE IS EXEMPT (full-width allowed).
   `--nat-w` is stamped per image by js/site.js â€” CSS cannot read naturalWidth,
   and the width attribute is not always the natural size.
   Exemplar /Luxury-Non-Diving-Packages.php,
   images/small/mabul-sunset-watervillage.webp, blown up to visible pixelation.

   Deliberately NOT `!important`: the â‘¦(a) full-width map law
   (`*-Dive-Site-Map.webp { width:100% !important }`) and any other `!important`
   width law keep winning, so this round cannot silently overturn an existing
   ruling. Those collisions are listed in the report.
   ============================================================================ */
@media (min-width: 1024px) {
  .content-card img { max-width: min(100%, var(--nat-w, 100%)); }
}

/* ============================================================================
   CSS-R14a â€” DESKTOP FLOAT LAW  (executes EN's _REQUEST-CSS-image-float-law.md)
   ----------------------------------------------------------------------------
   Standing owner law (EN-KAPALAI-IMAGES): a right float is allowed ONLY for a
   small non-map image that is the page's ONLY image, beside the FIRST paragraph,
   never beside a title. Everything else is a left-aligned block in flow.

   The three rule groups that break it do so BY CONSTRUCTION: they float on
   measured geometry (`img--narrow` = w<=360 and w>=h, `img--portrait` = h>w),
   and geometry cannot express "is this the page's only image". EN measured the
   damage after its markup pass: 123 images on 42 pages, triggers
   figure.figure 91 Â· .imgvilla2 27 Â· p img 4 Â· legacy align=right 1.

   `.img--lone` is exactly the law's permitted case â€” the render layer stamps it
   only when the page has ONE small image â€” so the float SURVIVES there and is
   retired everywhere else. Each selector below therefore carries
   `:not(.img--lone)`, which also lifts it above the original rule's specificity
   (the `:has()` rules sit at (0,3,2)).

   "Never beside a title" is expressed too: a lone image whose next sibling is a
   heading loses the float, so the one permitted float can only ever sit beside
   prose.

   Mobile is untouched by construction â€” CSS-R7/AD-9 already unfloats every
   in-prose image at <=1023px with `!important`, so this block can only matter at
   desktop. Proven by A/B, not asserted: see the report.
   ============================================================================ */
/* DESKTOP-SCOPED (found by the probe, fixed here): these declarations are not only
   floats â€” they also set margins, and unscoped they reached 414px and overrode the
   mobile centring from OWNER EYEBALL-2 R-E on 121 images across the probe set. The law
   is a DESKTOP law; mobile is already block-level via CSS-R7/AD-9. */
@media (min-width: 1024px) {
  .content-card p img.img--narrow:not(.img--lone),
  .content-card p img.img--portrait:not(.img--lone),
  .imgvilla2 > img.img--narrow:not(.img--lone),
  .imgvilla2 > img.img--portrait:not(.img--lone),
  .imgvilla2 > img[align="right"]:not(.img--lone),
  .content-card figure.figure:not(.img--lone):has(> img.img--narrow),
  .content-card figure.figure:not(.img--lone):has(> img.img--portrait),
  .content-card figure.figure:not(.img--lone):has(> a > img.img--narrow),
  .content-card figure.figure:not(.img--lone):has(> a > img.img--portrait),
  .imgvilla2 > .section:not(.img--lone):has(> img[align="right"]) {
    float: none;
    max-width: 100%;
    margin-left: 0;                    /* left-aligned block in flow */
    margin-right: auto;
    margin-top: var(--sp-4);           /* standard margins, as in the mobile law */
    margin-bottom: var(--sp-4);
  }
  /* Legacy inline float on a corpus wrapper (measured: /Mount-Kinabalu.php carries
     <div class="section" style="float:right;">). Only !important can reach an inline
     style. LEFT inline floats are deliberately untouched â€” they are the .imgvilla3 PAIR
     mechanism, governed by the pair law, not image placement. */
  .content-card .section[style*="float:right"],
  .content-card .section[style*="float: right"] {
    float: none !important;
    margin-left: 0;
    margin-right: auto;
  }
  /* Owner ruling F (Special-Offers hero sits RIGHT) survives exactly where the new law
     permits it â€” a page whose ONLY image is stamped .img--lone. Measured:
     /Special-Offers.php has 1 image, stamped `img--portrait img--lone`, so the float
     stands there; /fr/Sipadan.php reuses .so-hero with 2 images and no .img--lone, so
     under the law it becomes a block. */
  .imgvilla2.so-hero:not(:has(img.img--lone)) {
    float: none;
    margin-left: 0;
    margin-right: auto;
  }

  /* The legacy `align="right"` presentational hint, wherever it survives outside the
     permitted lone case â€” measured on /Bohey-Dulang-Resort.php, whose hero figure image
     carries it and floats with no author rule at all (UA stylesheet). */
  .content-card img[align="right"]:not(.img--lone) { float: none; }
  /* A listing-sequence member can never be "the page's only image", whatever the render
     layer stamped. Measured: /Borneo-Dive-Resorts.php carries a STALE .img--lone on
     sangalaki.webp inside figure.resort-thumb while the page holds 9 images. Reported to
     EN as a data issue; the style half refuses the float regardless. */
  .content-card .resort-thumb img.img--lone,
  .content-card figure.resort-thumb.img--lone,
  .content-card .seq-uniform img.img--lone { float: none; }

  /* the permitted float, but never beside a title */
  .content-card img.img--lone:has(+ :is(h1, h2, h3, h4)),
  .content-card figure.img--lone:has(+ :is(h1, h2, h3, h4)) {
    float: none;
    margin-left: 0;
    margin-right: auto;
  }
}

/* ============================================================================
   CSS-R14 â€” TOP-10 GROUP LABELS: BOLD, CARDED, NO BULLET MARKER (both viewports)
   ----------------------------------------------------------------------------
   EN's earlier style request (EN-034, re-queued in
   _REQUEST-CSS-video-facade-component.md): the markup is live on
   /Top-10-Dive-Sites.php â€” `div.ds-index-card > ul.ds-index >
   li.ds-index__group > strong + ul > li > a` â€” but the styling died in the
   main.css rollback, so the three group labels (Â«Sipadan's / Mabul's /
   Kapalai's Top Dive SitesÂ») render as plain list rows with markers.

   Card treatment matches the site's other cards (surface-2 + hairline +
   radius, the same recipe as .pk-card / .top10-card), and the markers are
   removed at both levels â€” the ruling says both viewports, so nothing here is
   media-scoped.
   ============================================================================ */
.ds-index-card {
  background: var(--c-surface-2);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-lg, 16px);
  padding: var(--sp-4) var(--sp-5);
  margin: var(--sp-5) 0;
  box-shadow: var(--shadow-sm);
}
.ds-index,
.ds-index ul {
  list-style: none;                    /* NO bullet marker, either level */
  margin: 0;
  padding: 0;
}
.ds-index li { list-style: none; margin: 0; }
.ds-index li::marker { content: none; }
.ds-index__group + .ds-index__group { margin-top: var(--sp-4); }
.ds-index__group > strong {
  display: block;
  font-weight: var(--fw-bold);
  color: var(--c-navy);
  font-size: var(--fs-md);
  margin-bottom: var(--sp-2);
}
.ds-index__group > ul > li {
  padding: var(--sp-1) 0;
  border-bottom: 1px dashed var(--c-hairline);
}
.ds-index__group > ul > li:last-child { border-bottom: 0; }
.ds-index__group a { text-decoration: none; }
.ds-index__group a:hover, .ds-index__group a:focus-visible {
  text-decoration: underline;
  color: var(--c-teal-dark);
}
@media (min-width: 900px) {
  .ds-index { columns: 3; column-gap: var(--sp-6); }
  .ds-index__group { break-inside: avoid; }
  .ds-index__group + .ds-index__group { margin-top: 0; }
}

/* ============================================================================
   CSS-R15 â€” PAGE-SPECIFIC UNIFORMITY (three owner-named pages)
   ----------------------------------------------------------------------------
   CSS has no URL selector, so each fix is scoped to the SHAPE that produces the
   defect. Where a shape turned out to be shared, that is stated in the report â€”
   the alternative (leaving 27 sibling pages inconsistent) is worse.

   â‘  /fr/Cours-de-plongee-PADI.php â€” Â«PADI DivemasterÂ» / Â«MSDTÂ» / Â«IDCÂ».
      Measured cause: the first row is `<li><h2><a>PADI Divemaster</a></h2></li>`
      while the other two are `<li><a><strong>â€¦</strong></a></li>`, so one row
      renders at h2 size and two at body size. `li > h2` occurs exactly ONCE in
      the whole corpus (verified), so this rule is page-specific in effect: an h2
      nested in a list item is a ROW LABEL, not a section heading.

   â‘¡ /fr/Hotels-de-plongee.php â€” Â«Mabul Water BungalowsÂ» and Â«Seaventures Dive
      RigÂ». Measured: `li.pk-toc__tier` renders 19px beside 16px
      `li.pk-toc__resort` SIBLINGS, and the ToC mixes rows that sit inside a `ul`
      with rows that sit directly under the nav, giving two left edges 21px apart
      (1122 vs 1143 at 1920; 59 vs 80 at 414).
      A tier row with NO sub-list is not a heading â€” it is a row â€” so it reads at
      row size. Indents are equalised only for a ToC that actually has the orphan
      shape (`:has(> li)`), so hierarchical ToCs keep their indent.

   â‘¢ /fr/Hotels-a-Semporna.php â€” Â«HomestaysÂ». Already uniform in the live tree:
      measured 16px on all 12 rows at both viewports (the frozen baseline had two
      sizes, 16px and 17px). No rule added; the numbers are in the report.
   ============================================================================ */
/* â‘  a list-item heading is a row label */
.content-card li > h2 {
  font-size: var(--fs-base);
  font-weight: var(--fw-semi);
  font-family: var(--font-sans);
  line-height: 1.4;
  margin: 0;
  letter-spacing: 0;
}
.content-card li > h2::after { content: none; }      /* no gold section divider on a row */

/* â‘¡ a tier row with no sub-list reads at row size, like its siblings */
/* Specificity, measured the hard way: the winning rule is the ROLE-based
   `html[lang="fr"] .pk-toc > ul > li.pk-toc__tier > a` at (0,3,4) â€” and its own comment
   says a tier row is large because it "HEADS a tier". Here it heads nothing: the page's
   ToC markup is malformed (a `</li>` without its opening `<ul>`), so the parser leaves
   the tier and the four resort rows as SIBLINGS and drops three more tiers outside the
   list entirely. `:not(:has(> ul))` states the intent and reaches (0,3,5), which is why
   it wins. Reported to FR/MAIN as a markup defect. Because the indent is set in `em`,
   normalising the size also collapses the two left edges into one. */
html[lang] .pk-toc > ul > li.pk-toc__tier:not(:has(> ul)) > a,
html[lang] .pk-toc > li.pk-toc__tier:not(:has(> ul)) > a,
html[lang] .pk-toc li.pk-toc__tier:not(:has(> ul)) { font-size: 1rem; }
/* â‘¡ one left edge for a ToC that mixes rows inside a list with orphan rows */
/* absolute px, not em: with `em` the orphan row and the in-list rows resolved to 20px
   and 19.2px and the edges stayed 1px apart. */
/* Both edges pinned to the SAME absolute value. Two reasons it has to be absolute and
   this specific: the FR rule `html[lang="fr"] .pk-toc ul {padding-left:1.25em}` sits at
   (0,3,2) and resolves to 21.25px at the list's 17px, while the orphan row inherits the
   16px I just set â€” so `em` on the two boxes can never agree. The `.pk-toc--bullets`
   variant reaches (0,3,3) and wins; measured result is one edge, 80px = 80px. */
.pk-toc:has(> li) > li,
html[lang] .pk-toc:has(> li) ul,
html[lang] .pk-toc.pk-toc--bullets:has(> li) ul { padding-left: 20px; }
.pk-toc:has(> li) > li, .pk-toc:has(> li) ul > li { margin-left: 0; }
.pk-toc:has(> li) > li { list-style: disc; }

/* ============================================================================
   CSS-R16 â€” CSS-D1 CLOSED: NO MOBILE HOMEPAGE OVERFLOW
   ----------------------------------------------------------------------------
   Measured cause, at last isolated to ONE element. At 390px the EN homepage's
   scrollWidth is 417 against a 390 viewport, and after excluding the off-canvas
   drawer (parked right of the viewport on purpose) exactly ONE element crosses
   the edge: `span.intro-line__place` holding Â«KapalaiÂ», x=300 w=118, +27.3px.

   Why it cannot wrap today: the markup is
     <span class="intro-line__place">Sipadan</span><span class="intro-line__dot">Â·</span>â€¦
   with NO whitespace between the spans, and each place is `white-space: nowrap`.
   A line with no break opportunity cannot wrap however narrow the column gets â€”
   so this was never a sizing problem, it was a missing break opportunity.

   The fix gives the line a break opportunity after each gold dot (a zero-width
   space, which paints nothing) and drops the trailing letter-space of the last
   place, which is the 3.4px that pushed Â«KapalaiÂ» past the edge even at 414.
   Mobile-scoped, so the desktop hero line stays byte-identical.
   ============================================================================ */
@media (max-width: 1023px) {
  .intro-line__dot::after { content: "\200B"; }        /* zero-width space: a break point */
  .intro-line__place:last-of-type { margin-right: -0.18em; }   /* trailing letter-space */
  .intro-line { text-wrap: balance; }
}

/* ============================================================================
   CSS-R18 â€” M-HOME v2: SEVEN NAV BUTTONS + FLAG ROW (mobile homepages)
   ----------------------------------------------------------------------------
   Supersedes the R17 card block (owner design correction). The markup is emitted
   by includes/chrome/home.cards.php ONLY on a mobile user agent, so these rules
   have nothing to match on desktop and the desktop homepages are untouched.

   NO BACKGROUND IMAGE INSIDE A BUTTON â€” ever. Photos behind text are ruled out
   for legibility, so the surface is flat: a light-blue wash mixed from the theme's
   own airy-blue token `--c-btn-call: #8ec9df` at 22% over white (= #e3f1f7), with
   `#e9f4f9` as the literal fallback where `color-mix()` is unavailable. That is the
   exact token proposed; a filled surface is a one-line change to
   `background: var(--c-btn-call)` if you prefer it stronger.

   Contrast, measured against the theme values: label `--c-navy #0e2a44` on
   #e3f1f7 â‰ˆ 13:1, far past AA. Rows are >=56px tall (min-height), full width, and
   the whole row is the tap target. The leading glyph mirrors the m-original's
   <i class="finger|dollar|hotel|â€¦"> row icons.
   ============================================================================ */
.home-nav {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-2);
  margin: var(--sp-3) 0 var(--sp-4);
}
.home-nav__btn {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: 56px;                       /* >= the 44px tap floor, with room */
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-sm, 8px);
  background: #e9f4f9;                                        /* fallback */
  background: color-mix(in srgb, var(--c-btn-call) 22%, #fff);
  background-image: none;                 /* the ruling, stated as a declaration */
  color: var(--c-navy);
  text-decoration: none;
}
.home-nav__btn:hover,
.home-nav__btn:focus-visible {
  background: #ddeef6;
  background: color-mix(in srgb, var(--c-btn-call) 34%, #fff);
  border-color: var(--c-teal);
}
.home-nav__btn:focus-visible { outline: 3px solid var(--c-gold); outline-offset: 2px; }
.home-nav__icon {
  flex: 0 0 24px;
  width: 24px; height: 24px;
  color: var(--c-teal-dark);
}
.home-nav__label {
  font-family: var(--font-sans);
  /* R20 â‘ : one step up the scale â€” var(--fs-base) 17px -> var(--fs-md) 19px,
     readable at arm's length */
  font-size: var(--fs-md);
  font-weight: var(--fw-semi);
  line-height: 1.25;
  letter-spacing: 0.01em;
}

/* â‘¡ the six flags, one line, directly below the buttons */
.home-flags {
  display: flex;
  flex-wrap: nowrap;
  justify-content: center;
  align-items: center;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-5);
  padding: var(--sp-2) 0 var(--sp-3);
  border-bottom: 1px solid var(--c-hairline);
}
.home-flags__flag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px; min-height: 44px;      /* tap floor on a flag too */
  border-radius: var(--radius-sm, 8px);
  text-decoration: none;
}
.home-flags__flag svg { width: 30px; height: 20px; display: block; border-radius: 2px; }
/* R20 â‘¢: the selected language is marked by a LIGHT-BLUE WAVY line under the flag.
   The gold curve is withdrawn. The wave is an inline SVG data-URI painted in the theme's
   airy-blue token `--c-btn-call: #8ec9df` (%238ec9df url-encoded), so it costs no request
   and no extra asset. */
.home-flags__flag { position: relative; }
.home-flags__flag.is-current { background: var(--c-surface-2); }
.home-flags__flag.is-current::after {
  content: "";
  position: absolute;
  left: 4px; right: 4px; bottom: 3px;
  height: 6px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='6' viewBox='0 0 24 6'%3E%3Cpath d='M0 4c2-2.6 4-2.6 6 0s4 2.6 6 0 4-2.6 6 0 4 2.6 6 0' fill='none' stroke='%238ec9df' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: repeat-x;
  background-size: 24px 6px;
}
@media (min-width: 1024px) {
  /* belt and braces: were a desktop UA ever served the block, it must not render */
  .home-nav, .home-flags { display: none; }
}

/* ============================================================================
   CSS-R19 â€” IMAGE NORMALIZATION: A CONTENT IMAGE IS A CONTENT IMAGE
   ----------------------------------------------------------------------------
   Owner regression find after R14a. Measured causes, all three of them:

   â‘  The placement wrapper is `figure.figure--left` â€” it does NOT carry the class
      `figure`, so `.figure img { border-radius; box-shadow }` and the block
      display never reached it. Measured on /Mataking-Resort.php desktop:
        MTK-ISLAND-KAYAK  in FIGURE.figure       -> 295px, radius 16px, shadow, block
        mataking-private-beach in FIGURE.figure--left -> 182px, radius 0, no shadow, INLINE
      Same page, same kind of photo â€” the only difference was the wrapper class.

   â‘¡ The CSS-R5 sequence module was grouping those `figure--left` figures (same
      parent + same className = a "sequence") and unifying them at the group's
      smallest natural: 182px on Mataking, 280px on Pom-Pom. That is what made
      them small. `figure--left` / `figure--right` are PLACEMENT classes, not run
      classes, so the module now skips them (js/site.js). `.figure--half` stays a
      run class and is untouched.

   â‘¢ The thin dark border is the legacy `border="1"` ATTRIBUTE in the corpus
      (measured `1px solid rgb(33,49,63)` on Beach-Villa, Water-Villa,
      Garden-Villas, Restaurant, Seafood-Restaurant, Yin-Yang-Spa,
      turtle-hatchery, pom-pom-liveaboard). CSS overrides the attribute.

   THE RULE: being alone-on-the-left changes nothing visually. Two exceptions
   only â€” the permitted lone float (CSS-R14a, untouched) and DIVE MAPS below.
   ============================================================================ */
.content-card .figure--left,
.content-card .figure--right {
  display: block;
  margin: var(--sp-5) 0;
}
.content-card .figure--left > img,
.content-card .figure--right > img,
.content-card .figure--left > a > img,
.content-card .figure--right > a > img,
.content-card > img,
.content-card p > img {
  display: block;
  border: 0;                        /* kills the legacy border="1" attribute */
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  height: auto;
}
/* the same two properties for any content image that carries the legacy attribute,
   wherever it sits */
.content-card img[border] { border: 0; }

/* ---- exception â‘¡: DIVE MAPS render at natural size, up to the container ----
   A map is read, not admired: it may never be downscaled by a sequence rule.
   Measured before: pom-pom-dive-map.webp, natural 500, rendered 280 (squashed by
   --seq-w). `--nat-w` is the natural width stamped by js/site.js.
   `!important` is required to beat `.seq-uniform { width: var(--seq-w) }`. */
.content-card img[src$="dive-map.webp"],
.content-card img[src$="dive-map.jpg"] {
  width: min(100%, var(--nat-w, 100%)) !important;
  max-width: 100% !important;
  height: auto;
}
.content-card .seq-uniform:has(> img[src$="dive-map.webp"]),
.content-card .seq-uniform:has(> a > img[src$="dive-map.webp"]) {
  width: auto !important;
  max-width: 100% !important;
}

/* ============================================================================
   CSS-R21a â€” FLEX-RESOLUTION LAW: A COMPONENT IMAGE IS SIZED BY THE LAYOUT
   ----------------------------------------------------------------------------
   Owner ruling: every facade in a grid renders at IDENTICAL dimensions; the box
   comes from the grid column and the picture is fitted into it â€” never sized by
   the file's natural resolution â€” and never exceeds its container.

   Measured cause on /Sipadan-Photo-Video.php at 1920 (cards all 351px wide):
     Sipadan-Island.webp     natural 995 -> thumb 351   (filled)
     baby-turtle.webp        natural 280 -> thumb 280   (short)
     barracuda-tornado.webp  natural 300 -> thumb 300   (short)
     diver-turtle.webp       natural 300 -> thumb 300   (short)
   i.e. CSS-R13b's desktop no-upscale cap `max-width: min(100%, var(--nat-w))`
   was quite correctly protecting CONTENT photographs â€” and quite wrongly sizing
   a COMPONENT part. The two laws are separated here:

     CONTENT image  â€” no-upscale stands (R13b/AD-16): a photograph in prose is
                      never blown past its own pixels.
     COMPONENT part â€” a thumbnail inside a card/grid whose box the layout owns:
                      it FILLS the box and is cropped to fit (object-fit: cover),
                      whatever the file measures. Swapping in a higher-resolution
                      file must never change a layout, and a 4K file must never
                      escape its frame.

   Component parts are named, not guessed: they are the BEM `__thumb` / `__media`
   image parts and the members of the grid components.
   ============================================================================ */
.video-facade__thumb,
.room__media img,
.gallery-grid figure img,
.snap-gallery img,
.carousel__slide img {
  width: 100% !important;          /* the layout decides */
  max-width: 100% !important;      /* ... and the frame is never exceeded */
  height: auto;
  object-fit: cover;
}
.video-facade__thumb { aspect-ratio: 16 / 9; }
/* the facade grid's rows are already equal (grid-auto-rows: 1fr, CSS-R18); with the
   thumb filling its box every card in a row is now identical in both dimensions. */

/* ============================================================================
   CSS-R21b â€” TOP FIGURE IS A PLAIN CENTRED FIGURE + ROUNDED CORNERS EVERYWHERE
   ----------------------------------------------------------------------------
   â‘  The top image on /dive-resorts.php and /Sipadan-Resorts.php.
      Measured at 1920: `mabul-sunset-watervillage.webp`, natural 300, rendered
      300 â€” inside a `.page-hero__media` wrapper **726px wide** carrying
      `border-radius: 20px`, `overflow: hidden` and a shadow. So the wrapper drew
      a card 726 wide around a 300px picture: the "dead right space", with the
      IMAGE itself square (radius 0, measured).
      Cause: HERO RULE v3 accepted upscaling a hero to the column, but CSS-R13b's
      no-upscale law now caps the image at its natural 300 â€” the wrapper kept the
      old full-column card.
      Fix: the wrapper shrinks to its picture (`width: fit-content`) and centres,
      drops the card (no shadow, no clip), and the IMAGE carries the four rounded
      corners. A full-width hero is unaffected: fit-content is then the column.

   â‘¡ Round-corner sweep. Measured square (radius 0) on
      /diving-holiday-packages.php and /Borneo-Dive-Resorts.php listing images
      while the same class rendered 8px on /dive-resorts.php. The standard
      treatment now applies to EVERY content image, whatever its wrapper class,
      on every page and language â€” stated once, here, instead of per class.
      Components that own a radius (the facade thumb) keep theirs.
   ============================================================================ */
.content-card .page-hero__media {
  width: fit-content;
  max-width: 100%;
  margin-left: auto;
  margin-right: auto;
  box-shadow: none;
  border-radius: 0;
  overflow: visible;
}
.content-card .page-hero__media img {
  border-radius: var(--radius);
  margin-left: auto;
  margin-right: auto;
}
/* â‘¡ one standard: every content image is rounded */
.content-card img {
  border-radius: var(--radius);
}
.content-card .island-circle__disk,
.content-card .video-facade__thumb {
  border-radius: var(--radius);      /* components keep the same value, stated */
}

/* ============================================================================
   CSS-R22a â€” THE LEAD IMAGE FLOATS (amendment to the placement law)
   ----------------------------------------------------------------------------
   Owner amendment: a page's LEAD image floats right of the FIRST PARAGRAPH even
   when other images exist below it â€” the REF-0 state of the dive-site family
   (/Sipadan/*, scuba-dive-*, Mataking-dive-site, Siamil) and of the small info
   pages. CSS-R14a's lone-image law is unchanged; this is a SECOND permitted
   float, and everything else stays a block in flow.

   Measured before, /Sipadan/Coral-gardens.php at 1920: the baseline (REF-0)
   floats `humpback.webp` right with margin-top 0; live had `float: none` after
   R14a. /scuba-dive-sipadan.php: baseline image top 475 = paragraph top 475;
   live 475 vs 809 â€” the un-floated block had pushed the prose down.

   `.is-lead` is stamped by js/site.js, which also writes the vertical offset:
   the module only stamps an image that has prose after it and NO heading
   immediately after it, so the HERO-TITLE rule stands â€” never beside the title.
   Desktop only; mobile keeps CSS-R7's block treatment.
   ============================================================================ */
@media (min-width: 1024px) {
  .content-card .is-lead {
    float: right !important;
    width: auto;
    max-width: min(42%, 420px);
    margin-right: 0 !important;
    margin-left: var(--sp-4) !important;
    margin-bottom: var(--sp-4) !important;
    /* margin-top is written by the module: the lead's top aligns with the top of
       the paragraph block that follows it (the ~14 "too high" pages) */
  }
  .content-card .is-lead > img,
  .content-card .is-lead > a > img { width: 100%; height: auto; }
  .content-card img.is-lead { width: auto; max-width: min(42%, 420px); }

  /* TAWAU-IMAGE-WIDTH-01 (owner ruling, AUDIT-05 item 1): the estate lead image
     standardises at 400px - the asset's own natural width.

     Eight trees already rendered 400 and did NOT need this: they are carried by
     `.content-card .imgvilla2:has(img[fetchpriority="high"]:not([align]))` -> max-width:100%,
     a rule written with an explicit `:not([align])`. fr, es and de fall outside it for one
     reason - THEIR MARKUP CARRIES align="right" on the lead img, which the other eight do
     not. That hands the wrapper to
     `.content-card .imgvilla2:has(> img[align="right"]):not(:has(> .section)):not(.so-hero)`
     -> max-width: min(50%, 540px), and 50% of the 726px host is exactly the 363px measured.

     So this is a MARKUP divergence answered in CSS, deliberately: the eleven Tawau.php files
     belong to the content terminals, and one rule here fixes three trees without a page edit.

     SPECIFICITY IS THE WHOLE DIFFICULTY, recorded so nobody "simplifies" this selector: the
     align-right rule computes (0,5,1) - :has() takes its most specific argument, so
     :has(> img[align="right"]) contributes (0,1,1), plus .content-card, .imgvilla2,
     :not(:has(> .section)) and :not(.so-hero). `.content-card .is-lead` is only (0,2,0) and
     loses, which is why the obvious overrides do nothing. Adding `.is-lead` to the same
     shape takes it to (0,6,1), which wins on specificity rather than on source order.

     Scoped to .is-lead AND align-right, so it reaches only those three trees; the eight at
     400 and every non-lead align-right image in the estate are untouched. */
  .content-card .imgvilla2.is-lead:has(> img[align="right"]):not(:has(> .section)):not(.so-hero) {
    max-width: 400px;
  }

  /* WIDE-FLOAT-STACK-01 (owner ruling) - a lead wider than 400px does not float.
     -------------------------------------------------------------------------------------
     `.is-stacked` is stamped by js/site.js from the RENDERED width, because the width that
     decides this is an authored `width=` attribute on the img and CSS has no numeric
     comparison for an attribute. This block only renders the state it is handed.

     CENSUS (shots/WIDE-FLOAT-01/census-1366.tsv, 1366 and 1920 byte-identical because
     --container-max caps the column at 726/824 either way): 534 floats sit beside text
     across eleven trees, 500 are <= 400px and 34 are not. Those 34 fall on 13 page-names -
     semporna.php in all eleven trees, then de (12 rows) and fr (7). Worst row in the
     estate: /de/Mantabuan.php floats 650px against a 24.9px text column, ratio 0.034.
     Nothing in the estate renders between 401 and 449px, so the boundary sits in a gap and
     is insensitive to rounding, zoom and font fallback.

     WHY fit-content AND NOT 100%: all 34 wide images are already served at exactly their
     natural width, so stretching them to the column would upscale every one - 1.12x at
     650px, 1.37x at 530px, and 1.83x for a 450px asset in the 824px column. The owner
     ruled the cap: the image sits above the prose at its own resolution, centred, and the
     text still runs full width beneath it. `max-width: 100%` keeps a future oversized
     asset inside the column.

     `display: block` is load-bearing for the `img.img--lone` shape (de/fr island pages):
     un-floating an inline img would drop it INTO the line box instead of above the prose.

     Desktop only, by construction - this lives inside the min-width:1024px block, where
     CSS-R7 already blocks floats on mobile, so mobile is untouched. */
  .content-card .is-lead.is-stacked {
    float: none !important;
    display: block;
    width: fit-content;
    max-width: 100%;
    margin-left: auto !important;
    margin-right: auto !important;
    margin-bottom: var(--sp-4) !important;
  }
  .content-card .is-lead.is-stacked > img,
  .content-card .is-lead.is-stacked > a > img,
  .content-card img.is-lead.is-stacked {
    width: auto;
    max-width: 100%;
    height: auto;
  }
}

/* ============================================================================
   CSS-R22b â€” DIVE-SITE MAP PAGES: LEFT-ALIGNED TEXT, MAPS AT FULL WIDTH
   ----------------------------------------------------------------------------
   Measured on /Dive-site-maps.php and /scuba-dive-kapalai.php at 1920:
     * paragraph text-align came out `center` / `-webkit-center` (the legacy
       corpus centres those blocks), against the site's left-aligned prose
     * `Sipadan-Dive-Site-Map.webp`, natural 898, rendered **347** â€” a map you
       cannot read, because its wrapper is 347px wide, not because of the image
   A map renders at the CONTAINER width, or its natural width if that is smaller;
   its wrapper is widened to the column so the â‘¦(a) full-width map law can act.
   ============================================================================ */
.content-card:has(img[src*="dive-map"]) p,
.content-card:has(img[src*="Dive-Site-Map"]) p,
.content-card:has(img[src*="dive-map"]) .text,
.content-card:has(img[src*="Dive-Site-Map"]) .text {
  text-align: left;
}
.content-card :is(figure, .section, .imgvilla, .imgvilla2, .imgvilla3):has(> img[src*="Dive-Site-Map"]),
.content-card :is(figure, .section, .imgvilla, .imgvilla2, .imgvilla3):has(> a > img[src*="Dive-Site-Map"]),
.content-card :is(figure, .section, .imgvilla, .imgvilla2, .imgvilla3):has(> img[src*="dive-map"]) {
  width: 100% !important;
  max-width: 100% !important;
  float: none !important;
  margin-left: 0 !important;
  margin-right: 0 !important;
}
.content-card img[src*="Dive-Site-Map"] {
  width: min(100%, var(--nat-w, 100%)) !important;
  max-width: 100% !important;
  height: auto;
}

/* ============================================================================
   CSS-R22c â€” A NARROW CONTENT IMAGE IS CENTRED, NOT LEFT-HUGGING
   ----------------------------------------------------------------------------
   Owner list: Regatta-Lepa (out of frame right, desktop), royal-resort-semporna
   top image, sea-star-resort, Seaventures (stray LEFT padding on desktop),
   Underwater-Wedding (left padding desktop). Measured at 1920: those images
   render 277â€“530px inside a 726px column with `margin-left: 0` and the slack
   entirely on the right (e.g. Regatta-Lepa 530 wide, 196px of empty space to the
   right of it).
   A content image narrower than its column is centred â€” the same treatment
   mobile already gets from OWNER EYEBALL-2 R-E. Floated images are exempt (the
   lone-image law and the lead-image law place those deliberately).
   ============================================================================ */
@media (min-width: 1024px) {
  /* at ANY depth: the owner's pages nest the image inside DIV.text > DIV.imgvilla2,
     so a direct-child selector reached only some of them (measured: Regatta-Lepa
     centred, royal-resort and sea-star still hugging left at gaps 0/0). */
  .content-card img:not(.is-lead):not(.img--lone):not(.video-facade__thumb),
  .content-card :is(figure, .section, .imgvilla, .imgvilla2, .imgvilla3):not(.is-lead):not(.img--lone):not(.figure-pair):not(.resort-thumb):not(.page-hero__media) {
    margin-left: auto;
    margin-right: auto;
  }
  .content-card :is(figure, .section, .imgvilla, .imgvilla2, .imgvilla3):not(.is-lead):not(.img--lone) > img {
    margin-left: auto;
    margin-right: auto;
    display: block;
  }
  /* the stray left padding the owner called out */
  .content-card .imgvilla2 > .section { padding-left: 0; }
}

/* ============================================================================
   CSS-R22d â€” TOP-10: THE MANTIS SITS RIGHT OF THE TOC, TOPS ALIGNED
   ----------------------------------------------------------------------------
   Measured before at 1920: the ToC card spans the full 726px column (x 743â€“1469)
   and `mantis-with-eggs` floats right at x=1269, y=408 â€” it OVERLAPS the card
   (measured overlap: true, top delta âˆ’202px).
   A block only flows beside a float when it establishes a block formatting
   context, so the card gets one; it then shrinks to the space left of the float
   instead of running under it. The float's top is aligned to the card's top by
   the CSS-R22a module, whose reference now includes a card-like block.
   ============================================================================ */
@media (min-width: 1024px) {
  .content-card .ds-index-card { overflow: hidden; }
}

/* ============================================================================
   CSS-R23 â€” .feat-row MOBILE ORDER (executes EN's request, owner EN-EYEBALL-R1 â‘ )
   ----------------------------------------------------------------------------
   Owner ruling: on /Budget-Non-Diving-Packages.php the anemonefish must sit ABOVE
   the Â«Budget PricesÂ» list on mobile. Desktop keeps list-left / image-right.

   EN is right that markup cannot do it: `.feat-row` is one flex container, so DOM
   order sets BOTH the desktop left/right order and the mobile stacking order.
   `order` is the one property that separates them â€” and it changes nothing on
   desktop, where this block does not apply.

   SCOPE â€” measured, so EN does not have to stamp anything. 14 pages carry
   `.feat-row` and they fall into exactly two families:
     * `class="feat-row"`            â€” the Budget-Non-Diving-Packages family (7 langs),
                                       the card the owner ruled on
     * `class="feat-row feat-row--toc"` â€” the ToC row (7 langs), whose figure CSS-R9
                                       deliberately places centred BELOW the ToC
   So `:not(.feat-row--toc)` hits exactly the ruled card in every language and
   leaves the ToC pages as R9 arranged them. `.feat-row--figfirst` is honoured too,
   for a future opt-in on a page that does carry `--toc`.
   ============================================================================ */
@media (max-width: 767px) {
  .feat-row:not(.feat-row--toc) > .feat-row__fig,
  .feat-row--figfirst > .feat-row__fig {
    order: -1;
  }
}

/* ============================================================================
   CSS-R23c-CN â€” CHINESE FLYOUT: ONE CONTIGUOUS HOVER REGION, NO CARET
   ----------------------------------------------------------------------------
   Owner live test: the card vanished as soon as the pointer left the flag, so
   nothing in it could ever be clicked. Two causes, both fixed here.

   1. A 6px DEAD GAP. The card was offset with `top: calc(100% + 6px)`, so the
      pointer crossed six pixels that belonged to neither the trigger nor the
      card. The offset is now transparent PADDING inside `.lang-cn__flyout`,
      which starts flush at `top: 100%`; the visible card is the inner
      `.lang-cn__card`. The hover region is therefore continuous from flag to
      options, and both live inside `.lang-cn`, one shared wrapper.
   2. NO TOLERANCE. Hover open/close is now driven by js/site.js with a 250ms
      grace period on leave, which CSS cannot express for a `display` switch.
      The `:focus-within` rule below stays as the no-JS keyboard path.

   R23c also REMOVES the caret from the flag strips (owner ruling: hover
   discovery is enough, the strip is pure flags). The trigger BUTTON survives â€”
   it is the keyboard and touch path and carries aria-expanded â€” but in the
   strips it is clipped to a focusable 1px. The mobile home line, where hover
   does not exist, keeps the visible caret it shipped with in R23b.
   ============================================================================ */
.lang-cn {
  position: relative;
  display: inline-flex;
  align-items: center;
}
/* strips: no caret, no gap in the flag rhythm â€” but still keyboard-reachable */
.lang-cn__toggle {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  background: transparent;
  color: inherit;
  cursor: pointer;
}
.lang-cn__caret { display: none; }

.lang-cn__flyout {
  position: absolute;
  top: 100%;                 /* flush with the trigger: no dead pixels between */
  left: 0;
  z-index: 120;
  display: flex;
  padding-top: 8px;          /* the visual gap, transparent and hoverable */
  background: transparent;
  border: 0;
}
.lang-cn__flyout[hidden] { display: none; }
/* the no-JS keyboard path; hover is js/site.js, which adds the 250ms tolerance */
.lang-cn:focus-within:not(.is-dismissed) .lang-cn__flyout { display: flex; }
.lang-cn__flyout.is-above {
  top: auto;
  bottom: 100%;
  padding-top: 0;
  padding-bottom: 8px;
}
.lang-cn__flyout.is-right { left: auto; right: 0; }

/* the card itself */
.lang-cn__card {
  display: flex;
  flex-direction: column;
  min-width: 132px;
  padding: 4px;
  /* STATED, not inherited â€” the footer is a dark surface */
  background: var(--c-white);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-sm, 8px);
  box-shadow: 0 12px 28px rgba(4, 12, 20, 0.28);
}
/* AP-3: `.site-footer a { color:#fff }` is (0,1,1) and beats a single-class rule,
   which is how the labels went white-on-white. Every colour rule for this card is
   written at (0,2,1) or better so it wins in the footer as well as the header. */
.lang-cn__card a.lang-cn__opt {
  position: relative;
  display: block;
  padding: 7px 10px 9px;
  border-radius: var(--radius-sm, 6px);
  color: var(--c-navy);
  background: transparent;
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  font-weight: var(--fw-reg);
  line-height: 1.25;
  text-decoration: none;
  white-space: nowrap;
}
.lang-cn__card a.lang-cn__opt:hover,
.lang-cn__card a.lang-cn__opt:focus-visible {
  background: #e9f4f9;
  background: color-mix(in srgb, var(--c-btn-call) 20%, #fff);
  color: var(--c-navy);
}
.lang-cn__card a.lang-cn__opt.is-active { font-weight: var(--fw-semi); color: var(--c-navy); }
.lang-cn__card a.lang-cn__opt.is-active::after {
  content: "";
  position: absolute;
  left: 10px; right: 10px; bottom: 2px;
  height: 4px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='6' viewBox='0 0 24 6'%3E%3Cpath d='M0 4c2-2.6 4-2.6 6 0s4 2.6 6 0 4-2.6 6 0 4 2.6 6 0' fill='none' stroke='%238ec9df' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: repeat-x;
  background-size: 24px 6px;
}

/* ============================================================================
   CSS-R45â‘  â€” THE "MORE LANGUAGES" PILL ON THE DESKTOP STRIPS
   ----------------------------------------------------------------------------
   Owner ruling: the desktop switcher shows SIX flags (en cn fr es de ko) and a
   pill; the panel behind it carries zh Â· it Â· ru Â· jp, zh first.

   The pill IS a `.lang-cn` group â€” same wrapper, same `.lang-cn__toggle`, same
   `.lang-cn__flyout` and `.lang-cn__card` â€” so it inherits the whole R23c card
   design and the whole R23c JS module (hover + 250ms grace, click, focus,
   Escape, outside click, edge flip) without one new line of either. All this
   block does is give the shared toggle a VISIBLE body in this one context.

   Two overrides are load-bearing, both written at (0,3,0) so they beat the
   base rule AND the `@media (hover: none)` 44px tap-target rule below,
   whatever the source order: a touch screen at >=1024px matches (hover: none)
   and would otherwise squash the pill into a 44px square.

   Height is pinned to 20px â€” the exact height of `.permit-bar__flags .lang-flag`
   â€” so the strip's rhythm and the permit bar's height are unchanged by this
   round. The 17px type floor (--fs-sm) is respected, not shrunk around.
   ============================================================================ */
.lang-cn.lang-more { position: relative; display: inline-flex; align-items: center; }

.lang-cn.lang-more .lang-cn__toggle {
  position: static;                 /* out of the strips' 1px clip */
  width: auto;
  height: 20px;                     /* = .lang-flag, so the row does not grow */
  padding: 0 9px;
  margin: 0;
  overflow: visible;
  clip-path: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  /* STATED, never inherited â€” this pill sits on the light permit bar and, when a
     page sets $HEADER_FLAGS, inside the header block. Same discipline as AP-3. */
  background: var(--c-white);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-pill);
  color: var(--c-navy);
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  font-weight: var(--fw-semi);
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
}
.lang-cn.lang-more .lang-cn__toggle:hover,
.lang-cn.lang-more .lang-cn__toggle:focus-visible {
  background: #e9f4f9;
  background: color-mix(in srgb, var(--c-btn-call) 20%, #fff);
  border-color: var(--c-teal);
  color: var(--c-navy);
}
.lang-more__label { display: block; }
.lang-more__caret {
  width: 0; height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
  opacity: 0.7;
}
/* the reader's own language can live INSIDE the panel (zh, it, ru, jp). The six
   visible flags then show no current-state anywhere, so the pill carries it. */
.lang-cn.lang-more.is-current-inside .lang-cn__toggle {
  border-color: var(--c-teal);
  font-weight: var(--fw-bold);
}
/* the panel is the last item of a right-aligned strip, so it would overflow the
   viewport; js/site.js already flips it with `.is-right`. Stated here so the
   dependency is visible from the stylesheet too. */
.lang-more__flyout .lang-cn__card { min-width: 148px; }

/* the mobile homepage line keeps R23b's visible caret and tap target: there is no
   hover on a phone, so the affordance has to stay */
.home-flags .lang-cn { gap: 2px; }
.home-flags .lang-cn__toggle {
  position: static;
  width: 18px;
  height: 44px;
  overflow: visible;
  clip-path: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.home-flags .lang-cn__caret {
  display: block;
  width: 0; height: 0;
  border-left: 3.5px solid transparent;
  border-right: 3.5px solid transparent;
  border-top: 4px solid currentColor;
  opacity: 0.65;
}
.home-flags .lang-cn__card a.lang-cn__opt { padding: 11px 12px 13px; font-size: var(--fs-base); }

/* the mobile language menu keeps the two script rows from R23 */
.lang-menu__list a.is-current {
  position: relative;
  font-weight: var(--fw-semi);
}
.lang-menu__list a.is-current::after {
  content: "";
  position: absolute;
  left: 12px; right: 12px; bottom: 2px;
  height: 4px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='6' viewBox='0 0 24 6'%3E%3Cpath d='M0 4c2-2.6 4-2.6 6 0s4 2.6 6 0 4-2.6 6 0 4 2.6 6 0' fill='none' stroke='%238ec9df' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: repeat-x;
  background-size: 24px 6px;
}

/* ============================================================================
   CSS-R24 â‘  â€” ERROR PAGES: THE GOLDEN RULE FOLLOWS ITS TITLE
   ----------------------------------------------------------------------------
   `.content-card h1::after` (the site-wide 56px gold rule) has no side margins,
   so it sits at the left edge of the h1's box. That is right for the ranged-left
   titles it was written for, and wrong on 404/403, where `.error-hero` centres
   the whole block via `text-align: center` â€” a block-level pseudo-element does
   not follow text-align, so the title was centred and its rule was not. The
   existing `h1.text-center::after` rule cannot help: the error hero centres from
   the PARENT, and the h1 carries no class.
   ============================================================================ */
.error-hero h1::after { margin-left: auto; margin-right: auto; }

/* ============================================================================
   CSS-R25 â€” GALLERY SECTIONS STACK: PHOTOS, THEN VIDEO
   ----------------------------------------------------------------------------
   `/fr/Gallerie-de-Photo-Video.php` rendered the video gallery to the RIGHT of the
   photos at 1920. Measured cause: on that page alone the `<section class="video-facades">`
   sits INSIDE `<div class="masonry">` (every other gallery page has it as a sibling of
   the masonry). `.masonry` is a 3-column multicol container, so a block placed inside it
   is column content â€” it flowed into the next column instead of below, and inherited the
   column's width (237px at 1920, 169px at 414, against 726/346px elsewhere).

   `column-span: all` takes the section out of the column flow: the figures before it fill
   the columns, the section spans the full width, and anything after resumes below. The
   order photos-then-video is therefore honoured wherever the markup puts the section.
   ============================================================================ */
.masonry > .video-facades,
.masonry .video-facades {
  column-span: all;
  width: 100%;
  margin-top: var(--sp-5);
}

/* ============================================================================
   CSS-R25b â‘  â€” MABUL-BACKPACKERS: CONTENT IMAGES ALIGN LEFT (DESKTOP)
   ----------------------------------------------------------------------------
   Owner PAGE ruling for /Mabul-Backpackers.php and its fr twin: single images sit
   at the column's left edge and a left+right pair is anchored left as a pair. This
   is a deliberate, page-scoped exception to two site-wide laws that otherwise
   centre them â€” AD-33/CSS-R22c ("a content image narrower than its column is
   centred", specificity (0,4,1), which beats `.figure--left img`) and CSS-R21b
   ("being alone-on-the-left changes nothing visually"). Measured before at 1920:
   lone images at column offsets 189 / 186 / 264 with the slack split evenly.

   The page hook is the canonical link, so no page file is touched (page content is
   not this terminal's scope) and both twins are covered by one suffix match. The
   whole block is >= 1024px: mobile keeps the standard centred stack.

   SPECIFICITY, counted rather than assumed: R22c's figure selector carries SEVEN
   classes (`.content-card` + the `:is()` argument + five `:not()`s) at (0,7,0). The
   first draft here reached only (0,6,2) - `:has()` contributes its argument's
   (0,2,1) - so the images moved while the half-figures stayed centred at 184.5px.
   The four `:not()`s below bring it to (0,8,2), and a lead or lone image on this
   page still obeys its own law.
   ============================================================================ */
@media (min-width: 1024px) {
  html:has(link[rel="canonical"][href$="/Mabul-Backpackers.php"]) .content-card
    :is(figure, .imgvilla, .imgvilla2, .imgvilla3, .section):not(.page-hero__media):not(.resort-thumb):not(.is-lead):not(.img--lone) {
    margin-left: 0;
    margin-right: auto;
    text-align: left;
  }
  html:has(link[rel="canonical"][href$="/Mabul-Backpackers.php"]) .content-card
    :is(figure, .imgvilla, .imgvilla2, .imgvilla3, .section):not(.page-hero__media):not(.resort-thumb):not(.is-lead):not(.img--lone) > img,
  html:has(link[rel="canonical"][href$="/Mabul-Backpackers.php"]) .content-card
    :is(figure, .imgvilla, .imgvilla2, .imgvilla3, .section):not(.page-hero__media):not(.resort-thumb):not(.is-lead):not(.img--lone) > a > img {
    margin-left: 0;
    margin-right: auto;
  }
  /* the pair travels together: anchored left, its own spacing untouched */
  html:has(link[rel="canonical"][href$="/Mabul-Backpackers.php"]) .content-card .figure-pair {
    justify-content: flex-start;
    margin-left: 0;
    margin-right: auto;
  }
}

/* ============================================================================
   CSS-R25b â‘¡ â€” A TABLE CAPTION SPANS ITS TABLE, NOT ITS FIRST COLUMN
   ----------------------------------------------------------------------------
   Owner defect: the dive-site comparison heading rendered "compressed inside the
   first column". Measured at 414 on /Mabul/froggy-lair.php: caption 119px wide and
   147px tall (7 wrapped lines) against a 346px table â€” 174px tall on the fr twin,
   whose heading is longer. Desktop was already correct (726 of 726).

   Cause: the mobile card rules set `.spot-table { display: block }` (and CSS-R3
   does the same for `.t-cards`), which leaves the caption as the only
   `display: table-caption` box in the subtree. It then belongs to an anonymous
   table wrapper that shrink-to-fits â€” so the heading sized itself to the widest
   label instead of to the table. Making the caption a block ties it back to the
   full container width, above the cards.
   ============================================================================ */
@media (max-width: 1023px) {
  .spot-table caption,
  .t-cards caption,
  .content-card table caption {
    display: block;
    width: 100%;
    text-align: left;
  }
}

/* ============================================================================
   CSS-R26 â€” SOURCE SERIF 4, SELF-HOSTED, ON ONE SAMPLE PAGE
   ----------------------------------------------------------------------------
   Owner ruling: Source Serif 4 site-wide, STAGED. This round ships the hosting and
   one representative page only; the sweep fires on the owner's confirm.

   SAMPLE PAGE: /scuba-dive-sipadan.php (EN) â€” chosen because it is long-form content
   with h1, five h2s, 27 paragraphs, figures, a list and a data table, AND because it
   is already a standing gate page (`divesites` en), so the round-to-round isolation
   gives an exact before/after with no extra capture.

   SELF-HOSTED, two files, no third-party request at runtime:
     css/fonts/source-serif-4-latin.woff2       50,824 bytes
     css/fonts/source-serif-4-latin-ext.woff2   42,040 bytes
   Both are the VARIABLE upstream files (weight axis 200-900), so one file per subset
   covers 400/600/700 â€” the three weights this site uses â€” with no synthetic bolding.

   FALLBACK STACK: "Source Serif 4", Georgia, serif.
   Georgia is deliberate: it is a metric-near serif present on every Windows and macOS
   install, so the fallback frame is close in x-height and advance width and a swap
   would move little. `serif` is the last resort.

   FONT-DISPLAY: block, with the page-scoped `<link rel="preload">` in header.php.
   The reasoning is measured, not assumed. Three candidates:
     swap     â€” paints Georgia, then swaps. A flash of fallback BY DEFINITION.
     optional â€” 100ms window, then Georgia for the rest of the visit, never swapping.
                No flash, but MEASURED on this page it lost the race every time: the
                sample rendered at Georgia's 324.89px instead of the webfont's 300.28px.
                The preload cannot save it, because these pages emit <head> themselves
                and header.php â€” the only head-adjacent file this terminal owns â€” is
                included after <body>, so the hint arrives level with the CSS request,
                not ahead of it.
     block    â€” the first painted glyphs are already Source Serif 4: no fallback frame
                and no swap, at the cost of a brief invisible-text window (capped at 3s
                by the spec, and in practice one frame here â€” 50KB, same origin,
                preloaded).
   `block` is the only one of the three that satisfies "no flash of fallback" AND
   actually shows the owner the typeface. If the sweep later needs the invisible window
   gone, the fix is a `size-adjust`/`ascent-override` fallback face so Georgia and
   Source Serif 4 occupy identical space â€” that is a sweep-time decision, not a
   sample-time one.
   ============================================================================ */
@font-face {
  font-family: 'Source Serif 4';
  font-style: normal;
  font-weight: 200 900;                 /* variable axis: 400/600/700 all real */
  font-display: block;
  src: url('/css/fonts/source-serif-4-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Source Serif 4';
  font-style: normal;
  font-weight: 200 900;
  font-display: block;
  src: url('/css/fonts/source-serif-4-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* METRIC-MATCHED FALLBACK. `font-display: block` keeps fallback GLYPHS off the screen,
   but the browser still lays the invisible text out in the fallback's metrics, so when
   the webfont lands the page reflows â€” measured: 29 of 40 boxes moved on the sample
   against 0 on the fontless control. Georgia sets the same string 6.47% narrower than
   Source Serif 4 (324.89px vs 345.92px at 17px), so Georgia is wrapped in an override
   face scaled by exactly that ratio. The fallback FAMILY is unchanged â€” this is still
   Georgia, and `serif` still ends the stack â€” it simply now occupies the same space,
   which is what removes the shift. Overriding the vertical metrics is unnecessary here:
   every line-height in this site is set in CSS, so line boxes do not depend on the
   font's own ascent/descent. */
@font-face {
  font-family: 'SS4 Metric Fallback';
  src: local('Georgia'), local('Times New Roman'), local('Times');
  size-adjust: 106.47%;
  font-display: swap;
}

/* R26-SWEEP: the sample page's `:has()` hook is RETIRED â€” the owner approved the
   roll-out, so the rule is global now and the hook would only be dead weight. The
   @font-face declarations, the stack, `font-display: block` and the metric-matched
   fallback above are untouched: only the reach changed. */

/* ============================================================================
   CSS-R28 â€” THE LEGACY align="right" ATTRIBUTE, NORMALISED TO THE FLOAT LAW
   ----------------------------------------------------------------------------
   Found by the AUDIT-100-C rendered pass: `/fr/Mabul/house-reef.php` and friends
   still floated an image at 414, and floated a non-lead image at 1920. Neither
   came from a stylesheet â€” `align="right"` is a presentational HTML attribute
   and the BROWSER'S OWN stylesheet turns it into `float: right`. No author rule
   ever mentioned it, so R7 (nothing floats on mobile) and R14a (only `.is-lead`
   / `.img--lone` may float on desktop) never reached these images: the laws were
   written against classes, and this shape carries none.

   Population: **248 occurrences across 238 files**, every tree. The attribute is
   page markup and not mine to delete; author rules beat the UA stylesheet, so the
   normalisation is done here and the pages are left alone.

   The two permitted floats are excluded by name, so the lead-image amendment
   (CSS-R22a) and the lone-image law (CSS-R14a) keep their exceptions â€” an image
   that is legitimately a lead still floats, attribute or no attribute. Everything
   else falls back to the standard treatment, which on desktop means CSS-R22c
   centres it if it is narrower than the column.
   ============================================================================ */
@media (max-width: 1023px) {
  /* R7: nothing floats on a phone â€” including a 330px image in a 348px column,
     which read as a right-hugging block rather than a wrap and was the defect the
     owner's list called out on eel-garden and froggy-lair. */
  .content-card img[align="right"],
  .content-card img[align="left"] {
    float: none;
  }
  .content-card :is(figure, .section, .imgvilla, .imgvilla2, .imgvilla3):has(img[align="right"]),
  .content-card :is(figure, .section, .imgvilla, .imgvilla2, .imgvilla3):has(img[align="left"]) {
    float: none;
  }
}
@media (min-width: 1024px) {
  /* R14a: only a lead or a lone image floats. `align="right"` is not a licence. */
  .content-card img[align="right"]:not(.is-lead):not(.img--lone),
  .content-card img[align="left"]:not(.is-lead):not(.img--lone) {
    float: none;
    /* R13b's no-upscale law, held through the change: once the float goes, the
       image falls into the normal flow where a width:100% rule was waiting for it
       â€” measured, `scorpion-fish.webp` (natural 300) stretched to 726 in the
       column. `width: auto` gives it its own size back; `max-width: 100%` keeps
       the column as the ceiling. Mobile is untouched: filling the column at 414 is
       the owner's own rule (EYEBALL-2 R-E), not an upscale defect. */
    width: auto;
    max-width: 100%;
  }
  /* the imgvilla2 shape: the WRAPPER floats and the image inside it is not a lead,
     which is how a non-lead image kept a desktop float at 1920 */
  .content-card :is(figure, .section, .imgvilla, .imgvilla2, .imgvilla3):has(img[align="right"]):not(.is-lead):not(.img--lone):not(:has(.is-lead)):not(:has(.img--lone)),
  .content-card :is(figure, .section, .imgvilla, .imgvilla2, .imgvilla3):has(img[align="left"]):not(.is-lead):not(.img--lone):not(:has(.is-lead)):not(:has(.img--lone)) {
    float: none;
  }
}

/* ============================================================================
   CSS-R26-SWEEP â€” SOURCE SERIF 4 FOR CONTENT TYPOGRAPHY, SITE-WIDE
   ----------------------------------------------------------------------------
   Owner verdict after the sample: APPROVED with scope. The typeface takes the
   READING surfaces â€” headings h1-h6, paragraphs, list items, table text,
   captions, blockquotes â€” in every tree. The CHROME keeps today's face: the main
   navigation, the left menu, every nav strip and fly-out, the footer nav, button
   and CTA labels, and form controls. The owner was explicit, so the exclusions
   below are written as their own rules rather than left to inheritance, and the
   probe measures them instead of trusting them.

   The @font-face declarations, the fallback stack, `font-display: block` and the
   metric-matched Georgia all stay exactly as staged in R26 â€” nothing about the
   loading strategy changes, only its reach. The sample page's `:has()` hook is
   retired here: it would be dead weight now that the rule is global.

   SCOPE MECHANISM: everything is anchored on `.content-card`, the article
   wrapper. Chrome lives outside it (header, `#site-nav`, footer are siblings), so
   the scope is structural, not a list of exceptions to maintain. The exceptions
   that follow exist for the chrome that is *inside* an article: CTA rows, the
   inline contact buttons, and any form the page carries.
   ============================================================================ */
/* CSS-R26b (owner verdict after seeing the sweep live): the serif is for HEADINGS.
   Body prose goes back to the face the menus use â€” the owner read the live pages and
   decided the serif works as a display voice, not as a reading one. */
.content-card :is(h1, h2, h3, h4, h5, h6) {
  font-family: 'Source Serif 4', 'SS4 Metric Fallback', Georgia, serif;
}
/* paragraph-level text reverts, stated rather than left to inheritance so the intent
   survives the next person reading this file */
.content-card :is(p, li, dd, dt, td, th, caption, blockquote, figcaption, address, summary),
.content-card :is(.dive-stats__num, .dive-stats__cap, .dive-stats__list dt,
                  .dive-stats__list dd) {
  font-family: var(--font-sans);
}

/* the excluded surfaces, stated: chrome that lives inside the article keeps the
   sans it has today. These are written at a higher specificity than the rule
   above so an element that is both (a CTA label inside a paragraph, a select
   inside a table cell) resolves to the chrome face. */
.content-card :is(button, input, select, textarea, label, optgroup, option, legend),
.content-card :is(.btn, .btn *, .innerContactUsButton, .innerContactUsButton *),
.content-card :is(.cta-row a, .cta-row button, .contact-card a),
.content-card :is(.site-nav, .site-nav *, .lang-menu, .lang-menu *),
.content-card :is(.home-nav, .home-nav *, .home-flags, .home-flags *),
.content-card :is(.permit-bar, .permit-bar *, .breadcrumb, .breadcrumb *),
.content-card :is(.lang-cn, .lang-cn *),
/* owner ruling: the page ToC is NAVIGATION, not prose â€” it reads in the sans with the
   menus, including its own heading, which is why `.pk-toc__title` is named here and no
   longer in the serif rule above. */
.content-card :is(.pk-toc, .pk-toc *, .pk-toc__title, .ds-index-card, .ds-index-card *) {
  font-family: var(--font-sans);
}
/* headings inside those surfaces follow the surface, not the article */
.content-card :is(.btn, .innerContactUsButton, .cta-row, .contact-card, .site-nav,
                  .lang-menu, .home-nav, .permit-bar, .pk-toc, .ds-index-card)
       :is(h1, h2, h3, h4, h5, h6, p, li, span) {
  font-family: var(--font-sans);
}


/* ---------- CSS-R29 Â· Â«ReservationsÂ» leads the mobile menu, in bold ---------- */
/* Owner ruling R29. The pinned reservations item is ALREADY the first child of
   .site-nav__list in the one shared nav markup (includes/nav.unified.php renders it
   before the $NAV loop), so no re-order is written here â€” R29 adds weight only.
   Scoped to the drawer (<1024px): the desktop sidebar keeps the --fw-semi it has
   always had, so the desktop menu is untouched, as ordered.
   Set on the LABEL SPAN, not the <a>, so the homepage tagline underneath stays at
   --fw-reg and reads as a subtitle (the R18/R20 two-line nav item rule). */
@media (max-width: 1023px) {
  .site-nav__link--cta .site-nav__label { font-weight: var(--fw-bold); }
}


/* ---------- CSS-R30 Â· the footer call row ---------- */
/* Intake _REQUEST-CSS-footer-phone.md. The request read "no css/** change is implied" â€”
   it is, and AP-3 is exactly why: `.site-footer a { color:#fff }` is (0,1,1) and BEATS
   `.cbtn--mobile { color: var(--c-deep) }` at (0,1,0), so an unqualified call button
   renders #fff on #8ec9df = 1.9:1 in the footer while looking correct on a contact page.
   Every colour this row needs is therefore stated at (0,2,1) or better, and the probe
   measures WCAG contrast in the FOOTER context, not on the contact card. */
.site-footer__contact {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
}
.site-footer a.cbtn--mobile,
.site-footer a.cbtn--call { color: var(--c-deep); }
.site-footer a.cbtn--mobile:hover,
.site-footer a.cbtn--call:hover { color: var(--c-deep); }


/* ---------- CSS-R36 Â· owner UX round 1 ---------- */

/* â‘  The popup affordance follows the popup binding. js/site.js now opens the lightbox for
   EVERY in-article image, not only those inside a <figure>, so the zoom cursor has to reach
   the same set â€” otherwise a dive map is clickable but looks inert. Same four exclusions as
   the JS: linked images keep the pointer their link deserves, facade thumbs keep theirs. */
.content-card img { cursor: zoom-in; }
.content-card a img,
.content-card .video-facade img,
.content-card .yt-facade img { cursor: pointer; }

/* â‘¢ The frontpage contact card reads as the destination, not as one more sibling.
   Siblings are color-mix(--c-btn-call 22%, #fff); this is the same hue carried further
   (42%) plus a bold label, so it is distinct by TWO channels, not by colour alone. */
.home-nav__btn--contact {
  background: #cfe6f1;                                          /* fallback */
  background: color-mix(in srgb, var(--c-btn-call) 42%, #fff);
  border-color: var(--c-teal);
}
.home-nav__btn--contact:hover,
.home-nav__btn--contact:focus-visible {
  background: #bfdcea;
  background: color-mix(in srgb, var(--c-btn-call) 54%, #fff);
}
.home-nav__btn--contact .home-nav__label { font-weight: var(--fw-bold); }

/* â‘£ The cn/zh script flyout could not be opened by a finger.
   R23c clipped `.lang-cn__toggle` to a focusable 1px and set the caret display:none in the
   FLAG STRIPS, leaving hover (desktop) and keyboard as the only two ways in. On a touch
   device neither exists, so the footer and permit-bar strips offered no way to reach
   Traditional at all â€” the tap area measured 1px^2. The M-HOME flag line already keeps its
   caret, which is the precedent this follows.
   Gated on (hover: none), so the desktop rhythm R23c ruled on is untouched: no caret, no
   gap, hover as before. AP-5 still holds â€” the panel keeps its own transparent padding. */
@media (hover: none) {
  .lang-cn__toggle {
    position: static;
    width: 44px;
    height: 44px;
    clip-path: none;
    overflow: visible;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: 2px;
  }
  .lang-cn__caret {
    display: block;
    width: 0; height: 0;
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    /* STATED, never inherited: this sits on the footer's dark surface AND on the light
       permit bar, so it is set per context below rather than left to currentColor. */
    border-top: 6px solid currentColor;
  }
  .site-footer .lang-cn__toggle { color: #fff; }
  .permit-bar .lang-cn__toggle,
  .header-flags .lang-cn__toggle { color: var(--c-navy); }
}

/* â‘¡ the custom dropdown's own surface. Touch only â€” the whole component is gated on
   (hover: none) in js/site.js, so none of this can reach a desktop reader. */
.sdd { position: relative; display: block; }
.sdd__trigger {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  background: transparent; border: 0; padding: 0; margin: 0;
  cursor: pointer;
}
.sdd__trigger:focus-visible { outline: 3px solid var(--c-gold); outline-offset: 2px; }
.sdd__backdrop {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(4, 12, 20, 0.55);
}
.sdd__panel {
  position: fixed; z-index: 201;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  /* NEAR-FULL-SCREEN WIDTH: the 2-line wrap of long options was the native popup being
     as narrow as the control. 16px of breathing room each side, never wider than 520. */
  width: min(calc(100vw - 32px), 520px);
  /* 70vh: the iPhone first-tap-short list. Tall enough to be a list, short enough to
     stay a sheet, and it SCROLLS rather than truncating. */
  max-height: 70vh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: var(--sp-2);
  background: var(--c-white);
  border-radius: var(--radius, 16px);
  box-shadow: 0 18px 40px rgba(4, 12, 20, 0.34);
}
.sdd__opt, .sdd__cat {
  display: block; width: 100%;
  text-align: left;
  border: 0; background: transparent;
  font-family: var(--font-sans);
  cursor: pointer;
}
/* one step DOWN from the control's 17px, per the order */
.sdd__opt {
  min-height: 44px;
  padding: 10px 12px;
  font-size: 16px;
  line-height: 1.3;
  color: var(--c-navy);
  border-radius: var(--radius-sm, 8px);
}
.sdd__opt:hover, .sdd__opt:focus-visible {
  background: color-mix(in srgb, var(--c-btn-call) 20%, #fff);
}
.sdd__opt.is-current { font-weight: var(--fw-semi); }
.sdd__opt--placeholder { color: #4a5b6d; }
/* the category: 1px below the options as ordered, but carrying weight, colour and a rule
   so "smaller" never reads as "fainter" â€” it is a heading AND a choosable row. */
.sdd__cat {
  min-height: 44px;
  margin-top: var(--sp-2);
  padding: 10px 12px;
  font-size: 15px;
  font-weight: var(--fw-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--c-teal-dark);
  border-top: 1px solid var(--c-hairline);
  border-radius: var(--radius-sm, 8px);
}
.sdd__cat:first-child { margin-top: 0; border-top: 0; }
.sdd__cat--heading { cursor: default; color: #4a5b6d; }
.sdd__cat--heading:hover { background: transparent; }
.sdd__cat:hover, .sdd__cat:focus-visible {
  background: color-mix(in srgb, var(--c-btn-call) 28%, #fff);
}


/* ---------- CSS-R37 Â· the Reservations CTA loses the golden brown ---------- */
/* Owner ruling. The pinned Reservations item read in brass (--c-gold-text #856831); the
   accent is rejected. It becomes DARK BLUE + BOLD on both mobile surfaces.

   WHICH TOKEN, and why not plain navy: a plain drawer item is already
   `--c-navy` (#0e2a44) at --fw-semi, so recolouring the CTA to navy would erase the very
   distinction the ruling says to keep. The CTA therefore takes **--c-deep (#0a1f33)** â€” the
   darkest blue in the family, one step below navy â€” at --fw-bold, over a faint navy tint.
   Three channels now say "CTA" (darker blue, heavier, tinted ground) and none of them is gold.

   R29 stands untouched: the item is still first and still the CTA. Only the colour language
   changes. DESKTOP IS DELIBERATELY LEFT ALONE â€” see the note under the media query. */
/* MEASURED, and it settles the desktop question the order left conditional: the DESKTOP rail
   CTA computes to #856831 = --c-gold-text, i.e. it IS the golden brown. So the "desktop may
   keep its current treatment if it isn't golden brown" exemption does not apply, and the
   colour rule is written OUTSIDE the media query - both viewports lose the brass.
   (0,2,0), so it beats the (0,1,0) `.site-nav__link--cta { color: var(--c-gold-text) }`
   without deleting it; the token keeps serving every other CTA on the site. */
.site-nav .site-nav__link--cta {
  color: var(--c-deep);
  background: #eef2f6;                                        /* fallback */
  background: color-mix(in srgb, var(--c-navy) 8%, #fff);
}
.site-nav .site-nav__link--cta:hover {
  background: #e3e9ef;
  background: color-mix(in srgb, var(--c-navy) 14%, #fff);
  color: var(--c-deep);
}

/* On DESKTOP the tint is what carries CTA-ness: --c-deep sits one step from a plain item's
   --c-navy, too fine a difference on its own. Weight is deliberately NOT changed there -
   R29 scoped bold to the mobile drawer and this ruling repeats that scope, so the desktop
   rail keeps --fw-semi and simply stops being brass. */
@media (max-width: 1023px) {
  /* the muted second line inherits the CTA colour; keep it a subtitle, not a second CTA */
  .site-nav .site-nav__link--cta .site-nav__tagline { color: inherit; opacity: 0.72; }

  /* â‘¢ the frontpage pinned card, so the two mobile surfaces speak the same language.
     It keeps the card shape of its siblings and takes the CTA's colour + weight. The
     R36 contact card stays on the call-blue tint, so the two remain tellable apart. */
  .home-nav__btn--reservations {
    background: #eef2f6;
    background: color-mix(in srgb, var(--c-navy) 8%, #fff);
    border-color: var(--c-navy);
    color: var(--c-deep);
  }
  .home-nav__btn--reservations:hover,
  .home-nav__btn--reservations:focus-visible {
    background: #e3e9ef;
    background: color-mix(in srgb, var(--c-navy) 14%, #fff);
  }
  .home-nav__btn--reservations .home-nav__label { font-weight: var(--fw-bold); }
  .home-nav__btn--reservations .home-nav__icon { color: var(--c-deep); }
}
/* DESKTOP: not touched by this round. The desktop sidebar CTA keeps --c-gold-text, which is
   the ruling's own scope ("the mobile popup-menu item" + "the frontpage card"). Stated here
   rather than assumed, and measured in the R37 report: if the owner wants the desktop rail
   recoloured too, this media query becomes unconditional. */


/* ---------- CSS-R45 Â· owner UX rulings ---------- */

/* â‘¡ DIVE-MAP ALIGNMENT. The card was already display:flex, so the map was MEANT to sit right
   of the stats; the defect is `flex-wrap: wrap` on line 2160 plus a map wide enough to make
   the pair exceed the content column - the map then wraps onto its own line and reads as
   "stacked below". South-Point's map is a narrow 193x267 portrait and looked fine; the wide
   landscape maps in the same family did not. Fix: at the desktop breakpoint the row does not
   wrap and the map column is capped, so stats-left / map-right holds for every dive map in
   the family regardless of the asset's natural width. Mobile keeps the R28b column stack. */
@media (min-width: 1024px) {
  .dive-stats-card {
    flex-wrap: nowrap;
    align-items: flex-start;
  }
  .dive-stats-card > .dive-stats { flex: 1 1 auto; min-width: 0; }
  /* CSS-R49a (owner ruling) â€” THE STATS TAKE THE WIDE SHARE; THE MAP TAKES ITS IMAGE.
     R45â‘¡ left two faults that only a rect measurement shows, because the rule LOOKED
     applied â€” computed flex really was 1/1/auto on the stats and 0/1/auto on the map:

       1. `margin-left: auto`. In flexbox an AUTO MARGIN absorbs the positive free
          space, and it does so instead of the stats' `flex-grow: 1`. Measured on
          /Sipadan/Barracuda-Point.php at 1920: the card's 694px content box went
          stats **222** / gap 16 / map **456** â€” the map column was more than twice
          the stats block, while the image inside it was **193px** and sat centred in
          131px of empty figure on each side. Exactly backwards from the intent.
       2. `max-width: 46%` never took effect at all: the figure's computed max-width
          is **100%**, so a later rule had already beaten it. A cap that is not in
          force is worse than no cap, because it reads as protection in the source.

     Ruled shape: the map column is the image's NATURAL width and nothing more, and
     the stats block takes everything else. `width: max-content` sizes the figure to
     its image (193 / 199 / 205 / 258 / 300px across the family â€” surveyed, those are
     every dive-map asset's natural width), `flex: 0 0 auto` stops it flexing at all,
     and dropping the auto margin hands the free space back to `flex-grow: 1` on the
     stats. The map still finishes at the card's right edge, because the stats now
     grow to push it there.

     Breathing is the card's own standard and is NOT doubled: `--sp-4` 16px of gap to
     the stats and 16px of card padding to the edge, both already in force at
     main.css:2160. The 340px cap is a backstop for a future oversized asset only â€”
     40px clear of the widest real map â€” and, unlike 46%, it is written where nothing
     later overrides it. */
  /* THE RULE THAT WAS BEATING BOTH OF US, found with CDP's own cascade rather than by
     reading the file: main.css:3615-3621, the â‘¦ image law â€”

       .content-card :is(figure, .section, .imgvilla, â€¦):has(> img[src*="dive-map"])
         { width: 100% !important; max-width: 100% !important; margin-left: 0 !important; }

     The map's src is `/Sipadan/maps/barracuda-point-dive-map.webp`, so `*="dive-map"`
     matches it and the figure is forced to `width: 100%` of the flex line. **That is why
     R45â‘¡'s `max-width: 46%` never took effect either** â€” computed max-width was 100%, not
     46%, on the day R45 shipped. The law is right for a full-width area map and wrong for
     the small stats-card map that happens to share the filename fragment.

     Specificity arithmetic, because this has to be deterministic rather than lucky:
     theirs is `.content-card`(0,1,0) + `:is(â€¦)` taking `.section`(0,1,0) + `:has(> img[srcâ€¦])`
     taking (0,1,1) = **(0,3,1) !important**. Mine below is `.content-card` + `.dive-stats-card`
     + `.dive-stats-card__map` + `figure` + `:has(> img)` = **(0,3,2) !important**, so it wins
     on specificity and does not depend on source order. Only the three contested properties
     are marked; nothing else here needs it. */
  .content-card .dive-stats-card > figure.dive-stats-card__map:has(> img) {
    flex: 0 0 auto;
    width: max-content !important;      /* = the image's natural width */
    max-width: 340px !important;        /* backstop only: widest real map is 300px */
    margin-left: 0 !important;
    padding: 0;
  }
  .dive-stats-card > .dive-stats-card__map img { width: auto; max-width: 100%; height: auto; }
}

/* â‘¢ TRAILING CTA ORDER. On the /Kapalai-Resorts.php class the final Â«Check availability at
   {resort}Â» paragraph is emitted AFTER the .discount-box contact card in the PAGE SOURCE, so
   it renders below it. The source order belongs to the content terminals; this reorders the
   PAINT only, exactly as CSS-R23 did for .feat-row - the DOM is untouched, so nothing about
   reading order in the markup changes for a consumer that re-renders these pages.
   Scoped to a content card that actually contains the pair, so no other page can be caught. */
.content-card:has(> .discount-box) { display: flow-root; }
.content-card:has(> .discount-box) > .discount-box { order: 2; }
.content-card:has(> .discount-box) > .cta-row { order: 1; }
@supports (display: flex) {
  .content-card:has(> .discount-box + .cta-row) {
    display: flex;
    flex-direction: column;
  }
  /* everything that is not the pair keeps document order around them */
  .content-card:has(> .discount-box + .cta-row) > * { order: 0; }
  .content-card:has(> .discount-box + .cta-row) > .discount-box { order: 2; }
  .content-card:has(> .discount-box + .cta-row) > .discount-box ~ .cta-row { order: 1; }
}


/* ---------- CSS-R45c Â· review provenance as quiet disclosure ---------- */
/* Owner ruling, all translated-review trees (de ru jp ko it; es joins when its round lands).
   The line sits INSIDE .review as the blockquote's first child, so it is read before the
   quote it qualifies - it should disclose, not announce. Parentheses come from ::before /
   ::after so NO TREE'S DATA CHANGES: each tree keeps emitting its own bare sentence
   (Â«Tradotto dall'ingleseÂ», Â«Aus dem Englischen Ã¼bersetztÂ», â€¦) and the punctuation is
   presentation. One rule in shared CSS; every tree inherits, including es on arrival.
   #4a5b6d is the muted slate this file already uses for subordinate text (nav taglines,
   dropdown placeholders) - readable, not pale grey. */
.review__provenance {
  font-size: 0.85em;
  font-style: italic;
  color: #4a5b6d;
  margin-top: 0;
  margin-bottom: 0.25em;          /* collapsed toward the quote it belongs to */
}
.review__provenance::before { content: "("; }
.review__provenance::after  { content: ")"; }
/* The class appears in TWO markup shapes and the rule must fit both: `it` nests it
   INSIDE the blockquote as first child, `jp`/`ko` place it as a SIBLING before the
   blockquote. An ancestor-scoped selector styled only the nested shape and left
   jp/ko without their parentheses - hence keying on the class alone. */
.review__provenance + .review { margin-top: 0.25em; }


/* ---------- CSS-R46 Â· Reservations fills (owner ruling) ---------- */
/* MOBILE popup: the Reservations card takes the SAME blue as Contact Details. R37 had given
   it a navy-8% tint to separate it from its siblings; the ruling is that the two pinned
   items should read as one pair, so it now takes the call-blue the R36 contact card uses.
   DESKTOP rail: the Reservations item goes back to PLAIN WHITE like every other item - R37's
   tint was what carried CTA-ness there once the brass was removed, and the owner has ruled
   that cost too high. The selected/active state is untouched: `.site-nav__item.active` still
   supplies its own background and gold inset, and it is written at (0,2,1) below so the
   white cannot override it. Colour still comes from --c-deep, so the item is not identical
   to a plain row - only its ground is. */
@media (min-width: 1024px) {
  /* CSS-R48 â‘  (owner ruling) â€” SUPERSEDES R46's value here, because R46's MEASUREMENT
     WAS WRONG. R46 recorded that the rail's siblings "sit on --c-surface-2 (#fcfaf5,
     the warm white the desktop rail uses)" and gave the CTA that token so it would
     match them. Re-measured at 1920 on `/` and `/diving-holiday-packages.php`: the
     rail card `.site-nav` is rgb(255,255,255) and EVERY ordinary item computes
     rgba(0,0,0,0) â€” transparent over white. Nothing in the rail sits on #fcfaf5. What
     R46 measured as "the rail's ground" is the hover FLY-OUT card `.site-nav__sub`,
     which does use the token and which this ruling leaves alone. The effect was the
     exact inverse of the intent: Reservations became the ONE item that was not white.
     The owner reads #fcfaf5 as yellowish, and it is â€” three points of red over blue.
     Now stated as an explicit #fff rather than `transparent`, which is R46's one
     correct lesson: transparent lets whatever is behind the rail show through. */
  .site-nav .site-nav__link--cta { background: var(--c-white); }
  .site-nav .site-nav__link--cta:hover { background: var(--c-sand); }
  /* selected state wins, exactly as for any other item â€” "selected keeps its current
     treatment", so the served page still takes the warm ground and the gold bar */
  .site-nav .site-nav__item.active > .site-nav__row .site-nav__link--cta {
    background: var(--c-surface-2);
  }
  /* CSS-R48 â‘¡ (owner ruling, PARITY WITH THE DRAWER) â€” white unless selected, here too.
     A section header whose CHILD is current keeps a SUBTLE cue and loses the full
     selected ground: the 2px gold inset bar from main.css:291 still says "you are in
     this section", the #fcfaf5 no longer says "you are on this page". The hover
     fly-outs are untouched, as ruled. (0,4,0) â€” same weight as main.css:291 and later
     in the file, so it wins on order; the box-shadow is deliberately NOT reset. */
  .site-nav__item.active-parent > .site-nav__row .site-nav__link {
    background: var(--c-white);
  }
}
@media (max-width: 1023px) {
  .home-nav__btn--reservations {
    background: #cfe6f1;                                       /* fallback */
    background: color-mix(in srgb, var(--c-btn-call) 42%, #fff);
    border-color: var(--c-teal);
  }
  .home-nav__btn--reservations:hover,
  .home-nav__btn--reservations:focus-visible {
    background: #bfdcea;
    background: color-mix(in srgb, var(--c-btn-call) 54%, #fff);
  }
  .home-nav__btn--reservations .home-nav__icon { color: var(--c-teal-dark); }
}


/* ============================================================================
   CSS-R48c â€” THE cn CLUSTER: BASELINE, CHEVRON COLOUR, CHEVRON SPACING
   ----------------------------------------------------------------------------
   â‘  BASELINE. Measured at 414 on /Borneo-Divers.php: `.site-footer__lang` is a
      flex row with `align-items: normal`; its eight plain `.lang-flag` children
      are 20px tall, but `span.lang-cn` measured **44px** because R39's
      `@media (hover: none)` rule turns `.lang-cn__toggle` into a 44x44 LAYOUT
      box. `.lang-cn { align-items: center }` then centres the cn flag inside
      that taller wrapper: (44-20)/2 = **12px**, and the measured drop was
      **exactly 12.0px** (cn flag y=658.3 against every sibling's 646.3).

      It is FOOTER-ONLY, and the same measurement says why: on the M-HOME line
      the drop is **0.0px**, because `.home-flags__flag` siblings are themselves
      44px tall, so a 44px cluster is correctly aligned there. A blanket layout
      change would have broken the surface that was already right.

      So the toggle stops being a 44px layout box and becomes a 20px one â€” the
      height of the flag beside it â€” while R39's tap target is preserved as an
      OVERLAY pseudo-element, which paints no ink and occupies no layout. The
      hit area is unchanged at 44x44; only the box that drives flex height
      shrinks. `.home-flags .lang-cn__toggle` keeps its own 18x44 by specificity
      (0,2,0 over this rule's 0,1,0), so the aligned surface stays aligned.

   â‘¡ CHEVRON COLOUR (owner ruling): the R46 mobile Reservations blue,
      `color-mix(in srgb, var(--c-btn-call) 42%, #fff)`. Stated on the CARET as
      `border-top-color` rather than inherited through `currentColor`, because
      `.site-footer .lang-cn__toggle { color:#fff }` and the permit-bar/header
      navy rule would otherwise repaint it per context â€” and the ruling is one
      component, one look.

   â‘¢ CHEVRON SPACING (owner ruling): the caret sat 19px from the flag's right
      edge â€” 2px of margin plus 17px of dead space inside a 44px-wide button
      whose caret is only 10px. Width 44 -> 14 and margin-left 2 -> 1 puts it
      ~3px from the flag, reading as attached rather than floating.
   ============================================================================ */
@media (hover: none) {
  .lang-cn__toggle {
    position: relative;        /* was static (R39) â€” anchors the overlay below */
    width: 14px;               /* â‘¢ was 44px: the dead space is what floated it */
    height: 20px;              /* â‘  = .lang-flag, so the cluster is 20px, not 44 */
    margin-left: 1px;          /* â‘¢ was 2px */
  }
  /* â‘  R39's >=44px tap target, kept EXACTLY, as a hit area with no layout box */
  .lang-cn__toggle::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
  }
  /* the M-HOME line keeps its own 18x44 box by specificity; it only needs the
     positioning context so the overlay centres on the button and not on .lang-cn */
  .home-flags .lang-cn__toggle { position: relative; }
}
/* â‘¡ one component, one look â€” outside the media query, since the caret is also
   visible on the M-HOME line, and (0,2,0) so it beats `.home-flags .lang-cn__caret`.

   OWNER RULING (R48c, after the measurement): the token is `--c-btn-call` UNDILUTED,
   not the R46 42%-with-white mix. Measured, which is why the ruling changed: the mix
   is #cfe6f1 and reads 12.92:1 on the footer's rgb(10,31,51) but **1.29:1** on the
   M-HOME line's white â€” invisible. Undiluted #8ec9df is 8.70:1 and 1.81:1, and the
   owner has ruled that acceptable for a decorative caret whose FLAG is the real
   affordance. One token, one look, legible on both grounds. */
.lang-cn .lang-cn__caret {
  border-top-color: var(--c-btn-call);              /* #8ec9df */
}


/* ============================================================================
   CSS-R49b â€” THE M-HOME FLAG LINE WRAPS TO TWO ROWS
   ----------------------------------------------------------------------------
   MEASURED, and it names a different element than "the mobile flags row" might
   suggest. At 360 the FOOTER strip fits exactly â€” content 328px in a 328px box,
   overflow 0 â€” and at 414 it has 32px to spare. The row that overflows is the
   M-HOME homepage line `.home-flags`: content **513px** inside a **294px**
   container at 360 (overflow **110px**, and the first flag already starts at
   x=-76), and 513 inside 346 at 414 (overflow **84px**). The cause is its own
   `flex-wrap: nowrap` against nine children that each carry R20's 44px tap
   floor: 9x44 + 8x12 gap = 492px, plus the cn cluster's chevron.

   Mechanism: `flex-wrap: wrap` with the existing `justify-content: center`, and
   an explicit `row-gap`. No count is hard-coded â€” at 360 the available 294px
   fits five 44px targets plus their gaps and the sixth wraps, so the line lands
   on the 5+4 the ruling describes, and it re-splits by itself on a narrower or
   wider frame instead of being pinned to one breakpoint.
   ============================================================================ */
.home-flags {
  flex-wrap: wrap;
  row-gap: var(--sp-2);
}
/* THE FOOTER STRIP IS DELIBERATELY LEFT ALONE, and the measurement is why.
   It is not the overflowing row: 0px overflow at 360, 32px of slack at 414. I
   added `flex-wrap: wrap` to it anyway as a safety for widths below the ruled
   range, and the probe rejected it â€” with wrap enabled the 328px of content in
   a 328px box no longer held one line and split **8 + 1**, one flag stranded on
   its own row, at the very width where it had been fine. A safety that damages
   the ruled range is not a safety, so it was removed rather than kept and
   explained. Below 360 the strip still runs out of frame; that is outside this
   ruling and is left as a known limit rather than traded for an 8+1 split. */


/* ============================================================================
   CSS-R49c â€” THE HEADER FLAG STRIP GETS THE PERMIT STRIP'S SPACING
   ----------------------------------------------------------------------------
   AUDIT of the desktop switcher across the six template families found ONE
   deviating renderer and one family with no switcher at all:

     standard / index / blog hub / blog article  -> `.permit-bar__flags`,
        identical on all four: inner x=370 w=1180, padding 12px 32px,
        strip x=1288 w=230, cn flag y=165.8 = sibling y=165.8.
     forms (`includes/form.page.php` sets $HIDE_PERMIT_BAR + $HEADER_FLAGS)
        -> `.header-flags`, a DIFFERENT element whose only rule was
        `margin-top: var(--sp-3)`. No `display: flex`, so no `gap` â€” hence the
        owner's "no side padding" â€” and its children were inline-level, so the
        `.lang-cn` inline-flex box sat on the text baseline instead of the flag
        row: hence "the cn flag off-baseline". Both symptoms, one missing rule.
     confirmation (`thankyou.php` sets $HIDE_PERMIT_BAR and NOT $HEADER_FLAGS)
        -> NO flag strip rendered at all on desktop.

   The strip's own spacing is normalised here to the permit bar's exact values
   (6px gap, 30x20 flags, the same hover brightness). `justify-content: flex-end`
   keeps it under the right-aligned header CTA it belongs to, which is that
   family's equivalent of the permit strip's `margin-left: auto`.
   ============================================================================ */
.header-flags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;                        /* = .permit-bar__flags, exactly â€” no row-gap of its
                                      own, so the computed value matches the standard
                                      page's byte for byte and the probe can assert
                                      equality rather than "close enough" */
}
.header-flags .lang-flag { width: 30px; height: 20px; }
.header-flags .lang-flag:hover,
.header-flags .lang-flag:focus-visible { filter: brightness(1.25); }


/* ============================================================================
   CSS-R48 â‘¢ â€” THE MOBILE POPUP: ONE HIGHLIGHTED ROW, AND EXPANSION IS NOT IT
   ----------------------------------------------------------------------------
   Owner ruling from the popup on /diving-holiday-packages.php: every row reads
   WHITE unless it is the served page; expanded accordion children included; the
   open/closed chevron must not change a background â€” OPEN â‰  SELECTED.

   MEASURED CAUSE, and it is two rules, not one:

     1. `.site-nav__sub { background: var(--c-surface-2) }` in the drawer block
        gave the whole expanded child LIST the warm ground (#fcfaf5) â€” keyed on
        EXPANSION, which is exactly the thing the ruling separates from selection.
     2. `.site-nav__item.active` and `.active-parent` share one rule (main.css
        :291) that paints BOTH the same #fcfaf5. `active-parent` fires when a
        CHILD is current or the page declares the section, so a group header lit
        up for pages that are not it.

   Together they are why the owner saw "nothing highlighted" while standing on a
   page in the open group: on /diving-holiday-packages.php the header IS `active`
   and IS #fcfaf5 â€” but the sub-list beneath it painted the SAME #fcfaf5, and
   every header already renders at --fw-semi, so neither ground nor weight told
   the current row apart from its neighbours. The selected state was present in
   the markup and invisible on the screen.

   So: the ground is taken away from expansion and given only to the served page,
   and the current row additionally goes --fw-bold, one step above the --fw-semi
   every other row carries. The gold inset bar is what actually carries the
   signal now that the warm white sits on white.

   SCOPE: the mobile drawer only. The desktop rail has no accordion â€” its
   sub-lists are hover fly-outs â€” so "open â‰  selected" cannot arise there, and
   its `active-parent` highlight still tells a reader which section they are in.
   Extending the ruling to the rail is one word if the owner wants parity.

   RESERVATIONS IS NOT RE-GROUNDED: `.site-nav .site-nav__link--cta` (0,2,0)
   supplies its navy-8% light blue, and if it is ever the served page it must
   stay that colour â€” so the ground rule below excludes it with :not() while the
   inset bar and the bold still apply. Both rulings hold at once.
   ============================================================================ */
@media (max-width: 1023px) {
  /* â‘  expansion loses its ground â€” the child list is white like the drawer */
  .site-nav__sub { background: var(--c-white); }

  /* â‘¡ a header whose CHILD is current is NOT itself selected */
  .site-nav__item.active-parent > .site-nav__row .site-nav__link {
    background: transparent;
    box-shadow: none;
  }

  /* â‘¢ the ONE row that matches the served page â€” header â€¦ */
  .site-nav__item.active > .site-nav__row .site-nav__link:not(.site-nav__link--cta) {
    background: var(--c-surface-2);
    color: var(--c-navy);
  }
  .site-nav__item.active > .site-nav__row .site-nav__link {
    box-shadow: inset 2px 0 0 var(--c-gold);
    font-weight: var(--fw-bold);
  }
  /* â‘£ CSS-R48 â‘¢ (owner ruling) â€” THE PINNED RESERVATIONS ROW IS LIGHT BLUE, and the
     reason the owner still measures GREY is that R46 STYLED THE WRONG ELEMENT.
     R46's ruling was "the mobile Reservations card takes the same blue as Contact
     Details", and it landed that on `.home-nav__btn--reservations` â€” the M-HOME
     HOMEPAGE popup button, which renders on language homepages only. The row the
     owner is looking at, in the drawer on /diving-holiday-packages.php, is
     `.site-nav__link--cta`, and it was never touched: it still carried R37's
     `color-mix(--c-navy 8%, #fff)` = rgb(236,238,240). That is a blue by construction
     and a GREY to the eye â€” 8% of a near-black navy desaturates to neutral. Not a
     specificity fault, not stale bytes: the rule that was supposed to fix this has
     never applied to this element on this page.
     The value is taken from `.home-nav__btn--reservations` unchanged, so the two
     mobile Reservations surfaces finally agree. */
  .site-nav .site-nav__link--cta {
    background: #cfe6f1;                                        /* fallback */
    background: color-mix(in srgb, var(--c-btn-call) 42%, #fff);
  }
  .site-nav .site-nav__link--cta:hover,
  .site-nav .site-nav__link--cta:focus-visible {
    background: #bfdcea;                                        /* fallback */
    background: color-mix(in srgb, var(--c-btn-call) 54%, #fff);
  }
  /* â€¦and Reservations keeps its OWN ground when it is the served page. Excluding it
     from the â‘¢ rule above was not enough: the base rule at main.css:291 is (0,4,0) and
     already beat `.site-nav .site-nav__link--cta` (0,2,0), so an active Reservations
     painted WARM before this round â€” the probe made it visible. Restated at (0,5,0),
     so the row is marked current by the gold bar and the weight while it keeps the
     blue that is its identity. */
  .site-nav .site-nav__item.active > .site-nav__row .site-nav__link--cta {
    background: #cfe6f1;                                        /* fallback */
    background: color-mix(in srgb, var(--c-btn-call) 42%, #fff);
  }
  /* â€¦ and child row, which until now got colour and weight but no ground at all */
  .site-nav__sub li.active > a {
    background: var(--c-surface-2);
    box-shadow: inset 2px 0 0 var(--c-gold);
    color: var(--c-navy);
    font-weight: var(--fw-bold);
  }
}


/* ============================================================================
   CSS-R49d â€” THE ARRIVAL DATE FIELD: TEXT + CALENDAR BUTTON
   ----------------------------------------------------------------------------
   Styling half of AUDIT-1B option B. FR landed the markup with its own class
   names â€” `.form-date`, `.form-date__btn`, `.form-date__native`, plus a hidden
   `name="date_iso"` â€” and NO CSS existed for any of them, so the button sat in
   normal flow and the native date control rendered as a visible second field.
   Both shapes are styled here: FR's, and the `.date-field` names my request had
   proposed, so neither can break the other.

   FR's button ships its own inline SVG, so the CSS-drawn calendar glyph is
   scoped to `.date-field__btn` ONLY â€” drawing it for both would double it.

   The native `type=date` is VISUALLY hidden, not `display:none` and not
   `hidden`: a display:none input cannot be focused and several browsers refuse
   showPicker() on one, which would take the fallback path away as well as the
   primary one.
   ============================================================================ */
.form-date,
.date-field { position: relative; display: block; }
.form-date > input[type="text"],
.date-field > input[type="text"] { width: 100%; padding-right: 44px; }

.form-date__btn,
.date-field__btn {
  position: absolute;
  top: 0;
  right: 0;
  width: 44px;                     /* R39's tap floor, on a control that needs it */
  height: 100%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 0 var(--radius-sm, 6px) var(--radius-sm, 6px) 0;
  background: transparent;
  color: var(--c-navy);
  cursor: pointer;
}
.form-date__btn:hover,
.form-date__btn:focus-visible,
.date-field__btn:hover,
.date-field__btn:focus-visible {
  background: #e9f4f9;
  background: color-mix(in srgb, var(--c-btn-call) 20%, #fff);
  color: var(--c-navy);
}
.form-date__btn svg { display: block; }
/* the calendar glyph for MY contract's button only â€” FR's carries its own SVG */
.date-field__btn::before {
  content: "";
  width: 17px;
  height: 16px;
  border: 2px solid currentColor;
  border-radius: 2px;
  border-top-width: 5px;           /* the header band of a calendar page */
  box-sizing: border-box;
}

/* (0,2,0) so it beats the shared `.form-field input { width: 100% }` at (0,1,1) â€”
   measured on WebKit: at (0,1,0) this box computed 346px wide, the whole field. */
.form-field .form-date > input.form-date__native,
.form-field .date-field > input.date-field__native {
  position: absolute;
  right: 12px;
  bottom: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: 0;
  border: 0;
  opacity: 0;
  overflow: hidden;
  /* NO clip-path here: Chrome anchors the picker popover to the input's box, and
     a clipped box drops the popover in the top-left corner of the viewport. A 1px
     transparent box at the field's right edge puts it under the button instead. */
  pointer-events: none;
}


/* ============================================================================
   CSS-R50 (3) â€” THE PAST-DATE WARNING STATE
   ----------------------------------------------------------------------------
   A typed arrival date in the past is not an ERROR â€” the form still submits and
   the server still judges it â€” so it must not borrow `.is-invalid`'s red, which
   this form uses for "you left this out". It takes the project's warm accent
   instead, at the same shape as the invalid state so the two read as one family:
   label tinted, border tinted, a soft ring, and one line of text underneath.

   The text itself is FR's (one ct() key, contract note filed). The JS paints the
   state whether or not the string has landed, so the field is never silently
   wrong, and it invents no English of its own.
   ============================================================================ */
.form-field.is-warning label { color: var(--c-gold-text); }
.form-field.is-warning input,
.form-field.is-warning select {
  border-color: var(--c-gold);
  box-shadow: 0 0 0 3px rgba(181, 146, 78, 0.16);
}
.form-warn {
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-sm);
  color: var(--c-gold-text);
}

/* ============================================================================
   CSS-R50 (5) â€” THE HONEYPOT
   ----------------------------------------------------------------------------
   Off-screen, NOT `display:none` and NOT `hidden`: the cheapest bots skip fields
   the browser would not render, and skipping is exactly what this must not let
   them do. It keeps a 1px box, is removed from the tab order and the a11y tree
   by FR's markup (`tabindex="-1"`, `aria-hidden="true"`, `autocomplete="off"`),
   and no human pointer or caret can reach it.

   `left: -9999px` rather than `clip-path`: a clipped box still takes focus on
   some browsers when a label points at it, and this field must never steal a
   keystroke from a real visitor.
   ============================================================================ */
.form-hp {
  position: absolute;
  left: -9999px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}


/* ============================================================================
   CSS-R51 (1) â€” LAW: THE MORE PILL IS A >=1024px CONSTRUCT, NEVER MOBILE
   ----------------------------------------------------------------------------
   THE REGRESSION IS MINE, from R49c. Before it, `.header-flags` was hidden below
   1024px by `@media (max-width: 1023px) { .header-flags { display: none } }` at
   main.css:165. R49c then gave `.header-flags` a `display: flex` of the SAME
   specificity (0,1,0) LATER in the file â€” so the later rule won and the desktop
   strip started rendering on mobile. On the form family, which is the only family
   that uses `.header-flags`, the phone header therefore showed the desktop widget
   (en + cn flags + the More pill) on top of the compact `.lang-menu` control that
   was already there: the crowding in the owner's screenshot, two switchers at once.

   The fix is the media query R49c should have carried, and the LAW is written as a
   second, independent rule: even if some future surface renders a pill below
   1024px, it does not paint. Two locks, because one of them was already silently
   picked once.
   ============================================================================ */
@media (max-width: 1023px) {
  .header-flags { display: none !important; }
  .lang-cn.lang-more { display: none !important; }
}

/* ============================================================================
   CSS-R51 (2) â€” THE FORM-FAMILY STRIP IS CENTRED IN THE BANNER
   ----------------------------------------------------------------------------
   Measured: `.header-flags` carried `margin-top: var(--sp-3)` and nothing below,
   so the strip sat flush on the banner's bottom edge with every pixel of slack
   above it. Equal breathing above (below the Check-availability CTA) and below
   (to the banner's edge) replaces it.
   ============================================================================ */
/* ============================================================================
   CSS-R53 â‘´â‘µâ‘· â€” THE PILL MOVES; NOTHING ELSE DOES
   ----------------------------------------------------------------------------
   R52 â‘· equalised the gaps by STRETCHING the actions column
   (`.site-header__inner > .site-header__actions { align-self: stretch }` and
   `.site-header__actions > .site-header__cta-wrap { height: 100% }`) and then
   letting `margin: auto` centre the strip inside it. It worked on the form
   family and it was the wrong mechanism, because **neither of those two
   selectors is scoped to the form family** â€” they are plain shared chrome. On
   every page WITHOUT a header strip the wrap holds only the CTA, so stretching
   it moved the CTA from the middle of the banner to the top of a now-full-height
   column: measured, the standard/home CTA sits at **top 42.5 / bottom 90.5** and
   the stretch would have pulled it to ~21. That is the homepage jump the owner
   saw, and R52 â‘¸'s "unchanged, asserted" missed it because the probe asserted the
   standard family's flags, pill and panel â€” never the CTA's box.

   R53 therefore touches ONE selector, `.header-flags`, which renders only where
   `$HEADER_FLAGS` is set â€” i.e. only where the permit bar is hidden. It cannot
   reach the standard family at all, so the fix is leak-proof by construction
   rather than by assertion.

   THE INVARIANT THAT FREEZES THE CTA: the two margins still SUM TO 24px, exactly
   as `var(--sp-3)` twice did before. The actions column's height is therefore
   unchanged, so `align-items: center` on `.site-header__inner` puts it in the
   same place and the CTA cannot move. Only the strip's position INSIDE that
   fixed column changes.

   THE ARITHMETIC, derived rather than tuned:
       banner bottom 134 Â· actions top 20.5 Â· CTA 48 tall Â· strip 20 tall
       above = M
       below = 134 âˆ’ (20.5 + 48 + M + 20) = 45.5 âˆ’ M
       M = 45.5 âˆ’ M  â†’  M = 22.75, and 24 âˆ’ 22.75 = 1.25
   Both gaps land on 22.75px. If the logo (which sets the banner height) ever
   changes, `probe_r53.py` fails on the equality rather than the layout drifting
   quietly â€” the constant is guarded by a test, not by hope.
   ============================================================================ */
/* â‘¶ THE STANDARD FAMILY. Its switcher is not in the banner at all â€” it is
   `.permit-bar__flags`, and it is ALREADY centred: `.permit-bar__inner` is
   `align-items: center`, and the strip measured 165.75..185.75 inside a content
   box of 146..205.5, i.e. 19.75px of slack on each side, exactly centred.
   Against the permit bar's TRUE bottom edge, though, it read 31.75 above and
   32.75 below â€” and that 1px is the bar's own 1px bottom border, the same class
   of residual the banner had. One line moves the whole strip (flags and pill
   together, so they keep their shared baseline) down by half of it. */
@media (min-width: 1024px) {
  .permit-bar__flags { margin-top: 1px; }   /* centred item: shifts by 0.5px */
}

/* ============================================================================
   CSS-TOPBAR-FLAGS-01 (2) - flag icons share a centre; the dropdown gets a real gap.
   MEASURED CAUSE: the strip was `display: block`, so its three children were INLINE-LEVEL
   boxes on a text baseline - `a.lang-flag` is inline-block (baseline = last line box) while
   `span.lang-cn` and the `.lang-more` pill are inline-flex (baseline = first item's). Three
   baselines, three centres: 178.1, 172.4 and 175.9 - a 5.7px spread. The same `display:
   block` is why `gap` did nothing, so the gaps measured [0, 0] and the flags and the 'Other
   languages' dropdown were touching. 6px is copied from `.header-flags`, whose own comment
   already claimed parity with this strip - never true, because it was never flex.
   The R54 shim above is unaffected: it moves the whole strip, not the children. */
.permit-bar__flags {
  display: flex;
  align-items: center;
  gap: 6px;                        /* = .header-flags, exactly */
}

/* ============================================================================
   CSS-R54 â‘´â‘µ â€” ONE CTA POSITION EVERYWHERE
   ----------------------------------------------------------------------------
   R53 froze the form family's CTA at its historical [20.5, 68.5]. The owner's
   ruling is that it must sit where the STANDARD family's does â€” [42.5, 90.5] â€”
   on every desktop page, and the reason it never has is structural: the strip
   shares the banner's centred actions column, so the CTA+strip PAIR is what gets
   centred and the CTA rides up by half the strip's height plus its margins.

   So the strip leaves the flow. `position: absolute` takes it out of the
   centring math entirely; the wrap is then exactly as tall as the CTA, which is
   precisely the standard family's situation, and the same
   `align-items: center` on `.site-header__inner` therefore produces the same
   number â€” 42.5 â€” without this round asserting it anywhere.

   SCOPED BY CONSTRUCTION, per R53's law and with ZERO shared-chrome selectors:
   `:has(> .header-flags)` matches the wrap ONLY on a page that actually renders
   the strip, i.e. only the form family. R52's leak was two unscoped structural
   selectors; this one cannot match a page that has no strip to move.

   THE OFFSET, derived:
       banner bottom 134 Â· CTA bottom 90.5 Â· strip 20 tall
       gap = (134 âˆ’ 90.5 âˆ’ 20) / 2 = 11.75
   and `top: 100%` is the wrap's bottom edge, which IS the CTA's bottom, so the
   whole expression reads as the ruling does. `probe_r54.py` asserts the EQUALITY
   of the two gaps, never the constant (R53 law), so a banner-height change fails
   loudly rather than drifting.
   ============================================================================ */
@media (min-width: 1024px) {
  .site-header__cta-wrap:has(> .header-flags) { position: relative; }
  .header-flags {
    position: absolute;
    top: 100%;                   /* = the CTA's bottom edge */
    /* CSS-R55 improves R54's offset: instead of a gap derived from the pill's HEIGHT,
       the strip spans the whole band between the CTA and the banner's bottom and the
       pill centres inside it (`align-items: center` is already on this rule from R49c).
       The two gaps are then equal FOR ANY PILL HEIGHT â€” which matters this round,
       because â‘¶ changes that height. One structural constant instead of one that had
       to be re-derived every time the pill was restyled. */
    height: 43.5px;              /* banner bottom 134 âˆ’ CTA bottom 90.5 */
    right: 0;
    margin: 0;
  }
}

/* ============================================================================
   CSS-R51 (3) â€” THE MORE PILL, BANNER VARIANT
   ----------------------------------------------------------------------------
   SURFACE-SCOPED: only inside `.header-flags`, which is the form family's blue
   banner. The white-ground surfaces (permit bar on every other family) keep the
   R45(1) pill exactly as ruled â€” solid white, hairline, semibold.

   On the blue the solid white pill glared, so it becomes glass: a 1px border, a
   half-transparent light-blue fill that lets the banner through, and a lighter,
   smaller label.
   ============================================================================ */
.header-flags .lang-cn.lang-more .lang-cn__toggle {
  background: rgba(142, 201, 223, 0.28);                      /* fallback */
  background: color-mix(in srgb, var(--c-btn-call) 28%, transparent);
  border: 1px solid rgba(255, 255, 255, 0.55);
  color: var(--c-navy);
  font-weight: var(--fw-reg);
  font-size: var(--fs-sm);
}
.header-flags .lang-cn.lang-more .lang-cn__toggle:hover,
.header-flags .lang-cn.lang-more .lang-cn__toggle:focus-visible {
  background: rgba(142, 201, 223, 0.45);
  background: color-mix(in srgb, var(--c-btn-call) 45%, transparent);
  border-color: rgba(255, 255, 255, 0.8);
}

/* ============================================================================
   CSS-R51 (4) â€” WHITE-EDGE FLAGS GET A HAIRLINE
   ----------------------------------------------------------------------------
   fr, it, ru, ko and jp all carry a white band that reaches the glyph's edge, so
   on a light ground the flag has no outline and the white bleeds into the page.
   en, cn, de and es have coloured edges and are left alone, per the ruling.

   Keyed on `hreflang`, which `sipadan_lang_row()` already emits on every flag
   anchor on every surface â€” so this needs no new class, no markup, and no
   per-surface repetition. NOTE jp's attribute is `ja` (the R32 folder-vs-attribute
   split: the KEY is jp, the ATTRIBUTE is ja); keying on `jp` would have missed it.
   ============================================================================ */
.lang-flag[hreflang="fr"] svg,
.lang-flag[hreflang="it"] svg,
.lang-flag[hreflang="ru"] svg,
.lang-flag[hreflang="ko"] svg,
.lang-flag[hreflang="ja"] svg,
.home-flags__flag[hreflang="fr"] svg,
.home-flags__flag[hreflang="it"] svg,
.home-flags__flag[hreflang="ru"] svg,
.home-flags__flag[hreflang="ko"] svg,
.home-flags__flag[hreflang="ja"] svg {
  box-shadow: 0 0 0 1px var(--c-btn-call);
  border-radius: 2px;
}

/* ============================================================================
   CSS-R51 (6) â€” THE CN CHEVRON, SUPERSEDING R48c
   ----------------------------------------------------------------------------
   R48c landed `--c-btn-call` (#8ec9df) undiluted after measuring the R46 mix at
   1.29:1 on the M-HOME line. The owner now rules it bigger and darker: +2px on
   both axes and a blue from the navy/call family rather than the palest member.
   `--c-teal` (#12a3b8) is that blue â€” the one cool accent between the call blue
   and the navy.

   Contrast is re-measured on BOTH grounds in the report, because the two surfaces
   pull in opposite directions: darker helps the white M-HOME line and costs the
   dark footer.
   ============================================================================ */
.lang-cn .lang-cn__caret {
  border-top-color: var(--c-teal);                  /* #12a3b8 */
}
@media (hover: none) {
  .lang-cn__toggle { width: 16px; }                 /* was 14px: +2px of room */
  .lang-cn__caret {
    border-left-width: 6px;                         /* was 5px  -> 12px wide */
    border-right-width: 6px;
    border-top-width: 8px;                          /* was 6px  -> +2px tall */
  }
}

/* ============================================================================
   CSS-R51 (7) â€” TOUCH: THE NATIVE DATE INPUT *IS* THE TAP TARGET
   ----------------------------------------------------------------------------
   On iOS `showPicker()` does not exist, so the calendar button had nothing to
   call and tapping it opened nothing. The fallback the owner ruled is the robust
   one: stop trying to open the picker programmatically and let the OS do what it
   already knows how to do â€” put the real `type=date` control under the finger.

   Gated on a COARSE POINTER, so the desktop path is untouched: there the button
   keeps its `showPicker()` call, which works and is nicer than a bare date input.
   The overlay sits above the button, is fully transparent, and is exactly the
   button's 44px box, so nothing moves and nothing else becomes clickable.
   ============================================================================ */
@media (hover: none) and (pointer: coarse) {
  /* (0,2,0), same reason as the base rule: at (0,1,0) the shared
     `.form-field input { width: 100% }` won and this overlay covered the ENTIRE
     field, swallowing every tap meant for the text input â€” measured 346px wide on
     WebKit, which is the whole control, not the 44px button. */
  .form-field .form-date > input.form-date__native,
  .form-field .date-field > input.date-field__native {
    position: absolute;
    top: 0;
    right: 0;
    bottom: auto;
    left: auto;
    width: 44px;
    height: 100%;
    min-height: 0;
    padding: 0;
    border: 0;
    opacity: 0;
    pointer-events: auto;             /* the finger lands HERE, not on the button */
    z-index: 2;
    -webkit-appearance: none;
    appearance: none;
  }
}


/* ============================================================================
   CSS-R52 (3) â€” THE PILL-ONLY BANNER SWITCHER, MOBILE PILL AS THE MODEL
   ----------------------------------------------------------------------------
   R51 (3) made the banner pill glass at 28% call blue with a 400 label, and the
   owner reads it as too faint. The mobile pill is the model, so: MORE fill, LESS
   transparency, a label that is visible without being bold, 1px border.

   Tokens, stated:
     fill    color-mix(--c-btn-call 46%, transparent)   was 28%
     border  1px solid rgba(255,255,255,0.72)           was 0.55
     label   --fw-semi (600) at --fs-sm                 was --fw-reg (400)
     ink     --c-navy                                   unchanged

   600 is "more visible, not bold" against the --fw-bold (700) the white-ground
   pill would use for a current language; it is the same weight the standard
   pill already carries, so the two surfaces differ in ground and not in voice.
   ============================================================================ */
.header-flags .lang-cn.lang-more .lang-cn__toggle {
  background: rgba(142, 201, 223, 0.46);                      /* fallback */
  background: color-mix(in srgb, var(--c-btn-call) 46%, transparent);
  border: 1px solid rgba(255, 255, 255, 0.72);
  color: var(--c-navy);
  font-weight: var(--fw-semi);
  font-size: var(--fs-sm);
}
.header-flags .lang-cn.lang-more .lang-cn__toggle:hover,
.header-flags .lang-cn.lang-more .lang-cn__toggle:focus-visible {
  background: rgba(142, 201, 223, 0.62);
  background: color-mix(in srgb, var(--c-btn-call) 62%, transparent);
  border-color: #fff;
}

/* the panel rows on this surface lead with a flag, so they need a gutter */
.lang-cn__card a.lang-cn__opt--flag {
  display: flex;
  align-items: center;
  gap: 9px;
}
.lang-cn__glyph {
  flex: 0 0 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 20px;
}
.lang-cn__glyph svg { width: 30px; height: 20px; display: block; border-radius: 2px; }
/* zh has no flag by R23 â€” its slot carries the ç¹ segment, which is what R23 says
   zh IS. Sized to read as a glyph rather than as body text. */
.lang-cn__seg {
  font-style: normal;
  font-size: var(--fs-sm);
  font-weight: var(--fw-semi);
  color: var(--c-navy);
  line-height: 1;
}
/* the R51 (4) hairline reaches these rows too, keyed on the same hreflang */
.lang-cn__opt--flag[hreflang="fr"] svg,
.lang-cn__opt--flag[hreflang="it"] svg,
.lang-cn__opt--flag[hreflang="ru"] svg,
.lang-cn__opt--flag[hreflang="ko"] svg,
.lang-cn__opt--flag[hreflang="ja"] svg {
  box-shadow: 0 0 0 1px var(--c-btn-call);
}




/* ============================================================================
   CSS-R55 â‘¶ â€” THE FORM-FAMILY PILL, MOBILE-SIMPLE
   ----------------------------------------------------------------------------
   The owner wants SIMPLE, with the mobile pill as the model, so this adopts
   `.lang-menu__btn`'s own treatment rather than inventing a third look:
   14px / 500 / .06em tracking, 6px 12px, a full pill radius, a barely-lighter
   white veil at .13 with a .32 border and a 4px backdrop blur, ink `--c-deep`
   at .85. That REPLACES R52 â‘¶'s call-blue tint on this surface â€” the two
   surfaces now differ only in which one carries the flags.

   Scoped to `.header-flags`, which renders only where `$HEADER_FLAGS` is set,
   so the standard family's white pill and Â«More languagesÂ» label are untouched
   by construction (R53/R54 law).
   ============================================================================ */
.header-flags .lang-cn.lang-more .lang-cn__toggle {
  height: auto;
  gap: 6px;
  padding: 6px 12px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.13);
  border: 1px solid rgba(255, 255, 255, 0.32);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  color: var(--c-deep);
  opacity: 0.85;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.06em;
  line-height: 1;
}
.header-flags .lang-cn.lang-more .lang-cn__toggle:hover,
.header-flags .lang-cn.lang-more .lang-cn__toggle:focus-visible {
  background: rgba(255, 255, 255, 0.22);
  border-color: rgba(255, 255, 255, 0.5);
  opacity: 1;
}
/* the mobile summary's chevron, same box, same 180deg on open */
.lang-more__chev { width: 11px; height: 8px; transition: transform var(--t-fast) var(--ease); }
.lang-cn.lang-more.is-open .lang-more__chev { transform: rotate(180deg); }


/* ============================================================================
   CSS-R56 (2)(4) â€” THE cn CHEVRON, NOW PER SURFACE
   ----------------------------------------------------------------------------
   R48c/R51 tuned one chevron for every surface at once, and the owner's two
   screenshots pull in OPPOSITE directions: on the frontpage strip it is too
   small and too light, in the mobile footer it is too dark and too tight. One
   look cannot satisfy both, so the component keeps its shape and each surface
   states its own size and colour. That supersedes R48c's "one component, one
   look" for COLOUR and SIZE only â€” the geometry, the tap target and the
   behaviour are still shared.

   (2) FRONTPAGE STRIP  â€” roughly double, clearly darker
         size   12x8px  ->  24x16px   (borders 6/6/8 -> 12/12/16)
         colour #12a3b8 (--c-teal)  ->  #0e828f (--c-teal-dark)
   (4) MOBILE FOOTER    â€” lighter, larger, and a left gap that matches the right
         size   12x8px  ->  16x11px   (borders 6/6/8 -> 8/8/11)
         colour #12a3b8 (--c-teal)  ->  #8ec9df (--c-btn-call)
         left gap 1px -> 8px, against the toggle's own 9px of right-hand room
   ============================================================================ */
@media (hover: none) {
  /* CSS-R59 (2) â€” the frontpage chevron is HALVED: R56 doubled it and the owner reads
     24x16 as too big. Colour unchanged (--c-teal-dark), which the owner kept. */
  .home-flags .lang-cn__caret {
    border-left-width: 6px;                      /* was 12px */
    border-right-width: 6px;                     /* was 12px */
    border-top-width: 8px;                       /* was 16px  -> 12x8, half of 24x16 */
    border-top-color: var(--c-teal-dark);        /* #0e828f, unchanged */
    opacity: 1;
  }
  .home-flags .lang-cn__toggle { width: 20px; }  /* was 30px, sized to the halved glyph */

  /* CSS-R59 (3) â€” the footer chevron loses a third and moves CLOSER to the flag. R56
     widened the left gap to match the right-hand room; the owner has reversed that, so
     the left gap is now deliberately SMALLER than the right padding. Colour unchanged. */
  .site-footer__lang .lang-cn__caret {
    border-left-width: 5.5px;                    /* was 8px  */
    border-right-width: 5.5px;                   /* was 8px  */
    border-top-width: 7px;                       /* was 11px -> 11x7, a third off 16x11 */
    border-top-color: var(--c-btn-call);         /* #8ec9df, unchanged */
    opacity: 1;
  }
  .site-footer__lang .lang-cn__toggle {
    width: 20px;                                 /* 11px glyph -> 4.5px of room each side */
    /* MEASURED, then corrected: at margin-left 2px the visual left gap was 7px against a
       5px right padding â€” still the larger of the two, which is the opposite of the
       ruling. The caret is centred inside the toggle, so the left gap is the margin PLUS
       that 4.5px of inner room; only a negative margin gets it under the right padding. */
    margin-left: -2px;                           /* left gap ~2.5px vs 4.5px right */
  }
}

/* ============================================================================
   CSS-R56 (3) â€” THE HAIRLINE REACHES EVERY FRONTPAGE FLAG
   ----------------------------------------------------------------------------
   R51 (4) gave the outline to the five white-bearing flags only. On the mobile
   frontpage strip the owner wants the whole series to read as one set, so every
   flag there gets it â€” STRIP ONLY. The footer keeps R51's five-flag rule, and
   the desktop strips are untouched.
   ============================================================================ */
.home-flags .home-flags__flag svg {
  box-shadow: 0 0 0 1px var(--c-btn-call);
  border-radius: 2px;
}

/* ============================================================================
   CSS-R56 (5) â€” PILL LABEL: ONE TREATMENT, AND CJK SITS COMFORTABLY
   ----------------------------------------------------------------------------
   MEASURED CAUSE of "fr is bold and looks like a different font": it is not a
   font at all. `.lang-cn.lang-more.is-current-inside .lang-cn__toggle` (R45 (1))
   set `--fw-bold`, and `is-current-inside` fires exactly when the reader's
   language lives in the panel â€” true on fr and zh, false on en and cn, whose
   flags are the two visible ones. Measured: en 600, fr 700, cn 600, zh 700,
   same family and size throughout. So the fix is to drop that weight, not to
   touch any font. The teal border stays as the quiet cue.

   CJK: 17px CJK glyphs are much taller than 17px Latin ones, and the cn chip
   nearly touched the pill's border. Keyed on the html lang attribute, which
   every tree already sets (zh-Hans / zh-Hant / ja / ko).
   ============================================================================ */
.lang-cn.lang-more.is-current-inside .lang-cn__toggle {
  font-weight: var(--fw-semi);        /* was --fw-bold â€” en is the reference */
}
html[lang^="zh"] .lang-more__label,
html[lang^="ja"] .lang-more__label,
html[lang^="ko"] .lang-more__label {
  font-size: 14px;
  line-height: 1.1;
}
/* CSS-POSTLAUNCH-01 (1) â€” THE SCOPE WAS THE BUG, not the padding.
   This rule dropped the CJK pill label to 12px and was scoped to `.header-flags`, which
   only exists on the FORM family. Every other page renders the same pill inside
   `.permit-bar__flags`, so the label stayed at 14px there â€” and that is the surface the
   owner photographed. Measured on /jp/index.php before this line: the glyph ink sat
   1.5px from the button's inner edge and 0.5px from the outer border, i.e. touching it.
   At 12px it clears by 4px.

   The button box is NOT the fault and is not touched: `align-items: center` on a fixed
   20px pill already centres the font box exactly â€” measured centring error 0.00px on all
   of en/jp/cn/ko/ru/ms, and a line-height override changes it by nothing. What overflowed
   was the glyphs, because a 14px CJK em box does not fit 18px of content.

   Both strip surfaces named, so the pill reads the same everywhere it renders. */
html[lang^="zh"] .header-flags .lang-more__label,
html[lang^="ja"] .header-flags .lang-more__label,
html[lang^="ko"] .header-flags .lang-more__label,
html[lang^="zh"] .permit-bar__flags .lang-more__label,
html[lang^="ja"] .permit-bar__flags .lang-more__label,
html[lang^="ko"] .permit-bar__flags .lang-more__label {
  font-size: 12px;                    /* the form pill is already 14px Latin */
}

/* ============================================================================
   CSS-R56 (7) â€” THE cn/zh HEADER TAGLINE
   ----------------------------------------------------------------------------
   `.intro-line` on the two Chinese homepages reads è©©å·´ä¸¹Â·é¦¬å¸ƒÂ·å¡å¸•èŠ with two of
   the three words as links, underlined by the global link rule. Style only â€”
   the hrefs are CN/ZH's to change. Scoped by the html lang attribute so it can
   reach neither the other eight trees nor any other `.intro-line` on the site.
   ============================================================================ */
html[lang^="zh"] .intro-line a {
  text-decoration: none;
  font-weight: var(--fw-bold);
  font-size: 1.08em;
}
html[lang^="zh"] .intro-line a:hover,
html[lang^="zh"] .intro-line a:focus-visible { text-decoration: underline; }
html[lang^="zh"] .intro-line { font-weight: var(--fw-semi); }


/* CSS-R57 â‘¶ â€” INTERIM GUARD while cn/zh converge on the EN skeleton.
   Their taglines currently carry a legacy inline run PLUS five EMPTY R56 spans. An
   empty `.intro-line__dot` still contributes `margin: 0 0.45em` and R16's zero-width
   space, so the phantom separators widen the line for no glyph. Hiding EMPTY ones costs
   nothing on EN (whose dots all carry a middot) and stops the interim shape from
   reading as broken. It retires itself the moment CN and ZH fill the skeleton. */
.intro-line__dot:empty,
.intro-line__place:empty { display: none; }


/* ============================================================================
   CSS-R60 â‘µ â€” THE HOME-BUTTON WEIGHT LAW, FIXED AT THE CAUSE
   ----------------------------------------------------------------------------
   Owner law: first (Reservations) and last (Contact) BOLD, every middle button
   NOT bold, all ten trees.

   MEASURED, and the CSS was already structurally right: computed weights read
   700 Â· 600 Ã—5 Â· 700 on every one of the nine trees that render the strip. The
   defect is not the rule, it is the NUMBER â€” 600 is a semi-bold, and whether it
   reads as "not bold" depends entirely on the font that ends up painting it:

     * LATIN (Source Sans Pro, self-hosted, real 400/600/700): 400 Â· 600 Â· 700
       measure 113.9 Â· 116.9 Â· 121px â€” 600 sits nearer bold than regular, which
       is why fr reads "uniformly semi-bold".
     * CJK â€” Source Sans Pro carries NO CJK glyphs, so cn/zh/jp/ko fall through
       to a SYSTEM font. Measured here, that fallback renders 400, 600 and 700 at
       IDENTICAL advance widths (cn 95/95/95, jp 152/152/152, ko 100.2 Ã—3): it
       exposes no weight axis at all, so 600 and 700 paint the same stroke and
       every button reads bold.

   So the middles move to a weight no font can round UP into bold. 400 is the one
   value every family ships, and it is the honest reading of "not bold".

   Where the fallback has only one weight, no CSS can create the distinction â€”
   that is a device font-coverage fact, not a rule. Requesting 400 is what makes
   the law render correctly on the devices that DO ship multi-weight CJK families
   (iOS PingFang, Android Noto Sans CJK), which is where the owner's screenshots
   come from. One rule, all ten trees, no per-script weight mapping needed.
   ============================================================================ */
.home-nav__btn .home-nav__label { font-weight: var(--fw-reg); }
.home-nav__btn--reservations .home-nav__label,
.home-nav__btn--contact .home-nav__label { font-weight: var(--fw-bold); }


/* ============================================================================
   CSS-R61 (1)(2) â€” EXPLICIT CJK STACKS, ONE PER SCRIPT, IDENTICAL FOR cn AND zh
   ----------------------------------------------------------------------------
   ROOT CAUSE of three non-reproducible cn/zh reports (R59 (4), R60 (3) and the
   tagline before them): with no CJK family named, the OS resolves one per
   `lang`. My probe machine resolves the SAME font for zh-Hans and zh-Hant, so
   both measured identical; the owner's Windows resolves Microsoft YaHei for
   zh-Hans and JhengHei for zh-Hant - two different typefaces, which is exactly
   the divergence reported and exactly what a computed-style probe cannot see.

   These override `--font-sans`, not `font-family`, because every component in
   this stylesheet asks for `var(--font-sans)` explicitly - an `html` level
   `font-family` would never reach them.

   ORDER, and why Source Sans Pro stays first: font-family resolves PER GLYPH,
   and Source Sans Pro carries no CJK, so Latin keeps the brand face while every
   CJK character falls to the first family that has it. Naming the CJK family
   first would hand Latin text to a system UI face on four trees for no gain.
   Every CJK family named here ships a real 400 and 700 on its platform, which is
   what makes the R60 bold law render rather than round.

   cn and zh are given the SAME string, deliberately: one stack, one typeface,
   whatever the OS would have chosen on its own.
   ============================================================================ */
html[lang^="zh"] {
  --font-sans: 'Source Sans Pro', 'Microsoft YaHei', 'PingFang SC', 'Noto Sans SC',
               'Hiragino Sans GB', -apple-system, 'Segoe UI', Arial, sans-serif;
}
html[lang^="ja"] {
  --font-sans: 'Source Sans Pro', 'Yu Gothic UI', 'Meiryo', 'Hiragino Kaku Gothic ProN',
               'Noto Sans JP', -apple-system, 'Segoe UI', Arial, sans-serif;
}
html[lang^="ko"] {
  --font-sans: 'Source Sans Pro', 'Malgun Gothic', 'Apple SD Gothic Neo', 'Noto Sans KR',
               -apple-system, 'Segoe UI', Arial, sans-serif;
}

/* ============================================================================
   CSS-R61 (3) â€” THE DRAWER LAW: FIRST AND LAST BOLD, MIDDLES AS ru RENDERS THEM
   ----------------------------------------------------------------------------
   ru is the ruled reference and it was MEASURED before being copied, at 414:
       every row  17px - letter-spacing 0.765px - uppercase
       first (Forma svyazi)  weight 700
       middles               weight 600
       last (Kontakty)       weight 600   <- the law is NOT met here today
   So only the LAST row changes: first is already 700 (the pinned contact-form
   CTA), the middles already carry ru's treatment, and `Contacts` was 600.
   ============================================================================ */
@media (max-width: 1023px) {
  #site-nav .site-nav__list > li:last-child > .site-nav__row > .site-nav__link {
    font-weight: var(--fw-bold);
  }
}


/* ============================================================================
   CSS-R63 â€” THE DRAWER WEIGHT LAW ON THE FOUR CJK TREES
   ----------------------------------------------------------------------------
   DIAGNOSED before it was fixed, at 414 with the drawer open. Computed weights
   were already correct everywhere - first 700, middles 600, last 700 - so the
   rules were never the defect. What differs is what PAINTS them:

     canvas advance for the same middle label, at 400 / 600 / 700
       cn  106.9 / 110.0 / 110.0     600 SNAPS to bold
       zh  106.9 / 110.0 / 110.0     600 SNAPS to bold
       jp  111.6 / 115.7 / 115.7     600 SNAPS to bold
       ko  116.2 / 119.2 / 119.2     600 SNAPS to bold
       ru   89.5 /  92.4 /  97.0     three distinct - no snap

   The R61 stacks name families that ship 400 and 700 and nothing between, so a
   requested 600 rounds UP to the bold face and every middle row paints bold.
   That is precisely the cn/zh report. It is the R60 button finding again, on a
   second surface - and the same remedy applies: ask for a weight that cannot
   round up.

   Middles therefore take 400 - the weight every one of these families truly has
   - and first/last are re-stated as 700 AT THE LINK on all four trees, so the
   law no longer depends on the first row's bold arriving via
   `.site-nav__link--cta .site-nav__label` (a different element, from R29's
   lineage). On jp/ko the owner sees first/last NOT bold where this machine
   computes 700 on both; stating 700 on the link is what makes the rule
   unambiguous wherever the bold face does resolve.

   SCOPE: the three pinned CJK stacks only. Not one Latin-tree selector - ru, it,
   en, fr and es are confirmed correct by the owner and are not touched.
   ============================================================================ */
@media (max-width: 1023px) {
  html[lang^="zh"] #site-nav .site-nav__list > li > .site-nav__row > .site-nav__link,
  html[lang^="ja"] #site-nav .site-nav__list > li > .site-nav__row > .site-nav__link,
  html[lang^="ko"] #site-nav .site-nav__list > li > .site-nav__row > .site-nav__link {
    font-weight: var(--fw-reg);
  }
  html[lang^="zh"] #site-nav .site-nav__list > li:first-child > .site-nav__row > .site-nav__link,
  html[lang^="zh"] #site-nav .site-nav__list > li:last-child  > .site-nav__row > .site-nav__link,
  html[lang^="ja"] #site-nav .site-nav__list > li:first-child > .site-nav__row > .site-nav__link,
  html[lang^="ja"] #site-nav .site-nav__list > li:last-child  > .site-nav__row > .site-nav__link,
  html[lang^="ko"] #site-nav .site-nav__list > li:first-child > .site-nav__row > .site-nav__link,
  html[lang^="ko"] #site-nav .site-nav__list > li:last-child  > .site-nav__row > .site-nav__link {
    font-weight: var(--fw-bold);
  }
  /* the first row's label carries its own 700 from R29; re-stated here so the
     first row cannot be left at 400 by the middles rule above on any tree */
  html[lang^="zh"] #site-nav .site-nav__list > li:first-child .site-nav__label,
  html[lang^="ja"] #site-nav .site-nav__list > li:first-child .site-nav__label,
  html[lang^="ko"] #site-nav .site-nav__list > li:first-child .site-nav__label,
  html[lang^="zh"] #site-nav .site-nav__list > li:last-child  .site-nav__label,
  html[lang^="ja"] #site-nav .site-nav__list > li:last-child  .site-nav__label,
  html[lang^="ko"] #site-nav .site-nav__list > li:last-child  .site-nav__label {
    font-weight: var(--fw-bold);
  }
}


/* ============================================================================
   CSS-R64 â€” CONTACT EMAIL BUTTONS: ICONLESS AT DESKTOP, ICON KEPT AT MOBILE
   ----------------------------------------------------------------------------
   Executes _REQUEST-CSS-contact-email-icon (FR/MAIN, CONTACT-POLISH-01), whole
   and unaltered. Owner ruling: the three email buttons are compact and iconless
   at desktop - which is why EN's own page fits them on one line - and keep the
   envelope at mobile. That is a VIEWPORT rule, not a markup rule, which is why
   FR's first cut (removing the icon from the emitted markup) fixed desktop and
   broke mobile.

   `cbtn--email-ico` is a dedicated hook the atom builder emits, added so this
   selector cannot reach the WhatsApp buttons or the bottom card - both keep
   their icons at every width. The class exists only on the six atom trees
   (it, es, de, jp, ko, ru); en, fr, cn and zh author their own contact blocks,
   carry no icon there at any width, and are untouched by construction.
   ============================================================================ */
@media (min-width: 1024px) {
  .contact-card__btns .cbtn--email-ico .icon { display: none; }
}


/* ============================================================================
   CSS-R66 - hero-alipay BADGE v2 + DE-CLICK   (supersedes R65's geometry,
   owner reviewed live; R65's mechanism finding stands and is restated below)
   ----------------------------------------------------------------------------
   WHY ABSOLUTE, STILL (R65's finding, unchanged): cn emits the badge BEFORE the
   <h1> and zh emits it AFTER it, so every flow mechanism reads source order and
   would land it in two different places. Out of flow, both trees render alike
   and no hero child moves.

   (1) 40x40, and the size carries !important: main.css:2130 and :2621 both set
   `.content-card img { width: ... !important }` under the mobile query, and the
   badge is now VISIBLE there (R66 (2) removed R65's <1024 hide). Without
   !important the badge stretched to the column width - measured 324x324 at
   390px in R65's own after-run. The CSS does not read the markup's width/height
   attributes at all, so CN/ZH re-stamping them 55 -> 40 changes nothing here.

   (3) POSITION - the overlap fix. The badge used to sit on the title's first
   line, which is why it covered a word on the long cn/zh Mataking title. It now
   rides the row ABOVE the title:

     * hero WITH an .eyebrow (110 of the 112 badges): vertically centred on the
       eyebrow's row, right-aligned to the hero's right edge. The eyebrow row is
       29.75px at 390 / 768 / 1024 / 1440 / 1920 - measured, not assumed - and a
       40px badge is 10.25px taller than it, so exact centring would put the
       badge's bottom edge 0.125px into the title's ascent box at >=768. The
       -3px lifts it clear: badge bottom sits 2.875px above the title's topmost
       text rect at 768-1920 and 4.875px above it at 390. Everything above the
       hero is the content-card's own padding, which is where the badge's 8.125px
       of upward spill lands.

     * hero with NO eyebrow: the badge takes its own band above the title, and
       the hero reserves that band with padding-top so nothing is overlapped.
       (No page ships this shape today - censused: 110 of 110 heroes carry an
       eyebrow - it is here so an eyebrow-less hero cannot land on the title.)

     * the thankyou .confirm-card (the 2 remaining badges, and the real
       eyebrow-less case): top-right of the card's CONTENT box, inside the 72px
       .confirm-mark band that already sits above the title. The badge is 40px,
       the band is 72px and the mark is centred, so the badge needs no reserved
       space and the title does not move. Offsets are the card's own padding
       tokens, `var(--sp-6)` / `var(--gutter)`, so they track every breakpoint -
       an absolutely positioned box resolves against the PADDING box, and a flat
       `right: 0` there would hang the badge outside the text column.

   (4) NO CLICK, NO POPUP. The binder is js/site.js, module "2. Lightbox
   (site-wide: galleries + any content image)" - CSS-R36 (1) widened it to
   `LB_IMG = '.content-card img'` with four structural exclusions in LB_SKIP,
   and both of these images are in-article <img>s inside .content-card, so it
   caught them. Fixed in BOTH layers, because they do different jobs:
     - js/site.js: `.hero-alipay` and `[src*="QR-code-ADH"]` added to LB_SKIP.
       This is the load-bearing half - it also removes them from collect(), so
       the prev/next arrows no longer walk THROUGH them. pointer-events alone
       would have left them in the carousel sequence.
     - here: pointer-events:none, which kills the click affordance itself (and
       with it the zoom-in cursor) for any other handler.
   The QR has no shared class hook - de/it carry `img--narrow`, the rest carry
   none - so `img[src*="QR-code-ADH"]` is the only cross-tree-stable selector,
   and it is exact: it matches the displayed <img> and NOT the
   `<a class="cbtn cbtn--wechat" href="/images/header/QR-code-ADH.webp">` button,
   whose href holds the same string. That anchor keeps working; the displayed QR
   sits in a bare <figure> on all 10 trees, wrapped in no link, so nothing is
   made unreachable.
   ============================================================================ */
:root {
  --alipay-size: 40px;
  --alipay-eyebrow-row: 29.75px;   /* .eyebrow's line box - measured, constant at every width */
}

.page-hero:has(> .hero-alipay),
.confirm-card:has(> .hero-alipay) { position: relative; }

img.hero-alipay {
  position: absolute;
  display: block;
  width:  var(--alipay-size) !important;
  height: var(--alipay-size) !important;
  margin: 0;
  pointer-events: none;
}

/* (3a) hero WITH an eyebrow - the badge rides the eyebrow's row */
.page-hero:has(> .eyebrow) > .hero-alipay {
  top: calc((var(--alipay-eyebrow-row) - var(--alipay-size)) / 2 - 3px);
  right: 0;
}

/* (3b) hero with NO eyebrow - its own reserved band above the title */
.page-hero:has(> .hero-alipay):not(:has(> .eyebrow)) {
  padding-top: calc(var(--alipay-size) + var(--sp-2));
}
.page-hero:not(:has(> .eyebrow)) > .hero-alipay { top: 0; right: 0; }

/* (3c) the thankyou confirm card - top-right of the content box.
   The right offset MIRRORS THE CARD'S OWN PADDING RULE at the same breakpoint:
   main.css:457 sets `padding: var(--sp-6) var(--gutter) var(--sp-7)` and
   main.css:465 overrides it to `var(--sp-6) var(--sp-7) var(--sp-7)` at >=1024.
   Anchoring on `--gutter` alone put the badge 15px PAST the content edge at
   1024 and 1440 - measured - because that is exactly `--sp-7` minus `--gutter`
   once the clamp has topped out. Two rules, one per breakpoint, same tokens as
   the padding they have to match. */
.confirm-card > .hero-alipay { top: var(--sp-6); right: var(--gutter); }
@media (min-width: 1024px) {
  .confirm-card > .hero-alipay { right: var(--sp-7); }
}

/* (4) the two non-article images are inert */
img[src*="QR-code-ADH"] { pointer-events: none; }

/* R-FOOTER-PHONE-LINE-01 (owner-approved 2026-10-09; FR/MAIN under the owner's one-change exception
   to the style freeze). The call line under the licence line. Font, size and colour come from
   .site-footer__reg, which the element also carries, so the line reads as footer text and not as a
   banner, and still looks right against a cached copy of this file that predates these rules.
   Added here: a closer gap to the licence line, and tap room. Each number is its own inline-block
   that never breaks inside, so on a phone the line wraps BETWEEN numbers; 10px above and below a
   16px line gives a 44px target. */
.site-footer__call { margin-top: var(--sp-2); }
.site-footer__call a { display: inline-block; padding: 10px 4px; white-space: nowrap; text-decoration: none; }
.site-footer__call a:hover,
.site-footer__call a:focus-visible { text-decoration: underline; }

/* R-HOME-UA-SNIFF-RETIRE-01 (owner style-freeze exception, 3 Nov 2026). The home pages are ONE responsive
   DOM: no User-Agent is read anywhere. <=1023 px = the mobile layout (button grid + flags, the island disks,
   the intro line at the foot); >=1024 px = the desktop layout (carousel, disks, intro line on top). The
   button grid and flags are already hidden at >=1024 px (rule above, "belt and braces"). Slide 1 sits in a
   <picture> whose <=1023 px source is a 1x1 inline GIF, so a phone downloads no banner image. */
.hero-fade__slide picture { display: block; width: 100%; height: 100%; }
@media (max-width: 1023px) {
  .hero-fade, .intro-line--top { display: none; }
}
@media (min-width: 1024px) {
  .intro-line--bottom { display: none; }
}
