/* ============================================================================
   MEDAL — Xiamen Medal International Trade Co.,Ltd
   Global stylesheet (single source of truth for all 12 pages)
   ----------------------------------------------------------------------------
   Contents
   0. Web fonts (self-hosted Inter / Manrope)
   1. Design tokens (CSS variables)
   2. Reset & base
   3. Typography & helpers
   4. Layout primitives (container / section / grids)
   5. Buttons
   6. Header & navigation
   7. Mobile drawer
   8. Language switcher
   9. Hero (home) & page-hero (inner pages)
  10. Home sections (stats / advantages / products / industries / certs / process)
  11. Cards
  12. Media frames (image placeholders)
  13. Breadcrumb
  14. Spec table
  15. FAQ (details/summary)
  16. Contact page
  17. CTA banner
  18. Footer
  19. Back to top
  20. Placeholder values
  21. Animations & reveal
  22. Responsive (1024 / 768 / 480)
  23. Accessibility & print
   ============================================================================ */

/* ===== 0. Web fonts (self-hosted Inter / Manrope) ===== */
/* Inter (wght 400..800) and Manrope (wght 500..800), downloaded as VARIABLE woff2 from Google Fonts'
   own CDN and served from this origin. Why self-hosted rather than the <link> to fonts.googleapis.com
   that used to sit in all 12 pages' <head>:
     - one less third-party origin: no preconnect pair, no cross-origin round trip, no CDN outage;
     - the site (and every measurement harness) now works with the network completely closed;
     - and the reason this was asked for: the guide TOC's chips WRAP, so its row count — and therefore
       the bar height that main.js measures and the --toc-h fallback constant — is font-dependent. A
       font that arrives from a CDN, with `display: swap`, cannot pin that constant to a real value.
       Every earlier measurement in this repo was taken with the Google Fonts request aborted, i.e. in
       Segoe UI; from here on the font that is measured IS the font that ships.
   ONLY the `latin` and `latin-ext` subsets are downloaded — 4 files, 169.2 KB in total. `latin` spans
   U+0000-00FF, which covers every Spanish accented character (a-acute e-acute i-acute o-acute
   u-acute n-tilde u-diaeresis inverted-question inverted-exclamation) and every English one;
   `latin-ext` adds the rest of Latin Extended-A/B. The other 9 subsets Google serves (cyrillic,
   cyrillic-ext, greek, greek-ext, vietnamese) are deliberately NOT included: this site has no such
   content, and an unused subset is pure weight. CJK is unaffected — Inter/Manrope carry no CJK glyphs,
   so Chinese keeps resolving to the system stack in --font-sans below, exactly as it did before.
   `font-weight: 400 800` is the variable-axis range; `font-display: swap` keeps text visible while the
   (local, therefore short) font load happens. `unicode-range` is kept verbatim from the source CSS, so
   a page never fetches a subset it has no character for. Paths are relative to THIS file. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('../fonts/inter/inter-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: 'Inter';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('../fonts/inter/inter-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;
}
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 500 800;
  font-display: swap;
  src: url('../fonts/manrope/manrope-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: 'Manrope';
  font-style: normal;
  font-weight: 500 800;
  font-display: swap;
  src: url('../fonts/manrope/manrope-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;
}

/* ===== 1. Design tokens ===== */
:root {
  /* Brand colours (extracted from logo) */
  --brand-primary: #1B23C0;
  --brand-primary-deep: #141C9E;
  --brand-light: #3B8FF0;
  --brand-accent: #E8342B;
  --brand-gradient: linear-gradient(135deg, #3B8FF0 0%, #1B23C0 100%);
  --brand-gradient-soft: linear-gradient(135deg, rgba(59, 143, 240, .12) 0%, rgba(27, 35, 192, .12) 100%);

  /* Neutrals */
  --white: #FFFFFF;
  --bg-soft: #F5F7FA;
  --bg-navy: #0B1046;
  --bg-navy-2: #101551;
  --border: #E5E7EB;
  --text: #374151;
  --text-strong: #111827;
  --text-soft: #6B7280;
  --text-on-dark: rgba(255, 255, 255, .86);
  --text-on-dark-soft: rgba(255, 255, 255, .66);

  /* Typography */
  --font-sans: 'Inter', 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI',
    'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Roboto, Helvetica, Arial, sans-serif;

  /* Elevation */
  --shadow-xs: 0 1px 2px rgba(15, 23, 42, .06);
  --shadow-sm: 0 2px 10px rgba(15, 23, 42, .06);
  --shadow-md: 0 12px 30px rgba(15, 23, 42, .10);
  --shadow-lg: 0 24px 56px rgba(15, 23, 42, .16);

  /* Radii */
  --radius-sm: 8px;
  --radius-md: 14px;
  --radius-lg: 22px;
  --radius-pill: 999px;

  /* Layout */
  --container: 1200px;
  --gutter: clamp(1.1rem, 4vw, 2rem);
  --header-h: 74px;
  /* --- Scroll-into-view geometry for guide.html's sticky table of contents ---
     --anchor-gap: the air we want between the bottom of the sticky stack and the heading a reader
       just asked to jump to.
    --toc-h: the sticky .toc bar's height. MEASURED, never guessed, and it cannot be expressed in
      CSS: it changes whenever the chips wrap. It must be an UPPER BOUND for its band, never an exact
      value: concealment (the heading landing under the bar) is the bug, overshoot is only slack.
      The authoritative numbers come from a full sweep of every integer width 320..1440 x 3 languages
      with the shipped self-hosted font loaded (_geom_pass/t1_sweep.json), in which the chip metrics
      change exactly once — the row-gap goes 4.48px -> 9.6px at 827 — so there are exactly two
      metric-homogeneous bands, plus the inherited <=552 split:
        >=827         1 row 67.9531 / 2 rows 117.3125  -> max needed 2 rows (en 827..840, es 827..912)
                      => 120px (2.69px slack). 913..1440 are one row in all three languages, so this
                      band deliberately overshoots by up to 52.05px. It CANNOT be made exact: at 827
                      the bar's content box is 763px and es needs ~849px, so no padding tweak fits es
                      on one row, and a boundary at es's 912/913 flip would be a wrap point.
        553..826      1 row 56.1406 / 2 rows  88.5625  -> max needed 2 rows (en 553..563, es 553..623)
                      => 91px (2.44px slack)
        <=552         1 row 56.1406 / 2 rows  88.5625 / 3 rows 120.9844
                      -> max needed 3 rows (en 320..350, es 320..384) => 123px (2.02px slack)
      This is the fix for F1's remaining hole: the 2026-09-16 copy pass lengthened guide.toc.barrier
      and guide.toc.select, which moved Spanish's two-row window from 552..~560 out to 553..623 and
      pushed en and es to THREE rows below 351/385. The old constants (68/56/89) therefore concealed
      the heading by up to 49.31px at >=827 and by up to 32.56px in 553..623.
      It serves the no-JS / native-fragment path ONLY; main.js initGuideToc() measures the real height
      at click time and is the authoritative path. initGuideToc() also READS --anchor-gap, so the two
      must agree.
     --scroll-pad-extra: how far html's scroll-padding-top (below) exceeds --header-h. Named so the
       two cannot drift apart. */
  --anchor-gap: 20px;
  --toc-h: 120px;
  --scroll-pad-extra: 16px;
  /* F1 (layout-fix): .section pads BOTH sides with this token, so two adjacent sections used to
     stack 2 x 88px = 176px of empty band at 1440 (107.5px @768, 104px @390) — the single largest
     contributor to page-level whitespace site-wide. Lowering the token to clamp(3rem, 5vw, 3.75rem)
     gives a 120px seam at 1440 and <=96px below 960px. Only consumed by `.section` (verified). */
  --section-y: clamp(3rem, 5vw, 3.75rem);
  --float-bottom: clamp(1.5rem, 5vw, 3.25rem);

  /* Motion */
  --ease: cubic-bezier(.22, .61, .36, 1);
  --dur-fast: .16s;
  --dur: .3s;
  --dur-slow: .6s;
}

/* ===== 2. Reset & base ===== */
*,
*::before,
*::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + var(--scroll-pad-extra));
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.7;
  color: var(--text);
  background: var(--white);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

body.has-drawer-open { overflow: hidden; }

img { max-width: 100%; display: block; }

a { color: var(--brand-primary); text-decoration: none; transition: color var(--dur-fast) var(--ease); }
a:hover { color: var(--brand-light); }

button { font-family: inherit; cursor: pointer; }

ul, ol { margin: 0; padding: 0; list-style: none; }

svg { display: block; }

/* [hidden] must win over component display rules such as .product-card { display: flex } */
[hidden] { display: none !important; }

:focus-visible {
  outline: 3px solid var(--brand-light);
  outline-offset: 2px;
  border-radius: 4px;
}

/* ===== 3. Typography & helpers ===== */
h1, h2, h3, h4, h5 {
  margin: 0 0 .6em;
  color: var(--text-strong);
  line-height: 1.2;
  font-weight: 800;
  letter-spacing: -.015em;
}

h1 { font-size: clamp(2rem, 4.4vw, 3.25rem); }
h2 { font-size: clamp(1.6rem, 3vw, 2.35rem); }
h3 { font-size: clamp(1.2rem, 2vw, 1.5rem); }
h4 { font-size: 1.05rem; }

p { margin: 0 0 1rem; }
p:last-child { margin-bottom: 0; }

.text-soft { color: var(--text-soft); }
.text-center { text-align: center; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

.eyebrow {
  display: inline-flex; align-items: center; gap: .5rem;
  font-size: .8rem; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
  color: var(--brand-primary);
  background: var(--brand-gradient-soft);
  padding: .35rem .85rem; border-radius: var(--radius-pill);
  margin-bottom: 1rem;
}
.eyebrow::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--brand-accent); }
.eyebrow.is-light { color: #fff; background: rgba(255, 255, 255, .14); }

/* Icon helper */
.icon { width: 24px; height: 24px; stroke-width: 1.8; flex: 0 0 auto; }

/* ===== 4. Layout primitives ===== */
.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section { padding-block: var(--section-y); }
.section--soft { background: var(--bg-soft); }
.section--navy { background: var(--bg-navy); color: var(--text-on-dark); }
.section--navy h2, .section--navy h3, .section--navy h4 { color: #fff; }

.section-head { max-width: 720px; margin: 0 auto 2.75rem; text-align: center; }
.section-head p { color: var(--text-soft); font-size: 1.05rem; }
.section-head.is-left { margin-inline: 0; text-align: left; }
.section--navy .section-head p { color: var(--text-on-dark-soft); }

.grid { display: grid; gap: 1.5rem; }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
.grid-auto { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }

/* F7 (layout-fix) — orphan-row centring modifier. A plain 3-col grid leaves 2/3 of the last row
   empty whenever the item count is not a multiple of 3 (filtered products, the 7 showcase cards,
   the 5 guide cards). `is-centered` swaps the grid for a wrapping flex line whose items are
   EXACTLY the grid track width: calc((100% - 3rem) / 3) reproduces 3x1fr tracks including the two
   1.5rem gaps, so a full page is pixel-identical (same widths, same left edges), while a partial
   last row is centred instead of hugging the left edge. `min-width: 0` mimics minmax(0,1fr);
   align-items keeps the flex default `stretch`, matching the grid's equal-height rows. Hidden
   cards ([hidden] -> display:none) simply don't participate in the flex line. */
.grid-3.is-centered { display: flex; flex-wrap: wrap; justify-content: center; align-items: stretch; }
/* F35: the cards carry `height: 100%`, which blocks flex `stretch` (it only applies to `auto`),
   so shorter texts left ragged card bottoms within one row. `height: auto` re-enables stretch —
   every card in a line matches its tallest sibling and `.card-link` sinks via margin-top:auto. */
.grid-3.is-centered > * { flex: 0 1 calc((100% - 3rem) / 3); min-width: 0; height: auto; }

/* ===== 5. Buttons ===== */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  padding: .8rem 1.5rem; border-radius: var(--radius-pill);
  font-weight: 700; font-size: .95rem; line-height: 1; border: 2px solid transparent;
  transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur) var(--ease),
    background var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease);
  white-space: nowrap;
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }

.btn-primary { background: var(--brand-primary); color: #fff; box-shadow: var(--shadow-sm); }
.btn-primary:hover { background: var(--brand-primary-deep); color: #fff; box-shadow: var(--shadow-md); }

.btn-gradient { background: var(--brand-gradient); color: #fff; box-shadow: var(--shadow-sm); }
.btn-gradient:hover { color: #fff; box-shadow: var(--shadow-md); }

.btn-outline { background: transparent; color: var(--brand-primary); border-color: var(--brand-primary); }
.btn-outline:hover { background: var(--brand-primary); color: #fff; }

.btn-ghost-light { background: rgba(255, 255, 255, .12); color: #fff; border-color: rgba(255, 255, 255, .5); }
.btn-ghost-light:hover { background: #fff; color: var(--brand-primary); border-color: #fff; }

.btn-light { background: #fff; color: var(--brand-primary); }
.btn-light:hover { color: var(--brand-primary-deep); box-shadow: var(--shadow-md); }

.btn-sm { padding: .55rem 1.05rem; font-size: .85rem; }

.btn-row { display: flex; flex-wrap: wrap; gap: .9rem; }
.btn-row.is-center { justify-content: center; }

/* ===== 6. Header & navigation ===== */
.site-header {
  position: fixed; inset: 0 0 auto 0; z-index: 100;
  height: var(--header-h);
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.site-header.is-scrolled {
  background: rgba(255, 255, 255, .94);
  -webkit-backdrop-filter: saturate(180%) blur(12px);
  backdrop-filter: saturate(180%) blur(12px);
  box-shadow: 0 6px 24px rgba(15, 23, 42, .08);
  border-bottom-color: var(--border);
}

.header-inner { height: 100%; display: flex; align-items: center; gap: 1.25rem; }

.brand { display: inline-flex; align-items: center; gap: .65rem; flex: 0 0 auto; }
/* Logo 是透明底 PNG：深色导航栏上直接用白色版，滚动变浅底后换成蓝色版。
   两个版本尺寸完全一致，切换时不会跳动，也不再需要白色底板。 */
.brand-logo-box { display: inline-flex; align-items: center; }
.brand-logo-box img { display: block; height: 42px; width: auto; }
.brand-logo-box .logo-on-light { display: none; }
.site-header.is-scrolled .brand-logo-box .logo-on-dark { display: none; }
.site-header.is-scrolled .brand-logo-box .logo-on-light { display: block; }

.main-nav { display: flex; align-items: center; gap: .35rem; margin-left: auto; }
.nav-link {
  position: relative; padding: .5rem .85rem; border-radius: var(--radius-pill);
  font-weight: 600; font-size: .95rem; color: rgba(255, 255, 255, .9);
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.nav-link::after {
  content: ""; position: absolute; left: 50%; bottom: .28rem; width: 0; height: 2px;
  background: var(--brand-accent); border-radius: 2px; transform: translateX(-50%);
  transition: width var(--dur) var(--ease);
}
.nav-link:hover { color: #fff; }
.nav-link:hover::after, .nav-link.is-active::after { width: 22px; }
.nav-link.is-active { color: #fff; }
.site-header.is-scrolled .nav-link { color: var(--text); }
.site-header.is-scrolled .nav-link:hover,
.site-header.is-scrolled .nav-link.is-active { color: var(--brand-primary); }

.header-actions { display: flex; align-items: center; gap: .75rem; margin-left: .35rem; }

.menu-toggle {
  display: none; flex-direction: column; justify-content: center; gap: 5px;
  width: 44px; height: 44px; border: none; border-radius: 12px;
  background: rgba(255, 255, 255, .12); align-items: center;
}
.menu-toggle span { width: 20px; height: 2px; background: #fff; border-radius: 2px; transition: var(--dur) var(--ease); }
.site-header.is-scrolled .menu-toggle { background: var(--bg-soft); }
.site-header.is-scrolled .menu-toggle span { background: var(--text-strong); }

/* ===== 7. Mobile drawer ===== */
.drawer-overlay {
  position: fixed; inset: 0; z-index: 199;
  background: rgba(11, 16, 70, .5);
  opacity: 0; visibility: hidden; transition: opacity var(--dur) var(--ease), visibility var(--dur);
}
.drawer-overlay.is-open { opacity: 1; visibility: visible; }

.mobile-drawer {
  position: fixed; inset: 0 0 0 auto; z-index: 200;
  width: min(340px, 86vw); background: #fff;
  display: flex; flex-direction: column; gap: 1rem;
  padding: 1.35rem; overflow-y: auto;
  transform: translateX(102%); visibility: hidden;
  transition: transform var(--dur) var(--ease), visibility var(--dur) var(--ease);
  box-shadow: var(--shadow-lg);
}
.mobile-drawer.is-open { transform: none; visibility: visible; }

.drawer-head { display: flex; align-items: center; justify-content: space-between; }
.drawer-title { font-weight: 800; font-size: 1.1rem; color: var(--text-strong); }
.drawer-close {
  width: 40px; height: 40px; border: 1px solid var(--border); border-radius: 12px;
  background: #fff; font-size: 1.5rem; line-height: 1; color: var(--text-strong);
}
.drawer-nav { display: flex; flex-direction: column; }
.drawer-nav a {
  padding: .9rem .25rem; border-bottom: 1px solid var(--border);
  font-weight: 600; font-size: 1.05rem; color: var(--text-strong);
}
.drawer-nav a.is-active { color: var(--brand-primary); }
.drawer-cta { margin-top: auto; width: 100%; }

/* ===== 8. Language switcher (single dropdown) ===== */
.lang-switcher { position: relative; display: inline-flex; }

.lang-trigger {
  display: inline-flex; align-items: center; gap: .45rem;
  padding: .42rem .58rem .42rem .55rem;
  border-radius: var(--radius-pill);
  border: 1px solid rgba(255, 255, 255, .3);
  background: rgba(255, 255, 255, .08);
  color: rgba(255, 255, 255, .95);
  font: inherit; font-size: .82rem; font-weight: 700; line-height: 1;
  cursor: pointer; white-space: nowrap;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.lang-trigger:hover { background: rgba(255, 255, 255, .16); }
.lang-trigger[aria-expanded="true"] { background: rgba(255, 255, 255, .2); border-color: rgba(255, 255, 255, .55); color: #fff; }
.lang-trigger .flag { width: 20px; height: 14px; border-radius: 3px; overflow: hidden; box-shadow: 0 0 0 1px rgba(0, 0, 0, .15); display: block; flex: 0 0 auto; }
.lang-trigger .chev { width: 12px; height: 12px; flex: 0 0 auto; transition: transform var(--dur) var(--ease); }
.lang-trigger[aria-expanded="true"] .chev { transform: rotate(180deg); }

.site-header.is-scrolled .lang-trigger { color: var(--text-strong); border-color: var(--border); background: #fff; }
.site-header.is-scrolled .lang-trigger:hover { background: var(--bg-soft); }
.site-header.is-scrolled .lang-trigger[aria-expanded="true"] { border-color: var(--brand-light); background: var(--brand-gradient-soft); color: var(--brand-primary); }

.lang-menu {
  position: absolute; top: calc(100% + .5rem); right: 0; z-index: 60;
  min-width: 10.5rem; padding: .3rem;
  display: grid; gap: .1rem;
  background: #fff; border: 1px solid var(--border); border-radius: 14px;
  box-shadow: 0 16px 40px rgba(15, 23, 42, .18);
  opacity: 0; visibility: hidden; transform: translateY(-6px);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease), visibility var(--dur) var(--ease);
}
.lang-menu.is-open { opacity: 1; visibility: visible; transform: translateY(0); }

.lang-option {
  display: flex; align-items: center; gap: .6rem; width: 100%;
  padding: .55rem .6rem; border: 0; border-radius: 10px;
  background: transparent; color: var(--text-strong);
  font: inherit; font-size: .88rem; font-weight: 600; text-align: left;
  cursor: pointer; white-space: nowrap;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.lang-option .flag { width: 22px; height: 15px; border-radius: 3px; overflow: hidden; box-shadow: 0 0 0 1px rgba(0, 0, 0, .12); display: block; flex: 0 0 auto; }
.lang-option .lang-native { flex: 1 1 auto; }
.lang-option .tick { width: 15px; height: 15px; flex: 0 0 auto; color: var(--brand-primary); opacity: 0; }
.lang-option:hover { background: var(--bg-soft); }
.lang-option.is-active { color: var(--brand-primary); background: var(--brand-gradient-soft); }
.lang-option.is-active .tick { opacity: 1; }

/* 移动端抽屉：下拉改为原地展开，避免被抽屉裁剪 */
.drawer-lang {
  position: relative; display: block;
  padding-bottom: .9rem; border-bottom: 1px solid var(--border);
}
.drawer-lang .lang-trigger {
  width: 100%; justify-content: flex-start;
  padding: .7rem .85rem; border-radius: 12px; font-size: .95rem;
  color: var(--text-strong); border-color: var(--border); background: #fff;
}
.drawer-lang .lang-trigger:hover { background: var(--bg-soft); }
.drawer-lang .lang-trigger .chev { margin-left: auto; }
.drawer-lang .lang-menu {
  position: static; min-width: 0; margin-top: .5rem; width: 100%;
  display: none; border-radius: 12px; box-shadow: none; transform: none;
}
.drawer-lang .lang-menu.is-open { display: grid; transform: none; }

/* ===== 9. Hero & page-hero ===== */
.hero {
  position: relative; overflow: hidden;
  padding-top: calc(var(--header-h) + clamp(2.5rem, 6vw, 4.5rem));
  padding-bottom: clamp(3rem, 7vw, 5.5rem);
  background: var(--bg-navy);
  color: var(--text-on-dark);
}
.hero::before {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(circle at 78% 12%, rgba(59, 143, 240, .45), transparent 46%),
    radial-gradient(circle at 12% 88%, rgba(27, 35, 192, .65), transparent 52%),
    linear-gradient(150deg, #0B1046 0%, #141C9E 100%);
}
.hero::after {
  content: ""; position: absolute; left: -12%; right: -12%; bottom: -42%;
  height: 62%; border-radius: 50%;
  background: var(--brand-gradient); opacity: .16; filter: blur(30px);
}
.hero .container { position: relative; z-index: 2; }
.hero-grid { display: grid; grid-template-columns: 1.05fr .95fr; gap: clamp(1.5rem, 4vw, 3.5rem); align-items: center; }
.hero-copy { max-width: 620px; }
.hero h1 { color: #fff; margin-bottom: 1.1rem; }
.hero-sub { font-size: clamp(1rem, 1.5vw, 1.15rem); color: var(--text-on-dark); margin-bottom: 1.75rem; max-width: 34rem; }
.hero-tags { display: flex; flex-wrap: wrap; gap: .55rem; margin-bottom: 1.9rem; }
.hero-tag {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .4rem .85rem; border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, .1); border: 1px solid rgba(255, 255, 255, .18);
  font-size: .85rem; font-weight: 600; color: #fff;
}
.hero-tag svg { width: 15px; height: 15px; }
.hero-visual { position: relative; }
.hero-visual .media-frame { aspect-ratio: 4 / 3; box-shadow: var(--shadow-lg); border-radius: var(--radius-lg); }
.hero-badge {
  position: absolute; right: -0.75rem; bottom: -0.9rem; z-index: 3;
  display: flex; align-items: center; gap: .6rem;
  background: #fff; color: var(--text-strong);
  padding: .7rem 1rem; border-radius: var(--radius-md); box-shadow: var(--shadow-md);
  font-weight: 700; font-size: .85rem;
}
.hero-badge svg { width: 26px; height: 26px; color: var(--brand-primary); }

.page-hero {
  position: relative; overflow: hidden;
  padding-top: calc(var(--header-h) + clamp(2.25rem, 5vw, 3.5rem));
  padding-bottom: clamp(2.25rem, 5vw, 3.5rem);
  background: var(--bg-navy); color: var(--text-on-dark); text-align: center;
}
.page-hero::before {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(circle at 82% 0%, rgba(59, 143, 240, .4), transparent 48%),
    linear-gradient(150deg, #0B1046 0%, #141C9E 100%);
}
.page-hero .container { position: relative; z-index: 2; }
.page-hero h1 { color: #fff; margin-bottom: .65rem; }
.page-hero p { color: var(--text-on-dark-soft); max-width: 44rem; margin-inline: auto; }

/* Compact inner hero with breadcrumb + optional visual */
.detail-hero { text-align: left; }
.detail-hero .container { display: grid; grid-template-columns: 1.1fr .9fr; gap: clamp(1.5rem, 4vw, 3rem); align-items: center; }
.detail-hero .page-hero-copy { max-width: 620px; }
/* The second item the audit opened was `.detail-hero h1` at zh 62.39 / en 62.39 / es 124.78 (1440) —
   again an element-HEIGHT delta (+62.4px = exactly one line-height of 62.4px), NOT horizontal
   overflow: measured with the shipped font, es "Bolsa Doypack con Boquilla" needs 686.69px of
   single-line advance inside a 598.39px column (natOver +88.3px), so it wraps at a space and nothing
   overflows its box (`scrollWidth - clientWidth` = 0). It is 2 lines for every width 1030..1440 and
   one line below 1030; en needs 564.25px and fits with 34.14px to spare.
   `overflow-wrap: break-word` is added as hardening only — it guarantees no FUTURE copy can push a
   single unbreakable token past the column, which is the failure mode being guarded against. It does
   not change today's line count, and neither does `hyphens: auto` (both measured: es stays 2 lines /
   124.78px, en stays 1 line / 62.39px — the phrase is too long for the column, so no intra-word break
   can help). Making es one line at >=1030px would need the shared h1 type scale to drop ~13% (52px ->
   ~45px, affecting every page) or the copy column to widen ~15% (shrinking the hero image). Both are
   design decisions, not geometry bugs, so they are reported rather than taken. */
.detail-hero h1 { margin-bottom: .75rem; overflow-wrap: break-word; }
.detail-hero .hero-sub { color: var(--text-on-dark); }
.detail-hero .media-frame { aspect-ratio: 1 / 1; box-shadow: var(--shadow-lg); border-radius: var(--radius-lg); }
.detail-hero .media-frame img { object-fit: contain; }

/* ===== 10. Home sections ===== */
.stats {
  background: var(--bg-navy-2);
  border-top: 1px solid rgba(255, 255, 255, .08);
  padding-block: clamp(2.5rem, 5vw, 3.75rem);
}
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: clamp(1.5rem, 3vw, 2.5rem); text-align: center; }
.stat { display: flex; flex-direction: column; justify-content: center; padding-inline: .5rem; }
.stat-value { font-size: clamp(2.1rem, 4.5vw, 3.1rem); font-weight: 800; line-height: 1; color: #fff; letter-spacing: -.02em; }
.stat-value .stat-suffix { color: var(--brand-light); }
.stat-label { margin-top: 1rem; font-size: .95rem; font-weight: 600; line-height: 1.5; color: var(--text-on-dark-soft); }
.stat + .stat { border-left: 1px solid rgba(255, 255, 255, .1); }

/* Advantages */
.feature-card {
  background: #fff; border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: 1.75rem; height: 100%;
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.feature-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-md); border-color: transparent; }
.feature-icon {
  display: inline-grid; place-items: center; width: 54px; height: 54px; border-radius: 15px;
  background: var(--brand-gradient); color: #fff; margin-bottom: 1.1rem; box-shadow: var(--shadow-sm);
  /* `flex: 0 0 auto` keeps the chip square. Without it the default `flex-shrink: 1` lets a
     long sibling text block squeeze it — the 46px chip collapsed to 30.9px in EN and 22px at
     390px, because the English address needs two lines where the Chinese one needs one. */
  flex: 0 0 auto;
}
.feature-icon svg { width: 26px; height: 26px; }
.feature-card h3 { font-size: 1.15rem; margin-bottom: .5rem; }
.feature-card p { color: var(--text-soft); font-size: .95rem; margin: 0; }

/* Products */
.product-card {
  display: flex; flex-direction: column;
  background: #fff; border: 1px solid var(--border); border-radius: var(--radius-md);
  overflow: hidden; height: 100%;
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.product-card:hover { transform: translateY(-5px); box-shadow: var(--shadow-md); border-color: transparent; }
/* Square frame + `contain`: the product photos are portrait (4:5) or square, so a wide
   16:11 box with `cover` used to chop 25–45% off every image. A square box shows them whole
   and keeps every card the same height; the blurred fill behind covers the leftover edges. */
.product-card .media-frame { border-radius: 0; aspect-ratio: 1 / 1; }
.product-card .media-frame img { object-fit: contain; }
.product-card-body { padding: 1.35rem 1.5rem 1.5rem; display: flex; flex-direction: column; flex: 1; }
.product-card h3 { font-size: 1.15rem; margin-bottom: .35rem; }
.product-card .product-en { display: block; font-size: .82rem; font-weight: 600; color: var(--brand-light); letter-spacing: .02em; margin-bottom: .65rem; }
/* The English trade name is only meaningful alongside the Chinese name; in en/es it just repeats the <h3>,
   so hide it. Keyed off <html lang>, which applyLang() — and the inline <head> bootstrap on index.html —
   keep in sync with the active language (zh-CN / en / es). */
html:not([lang^="zh"]) .product-card .product-en { display: none; }
.product-card p { color: var(--text-soft); font-size: .93rem; margin-bottom: 1.1rem; }
.product-card .card-link { margin-top: auto; }
/* Customer-case cards show only a photo and a name, so tidy the title-only body. */
.product-card--case .product-card-body { padding: 1.15rem 1.35rem 1.35rem; text-align: center; }
.product-card--case h3 { margin-bottom: 0; font-size: 1.05rem; }
.card-link {
  display: inline-flex; align-items: center; gap: .4rem;
  font-weight: 700; font-size: .9rem; color: var(--brand-primary);
}
.card-link svg { width: 17px; height: 17px; transition: transform var(--dur-fast) var(--ease); }
.card-link:hover svg { transform: translateX(4px); }

/* Industries */
/* F3 (layout-fix): `repeat(auto-fit, minmax(150px, 1fr))` resolved to 6 columns at 1440, so the
   7th card sat alone in row 2 with 5 empty tracks (~111k px2). A centred wrapping flex line puts
   every partial row in the middle instead; flex-shrink 1 (never grow!) stops a lone last-row card
   from being stretched full-width. The basis is 148px, NOT 150px: 7 x 150 + 6 x 16 gaps = 1146px
   exceeds the 1136px container content box, so 150px still wrapped 6+1 — 148px fits all 7 cards
   on ONE line at 1440 (7 x 148 + 96 = 1132 <= 1136) with zero empty tracks. */
.industry-grid { display: flex; flex-wrap: wrap; justify-content: center; gap: 1rem; }
.industry-card {
  flex: 0 1 148px;
  display: flex; flex-direction: column; align-items: center; gap: .7rem;
  padding: 1.5rem 1rem; border-radius: var(--radius-md);
  background: rgba(255, 255, 255, .05); border: 1px solid rgba(255, 255, 255, .1);
  color: var(--text-on-dark); text-align: center; font-weight: 600; font-size: .92rem;
  transition: transform var(--dur) var(--ease), background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.industry-card:hover { transform: translateY(-4px); background: rgba(255, 255, 255, .1); border-color: var(--brand-light); }
.industry-card svg { width: 30px; height: 30px; color: var(--brand-light); }

/* Certifications — carousel */
/* Cards are generated by main.js from assets/data/certificates.js. To add one, drop an image
   into assets/img/certificates/ and run `python _tools/build_certs.py`; nothing in this file
   has to change.
   The track is a NATIVE scroll container (`overflow-x: auto` + `scroll-snap-type: x
   mandatory`). That buys touch dragging, momentum and snapping from the browser instead of
   from script: the arrows and dots only call scrollBy/scrollTo on it, so there is no
   transform arithmetic to get wrong and nothing to clip.
   `--per-view` is the single knob for how many cards share the viewport. main.js reads it to
   compute one step, so changing it here is all it takes. */
.cert-carousel {
  position: relative;
  --per-view: 3;
  --gap: 1.25rem;
}

.cert-track {
  display: flex;
  align-items: stretch;
  gap: var(--gap);
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  /* vertical only: the hover lift would otherwise be clipped by the scroll container */
  padding: .35rem 0;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.cert-track::-webkit-scrollbar { display: none; }
.cert-track:focus-visible {
  outline: 2px solid var(--brand-light);
  outline-offset: 3px;
  border-radius: var(--radius-md);
}

.cert-slide {
  flex: 0 0 calc((100% - (var(--per-view) - 1) * var(--gap)) / var(--per-view));
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.cert-slide:hover { transform: translateY(-4px); box-shadow: var(--shadow-md); border-color: transparent; }

/* An <a>-free button so the whole card is one large tap target; `zoom-in` says what the click
   does. The frame is white, NOT --bg-soft: the scans carry their own white margin, and a
   tinted frame paints a second visible rectangle around the certificate. `contain` keeps each
   page whole -- cropping a certificate is not a design choice anyone should make by accident. */
.cert-open {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
}
.cert-open:focus-visible { outline: 2px solid var(--brand-light); outline-offset: -3px; }

.cert-frame {
  display: block;
  aspect-ratio: 4 / 3;
  background: #fff;
  border-bottom: 1px solid var(--border);
  overflow: hidden;
}
.cert-frame img { display: block; width: 100%; height: 100%; object-fit: contain; }

.cert-body {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: .85rem 1rem 1rem;
}
.cert-name {
  margin: 0;
  font-size: .92rem;
  font-weight: 600;
  line-height: 1.35;
  color: var(--text-strong);
  text-align: center;
}

/* The arrows ride INSIDE the cards' edges. Parked just outside the container they would add
   to its scrollWidth, which is how a carousel starts producing a page-level horizontal
   scrollbar on a narrow viewport. */
.cert-nav {
  position: absolute;
  top: 0;
  bottom: 4.6rem;
  margin: auto 0;
  width: 42px;
  height: 42px;
  z-index: 3;
  display: grid;
  place-items: center;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: rgba(255, 255, 255, .95);
  color: var(--text-strong);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease), opacity var(--dur) var(--ease), visibility var(--dur) var(--ease);
}
.cert-nav:hover { background: var(--brand-primary); border-color: var(--brand-primary); color: #fff; }
.cert-nav:focus-visible { outline: 2px solid var(--brand-light); outline-offset: 2px; }
.cert-nav svg { width: 20px; height: 20px; }
.cert-nav--prev { left: .45rem; }
.cert-nav--next { right: .45rem; }
.cert-nav[hidden] { display: none; }   /* `display: grid` above would beat [hidden] */

/* F11: on hover-capable devices the arrows stay invisible until the pointer (or keyboard
   focus) enters the carousel; touch devices have no hover to give, so the arrows remain
   visible there and the track still scrolls the usual way. F12: a CLICK's focus must not
   pin the arrows up after the pointer leaves — only :focus-visible (real keyboard) does. */
@media (hover: hover) {
  .cert-nav { opacity: 0; visibility: hidden; }
  .cert-carousel:hover .cert-nav,
  .cert-carousel:has(:focus-visible) .cert-nav { opacity: 1; visibility: visible; }
}

.cert-foot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .8rem;
  margin-top: 1.15rem;
}

.cert-dots { display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem; }
.cert-dot {
  width: 10px;
  height: 10px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: #fff;
  cursor: pointer;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.cert-dot:hover { border-color: var(--brand-primary); }
.cert-dot.is-active { background: var(--brand-primary); border-color: var(--brand-primary); transform: scale(1.25); }
.cert-dot:focus-visible { outline: 2px solid var(--brand-light); outline-offset: 2px; }

/* Doubles as the no-JavaScript notice: main.js hides it only after the cards are in. */
.cert-empty { margin: 0; font-size: .92rem; color: var(--text-soft); text-align: center; }
.cert-empty[hidden] { display: none; }

/* Full-size scan. */
.cert-modal {
  position: fixed;
  inset: 0;
  z-index: 220;
  display: grid;
  place-items: center;
  padding: clamp(1rem, 4vw, 2.5rem);
  background: rgba(11, 16, 70, .74);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
.cert-modal[hidden] { display: none; }   /* same [hidden] trap as .cert-nav above */
.cert-modal.is-open { opacity: 1; }

.cert-modal__panel {
  position: relative;
  max-width: min(1080px, 100%);
  max-height: 100%;
  overflow: auto;
  background: #fff;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}
.cert-modal__panel img { display: block; width: 100%; height: auto; }

.cert-modal__close {
  position: absolute;
  top: .55rem;
  right: .55rem;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: rgba(255, 255, 255, .95);
  color: var(--text-strong);
  font-size: 1.35rem;
  line-height: 1;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
}
.cert-modal__close:hover { background: var(--brand-primary); border-color: var(--brand-primary); color: #fff; }

@media (max-width: 1023px) {
  .cert-carousel { --per-view: 2; }
}
@media (max-width: 767px) {
  .cert-carousel { --per-view: 1; --gap: 1rem; }
  .cert-nav { bottom: 4.2rem; width: 38px; height: 38px; }
}
@media (prefers-reduced-motion: reduce) {
  .cert-track { scroll-behavior: auto; }
  .cert-slide, .cert-nav, .cert-dot, .cert-modal { transition: none; }
}

/* ---------- product type cards (homepage) ---------- */
/* One .product-card per bag type: the section keeps the card format it has always had, so every
   card, frame and link rule above applies unchanged. Only three things are specific to this grid. */
.type-media-open {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
  text-align: inherit;
}
.type-media-open:focus-visible { outline: 2px solid var(--brand-light); outline-offset: 3px; }

/* Diagrams are line art on white: keep the strokes whole (contain, with breathing room) and drop
   the blurred duplicate behind them, which would smear the drawing across the frame. */
.type-card--diagram .media-frame { background: #fff; }
.type-card--diagram .media-frame::before { background-image: none; }
.type-card--diagram .media-frame img { padding: 8%; }

/* Says out loud what the picture is: a construction diagram, not a photo of a customer's bag. */
.type-badge {
  position: absolute;
  top: .65rem;
  left: .65rem;
  z-index: 2;
  padding: .22rem .65rem;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, .92);
  border: 1px solid var(--border);
  font-size: .74rem;
  font-weight: 600;
  color: var(--text-soft);
  pointer-events: none;
}

.types-empty { margin: 1rem 0 0; font-size: .92rem; color: var(--text-soft); text-align: center; }
.types-empty[hidden] { display: none; }

/* ---------- type detail overlay (homepage, F11) ---------- */
/* Same shell as the certificate lightbox (.cert-modal on the wrapper), but the panel is a
   sheet: photo on one side, construction highlights on the other. No MOQ, no pricing in
   here by design — the sheet answers "which construction do I need?", not "what does it cost". */
.type-detail__panel {
  position: relative;
  width: min(880px, 100%);
  max-height: 100%;
  overflow: auto;
  background: #fff;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}
.type-detail__media { background: var(--brand-gradient); }
.type-detail__media img { display: block; width: 100%; height: auto; }
.type-detail__body { padding: 1.35rem 1.5rem 1.6rem; }
.type-detail__body h3 { margin: 0; font-size: 1.3rem; color: var(--text-strong); }
.type-detail__body .product-en { display: block; margin-top: .2rem; font-size: .86rem; color: var(--text-soft); }
html:not([lang^="zh"]) .type-detail .product-en { display: none; }
.type-detail__desc { margin: .7rem 0 0; font-size: .95rem; line-height: 1.65; color: var(--text-soft); }
.type-detail__label {
  margin: 1.15rem 0 .45rem;
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-strong);
}
.type-detail__list { margin: 0; padding: 0; list-style: none; }
.type-detail__list li {
  position: relative;
  padding: .3rem 0 .3rem 1.15rem;
  font-size: .92rem;
  line-height: 1.6;
  color: var(--text-soft);
}
.type-detail__list li::before {
  content: "";
  position: absolute;
  left: .15rem;
  top: .82em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--brand-primary);
}
@media (min-width: 768px) {
  .type-detail__panel { display: grid; grid-template-columns: 42% 1fr; }
  .type-detail__media img { height: 100%; object-fit: cover; }
  .type-detail__body { padding: 1.8rem 1.9rem 1.9rem; }
}

/* Process */
.process-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1rem; counter-reset: step; }
.step {
  position: relative; padding: 1.6rem 1.25rem; border-radius: var(--radius-md);
  background: #fff; border: 1px solid var(--border); height: 100%;
}
.step-num {
  display: inline-grid; place-items: center; width: 40px; height: 40px; border-radius: 50%;
  background: var(--brand-primary); color: #fff; font-weight: 800; margin-bottom: .9rem;
}
.step h4 { margin-bottom: .35rem; font-size: 1rem; }
.step p { margin: 0; font-size: .88rem; color: var(--text-soft); }
.step::after {
  content: ""; position: absolute; top: 50%; right: -0.85rem; width: .7rem; height: 2px;
  background: var(--border);
}
.step:last-child::after { display: none; }

/* Capability list (about page) */
.cap-list { display: grid; gap: 1rem; }
.cap-item {
  display: flex; gap: 1rem; align-items: flex-start;
  padding: 1.35rem 1.5rem; background: #fff; border: 1px solid var(--border); border-radius: var(--radius-md);
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.cap-item:hover { transform: translateX(4px); box-shadow: var(--shadow-sm); }
.cap-item .feature-icon { margin-bottom: 0; width: 46px; height: 46px; border-radius: 13px; }
.cap-item .feature-icon svg { width: 22px; height: 22px; }
.cap-item h4 { margin-bottom: .3rem; }
.cap-item p { margin: 0; color: var(--text-soft); font-size: .94rem; }

/* Value / MVV cards */
.value-card { background: #fff; border: 1px solid var(--border); border-radius: var(--radius-md); padding: 1.75rem; height: 100%; }
.value-card h3 { font-size: 1.2rem; }
.value-card p { color: var(--text-soft); margin: 0; }
.value-card .icon-wrap { display: inline-grid; place-items: center; width: 50px; height: 50px; border-radius: 14px; background: var(--brand-gradient); color: #fff; margin-bottom: 1rem; flex: 0 0 auto; }
.value-card .icon-wrap svg { width: 24px; height: 24px; }

/* Why-choose list */
.why-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.25rem; }
.why-item { display: flex; gap: .85rem; padding: 1.25rem; background: rgba(255, 255, 255, .05); border: 1px solid rgba(255, 255, 255, .1); border-radius: var(--radius-md); }
.why-item svg { width: 24px; height: 24px; color: var(--brand-light); flex: 0 0 auto; margin-top: 3px; }
.why-item h4 { font-size: 1rem; margin-bottom: .3rem; color: #fff; }
.why-item p { margin: 0; font-size: .88rem; color: var(--text-on-dark-soft); }

/* Split media section */
/* F4 (layout-fix): `align-items: center` re-centred the fixed 4:3 image whenever the text column
   grew (es), leaving pure-background bands above/below the photo (~50px total in es). `stretch`
   makes the shorter column fill the row instead. The media frame gives up its 4:3 ratio and fills
   the row height; the img keeps the global `object-fit: cover` (never `contain` — a letterbox
   regression here once produced blue fill bars). Single-column (<=768px) is unaffected: each item
   gets its own row, so stretch is a no-op there. */
.split { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(1.5rem, 4vw, 3.5rem); align-items: stretch; }
.split .media-frame { height: 100%; aspect-ratio: auto; border-radius: var(--radius-lg); box-shadow: var(--shadow-md); }
.check-list { display: grid; gap: .85rem; margin-top: 1.25rem; }
.check-list li { display: flex; gap: .65rem; align-items: flex-start; font-size: .97rem; }
.check-list svg { width: 22px; height: 22px; color: var(--brand-primary); flex: 0 0 auto; margin-top: 2px; }

/* ===== 12. Media frames (image placeholders) ===== */
.media-frame {
  position: relative; overflow: hidden;
  background: var(--brand-gradient);
  border-radius: var(--radius-md);
  aspect-ratio: 4 / 3;
  display: grid; place-items: center;
  /* An implicit `auto` row would be sized by the image itself, so the img's `height: 100%`
     resolves against an indefinite track and falls back to `auto` — the portrait photos then
     overflow a square frame and `overflow: hidden` chops ~20% off the top and bottom.
     Pinning both tracks to 100% makes the percentage definite, so the img box always equals
     the frame and `object-fit: contain` can show the whole photo. */
  grid-template-columns: 100%; grid-template-rows: 100%;
  isolation: isolate;
}
/* Blurred copy of the photo, filling the frame behind the sharp image, so images that are
   shown whole (object-fit: contain) never sit on bare empty bars. JS copies the <img> src
   into --frame-img, so no extra markup is needed.
   Paint order is ::before -> ::after -> img (z-index 1), which puts the watermark above the
   blur but below the real photo — exactly what the empty state needs. */
.media-frame::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  background-image: var(--frame-img, none);
  background-size: cover; background-position: center;
  filter: blur(26px) saturate(1.15);
  transform: scale(1.15);            /* hide the soft transparent edge blur leaves behind */
  pointer-events: none;
}
.media-frame::after {
  content: "MEDAL";
  position: absolute; inset: 0; z-index: 0;
  display: grid; place-items: center;
  font-size: clamp(1.6rem, 5vw, 2.6rem); font-weight: 800; letter-spacing: .22em;
  color: rgba(255, 255, 255, .2); text-indent: .22em;
  background: radial-gradient(circle at 30% 20%, rgba(255, 255, 255, .22), transparent 55%);
  pointer-events: none;
}
.media-frame img {
  position: relative; z-index: 1;
  width: 100%; height: 100%; object-fit: cover; display: block;
}
.media-frame.is-empty img { display: none; }
.media-frame.is-empty::before { background-image: none; }

/* ===== 13. Breadcrumb ===== */
.breadcrumb { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; font-size: .85rem; margin-bottom: 1rem; }
.breadcrumb a { color: var(--text-on-dark-soft); }
.breadcrumb a:hover { color: #fff; }
.breadcrumb .sep { opacity: .5; color: var(--text-on-dark-soft); }
.breadcrumb .current { color: #fff; font-weight: 600; }
.breadcrumb.is-light a { color: var(--text-soft); }
.breadcrumb.is-light a:hover { color: var(--brand-primary); }
.breadcrumb.is-light .sep { color: var(--text-soft); }
.breadcrumb.is-light .current { color: var(--text-strong); }

/* ===== 14. Spec table ===== */
.spec-table { width: 100%; border-collapse: collapse; background: #fff; border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; }
.spec-table th, .spec-table td { padding: 1rem 1.25rem; text-align: left; border-bottom: 1px solid var(--border); vertical-align: top; }
.spec-table tr:last-child th, .spec-table tr:last-child td { border-bottom: none; }
.spec-table th { width: 34%; background: var(--bg-soft); font-weight: 700; color: var(--text-strong); font-size: .92rem; }
.spec-table td { color: var(--text); font-size: .95rem; }

/* Data tables (guide) */
.data-table-wrap { overflow-x: auto; border-radius: var(--radius-md); border: 1px solid var(--border); }
.data-table { width: 100%; border-collapse: collapse; min-width: 640px; background: #fff; }
.data-table th, .data-table td { padding: .9rem 1.1rem; text-align: left; border-bottom: 1px solid var(--border); font-size: .9rem;
  /* Spanish is the long language in a narrow cell. The audit that opened this item
     (`_tools/_layout_audit/REPORT.md` §4) measured `.data-table` at zh 431.75 / en 431.75 / es 554.17
     at 1440 — an element-HEIGHT delta of +122.4px, i.e. Spanish needing extra wrapped lines, NOT
     horizontal overflow (`scrollWidth - clientWidth` is 0 here, and the page has zero unclipped
     overflow — see `_geom_pass/t3_diag.txt`). The remedy for extra wrapped lines is a breaking
     opportunity, so hyphenation is enabled here rather than the container being widened:
     `hyphens: auto` + `<html lang="es">` lets Chrome's Spanish dictionary break long words, and it
     measurably shortens that table: measured 626.25px -> 601.77px and 72 -> 69 wrapped lines, moving
     the es-vs-en delta from +73.45px to +48.97px. It changes nothing in zh (no dictionary wins) and
     nothing in en at this width, and it can only ever REMOVE lines — never add them.
     `overflow-wrap: break-word` is belt-and-braces: with `hyphens: auto` alone an unbreakable token
     longer than the cell would still overflow horizontally, which is the failure mode the client's
     report is about. Neither rule HERE can introduce horizontal scrolling — but `min-width: 640px`
     on the .data-table rule below does, and that IS the client's later report ("包装指南这里缩小到
     一定程度会挡住"): below ~690px all four guide tables stayed 640px wide inside a narrower box,
     hanging up to 357px of themselves outside it. SEE the `RESPONSIVE DATA TABLES` block in the
     responsive section: it resets that floor and turns each row into a card below 768px. */
  -webkit-hyphens: auto; hyphens: auto; overflow-wrap: break-word; }
.data-table thead th { background: var(--brand-primary); color: #fff; font-weight: 700; font-size: .85rem; letter-spacing: .02em; }
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr:nth-child(even) { background: var(--bg-soft); }
.data-table td strong { color: var(--text-strong); }
.badge {
  display: inline-block; padding: .15rem .6rem; border-radius: var(--radius-pill);
  font-size: .78rem; font-weight: 700;
}
.badge-high { background: #E7F5EE; color: #1B7F4B; }
.badge-mid { background: #FFF4E0; color: #A96A00; }
.badge-low { background: #F0F1F4; color: #5B6270; }

/* ===== 15. FAQ ===== */
.faq-list { display: grid; gap: .85rem; max-width: 860px; margin-inline: auto; }
.faq-item { background: #fff; border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; transition: box-shadow var(--dur) var(--ease); }
.faq-item[open] { box-shadow: var(--shadow-sm); }
.faq-item summary {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 1.15rem 1.35rem; cursor: pointer; font-weight: 700; color: var(--text-strong);
  list-style: none;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: "+"; font-size: 1.5rem; line-height: 1; color: var(--brand-primary); font-weight: 400;
  transition: transform var(--dur) var(--ease); flex: 0 0 auto;
}
.faq-item[open] summary::after { transform: rotate(45deg); }
.faq-item .faq-body { padding: 0 1.35rem 1.25rem; color: var(--text-soft); font-size: .95rem; }

/* Guide nav anchors + product filter chips */
.toc {
  display: flex; flex-wrap: wrap; gap: .6rem; justify-content: center;
  position: sticky; top: var(--header-h); z-index: 20;
  background: rgba(255, 255, 255, .94);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  padding: .85rem var(--gutter); border-bottom: 1px solid var(--border);
}
.toc a,
.toc .chip {
  padding: .45rem .9rem; border-radius: var(--radius-pill); background: var(--bg-soft);
  font-size: .86rem; font-weight: 600; color: var(--text); border: 1px solid transparent;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.toc a:hover,
.toc .chip:hover { background: var(--brand-gradient-soft); color: var(--brand-primary); border-color: var(--brand-light); }
.toc .chip.is-active {
  background: var(--brand-gradient); color: #fff; border-color: transparent;
  box-shadow: 0 6px 18px rgba(27, 35, 192, .26);
}

/* Product grid pagination */
.pager {
  display: flex; align-items: center; justify-content: center; gap: .75rem;
  margin-top: clamp(2rem, 4vw, 3rem);
}
.pager-btn {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .6rem 1.15rem; border-radius: var(--radius-pill);
  border: 1px solid var(--border); background: #fff;
  font-size: .9rem; font-weight: 700; color: var(--text-strong);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.pager-btn svg { width: 16px; height: 16px; flex: 0 0 auto; }
.pager-btn:hover:not(:disabled) {
  background: var(--brand-gradient-soft); color: var(--brand-primary);
  border-color: var(--brand-light); box-shadow: var(--shadow-sm);
}
.pager-btn:disabled { opacity: .35; cursor: not-allowed; }
.pager-status {
  min-width: 4.25rem; text-align: center;
  font-size: .92rem; font-weight: 700; color: var(--text-soft);
  font-variant-numeric: tabular-nums;
}
.product-empty {
  padding: clamp(2.5rem, 6vw, 4rem) 1rem; text-align: center;
  color: var(--text-soft); font-size: 1rem; font-weight: 600;
}

/* ===== Guide page: sticky-TOC anchor landing + section rhythm ===== */
/* F15: small-batch printing note at the end of the printing section — a quiet, centred line so
   it reads as guidance rather than a sixth feature card. */
.section-note {
  margin-top: 1.75rem; text-align: center; font-size: .95rem;
  color: var(--text-soft);
}
/* ANCHOR LANDING. `html`'s scroll-padding-top and a target's scroll-margin-top BOTH apply to
   fragment navigation and ADD together — they do not replace one another. The old rule
   (`calc(var(--header-h) + 70px)`) stacked on top of scroll-padding-top, landing the target at
   234px against a 141.95px sticky stack: measured, the heading sat 152.3px below the bar at 1440
   and 200px at <=768, with the previous section's table still visible under it.
   This formula lands the block's CONTENT — not its box — --anchor-gap under the sticky stack, by
   subtracting the block's own padding-top and the part of scroll-padding-top that --header-h does
   not already cover (so --header-h cancels out and does not appear below).
   NO-JS FALLBACK ONLY: --toc-h cannot be measured from CSS, so it is written by hand as a per-band
   UPPER BOUND — 120px (one or two rows, desktop chip metrics) for >=827, 91px (up to two rows,
   tightened chip metrics) for 553..826, and 123px (up to three rows) for <=552. The band edges are
   METRIC change points, not wrap points: the chip row-gap flips 4.48px -> 9.6px at exactly 827, and
   the 552 edge is inherited from the previous pass. A boundary placed at "the width where a language
   flips to two rows" has ~0 slack by construction and goes red the next time the copy changes — this
   project already paid for that twice (553 and 826/827).
   WHY THE CONSTANT IS LANGUAGE-INDEPENDENT: one --toc-h serves all three languages, so it must cover
   the worst one. 913..1440 are one row in every language (67.9531px) against a 120px constant: that
   is 52.05px of overshoot, and it is the safe direction. Making the constant per-language (e.g.
   `html[lang^="es"]`) was rejected because each language's band edge would then sit on that
   language's own wrap point. The old band edge at 552 concealed the heading by 29.31px before F1 and
   would conceal it again after the copy pass, so the <=826 value is now 91px, not 56px.
   initGuideToc() in main.js measures the live height at click time and is authoritative; this is the
   fallback approximation.
   SECTION RHYTHM. Guide blocks are table-dense, so the global 2 x --section-y seam (120px at 1440)
   read as a hole between two tables — the client reported it twice. --guide-y halves it and is
   deliberately its own variable so the other 11 pages and --section-y are unaffected.
   SCOPE: .guide-block is used by guide.html only. */
.guide-block {
  --guide-y: clamp(1.5rem, 2.6vw, 2rem);
  padding-block: var(--guide-y);
  scroll-margin-top: calc(var(--toc-h) + var(--anchor-gap) - var(--guide-y) - var(--scroll-pad-extra));
}

/* ===== 16. Contact page ===== */
/* F5 (layout-fix): `align-items: start` let the left info column and the right form card end at
   different heights, and WHICH one was taller flipped with the language (en: info col +34.5px).
   `stretch` equalises both columns to the row height (diff -> 0). Single-column (<=1024px) is a
   no-op there. */
.contact-grid { display: grid; grid-template-columns: .9fr 1.1fr; gap: clamp(1.5rem, 4vw, 3rem); align-items: stretch; }
.info-card { display: flex; gap: 1rem; padding: 1.25rem 1.35rem; background: #fff; border: 1px solid var(--border); border-radius: var(--radius-md); }
.info-card + .info-card { margin-top: 1rem; }
.info-card .feature-icon { margin-bottom: 0; width: 46px; height: 46px; border-radius: 13px; }
.info-card .feature-icon svg { width: 22px; height: 22px; }
.info-card h4 { margin-bottom: .2rem; font-size: .95rem; }
.info-card p { margin: 0; font-size: .95rem; color: var(--text); word-break: break-word; }

.form-card { background: #fff; border: 1px solid var(--border); border-radius: var(--radius-lg); padding: clamp(1.5rem, 3vw, 2.25rem); box-shadow: var(--shadow-sm); }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.form-field { margin-bottom: 1.1rem; }
.form-field label { display: block; font-weight: 600; font-size: .88rem; margin-bottom: .4rem; color: var(--text-strong); }
.form-field .req { color: var(--brand-accent); }
.form-field input, .form-field select, .form-field textarea {
  width: 100%; padding: .8rem .95rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  font-family: inherit; font-size: .95rem; color: var(--text); background: var(--white);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.form-field textarea { min-height: 130px; resize: none; overflow-y: hidden; }
.form-field input:focus, .form-field select:focus, .form-field textarea:focus {
  outline: none; border-color: var(--brand-light); box-shadow: 0 0 0 3px rgba(59, 143, 240, .18);
}
.form-note { font-size: .82rem; color: var(--text-soft); margin-top: 1rem; }
.form-success {
  display: none; align-items: flex-start; gap: .75rem;
  background: #E7F5EE; border: 1px solid #B7E4CB; color: #1B7F4B;
  border-radius: var(--radius-md); padding: 1.1rem 1.25rem; font-weight: 600; font-size: .95rem;
}
.form-success.is-visible { display: flex; }
.form-success svg { width: 24px; height: 24px; flex: 0 0 auto; margin-top: 2px; }

/* Failure state of the inquiry form: the message is kept in the form and offered as a
   pre-composed mailto, so a failed automatic send never loses a lead. */
.form-error {
  display: none; align-items: flex-start; gap: .75rem;
  background: #FDECEA; border: 1px solid #F3C1BA; color: #9C2A1E;
  border-radius: var(--radius-md); padding: 1.1rem 1.25rem; font-size: .92rem;
  margin-top: 1rem;
}
.form-error.is-visible { display: flex; }
.form-error svg { width: 22px; height: 22px; flex: 0 0 auto; margin-top: 2px; }
.form-error p { margin: 0; }
.form-error p + p { margin-top: .35rem; }
.form-error a { color: inherit; font-weight: 700; text-decoration: underline; }

/* Honeypot: present in the DOM so bots fill it, invisible and untabbable for people. */
.form-honeypot {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.btn.is-busy { opacity: .72; cursor: progress; }

/* F19 (2026-09-21): the company-location map was removed (see contact.html), so the map frame,
   loading status, zoom/the "back to MEDAL" control and marker-label rules all went with it.
   The contact page now goes straight from the form to the social row. */
/* F25/F26 (2026-09-21): the old six-icon social row became three large platform cards
   (WeChat Channels / Facebook / Instagram) under a centered, brand-blue heading + subtitle.
   F26: each card wears its platform colour — a rounded tinted icon badge and a pill CTA that
   fills solid on hover; corners got a generous radius instead of the default boxy one. */
.social-section .container > h2 { text-align: center; color: var(--brand-primary); }
.social-sub { margin: -.25rem 0 2rem; color: var(--muted); font-size: .95rem; text-align: center; }
.social-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }
.social-card {
  --pc: var(--brand-primary); --pt: rgba(59, 143, 240, .12);   /* platform colour + tint */
  display: flex; flex-direction: column; align-items: center; gap: 1rem;
  padding: 2.5rem 1.25rem 2.25rem; background: #fff;
  border: 1px solid var(--border); border-radius: 28px; text-align: center;
  transition: transform var(--dur) var(--ease), border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.social-card--wechat { --pc: #07C160; --pt: rgba(7, 193, 96, .12); }
/* The WeChat Channels card is a <button> (opens the share-QR dialog), not a link — strip the
   browser's default button chrome so it looks identical to its two <a> siblings. */
button.social-card {
  cursor: pointer; font: inherit; color: inherit; appearance: none; -webkit-appearance: none; width: 100%;
}
.social-card--facebook { --pc: #1877F2; --pt: rgba(24, 119, 242, .12); }
.social-card--instagram { --pc: #DD2A7B; --pt: rgba(221, 42, 123, .10); }
.social-card:hover { transform: translateY(-4px); border-color: var(--pc); box-shadow: var(--shadow-sm); }
.social-badge {
  width: 64px; height: 64px; border-radius: 22px; display: grid; place-items: center;
  background: var(--pt); color: var(--pc);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.social-badge svg { width: 30px; height: 30px; }
.social-card:hover .social-badge { background: var(--pc); color: #fff; }
.social-card--instagram:hover .social-badge {
  background: linear-gradient(135deg, #F58529 0%, #DD2A7B 50%, #8134AF 100%); color: #fff;
}
.social-card--instagram:hover .social-badge svg rect,
.social-card--instagram:hover .social-badge svg circle { stroke: #fff; }
.social-card--instagram:hover .social-badge svg circle:last-of-type { fill: #fff; stroke: none; }
.social-card-name { font-weight: 700; font-size: 1.05rem; color: var(--text); }
.social-card-cta {
  font-size: .85rem; font-weight: 600; color: var(--pc);
  background: var(--pt); padding: .45rem 1.2rem; border-radius: 999px;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.social-card:hover .social-card-cta { background: var(--pc); color: #fff; }
.social-card--instagram:hover .social-card-cta {
  background: linear-gradient(135deg, #F58529 0%, #DD2A7B 50%, #8134AF 100%);
}
@media (max-width: 767px) { .social-cards { grid-template-columns: 1fr; } }

/* ===== 17. CTA banner ===== */
.cta-banner { position: relative; overflow: hidden; background: var(--brand-gradient); color: #fff; border-radius: var(--radius-lg); padding: clamp(2rem, 5vw, 3.5rem); text-align: center; }
.cta-banner::before { content: ""; position: absolute; inset: 0; background: radial-gradient(circle at 85% 20%, rgba(255, 255, 255, .25), transparent 45%); }
.cta-banner > * { position: relative; z-index: 1; }
.cta-banner h2 { color: #fff; }
.cta-banner p { color: rgba(255, 255, 255, .9); max-width: 40rem; margin-inline: auto; }

/* ===== 18. Footer ===== */
.site-footer { background: var(--bg-navy); color: var(--text-on-dark-soft); }
.footer-top { padding-block: clamp(2.75rem, 5vw, 4rem); }
.footer-grid { display: grid; grid-template-columns: 1.6fr 1fr 1fr 1.2fr; gap: 2rem; }
/* 页脚用完整的白色 Logo（含「美笛国际」中文行），放大到可辨识尺寸 */
.footer-brand .brand-logo-box { margin-bottom: 1.15rem; }
.footer-brand .brand-logo-box img { height: 78px; }
.footer-about { font-size: .92rem; max-width: 26rem; }
.footer-col h4 { color: #fff; font-size: .95rem; margin-bottom: 1rem; letter-spacing: .02em; }
.footer-col ul { display: grid; gap: .55rem; }
.footer-col a { color: var(--text-on-dark-soft); font-size: .92rem; }
.footer-col a:hover { color: #fff; }
.footer-contact li { display: flex; gap: .6rem; align-items: flex-start; font-size: .92rem; }
/* F23: email/phone in the footer are plain text, not links — no hover feedback either. */
.footer-contact span { color: var(--text-on-dark-soft); }
.footer-contact svg { width: 18px; height: 18px; color: var(--brand-light); flex: 0 0 auto; margin-top: 3px; }
.footer-bottom { border-top: 1px solid rgba(255, 255, 255, .1); padding-block: 1.25rem; font-size: .85rem; }
.footer-bottom .container { display: flex; flex-wrap: wrap; gap: .5rem 1.5rem; justify-content: space-between; align-items: center; }

/* ===== 19. Back to top ===== */
.back-to-top {
  position: fixed; right: clamp(1rem, 3vw, 1.75rem); bottom: var(--float-bottom); z-index: 90;
  width: 46px; height: 46px; border: none; border-radius: 50%;
  background: var(--brand-primary); color: #fff; display: grid; place-items: center;
  box-shadow: var(--shadow-md); opacity: 0; visibility: hidden; transform: translateY(12px);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease), background var(--dur) var(--ease), visibility var(--dur);
}
.back-to-top.is-visible { opacity: 1; visibility: visible; transform: none; }
.back-to-top:hover { background: var(--brand-primary-deep); }
.back-to-top svg { width: 22px; height: 22px; }

/* ===== 19b. Floating contact rail + WeChat dialog ===== */
/* Lock page scroll while the WeChat dialog is open. */
body.is-modal-open { overflow: hidden; }

.contact-rail {
  position: fixed; left: 0; top: 50%; z-index: 90;
  /* `align-items: center` — the panel keeps its OWN content height and is simply centred in the rail.
     It must NOT be stretched to the tab's height: the tab's raw height has been language-dependent
     (its vertical label is 4 chars in zh, 13 in en, 15 in es), so stretching used to pad the panel out
     to match it and leave a large blank strip under the last row. Measured blank below the last row
     at 1440x900 when that was still possible: zh 6.4px, en 59.4px, es 59.3px.
     Since the compaction pass the tab's natural height is deliberately held BELOW the panel's in every
     language, so `.rail-toggle { align-self: stretch }` (below) makes the tab exactly as tall as the
     panel — the card is one rectangle in all three languages, and neither box can overhang the other. */
  /* `row-reverse` mirrors the rail: the PANEL comes first, the TAB second. The container's left edge is
     pinned to x=0 and it is shrink-to-fit, so collapsed (panel 0 wide) the tab sits at x 0..40 while open
     it sits at x panelW..panelW+40. The tab therefore moves a full panel width to the right, which IS the
     "the blue bar drags the white panel out of the screen edge" effect — pure layout, no JS positions anything. */
  display: flex; flex-direction: row-reverse; align-items: center;
  /* The entrance offset is folded into the centring transform — a bare translateY(12px) would clobber the -50%. */
  transform: translateY(calc(-50% + 12px));
  opacity: 0; visibility: hidden;
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease), visibility var(--dur);
}
.contact-rail.is-visible { opacity: 1; visibility: visible; transform: translateY(-50%); }

/* Open state: the tab and the panel must read as ONE card, so no shadow may land on the shared edge.
   The geometry is now mirrored (panel on the LEFT, tab on the RIGHT), so the rules mirror too:
   - the tab KEEPS its right-hand rounding — it is the component's outer right edge — and casts its
     shadow to the RIGHT (`6px 0 18px`), away from the panel;
   - the panel's shadow is purely VERTICAL (`0 10px 26px`): its left edge is flush with the viewport so a
     leftward offset would be clipped by the screen, and a rightward offset would fall on the tab.
   (Previously the mirror image: tab squared off, panel shadow `4px 10px 26px`.) */
.contact-rail.is-open .rail-toggle { border-radius: 0 12px 12px 0; box-shadow: 6px 0 18px rgba(15, 23, 42, .14); }

/* Collapsed state: a vertical brand-coloured tab pinned to the left edge.
   `align-self: stretch` = the tab always spans the FULL rail height. The rail's height is the taller of
   the two boxes' content; because the tab's own content is now deliberately kept SHORTER than the panel
   in every language (see the budget on .rail-toggle__text below), the panel is always the taller one and
   the tab is stretched to exactly the panel's height. `min-height` must stay 0 for the same reason: any
   floor above the panel's height would make the tab overhang the panel again — which is the defect this
   pass removes.
   The row box is the content height driver inside the panel, so it is worth stating explicitly:
   row = icon 36px (or the 2-line text block, whichever is taller) + 2 x .5rem padding. */
.rail-toggle {
  flex: 0 0 auto;                          /* this site has been bitten by flex-shrink squashing icons — keep it off */
  align-self: stretch;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .4rem; width: 40px; min-height: 0;
  padding: .5rem .35rem; border: none; cursor: pointer;
  border-radius: 0 12px 12px 0;
  background: var(--brand-primary); color: #fff;
  box-shadow: var(--shadow-md);
  transition: background var(--dur) var(--ease), padding var(--dur) var(--ease);
}
.rail-toggle:hover { background: var(--brand-primary-deep); }
.rail-toggle:focus-visible { outline: 3px solid var(--brand-light); outline-offset: 2px; }
.rail-toggle[aria-expanded="true"] { padding-right: .2rem; }
.rail-toggle__icon { display: grid; place-items: center; }
.rail-toggle__icon svg { width: 18px; height: 18px; }
/* The vertical label, in the copy that ships today: zh「快速联系」46.469px, en "Quick Contact" 87.203px,
   es "Contacto rápido" 98.656px of single-column advance at .66rem / .1em tracking (measured with the
   shipped font). The 2026-09-16 copy pass changed the label from ONE word per language to TWO, so the
   old "always exactly one column" premise is gone: en and es now wrap to two columns, and the numbers
   below are the window that wrap has to live in.
   LINE-HEIGHT 1.35 IS WHAT MAKES THE COLUMNS FIT. A rotated Latin glyph's ink box is ~1.23em, but a
   line box at `line-height: 1` is only 1em, so the ink poked ~2px out of the label's border box and
   `scrollWidth` exceeded `clientWidth` by 2 in EVERY language and at every width (measured; that is
   the "es label is clipped" symptom). At 1.35 the column box is 14.256px against ~13px of ink, so the
   scrollable overflow is 0 in all three languages at every width. Cost: the label's block size grows
   from 21.13px to 28.51px; the tab's content box is 28.8px wide above 560px and 24.4px below it, so
   under 560px the two columns borrow ~2px of the tab's 4.8px side padding — the ink still sits inside
   the tab's border box (nothing is clipped), it just loses some breathing room.
   WRITING-MODE `vertical-lr`, NOT `vertical-rl`. The two differ only in which side the first column
   lands on: vertical-rl stacks lines right-to-left, so the shipped en label rendered as
   [Contact][Quick] read left-to-right — the words in reverse. vertical-lr stacks them left-to-right,
   which is the natural reading order for Latin. Measured: en {Quick}{Contact}, es {Contacto}{rápido}.
   THE 3.8rem (60.8px) GUARD IS THE WRAP CONTROL, and it has to sit STRICTLY BETWEEN the longest
   single word and the whole label, or the browser breaks mid-word instead of at the space:
     longest single word  es "Contacto" 55.828px  (en "Contact" 48.391px)
     whole label          en 87.203px / es 98.656px
     available in the tab panel 130.51px - (16 padding + 18 icon + 12.8 gaps + 18 chev = 64.8) = 65.71px
   so the window is (55.828, 65.71) and 60.8px sits ~5px from both ends. The guard used to be 3.4rem
   (54.4px) — BELOW es's 55.828px single word — which is exactly what forced the mid-word split
   ("Contact" / "o" / "Rápido"). Keeping the guard under the available height is what guarantees the
   tab can never be the taller box, so `tabH == panelH` holds whatever a future translation says;
   `overflow: hidden` enforces it even if the guard is somehow reached.
   CORRECTED after independent QA falsified the earlier wording here: the guard bounds the column LENGTH
   (its inline axis), NOT the column COUNT, so `overflow-wrap: break-word` does NOT keep an over-long
   label inside the tab. A label that needs a third column spills SIDEWAYS past the tab's edges
   (measured: a 3-word label -3.56 / +5.19px, a 35-char label -10.7 / +12.3px, at 34px and 40px tab
   widths). No SHIPPED label reaches that point (0 of 6726 measured cells spill), and `tabH == panelH`
   still holds structurally, because the tab's own maximum (125.6px) stays below the panel's (130.5px).
   Removing the guard entirely is NOT an option: with `max-height: none` en renders one 87.2px column
   and es one 98.7px column, both taller than the panel, and the tab grows to 147.98px / 159.44px.
   This span is a flex item of .rail-toggle, so it is blockified and max-height does apply. It limits
   the column LENGTH (the inline axis); the column COUNT follows from it. */
.rail-toggle__text {
  writing-mode: vertical-lr; text-orientation: mixed;
  max-height: 3.8rem;
  overflow: hidden; overflow-wrap: break-word;
  letter-spacing: .1em; font-size: .66rem; font-weight: 700; line-height: 1.35;
}

/* ">" affordance telling the user the tab opens. The rotation lives on the span and the nudge
   on the inner svg — two layers, so the two transforms can never overwrite each other.
   THE BOX MUST BE 18px TALL, THE SAME AS `.rail-toggle__icon`. The tab is a centred column flex
   container, so its three children are laid out symmetrically; with an 18px icon and a 14px chevron
   the middle child (the label) sat (18 - 14) / 2 = 2px BELOW the tab's centre — measured label centre
   451.99 / 452.00 against the rail's 450.00 in all three languages. At 18px the label centre measures
   449.99 (-0.01px). The svg inside stays 12px, so the chevron's drawn size is unchanged.
   The width also goes to 18px: the nudge keyframe translates the svg +3px, and inside a 14px box that
   pushed the chevron's own scrollWidth to 16px against a 14px clientWidth (measured, `clip_scan`
   flagged `span.rail-toggle__chev 16>14`). At 18px the svg travels 3..15px, inside the box. */
.rail-toggle__chev { display: grid; place-items: center; width: 18px; height: 18px;
  transition: transform var(--dur) var(--ease); }
.rail-toggle__chev svg { width: 12px; height: 12px; }
.rail-toggle[aria-expanded="true"] .rail-toggle__chev { transform: rotate(180deg); }
.rail-toggle:not([aria-expanded="true"]) .rail-toggle__chev svg {
  animation: rail-chev-nudge 1.8s ease-in-out infinite;
}
@keyframes rail-chev-nudge { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(3px); } }

/* The panel is ALWAYS in the DOM and collapses by WIDTH — never by `display`, and no longer by a
   clip-path wipe. A width transition is what makes the tab read as if it is *dragging* the panel out;
   `display:none` cannot be transitioned and would teleport. It also removes two fragile hacks: no
   forced-reflow read is needed, and nothing depends on `transitionend` (under reduced motion the global
   rule drops the duration to .001ms, which completes instantly instead of stranding the panel mid-way).
   Collapsed baseline: 0 wide horizontally, clipped. Global `box-sizing: border-box` (see top of file)
   means `width: 0` really is 0 — no horizontal padding left behind, so the rail is exactly tab-wide.
   VERTICAL PADDING IS KEPT IN BOTH STATES (`var(--rail-pad) 0` collapsed, `var(--rail-pad)` open).
   `padding: 0` used to drop the panel's border box by 2 x .35rem = 11.2px when it closed, and because
   `.rail-toggle { align-self: stretch }` takes the rail's height from the taller of the two boxes,
   the whole card SHRANK on open: measured 107.25 -> 116.19 (zh) and 119.31 -> 130.50 (en/es), i.e. the
   card jumped 8.94-11.19px the moment the user opened it. Zeroing only the HORIZONTAL padding keeps
   `width: 0` exact while pinning the height in both states. */
.rail-panel {
  --rail-panel-w: min(236px, calc(100vw - 4.75rem));
  --rail-pad: .35rem;
  width: 0; padding: var(--rail-pad) 0; overflow: hidden;
  background: var(--white);
  /* flush with the viewport on the left and with the tab on the right ⇒ square corners */
  border-radius: 0;
  box-shadow: none;
  transition: width var(--dur) var(--ease), padding var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.contact-rail.is-open .rail-panel {
  width: var(--rail-panel-w);
  padding: var(--rail-pad);
  box-shadow: 0 10px 26px rgba(15, 23, 42, .16);
}
/* Data-attribute `hidden` must be re-asserted (same trap that once bit .wx-modal — a `display` rule would win).
   The markup ships `hidden` purely for the NO-JS case; initFloatingUI() clears it and switches to width/inert. */
.rail-panel[hidden] { display: none; }

/* One contact row — an <a> (WhatsApp) and a <button> (WeChat) styled identically. */
.rail-item {
  display: flex; align-items: center; gap: .65rem;
  /* Pinned to the panel's OPEN content width (not `width: 100%`). Inside a 0-wide panel `100%` means 0,
     the title/meta spans re-wrap to one character per line and the panel's auto height explodes —
     measured on the collapsed rail: tab 142 -> 361px (zh), 169 -> 316px (en), 169 -> 336px (es), because
     `.rail-toggle { align-self: stretch }` is dragged along by the taller panel. Fixing the row width
     keeps the panel's height identical in both states, so the tab height is unchanged, and the panel
     uncovers already-laid-out content instead of reflowing text while it animates. When the panel IS
     open this equals the panel's content box, i.e. visually identical to `width: 100%`. */
  width: calc(var(--rail-panel-w, min(236px, calc(100vw - 4.75rem))) - 2 * var(--rail-pad, .35rem));
  padding: .5rem .65rem; border: 0; border-radius: var(--radius-sm);
  background: transparent; font: inherit; text-align: left; cursor: pointer;
  text-decoration: none; color: var(--text-strong);
  transition: background var(--dur-fast) var(--ease);
}
.rail-item:hover { background: var(--bg-soft); }
.rail-item:focus-visible { outline: 3px solid var(--brand-light); outline-offset: -2px; }
.rail-item + .rail-item { border-top: 1px solid var(--border); border-radius: 0 0 var(--radius-sm) var(--radius-sm); }
.rail-item__icon {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 36px; height: 36px; border-radius: 50%; color: #fff;
}
.rail-item__icon svg { width: 20px; height: 20px; }
.rail-item--whatsapp .rail-item__icon { background: #25D366; }
.rail-item--wechat .rail-item__icon { background: #07C160; }
.rail-item__text { display: flex; flex-direction: column; min-width: 0; }
/* `line-height: 1.3` (body is 1.7) is what actually compacts a row vertically. The two lines are
   adjacent flex items with NO gap and NO margin between them, so the visible space between the title
   and the meta line comes entirely from the line boxes. At 1.7 the two line boxes were 24.48 + 20.672
   = 45.152px tall — TALLER than the 36px icon — so the text block, not the icon, drove the row height
   to 64.33px. At 1.3 the block is 18.72 + 15.808 = 34.53px, just under the icon, so the ICON becomes
   the height driver and the row is a predictable 36 + 2 x .5rem = 52px in every language. */
.rail-item__title { font-size: .9rem; line-height: 1.3; font-weight: 700; color: var(--text-strong); }
.rail-item__meta { font-size: .76rem; line-height: 1.3; color: var(--text-soft); }
/* THE CARD HEIGHT IS A PROPERTY OF THE DESIGN, NOT OF THE CAPTION.
   `align-self: stretch` on the tab resolves on the CROSS axis (vertical), so the panel's height drives
   the whole card — and the panel's height is its two rows, one of which has a caption whose line count
   is language-dependent: the WeChat caption is 「扫码或搜索微信号」 (one line, 15.808px) in zh but
   "Scan the QR or search the ID" / "Escanee el QR o busque el ID" (two lines, 31.616px) in en and es.
   The WhatsApp row's caption is a phone number and is one line in every language, so it needs nothing.
   Reserving exactly two lines on the WeChat caption makes the panel's content height 119.31px in all
   three languages (52.00 + 67.31), i.e. the card is 130.51px at >560px and 132.22px at <=560px in zh,
   en and es alike — sized up to the TALLEST language, so nothing is squeezed. Sizing down instead is
   impossible without editing copy or the panel width.
   `2.6em` is exactly two line boxes (`line-height: 1.3` => 2 x 1.3em), so the reservation follows the
   font-size if it ever changes; the zh caption simply gains ~15.8px of trailing air inside its row
   (split above/below by `.rail-item { align-items: center }`), which is the price of a language-invariant
   height and is far cheaper than the old 59.4px blank strip below the last row.
   NOTE: this does NOT replace the pinning of `.rail-item`'s WIDTH to the panel's open content width
   (see above) — that is what keeps the collapsed and expanded heights identical. */
.rail-item--wechat .rail-item__meta { min-height: 2.6em; }

/* WeChat QR dialog */
.wx-modal {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center; padding: 1.25rem;
}
/* MUST override display:grid, otherwise the `hidden` attribute would not hide the dialog. */
.wx-modal[hidden] { display: none; }
.wx-modal__backdrop { position: absolute; inset: 0; background: rgba(11, 16, 70, .55); }
.wx-modal__card {
  position: relative; z-index: 1; background: var(--white); color: var(--text);
  width: min(320px, 100%); max-height: calc(100vh - 2.5rem); overflow: auto;
  border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
  padding: 1.5rem 1.5rem 1.25rem; text-align: center;
  animation: wx-pop .22s var(--ease) both;
}
@keyframes wx-pop {
  from { opacity: 0; transform: translateY(10px) scale(.98); }
  to   { opacity: 1; transform: none; }
}
.wx-modal__close {
  position: absolute; top: .6rem; right: .6rem;
  width: 34px; height: 34px; flex: 0 0 auto; border: none; border-radius: 50%;
  background: var(--bg-soft); color: var(--text-soft);
  display: grid; place-items: center; cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.wx-modal__close:hover { background: var(--border); color: var(--text-strong); }
.wx-modal__close:focus-visible { outline: 3px solid var(--brand-light); outline-offset: 2px; }
.wx-modal__close svg { width: 18px; height: 18px; }
.wx-modal__title { font-size: 1.05rem; color: var(--text-strong); margin-bottom: .9rem; }
.wx-modal__qr {
  display: block; width: 100%; max-width: 260px; height: auto; margin: 0 auto .85rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.wx-modal__hint { font-size: .85rem; color: var(--text-soft); margin-bottom: .6rem; }
.wx-modal__id { display: flex; align-items: center; justify-content: center; gap: .5rem; flex-wrap: wrap; }
.wx-modal__id-label { font-size: .8rem; color: var(--text-soft); }
.wx-modal__id-value {
  font-size: .95rem; font-weight: 700; color: var(--text-strong);
  user-select: all; -webkit-user-select: all;   /* whole ID selectable; it's an ID, not a phone link */
}

@media (max-width: 560px) {
  /* `min-height: 0` for the same reason as the base rule: the tab must be free to be exactly as tall as
     the panel. A floor of 118px would overhang the ~132px panel at this width. */
  .rail-toggle { width: 34px; min-height: 0; padding: .5rem .3rem; }
  /* BOTH LABEL GUARD RAILS ARE DELIBERATELY ABSENT FROM THIS BLOCK NOW.
     The base `max-height: 3.8rem` (60.8px) and the base 18px chevron box apply at every width. Neither
     needs a narrow-width override, and the two that used to live here are what made this breakpoint
     disagree with the others:
       - `.rail-toggle__text { max-height: 3.45rem }` (55.2px) was BELOW es's 55.828px single word
         "Contacto", which is precisely what broke the Spanish label MID-WORD here;
       - `.rail-toggle__chev { width: 14px; height: 14px }` re-introduced the 2px icon/chevron height
         mismatch, so the label was off-centre below 560px even once it was centred above it.
     The budget still holds at this width: the panel measures 132.22px, the tab's non-label parts are
     16 padding + 18 icon + 12.8 gaps + 18 chev = 64.8px, leaving 67.42px for a 60.8px guard.
     (`.rail-toggle__chev svg { width: 12px; height: 12px }` was byte-identical to the base rule and is
     dropped for the same reason.) */
  /* Re-declare the width variable, not `.rail-panel { width }`: a plain `.rail-panel` rule here would
     out-rank the collapsed `width: 0` baseline declared earlier and the panel could never close. */
  .rail-panel { --rail-panel-w: min(210px, calc(100vw - 4rem)); }
  .rail-item { padding: .55rem; gap: .55rem; }
  .rail-item__icon { width: 32px; height: 32px; }
}

/* ===== 20. Placeholder values ===== */
.placeholder-value {
  display: inline-block; font-style: normal;
  background: #FFF8E1; border: 1px dashed #F0B429; color: #8A6100;
  padding: .12rem .55rem; border-radius: 6px; font-size: .92em; font-weight: 600;
}
.site-footer .placeholder-value { background: rgba(240, 180, 41, .16); border-color: rgba(240, 180, 41, .6); color: #FFE08A; }

/* ===== 21. Animations & reveal ===== */
.reveal { opacity: 0; transform: translateY(24px); transition: opacity var(--dur-slow) var(--ease), transform var(--dur-slow) var(--ease); }
.reveal.is-visible { opacity: 1; transform: none; }
.reveal-delay-1 { transition-delay: .08s; }
.reveal-delay-2 { transition-delay: .16s; }
.reveal-delay-3 { transition-delay: .24s; }

/* Language switch fade to avoid content flash */
html.i18n-switching [data-i18n],
html.i18n-switching [data-i18n-placeholder],
html.i18n-switching [data-i18n-alt],
html.i18n-switching [data-i18n-aria] { opacity: .3; transition: opacity 120ms var(--ease); overflow: hidden; }

/* Cold-start guard: the <head> bootstrap adds .i18n-pending; applyLang() removes it at its very end.
   Hiding the i18n text until then stops a non-en visitor from seeing the English fallback swap.
   visibility (not display) keeps the layout, so only the swapping frame is hidden.
   :not([lang="en"]) is deliberate — en text is already aligned byte-for-byte in the HTML, so it needs
   no hiding and must not pay an invisibility window. Relies on the head script setting lang first. */
html.i18n-pending:not([lang="en"]) [data-i18n] { visibility: hidden; }

/* The rule above hides only the i18n TEXT. `visibility` keeps the boxes, and those boxes still
   RESIZE when the language is applied — so the *visible* content around them (logo, hero image,
   cards) is dragged up/down by the same amount. Measured on /index.html: the en-layout body is
   6581px, the zh-layout body 6212px, so a zh visitor saw everything below the fold jump ~369px
   at cold start (CLS 0.164). Hiding the WHOLE page for the same window removes that jump: nothing
   is painted until the text is already correct, so the first visible frame is the final one.
   The window length is unchanged (head bootstrap -> end of applyLang); the 1500ms head timer and
   the !I18N branch in boot() keep it from ever sticking, and `visibility` (not `display`) means
   measuring code (offsetHeight/scrollHeight) still works normally. */
html.i18n-pending:not([lang="en"]) body { visibility: hidden; }

/* ===== 22. Responsive ===== */
/* F6 (layout-fix): on desktop the en/es hero copy column is ~151px taller than the fixed 4:3
   visual, and `align-items: center` left ~76px of dead space above AND below the image. The grid
   switches to stretch and the visual fills the row height; the img keeps the global
   `object-fit: cover` (never `contain` — letterbox regression). Scoped to >1024px: below that the
   hero is single-column with its own explicit aspect-ratio + static-badge layout, which must not
   change. */
@media (min-width: 1025px) {
  .hero-grid { align-items: stretch; }
  .hero-visual { align-self: stretch; }
  .hero-visual .media-frame { height: 100%; aspect-ratio: auto; }
}

@media (max-width: 1024px) {
  .main-nav { display: none; }
  .header-cta { display: none; }
  .menu-toggle { display: flex; }
  .lang-switcher { display: none; }
  /* On desktop .main-nav carried margin-left:auto and pushed everything right.
     With it hidden nothing did, so the hamburger stuck to the logo. Re-anchor here. */
  .header-actions { margin-left: auto; }
  .hero-grid, .detail-hero .container { grid-template-columns: 1fr; }
  /* TRAP: `max-width + margin-inline:auto` on a grid item makes the auto margins eat all
     free space, so the item shrink-wraps to its content instead of stretching — the visual
     (and the absolutely positioned badge inside it) collapsed to ~240px. Use an explicit
     width + justify-self instead. */
  .hero-visual { width: 100%; max-width: 560px; justify-self: center; }
  /* Single column puts the visual BELOW the copy. Take the badge out of absolute
     positioning so it can neither overlap the copy nor collapse into a vertical sliver.
     Kept flush to the RIGHT edge, matching the desktop placement. */
  .hero-badge {
    position: static; left: auto; bottom: auto; right: auto;
    width: fit-content; max-width: 100%;
    margin: 1.1rem 0 0 auto;
  }
  .hero-badge svg { flex: 0 0 auto; }
  /* Single column stacks copy above the (centred) visual. Left-aligned copy on top of a
     centred image reads as broken, so centre the whole copy block and every row inside it.
     `.hero-copy` is capped at 620px, so it also needs justify-self to sit in the middle. */
  .hero-copy { justify-self: center; text-align: center; }
  .hero-sub { margin-inline: auto; }
  .hero-tags { justify-content: center; }
  .hero .btn-row { justify-content: center; }
  .hero-visual .media-frame { aspect-ratio: 16 / 10; }
  .grid-4 { grid-template-columns: repeat(2, 1fr); }
  .process-grid { grid-template-columns: repeat(2, 1fr); }
  .step::after { display: none; }
  .why-grid { grid-template-columns: repeat(2, 1fr); }
  .footer-grid { grid-template-columns: 1fr 1fr; }
  .contact-grid { grid-template-columns: 1fr; }
}

@media (max-width: 768px) {
  :root { --header-h: 66px; }
  .brand-logo-box img { height: 38px; }
  .footer-brand .brand-logo-box img { height: 64px; }
  /* THE HEADER is now the only thing that changes at 768px. The chip tightening and the one-row
     --toc-h used to live in this block; F1 moved them out to the 826px block below, because the bar
     is PINNED here (the client reported it "doesn't stay fixed once the window is narrowed",
     reproduced at 24/45 cells, every width <=768) and the boundary of a pinned bar has to follow the
     metrics it was measured with, not the header breakpoint.
     F2 (2026-09-17) CLOSED THE OTHER HALF. F1 pinned guide.html's bar only and left products' filter
     bar `position: static` here, so it still scrolled out of reach — the client reported exactly that,
     for the second time. Both bars now pin at every width and both take the same tightened metrics at
     <=826, so the two of them can no longer drift apart. */
  /* F3 (2026-09-17) — THE CENTRING OVERRIDE IS GONE. This line read
     `.toc { flex-wrap: wrap; justify-content: flex-start }`, and it was the entire reason both bars
     snapped to a hard left edge the moment the window crossed 768px: the base rule
     (style.css:833) already declares `flex-wrap: wrap` AND `justify-content: center`. The client
     reported the snap — "these two places don't centre once the window is narrowed" — and the report
     reproduces exactly: 481..768 hard left, 769..826 centred, a flip with no visual cause at a 1px
     crossing. Both properties the line set are already on the base rule, so DELETING it restores
     centring at every width and changes nothing else. Do not re-add a `justify-content` override
     here: centred at 320, at 826 and at 1440 is now the invariant.
     Centring is what a hand-measured check is for, because it is invisible to almost every other
     probe: a left-aligned bar and a centred bar have the SAME height, the SAME row count, the same
     `position`, and the same zero horizontal overflow. Only comparing each row's left gap to its
     right gap sees it. That is `node _tools/qa/toc_fit.js` (verify mode). */
  .toc a,
  .toc .chip {
    flex: 0 0 auto;            /* keep chips at their natural width rather than squeezing their label */
    max-width: 100%;           /* a single long label can never overflow the viewport … */
    overflow-wrap: anywhere;   /* … and breaks internally if it still cannot fit */
  }
  .grid-3 { grid-template-columns: 1fr; }
  /* F7 (layout-fix): the flex twin of the single-column rule above — without it an `is-centered`
     grid would keep 3-across flex items below this breakpoint while the plain grids go 1-col. */
  .grid-3.is-centered > * { flex-basis: 100%; }
  .grid-2 { grid-template-columns: 1fr; }
  .split { grid-template-columns: 1fr; }
  .stats-grid { grid-template-columns: repeat(2, 1fr); gap: 2.25rem 1rem; }
  .stat + .stat { border-left: none; }
  .stat:nth-child(odd) { border-right: 1px solid rgba(255, 255, 255, .1); }
  .process-grid { grid-template-columns: 1fr; }
  .why-grid { grid-template-columns: 1fr; }
  .form-row { grid-template-columns: 1fr; }
  .footer-grid { grid-template-columns: 1fr; gap: 1.75rem; }
  /* THERE IS NO PIN SPLIT ANY MORE. F1 used to opt products' bar out right here
     (`#productFilter { position: static }`) so that `nav.toc:not(#productFilter)` could carry the
     tightened metrics for guide alone. Since F2 BOTH bars inherit the base rule's
     `position: sticky; top: var(--header-h)` at every width, and the 826px block below narrows the
     chips instead — that is what keeps a pinned bar affordable (its height budget is stated there).
     NOTHING IN THIS BLOCK MAY RE-INTRODUCE A `position` OVERRIDE FOR EITHER BAR. */
  .description-grid { grid-template-columns: 1fr; }
}

/* ===== RESPONSIVE DATA TABLES (guide.html): below 768px a 3-to-5 column table cannot be read as a
   table, so it becomes one card per row =====
   WHAT WAS WRONG. `.data-table { min-width: 640px }` is a hard floor, so for every viewport under
   ~690px all four tables stayed 640px wide inside a narrower `.data-table-wrap`: measured
   `scrollWidth - clientWidth` up to 357px at 320, in all three languages, on all four tables
   (320..688 overflows). Nothing was permanently unreachable — scrolling the wrapper fully right puts
   the last cell 0.81px inside the box — but the horizontal scrollbar sits at the BOTTOM of a
   ~500px-tall table, so from the reader's first rows it is indistinguishable from cut-off content.
   That is the report: "缩小到一定程度会挡住".
   WHY STACK RATHER THAN SHRINK. min-content widths, the width below which a table cannot stop
   overflowing (`_tools/qa/guide_table_fit.js`): with the `overflow-wrap: break-word` already set
   above, the word-level floor for the 5-column materials table is en 582.94 / es 398.95 / zh 316.05.
   So deleting `min-width` alone changes almost nothing, and tightening the cells to .8rem/.55rem
   still leaves that table overflowing 320..480 (worst 202px). The only CSS-only way to make it fit is
   `overflow-wrap: anywhere`, which buys "no overflow" by breaking words mid-token — at 320px a
   5-column table gives each column ~57px and `Transparencia` lands as two fragments. Dropping the
   columns is the honest fix: one cell per line, each labelled by its column header.
   WHERE THE LABELS COME FROM. main.js's mirrorTableLabels() copies each <thead> cell into the
   matching <tbody> cell's `data-label` and runs after every [data-i18n] pass, so the labels are in
   the same language as the headers, in all three languages, with no second copy of the copy.
   `min-width: 0` below is load-bearing, not cosmetic: the 640px floor survives `display: block`, and
   without the reset the cards themselves would overflow.
   THE FIRST COLUMN IS TREATED AS THE CARD TITLE (its label is suppressed and it is emphasised).
   That holds for all four tables on this page, whose first columns are the key of each row — 产品形态
   / 材质牌号 / 阻隔等级 / 关键词. If a table is ever added whose first column is a VALUE rather than a
   key, drop the two `td:first-child` overrides below rather than shipping a card whose title is a
   value.
   THIS BLOCK MUST STAY AFTER the `@media (max-width: 768px)` block above: same specificity, and the
   overrides here are meant to win.
   F5 RESTYLE (2026-09-17). The client looked at the first cut and said it was not nice to look at and
   should be centred ("有点不好看，而且最好居中显示"). What was actually wrong, all three of it
   measured rather than judged:
     - THE DESKTOP ZEBRA WAS STILL PAINTED. `.data-table tbody tr:nth-child(even)` (section 14) tints
       every even row, and the F4 block never overrode it. Sampled off the client's own screenshot
       (shots/f4_select_412_zh.png, x=770 device = CSS 385, down the stack): #FFFFFF, #F5F7FA,
       #FFFFFF, #F5F7FA. One row per card turns "alternating rows" into "two kinds of card".
     - THE CONTENT WAS LEFT-ALIGNED and the card ran the full 706px container at 767px, so a one-line
       answer sat against one edge of 700px of nothing.
   The cards are now a centred column capped at 560px, cells are centred on both axes, the row key is
   a tinted band, the label is a pill in the brand colour of the <th> it was mirrored from (that
   colour is information, not decoration: the label IS that column's header), and pairs are divided
   by a hairline inset from both edges. Nothing here changes the desktop table, which is why the
   desktop half of guide_table_stack.js still has to pass unchanged. */
@media (max-width: 768px) {
  /* the scroll container has nothing left to scroll once every cell is on its own line */
  .data-table-wrap { overflow: visible; border: 0; border-radius: 0; }
  /* The cards are a CENTRED COLUMN, not a full-bleed strip. At 767px the container is 706px wide, so a
     full-width card leaves a one-line answer stranded against one edge of 700px of nothing — which is
     half of the client's "不好看". The cap only bites above 560px: below that the container is already
     narrower and `max-width` does nothing. */
  .data-table { display: block; min-width: 0; max-width: 560px; margin-inline: auto; background: transparent; }
  /* the real header row must stay in the document for screen readers, but take up no space. It cannot
     simply be `display: none`: the per-cell labels would then be the only copy of the column names,
     and a reader using assistive tech would lose the true table header entirely. */
  .data-table thead { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }
  /* every cell is centred on both axes. The desktop rule this undoes is
     `.data-table th, .data-table td { text-align: left }` up in section 14 — same specificity as the
     line below, so source order alone decides it, and this block is deliberately last. */
  .data-table tbody, .data-table tr, .data-table td { display: block; text-align: center; }
  /* the desktop zebra has to go, and it has to be overridden HERE rather than at the source: it is a
     section-14 rule and the same specificity, so only source order can beat it. Same specificity and
     further down wins. */
  .data-table tbody tr:nth-child(even) { background: #fff; }
  .data-table tr {
    background: #fff; border: 1px solid var(--border); border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm); overflow: hidden;
  }
  .data-table tr + tr { margin-top: 1rem; }
  .data-table td, .data-table tbody tr:last-child td { border-bottom: 0; padding: .85rem 1.35rem; }
  /* belt-and-braces: a token longer than the card must still not push the card wide */
  .data-table td { overflow-wrap: anywhere; font-size: .95rem; line-height: 1.6; color: var(--text-strong); }
  /* The label is a centred pill, not a plain line of text, and it keeps the brand colour of the <th>
     it was mirrored from — it IS that column's header, so the colour is information. `display: block`
     is load-bearing: an inline-block here would sit on the SAME line as the value and read as
     "推荐袋型 自立吸嘴袋". `width: fit-content` is what shrinks the block back down to a pill, and
     fit-content can never exceed the cell it sits in. */
  .data-table td::before {
    content: attr(data-label);
    display: block; width: fit-content; margin: 0 auto .45rem;
    padding: .16rem .62rem; border-radius: var(--radius-pill);
    background: var(--brand-gradient-soft);
    font-size: .7rem; font-weight: 700; letter-spacing: .02em; line-height: 1.45;
    color: var(--brand-primary);
  }
  /* first cell = the card's title band: the row key sits on a tinted header so the card has a top. */
  .data-table td:first-child {
    font-size: 1rem; font-weight: 700; line-height: 1.5; color: var(--brand-primary);
    background: var(--brand-gradient-soft);
    padding: .95rem 1.35rem 1rem;
  }
  .data-table td:first-child::before { display: none; }
  /* a hairline BETWEEN pairs only, and inset from both edges: full-bleed it reads as another table
     row, inset it reads as a divider. Drawn as a background so it adds no box to the layout. */
  .data-table td + td {
    background-image: linear-gradient(90deg, transparent 12%, var(--border) 12%, var(--border) 88%, transparent 88%);
    background-repeat: no-repeat; background-size: 100% 1px; background-position: top center;
  }
}

/* ===== F1. The tightened chip metrics, and the one-row --toc-h, cover every width where DESKTOP
   metrics would wrap =====
   Spanish on desktop metrics needs TWO rows (117.3125px) for every integer width 827..912 and drops
   back to one row at 913. en needs two rows for 827..840. Above 826 the tightening does not apply, so
   --toc-h used to read 68px while the bar was really 117.3125px: the heading landed 49.31px UNDER the
   bar at every one of those 86 widths. That is not a JS-off nicety — initGuideToc() only runs on
   CLICK, so a bookmarked or shared `guide.html#select`, a typed URL, or back/forward navigates
   natively and uses THIS CSS path with JavaScript fully enabled.
   The band edge stays at the METRIC change (826/827), which is the only place the bar's own metrics
   change: the chip row-gap flips 4.48px -> 9.6px there.
   NOTE, REWRITTEN AT F3. It used to say that this metric edge IS ALSO the 1-row -> 2-row flip for
   both en and es "at distance 0.0px". That was true at .66rem and is false now: F3 widened the chips,
   so the 2-row regions end at 625 (en) / 693 (es), far below the edge. No band edge coincides with a
   wrap point any more.
   That tidiness is NOT what makes it safe, though, and the difference matters: the governing rule was
   always "size each band for its own worst case". Both bands are sized for the 2-row bar of their OWN
   metric set (91 >= 86.25 and 120 >= 117.3125), so a row count changing near the edge costs nothing
   either way. What would re-open the defect is a THIRD row at some width >= 553, which needs a >40%
   chip-width increase.
   Raising --toc-h to the band's worst case
   (117.3125px + slack = 120px, declared in :root) is what makes the constant an upper bound. The
   alternative — extending the tightened metrics past 826 as well — would shrink the desktop chips
   from .86rem to .75rem on both bars, a chip-metric change that is out of scope for this pass. F3
   moved the NARROW band's chips and left the desktop ones alone: 827..912 measures 67.95px (1 row) and
   117.31px (2 rows) both before and after.
   BOTH BARS as of F2 — the `:not(#productFilter)` scope is gone, because products' bar is pinned
   too and a pinned bar has to be affordable at every width, exactly as guide's does.
   PLACED BEFORE the 552px block below on purpose: equal specificity, so the narrower band wins by
   source order and needs no !important. */
@media (max-width: 826px) {
  :root { --toc-h: 91px; }
  nav.toc { gap: .28rem; }
  /* BOTH kinds of bar item have to be named. The base rule is `.toc a, .toc .chip`; an override that
     lists only `> a` (guide's anchors) leaves products' `<button class="chip">` at desktop metrics,
     and then the pinned bar is paced by whichever items did NOT shrink. Specificity: `nav.toc .chip`
     is (0,2,1) and outranks the base `.toc .chip` (0,2,0).
     F3 (2026-09-17) — `line-height` is stated here, and THAT, not the font size, is what made the
     increase affordable. guide's items are `<a>` and inherit the global 1.7; products' are
     `<button>` and resolve to `normal` (~1.21). At .66rem that made guide's row 27.95px against
     products' 22.79px — the divergence TRAP 49 recorded — and it is GUIDE's taller row, not the font,
     that sizes every `--toc-h` band. Pinning BOTH to an explicit 1.4 makes the two bars
     metric-identical AND lowers the worst case: the guide bar's 3-row height — the binding one, which
     en and es both reach at 320px — is 117.52px after the change against 120.98px before it. Every
     row count is lower, not just that one: 54.98 / 86.25 / 117.52 versus 56.1406 / 88.5625 / 120.9844.
     The font goes .66rem -> .75rem (10.56px -> 12px). It was the only text on the site below 12px,
     which is what the client reported, and 12px is the conventional floor for a control label.
     `--toc-h` NEEDS NO EDIT. Swept 320..826 at 6px steps plus every critical edge, three languages,
     both bars (`node _tools/qa/toc_fit.js --sweep`); heights are measured bar heights, slack is
     against the shipped band constants 123 / 91:
       candidate       row H    worst <=552 (3 rows)    worst 553-826 (2 rows)
       .66rem/1.7 (*)  27.95     120.98   slack +2.02      88.56   slack +2.44     (* = as shipped)
       .72rem/1.4      26.11     115.45   slack +7.55      84.88   slack +6.12
       .75rem/1.4      26.80     117.52   slack +5.48      86.25   slack +4.75   <-- shipped
       .78rem/1.4      27.47     119.53   slack +3.47      87.59   slack +3.41
       .80rem/1.4      27.91     120.84   slack +2.16      88.47   slack +2.53
     .78rem also fits but spends 1.4px of slack for 0.48px of glyph; .80rem is already back to the
     shipped slack, so .75rem is both the round number and the safe end of the range.
     ROW COUNTS HAD TO BE RE-DERIVED, NOT ASSUMED. The first version of toc_fit.js bisected on
     `rowCount >= to`, which is true on BOTH sides of a flip, so it converged to prev.w+1 and printed
     grid artifacts that looked like precise flip points. With the predicate corrected to
     `rowCount === rows` maximised, the harness reproduces the F1 numbers EXACTLY — 411/412 (zh),
     350/351 and 563/564 (en), 384/385 and 623/624 (es) at .66rem — which is the cross-check that it
     is measuring the same thing the 552px block below was written from.
     With .75rem the bands survive because the ROW COUNTS PER BAND do not change: <=552 still tops out
     at three rows and 553..826 at two. Only the widths at which rows appear move, and they move away
     from the band edge — the 2-row regions now END at 625 (en) / 693 (es) instead of 563/623, so the
     "the band edge is itself a wrap point" note in the block below no longer applies. */
  nav.toc > a,
  nav.toc .chip { padding: .25rem .35rem; font-size: .75rem; line-height: 1.4; }
}

/* The band where the chips need THREE rows, and therefore the third --toc-h value. Measured with the
   tightened metrics above, which are .75rem/1.4 since F3: the 1-row -> 2-row flips sit at 449/450
   (zh), 625/626 (en) and 693/694 (es); the 2-row -> 3-row flips at 383/384 (en) and 423/424 (es).
   The real heights are 54.98 / 86.25 / 117.52.
   RE-MEASURED AT F3, and every number here moved. At .66rem the same flips were 411/412, 563/564,
   623/624, 384/385, 350/351 and the heights 56.1406 / 88.5625 / 120.9844. The flips moved RIGHT (the
   chips are wider) and every height moved DOWN (the rows are shorter, because the explicit 1.4
   replaced an inherited 1.7). The 552 band edge is inherited from an earlier pass — it used to sit
   11px below en's 563/564 flip, and now sits 73px below en's 625/626.
   A copy change moves a flip AT MOST a few tens of px, in a direction where either neighbouring band
   already covers the new row count: shortening pulls en's one-row start down towards 552 (band <=552
   is sized for three rows, so it still covers), lengthening pushes it up into 553..826 (sized for two
   rows, which is what en then needs). Only a copy growth large enough to make a width >= 553 need
   three rows would re-open this, and that is a >40% chip-width increase.
   CAVEAT, stated because it is not measurable here: --toc-h is language-independent by design, so
   inside <=552 zh (one row from 450px) reads 123px where the bar is 54.98px: up to 68.02px of
   overshoot. Overshoot, never concealment. JS is the authoritative path. */
@media (max-width: 552px) {
  :root { --toc-h: 123px; }
}

@media (max-width: 480px) {
  .grid-4 { grid-template-columns: 1fr; }
  .brand-logo-box img { height: 34px; }
  .footer-brand .brand-logo-box img { height: 56px; }
  .hero-tag { font-size: .8rem; }
  .hero-badge { font-size: .8rem; padding: .6rem .85rem; }
  .btn { width: 100%; }
  .btn-row { flex-direction: column; }
  /* Let the pager row wrap so the widest label set (es "Anterior"/"Siguiente") never overflows a 320px
     screen. At >=390px the three items still fit on one row, so this changes nothing there. row-gap only
     applies once it actually wraps, keeping the single-row look at wider sizes byte-identical. */
  .pager { flex-wrap: wrap; row-gap: .6rem; }
}

/* ===== 22b. Products page: narrow-screen "More filters" collapse =====
   At 320px the Spanish chip labels wrap to ~7 rows (~325px tall), pushing the grid far below
   the fold. Below 480px we keep the first three chips and tuck the rest behind a toggle. The
   `is-collapsible` / `is-expanded` classes are set by main.js ONLY, so with JS disabled every
   chip stays visible (no dead-end). Scoped to #productFilter, so guide.html's anchor `.toc`
   is untouched. */
@media (max-width: 480px) {
  .toc-more {
    display: inline-flex; align-items: center; justify-content: center; gap: .3rem;
    padding: .45rem .9rem; border-radius: var(--radius-pill); background: var(--bg-soft);
    font-family: inherit; font-size: .86rem; font-weight: 600; color: var(--brand-primary);
    border: 1px solid var(--brand-light); cursor: pointer;
    transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
                border-color var(--dur) var(--ease);
  }
  /* An author `display` rule beats the UA `[hidden]{display:none}` (author > user-agent, same
     specificity), so the hidden state needs its own rule or the button would always show. */
  .toc-more[hidden] { display: none; }
  /* F2: the toggle is a bar ITEM, so it shrinks with the chips. Left at its own .86rem pill it is the
     widest thing in a collapsed 3-chip bar and paces the row grid single-handedly — measured, the
     collapsed bar is 2 rows / 88px with the fat toggle and 1 row / 51px once it matches the chips.
     Scoped to #productFilter because `.toc-more` only ever exists in products.html's bar.
     The 88px / 51px pair above was measured at .66rem. F3 raises the chips AND the toggle together to
     .75rem/1.4, so they stay one grid, and at .75rem the COLLAPSED bar is one row on every language
     down to 320px (measured: `prod zh 1row 320..480`).
     F3 ALSO REPAIRED A BREAKAGE IT CAUSED HERE. The patch appended this note after the comment had
     already closed, leaving stray prose and a second comment-CLOSE token in the PRELUDE of
     `#productFilter .toc-more`. CSS error recovery does not skip stray top-level text — the parser
     folds it into the next rule's selector, so the selector became invalid and the entire rule was
     DROPPED, leaving the toggle at .86rem (13.76px) beside 12px chips. Nothing else noticed: the row
     counts, the `--toc-h` budget, the centring and the zero-overflow checks all still passed.
     Balanced braces do not detect it, and neither does "each anchor matched once" — both were true
     throughout. The checks that do are the comment-residue assertion in _f3_notes.py, which is why
     this paragraph deliberately does NOT quote the two-character token it is describing, and
     toc_fit.js's toggle-vs-chips comparison, which reported 243/243 cells bad against the broken
     file. */
  #productFilter .toc-more { padding: .25rem .5rem; font-size: .75rem; line-height: 1.4; }
  #productFilter.is-collapsible:not(.is-expanded) .chip:nth-child(n+4) { display: none; }
  /* the selected chip must never be the one that disappears */
  #productFilter.is-collapsible:not(.is-expanded) .chip.is-active { display: inline-flex; }
}
/* Belt + braces: on wider screens the toggle is never shown, even if JS left it un-hidden. */
@media (min-width: 481px) {
  .toc-more { display: none !important; }
}

/* ===== 23. Accessibility & print ===== */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
  .reveal { opacity: 1; transform: none; }
  .wx-modal__card { animation: none; }
  /* No-op safety net: the panel has no animation any more (it is collapsed by WIDTH, and the global rule
     above already cuts the transition to .001ms so it snaps straight to its final width). */
  .rail-panel { animation: none; }
  /* The chevron nudge is `infinite`; the global rule above only shrinks its DURATION (.001ms),
     leaving a 0.001ms x infinite loop. Kill it explicitly, like its siblings above.
     `!important` is required: the base rule's selector `.rail-toggle:not([aria-expanded="true"])
     .rail-toggle__chev svg` has specificity (0,3,1), which beats a plain `.rail-toggle__chev svg`
     (0,1,1) — so a non-important `animation: none` here would silently lose and the nudge would
     keep running (verified: without it, computed animation-name stays `rail-chev-nudge`). */
  .rail-toggle__chev svg { animation: none !important; }
}

@media print {
  .site-header, .mobile-drawer, .drawer-overlay, .back-to-top, .contact-rail, .wx-modal, .toc { display: none !important; }
  body { color: #000; }
  .hero, .page-hero { background: none; color: #000; }
}
