:root {
  /* Official IP palette */
  --azure: #00b7ce;
  --midnight: #191970;
  --white: #ffffff;
  --ice: #e6f6fa;

  /* Working tints derived from the four brand colours */
  --azure-deep: #0092a6;
  --azure-soft: rgba(0, 183, 206, 0.12);
  --midnight-soft: rgba(25, 25, 112, 0.08);
  --ink: #0f1b3d;
  --muted: #5a6b8c;
  --line: #d7e9f2;

  /* ---- site-wide type scale ----------------------------------------
     Every font-size below is calc(size * var(--ts)). Change this one
     number to resize the whole site. Back at 1 after 1.2 read as too
     large on a desktop screen. 1.1 is a gentle lift; 1.5 crowds the
     nav; 2 breaks it onto two lines and splits h1 words on phones.

     Pulled back to 0.94: paired with the narrower --wrap below, 1.1 sized
     every heading and label for a wider container than the page has now,
     which is what read as "too big" rather than compact and considered. */
  --ts: 0.94;

  /* Noto Sans SC sits after the Latin faces in both stacks: Latin glyphs are
     taken from Barlow/Inter as before, and only the characters those faces do
     not contain - every Chinese one - fall through to Noto. Before this that
     fall-through went to whatever face the visitor's OS happened to have, so
     Chinese headings never looked the same on two machines. */
  --font-display: 'Barlow Condensed', 'Arial Narrow', 'Noto Sans SC', sans-serif;
  --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans SC', sans-serif;

  --nav-h: 78px;
  /* Narrowed from 1240: on a wide desktop window that measured content
     lines and card rows well past a comfortable reading width and left
     every section feeling stretched rather than composed. 1140 keeps the
     same 32px side padding but pulls the container in by 100px, which is
     the width difference that reads as "tighter, more considered" without
     forcing any grid or card to reflow. */
  --wrap: 1140px;
  --radius: 20px;
  --shadow-sm: 0 2px 16px rgba(25, 25, 112, 0.06);
  --shadow-md: 0 12px 40px rgba(25, 25, 112, 0.10);
  --shadow-lg: 0 28px 80px rgba(0, 183, 206, 0.18);
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
}

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

html { scroll-behavior: smooth; }

body {
  font-family: var(--font-body);
  font-size: calc(17px * var(--ts));
  line-height: 1.65;
  color: var(--ink);
  background: var(--white);
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; height: auto; display: block; }
a { color: var(--azure-deep); text-decoration: none; }

h1, h2, h3, h4 { line-height: 1.08; font-weight: 800; }

.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 32px; }
.section { padding: 104px 0; }
.section-ice { background: var(--ice); }
.section-mid { background: var(--midnight); color: var(--white); }

/* ---------- shared section furniture ---------- */
.eyebrow {
  display: inline-flex; align-items: center; gap: 9px;
  font-size: calc(12.5px * var(--ts)); font-weight: 700; letter-spacing: 2.6px;
  text-transform: uppercase; color: var(--azure-deep); margin-bottom: 14px;
}
.eyebrow::before {
  content: ''; width: 26px; height: 2px; background: var(--azure); border-radius: 2px;
}
.section-mid .eyebrow,
.flix-section .eyebrow { color: var(--azure); }

.headline {
  font-family: var(--font-display);
  font-size: calc(clamp(38px, 5vw, 62px) * var(--ts));
  font-weight: 800; text-transform: uppercase;
  letter-spacing: -0.5px; line-height: 0.98; color: var(--midnight);
}
.section-mid .headline,
.flix-section .headline { color: var(--white); }

.lede { font-size: calc(18px * var(--ts)); color: var(--muted); margin-top: 16px; max-width: 56ch; }
.section-mid .lede,
.flix-section .lede { color: rgba(255, 255, 255, 0.72); }

.center { text-align: center; }
.center .lede { margin-left: auto; margin-right: auto; }
.center .eyebrow::before { display: none; }

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 15px 30px; border-radius: 12px; border: none;
  font-family: var(--font-body); font-size: calc(15.5px * var(--ts)); font-weight: 700;
  cursor: pointer; transition: transform .25s var(--ease), box-shadow .25s var(--ease), background .2s;
}
.btn-primary { background: var(--azure); color: var(--white); box-shadow: 0 8px 26px rgba(0, 183, 206, 0.34); }
.btn-primary:hover { background: var(--azure-deep); transform: translateY(-3px); box-shadow: 0 14px 38px rgba(0, 183, 206, 0.45); }
.btn-ghost { background: transparent; color: var(--midnight); border: 2px solid var(--line); }
.btn-ghost:hover { border-color: var(--azure); color: var(--azure-deep); transform: translateY(-2px); }
.btn-on-dark { background: rgba(255, 255, 255, 0.1); color: var(--white); border: 1.5px solid rgba(255, 255, 255, 0.28); }
.btn-on-dark:hover { background: rgba(255, 255, 255, 0.18); transform: translateY(-2px); }

/* ---------- navigation ---------- */
.nav {
  position: fixed; inset: 0 0 auto 0; z-index: 900; height: var(--nav-h);
  display: flex; align-items: center;
  background: rgba(255, 255, 255, 0.9);
  backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
  border-bottom: 1px solid rgba(25, 25, 112, 0.07);
  transition: height .3s var(--ease), box-shadow .3s var(--ease);
}
.nav.shrink { height: 64px; box-shadow: 0 4px 26px rgba(25, 25, 112, 0.08); }
.nav-inner {
  width: 100%; max-width: var(--wrap); margin: 0 auto; padding: 0 32px;
  display: flex; align-items: center; gap: 28px;
}
.nav-logo { display: flex; align-items: center; gap: 11px; margin-right: auto; }
.nav-logo img { height: 42px; width: auto; transition: height .3s var(--ease); }
.nav.shrink .nav-logo img { height: 34px; }
.nav-logo-text { font-family: var(--font-display); font-size: calc(24px * var(--ts)); font-weight: 800; color: var(--midnight); letter-spacing: 0.5px; line-height: 1; }
.nav-logo-text small { display: block; font-family: var(--font-body); font-size: calc(9.5px * var(--ts)); font-weight: 600; letter-spacing: 2.2px; color: var(--azure-deep); text-transform: uppercase; margin-top: 3px; }

.nav-links { display: flex; align-items: center; gap: 4px; list-style: none; }
.nav-links a {
  display: block; padding: 9px 15px; border-radius: 9px; line-height: 1;
  font-size: calc(15px * var(--ts)); font-weight: 600; color: var(--ink); transition: all .2s;
}
.nav-links a:hover { background: var(--ice); color: var(--midnight); }
.nav-links a { white-space: nowrap; }
@media (max-width: 1400px) {
  .nav-inner { gap: 16px; padding: 0 22px; }
  .nav-links { gap: 0; }
  .nav-links a { padding: 9px 10px; font-size: calc(14px * var(--ts)); }
  .nav-cta { padding: 9px 16px !important; }
  .nav-logo img { height: 36px; }
  .lang-switch { margin-left: 8px; }
  .lang-switch a { padding: 5px 9px; font-size: calc(12px * var(--ts)); }
}
/* padding matched to .nav-links a's 9px vertical (was 10px) so the pill's
   optical centre sits on the exact same line as the plain nav links either
   side of it, instead of 1px low. line-height: 1 for the same reason the
   nav links carry it — without it the button's own font metrics decide the
   baseline instead of the shared nav row. */
.nav-cta { background: var(--azure) !important; color: var(--white) !important; padding: 9px 24px !important; line-height: 1; border-radius: 50px !important; box-shadow: 0 4px 16px rgba(0, 183, 206, 0.3); }
.nav-cta:hover { background: var(--azure-deep) !important; transform: translateY(-1px); }

.nav-toggle { display: none; background: none; border: none; font-size: calc(24px * var(--ts)); color: var(--midnight); cursor: pointer; padding: 6px; }

/* ---------- language switcher ---------- */
.lang-switch {
  display: flex; align-items: center; gap: 2px; margin-left: 14px;
  border: 1px solid var(--line); border-radius: 50px; padding: 3px; background: var(--white);
}
.lang-switch a {
  display: block; padding: 5px 11px; border-radius: 50px; line-height: 1;
  font-size: calc(12.5px * var(--ts)); font-weight: 700; color: var(--muted);
  letter-spacing: .3px; transition: all .2s var(--ease); white-space: nowrap;
}
.lang-switch a:hover { color: var(--midnight); background: var(--ice); }
.lang-switch a.lang-on { background: var(--azure); color: var(--white); }

/* Chinese and Malay run longer than English — let the nav breathe. */
.lang-zh .nav-links a, .lang-ms .nav-links a { padding: 9px 11px; font-size: calc(14.5px * var(--ts)); }
.lang-ms .nav-inner { gap: 16px; }

/* ---------- Chinese typography ----------
   Barlow Condensed carries no Chinese glyphs, and the display sizes are tuned
   for Latin: 0.94 line-height and negative tracking make CJK collide. Give the
   Chinese pages a proper CJK stack and room to breathe. */
/* Prefixed with `body` on purpose. Plain `.lang-zh h1` ties with section rules
   like `.hero h1` / `.svc-card h3` on specificity, and those come later in this
   file, so they won — the headings fell back to Barlow, which has no Chinese
   glyphs at all. `body.lang-zh h1` outranks every single-class rule here. */
body.lang-zh h1,
body.lang-zh h2,
body.lang-zh h3,
body.lang-zh h4,
body.lang-zh legend,
body.lang-zh .headline,
body.lang-zh .hero-sub,
body.lang-zh .dlv-strip-copy strong {
  /* Noto Sans SC leads now that functions.php actually downloads it. It used
     to sit fourth, behind three system faces, which meant Windows rendered
     these headings in YaHei, macOS in PingFang and Android in something else
     again - the site never looked the same on two machines. A downloaded face
     first makes the rendering identical everywhere; the system faces stay
     behind it as the fallback if the request fails. */
  font-family: 'Noto Sans SC', 'PingFang SC', 'Hiragino Sans GB',
               'Microsoft YaHei', 'Source Han Sans SC', var(--font-body), sans-serif;
  letter-spacing: 0;
}
/* .hero h1 sets letter-spacing:-1px and appears later in this file, so it
   wins on equal specificity — restate it here or CJK gets squeezed. */
/* Raised from a 66px cap: the hero is now a single full-height statement
   rather than a column beside a lorry, and 66px in that space reads as
   timid. Chinese stops below the Latin ceiling on purpose - the glyphs are
   dense, so the same optical weight arrives at a smaller size. */
/* Sized for the split, not the full-bleed hero it replaced: the headline
   now shares the screen with a photograph instead of owning it. */
body.lang-zh .hero h1 { line-height: 1.2; font-size: calc(clamp(32px, 4.4vw, 64px) * var(--ts)); letter-spacing: .01em; }
.lang-zh .headline { line-height: 1.2; font-size: calc(clamp(30px, 3.6vw, 46px) * var(--ts)); }
.lang-zh .hero-sub { letter-spacing: .5px; }
.lang-zh .eyebrow { letter-spacing: 1.6px; }
.lang-zh .card h3 { font-size: calc(20px * var(--ts)); }
.lang-zh .step h4 { font-size: calc(19px * var(--ts)); }
.lang-zh .milestone .year,
.lang-zh .hstat-num { font-family: var(--font-display); }   /* digits stay condensed */

/* Small uppercase labels are sized for Latin. Chinese glyphs carry far more
   detail per character, so the same 11–12px reads as mush — nudge them up. */
.lang-zh .dlv-strip-chip  { font-size: calc(14px * var(--ts)); letter-spacing: .5px; }
.lang-zh .eyebrow         { font-size: calc(13.5px * var(--ts)); }
.lang-zh .svc-tag         { font-size: calc(12.5px * var(--ts)); letter-spacing: .5px; }
.lang-zh .track-node small,
.lang-zh .lang-zh .q-tag           { font-size: calc(13px * var(--ts)); }
.lang-zh .q-quote-ttl,
.lang-zh .q-rates th      { font-size: calc(13px * var(--ts)); letter-spacing: .3px; }
.lang-zh .q-note,
.lang-zh .q-hint          { font-size: calc(13.5px * var(--ts)); }
.lang-zh .q-size-card small { font-size: calc(12.5px * var(--ts)); }
.lang-zh .footer-bottom   { font-size: calc(13.5px * var(--ts)); }

/* ---------- hero ---------- */
.hero {
  position: relative; margin-top: var(--nav-h);
  min-height: calc(100vh - var(--nav-h)); display: flex; align-items: center;
  background: linear-gradient(160deg, var(--ice) 0%, #ffffff 55%, var(--ice) 100%);
  overflow: hidden; padding: 56px 0;
}
/* The lorry is the hero image — it earns the wider column. The copy side still
   holds the Chinese h1 at its largest step without a bad break. */
@keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .35; transform: scale(1.5); } }

.hero h1 {
  font-family: var(--font-display);
  font-size: calc(clamp(46px, 6.4vw, 88px) * var(--ts));
  text-transform: uppercase; color: var(--midnight);
  letter-spacing: -1px; line-height: 0.94;
}
.hero h1 .accent { color: var(--azure); display: block; }
.hero-lede { font-size: calc(17.5px * var(--ts)); color: var(--muted); margin-top: 16px; max-width: 46ch; }
/* width > 100% lets the lorry spill past its column; the flex centring splits
   that overflow evenly, ~41px each side. Left stays inside the 48px gutter,
   right stays inside the page margin — so at 1280px and up nothing is clipped
   and nothing touches the copy. Raising max-width past 700px breaks one or the
   other, which is why this is the ceiling for a side-by-side hero. */
/* trust strip under the hero */

/* ---------- generic card grids ---------- */
.grid { display: grid; gap: 22px; margin-top: 46px; }
.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-5 { grid-template-columns: repeat(5, 1fr); }

.card {
  background: var(--white); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 30px 28px; transition: transform .3s var(--ease), box-shadow .3s var(--ease), border-color .3s;
}
.card:hover { transform: translateY(-7px); border-color: rgba(0, 183, 206, 0.4); box-shadow: var(--shadow-md); }
.card h3 { font-family: var(--font-display); font-size: calc(24px * var(--ts)); text-transform: uppercase; color: var(--midnight); margin-bottom: 9px; letter-spacing: .3px; }
.card p { font-size: calc(15px * var(--ts)); color: var(--muted); }
.card-icon {
  width: 52px; height: 52px; border-radius: 14px; margin-bottom: 18px;
  background: var(--azure-soft); display: flex; align-items: center; justify-content: center;
  font-size: calc(25px * var(--ts)); transition: transform .3s var(--ease);
}
.card:hover .card-icon { transform: scale(1.08) rotate(-5deg); }
.card-num {
  font-family: var(--font-display); font-size: calc(15px * var(--ts)); font-weight: 800;
  color: var(--azure); letter-spacing: 2px; margin-bottom: 10px;
}

/* service cards carry a photo slot */
.svc { padding: 0; overflow: hidden; display: flex; flex-direction: column; color: inherit; }
.svc-cta { margin-top: 14px; font-size: calc(14px * var(--ts)); font-weight: 700; color: var(--azure-deep); display: flex; align-items: center; gap: 6px; }
.svc-cta::after { content: '→'; transition: transform .2s var(--ease); }
.svc:hover .svc-cta::after { transform: translateX(5px); }
.svc-photo {
  aspect-ratio: 4 / 3; background: var(--ice); position: relative; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.svc-photo img { width: 100%; height: 100%; object-fit: cover; transition: transform .6s var(--ease); }
.svc:hover .svc-photo img { transform: scale(1.07); }
.svc-tag {
  position: absolute; top: 13px; left: 13px; z-index: 2;
  background: var(--midnight); color: var(--white);
  font-size: calc(11px * var(--ts)); font-weight: 800; letter-spacing: 1.4px; text-transform: uppercase;
  padding: 5px 12px; border-radius: 7px;
}
.svc-body { padding: 24px 24px 28px; flex: 1; display: flex; flex-direction: column; }
.svc-body p { flex: 1; }
.svc-zh { font-size: calc(13.5px * var(--ts)); font-weight: 700; color: var(--azure-deep); letter-spacing: .5px; margin-bottom: 6px; }

/* ---------- what we carry ---------- */
.cargo-card { padding: 0; overflow: hidden; }
.cargo-photo { position: relative; aspect-ratio: 4 / 3; overflow: hidden; background: var(--ice); }
.cargo-photo img { width: 100%; height: 100%; object-fit: cover; transition: transform .6s var(--ease); }
.cargo-card:hover .cargo-photo img { transform: scale(1.06); }

/* Icon tile riding the seam between photo and text. It lives in the body flow
   and is pulled upward, so the card's overflow:hidden cannot clip it. */
.cargo-badge {
  display: flex; align-items: center; justify-content: center;
  width: 54px; height: 54px; border-radius: 16px;
  margin: -27px 0 14px; position: relative; z-index: 2;
  background: var(--white); color: var(--azure-deep);
  box-shadow: 0 8px 22px rgba(25, 25, 112, 0.16);
  transition: transform .3s var(--ease);
}
.cargo-card:hover .cargo-badge { transform: translateY(-4px) rotate(-6deg); }
.cargo-body { padding: 0 26px 28px; }
.cargo-body h3 {
  font-family: var(--font-display); font-size: calc(26px * var(--ts)); text-transform: uppercase;
  color: var(--midnight); letter-spacing: .3px; margin-bottom: 8px;
}
.cargo-body p { font-size: calc(15px * var(--ts)); color: var(--muted); }

/* the "anything else?" strip under the three cargo types */
.cargo-custom {
  display: flex; align-items: center; gap: 26px; margin-top: 30px;
  padding: 30px 34px; border-radius: var(--radius);
  background: linear-gradient(120deg, var(--midnight) 0%, #10104f 60%, #0c2f52 100%);
  color: var(--white); position: relative; overflow: hidden;
}
.cargo-custom::after {           /* cold glow in the far corner */
  content: ''; position: absolute; right: -80px; top: -110px;
  width: 320px; height: 320px; border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle, rgba(0, 183, 206, 0.28) 0%, transparent 68%);
}
.cargo-custom > * { position: relative; z-index: 1; }
.cargo-custom-icon {
  flex: 0 0 auto; width: 60px; height: 60px; border-radius: 18px;
  background: rgba(0, 183, 206, 0.18); border: 1px solid rgba(0, 183, 206, 0.4);
  color: var(--azure); display: flex; align-items: center; justify-content: center;
}
.cargo-custom-text { flex: 1; }
.cargo-custom-eyebrow {
  display: block; font-size: calc(12px * var(--ts)); font-weight: 700; letter-spacing: 2.4px;
  text-transform: uppercase; color: var(--azure); margin-bottom: 6px;
}
.cargo-custom h3 { font-size: calc(22px * var(--ts)); color: var(--white); margin-bottom: 6px; }
.cargo-custom p { font-size: calc(15px * var(--ts)); color: rgba(255, 255, 255, 0.75); max-width: 62ch; }
.cargo-custom .btn { flex: 0 0 auto; }

/* placeholder shown wherever a real photo is still needed */
.photo-todo {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 6px; padding: 16px;
  background: repeating-linear-gradient(45deg, var(--ice) 0 12px, #dcedf5 12px 24px);
  color: var(--midnight); text-align: center;
}
.photo-todo span { font-size: calc(26px * var(--ts)); opacity: .5; }
.photo-todo small { font-size: calc(11.5px * var(--ts)); font-weight: 700; letter-spacing: .6px; opacity: .72; line-height: 1.4; }

/* ---------- normal vs cold truck comparison ---------- */

/* ---------- process timeline ---------- */
.steps { position: relative; margin-top: 52px; }
.steps::before {
  content: ''; position: absolute; left: 27px; top: 12px; bottom: 12px; width: 2px;
  background: var(--line);
}
.steps-fill {
  position: absolute; left: 27px; top: 12px; width: 2px; height: 0;
  background: linear-gradient(180deg, var(--azure), var(--midnight));
  transition: height .25s linear;
}
.step { position: relative; padding: 0 0 34px 76px; }
.step:last-child { padding-bottom: 0; }
.step-dot {
  position: absolute; left: 12px; top: 0; width: 34px; height: 34px; border-radius: 50%;
  background: var(--white); border: 2px solid var(--line);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display); font-size: calc(15px * var(--ts)); font-weight: 800; color: var(--muted);
  transition: all .35s var(--ease); z-index: 2;
}
.step.on .step-dot { background: var(--azure); border-color: var(--azure); color: var(--white); transform: scale(1.12); box-shadow: 0 0 0 6px rgba(0, 183, 206, 0.16); }
.step h4 { font-family: var(--font-display); font-size: calc(22px * var(--ts)); text-transform: uppercase; color: var(--midnight); letter-spacing: .4px; margin-bottom: 5px; }
.step p { font-size: calc(15px * var(--ts)); color: var(--muted); }

/* ---------- process ----------
   Its own grid, not .about-grid. The two sections shared that class until the
   About redesign widened the picture column, which left this one lopsided —
   six steps squeezed beside a slab of photo. The steps are the content here,
   so they take the wider column. */
/* Column split and photo shape are tuned together. At 1.32/0.68 with a 4:5
   frame the photo came out 384x480 against a 724px column of steps — a third
   of the column was dead space under it. Widening the photo column and using
   the portrait's own 2:3 ratio closes that gap without cropping the image. */
.process-grid { display: grid; grid-template-columns: 1.22fr 0.78fr; gap: 46px; align-items: start; }
/* The slot the video sits in. It used to hold a portrait photograph at 2:3;
   video is shot 16:9 almost without exception, so a portrait frame would
   letterbox every clip. Sticky, so the video stays in view while the reader
   works down the six steps beside it. */
.process-video {
  margin: 0; border-radius: var(--radius); overflow: hidden;
  /* 9:16, matching the footage. This was 16:9 on the assumption that video is
     always shot landscape; the clip supplied is 1080x1920, shot on a phone, and
     a portrait clip in a landscape frame under object-fit: cover shows only a
     narrow band out of its middle. If the video is ever reshot landscape this
     ratio has to change with it. */
  aspect-ratio: 9 / 16;
  /* Capped and centred in its column. Filling a 441px column at 9:16 makes a
     783px-tall frame next to a 512px list of steps, which reads as lopsided;
     at 320px the frame is about as tall as the list beside it. */
  max-width: 320px; justify-self: center;
  position: sticky; top: calc(var(--nav-h) + 22px);
  background: var(--midnight);
}
.process-video video { width: 100%; height: 100%; object-fit: cover; display: block; background: var(--midnight); }

.flix-section { background: linear-gradient(150deg, var(--midnight) 0%, #0d1145 100%); color: var(--white); overflow: hidden; position: relative; }
.flix-grid { display: grid; grid-template-columns: 0.9fr 1.1fr; gap: 52px; align-items: center; }
.flix-stage { position: relative; display: flex; align-items: center; justify-content: center; min-height: 380px; }
.flix-halo {
  position: absolute; width: 330px; height: 330px; border-radius: 50%;
  background: radial-gradient(circle, rgba(0, 183, 206, 0.32) 0%, transparent 68%);
  animation: halo 5s ease-in-out infinite;
}
@keyframes halo { 0%, 100% { transform: scale(1); opacity: .85; } 50% { transform: scale(1.13); opacity: 1; } }
.flix-stage img { position: relative; width: 260px; filter: drop-shadow(0 22px 44px rgba(0, 0, 0, 0.4)); transition: transform .4s var(--ease); }
.flix-stage:hover img { transform: translateY(-10px) scale(1.04); }

.flix-traits { display: grid; grid-template-columns: repeat(2, 1fr); gap: 13px; margin-top: 30px; }
.trait {
  background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.11);
  border-radius: 14px; padding: 16px 18px; transition: all .3s var(--ease); cursor: default;
}
.trait:hover { background: rgba(0, 183, 206, 0.16); border-color: var(--azure); transform: translateY(-3px); }
.trait strong { display: block; font-size: calc(15px * var(--ts)); color: var(--white); margin-bottom: 3px; }
.trait span { font-size: calc(13px * var(--ts)); color: rgba(255, 255, 255, 0.6); }

/* ---------- track box ---------- */
.track-box {
  max-width: 760px; margin: 44px auto 0; background: var(--white);
  border: 1px solid var(--line); border-radius: 24px; padding: 42px;
  box-shadow: var(--shadow-md); text-align: center;
}
.track-flow { display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; margin-top: 34px; padding-top: 30px; border-top: 1px solid var(--line); }
/* Capped so a long label ("Temperature Controlled") wraps instead of stretching
   the row and pushing the five nodes onto two lines. min-height reserves the
   second line on every node, which keeps the icons and the arrows on one
   baseline whether a label wraps or not. */
.track-node { text-align: center; min-width: 92px; max-width: 112px; }
.track-node .ic { font-size: calc(25px * var(--ts)); margin-bottom: 6px; transition: transform .3s var(--ease); }
.track-node:hover .ic { transform: translateY(-4px) scale(1.15); }
.track-node small { display: block; min-height: 2.5em; font-size: calc(11.5px * var(--ts)); font-weight: 700; text-transform: uppercase; letter-spacing: .7px; line-height: 1.25; color: var(--muted); }
.track-arrow { color: var(--line); font-size: calc(17px * var(--ts)); }

/* ---------- about ---------- */
/* Photo-led About. The mosaic gets the wider column: this is the only place
   on the page showing the fleet and the people, and it does more for a buyer
   than another paragraph would. */
.about-grid { display: grid; grid-template-columns: 0.92fr 1.08fr; gap: 46px; align-items: start; }
.about-copy .lede { max-width: 46ch; }

/* Mission and vision side by side — same words as before, half the height. */
.mv-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; margin-top: 28px; }
.mv { border-left: 3px solid var(--azure); padding-left: 18px; margin-top: 0; }
.mv h4 { font-family: var(--font-display); font-size: calc(19px * var(--ts)); text-transform: uppercase; color: var(--azure-deep); margin-bottom: 6px; }
.mv p { font-size: calc(14.5px * var(--ts)); color: var(--muted); line-height: 1.6; }

/* One tall lead frame, two stacked beside it, one wide underneath. Fixed row
   heights keep the mosaic from growing taller than the copy next to it. */
/* Three photos, not four: one tall lead on the left, two stacked beside it.
   The old fourth cell ran full width underneath — with three that rule leaves
   a hole in the second column, so the full-width row is gone. */
.about-mosaic { display: grid; grid-template-columns: 1.2fr 1fr; grid-template-rows: 250px 250px; gap: 14px; }
.about-mosaic figure { margin: 0; border-radius: 16px; overflow: hidden; background: var(--ice); }
.about-mosaic img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .6s var(--ease); }
.about-mosaic figure:hover img { transform: scale(1.06); }
.about-mosaic .m-lead { grid-row: span 2; }

/* Credentials read as a scannable band, not a stacked column. */
/* 2026-09: client asked for the certificate to sit directly under the 2026
   milestone rather than beside the timeline as its own card - front-page.php
   now nests the cert <a> as a fourth child of .ab-milestones, right after
   the foreach, so it is in normal block flow under the last row and needs
   no grid, no end-alignment, and no translateY nudge to land on the 2026
   line; all three are gone. .cert's own base rule below (margin-top,
   padding-top, border-top) is what now separates it from the milestones
   above it. */
.about-band { margin-top: 44px; padding-top: 34px; border-top: 1px solid var(--line); }
.ab-milestones .milestone { padding: 12px 0; }
.ab-milestones .milestone .year { flex: 0 0 62px; font-size: calc(26px * var(--ts)); }
.ab-milestones .milestone p { font-size: calc(14px * var(--ts)); line-height: 1.6; }
.milestone { display: flex; gap: 16px; padding: 18px 0; border-bottom: 1px solid var(--line); }
.milestone:last-child { border-bottom: none; }
.milestone .year { font-family: var(--font-display); font-size: calc(30px * var(--ts)); font-weight: 800; color: var(--azure); line-height: 1; flex: 0 0 74px; }
.milestone p { font-size: calc(15px * var(--ts)); color: var(--muted); }

/* Certification mark under the milestones. The supplied artwork already draws
   its own black keyline, so the wrapper carries no border of its own — two
   nested frames would read as a mistake. */
.cert { display: flex; align-items: center; gap: 20px; margin-top: 22px; padding-top: 22px; border-top: 1px solid var(--line); }
.cert img { flex: 0 0 200px; width: 200px; height: auto; display: block; }
.cert-text strong { display: block; font-family: var(--font-display); font-size: calc(19px * var(--ts)); text-transform: uppercase; color: var(--midnight); letter-spacing: .3px; margin-bottom: 5px; }
.cert-text p { font-size: calc(13.5px * var(--ts)); color: var(--muted); line-height: 1.6; }

/* Team strip under About. Fixed aspect so three photos of different shapes
   still line up; object-fit crops rather than distorts faces. */
.team-strip { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 44px; }
.team-strip figure { margin: 0; border-radius: var(--radius); overflow: hidden; background: var(--ice); aspect-ratio: 4 / 3; }
.team-strip img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .6s var(--ease); }
.team-strip figure:hover img { transform: scale(1.05); }
.lang-zh .cert-text strong { font-size: calc(17px * var(--ts)); }

/* Locations kept the even split it always had; it shared .about-grid until the
   About redesign changed those proportions. */
/* align-items:stretch (the default - no override here on purpose) is what
   lets .map-frame's height:100% below actually mean something: the map
   column matches whichever column - normally the address list - ends up
   taller, instead of the two drifting to different heights. */
.loc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 52px; }
/* ---------- locations ---------- */
/* Each location is a button: clicking it swaps the map beside it. */
.loc {
  display: flex; gap: 15px; width: 100%; text-align: left;
  padding: 20px; border: 1px solid var(--line); border-radius: 16px;
  background: var(--white); margin-bottom: 13px; cursor: pointer;
  font-family: inherit; transition: all .3s var(--ease);
}
.loc:hover { border-color: var(--azure); box-shadow: var(--shadow-sm); transform: translateX(3px); }
.loc .ic { flex: 0 0 auto; color: var(--muted); transition: color .3s var(--ease); }
.loc h4 { font-size: calc(16px * var(--ts)); font-weight: 800; color: var(--midnight); margin-bottom: 4px; }
.loc p { font-size: calc(14px * var(--ts)); color: var(--muted); line-height: 1.55; }

/* Selected location — the one the map is currently showing. */
.loc.active {
  border-color: var(--azure); background: var(--white);
  box-shadow: 0 0 0 2px rgba(0, 183, 206, 0.28), var(--shadow-sm);
}
.loc.active .ic { color: var(--azure); }
.loc.active h4 { color: var(--azure-deep); }
.loc:focus-visible { outline: 3px solid var(--azure); outline-offset: 2px; }
/* height:100% now means something: .loc-grid stretches its row (see above),
   so the map matches whichever column is taller - normally the address
   list - instead of the two drifting apart. min-height is the floor for the
   rare case that column is short. */
.map-frame {
  position: relative; border-radius: var(--radius); overflow: hidden; border: 1px solid var(--line);
  height: 100%; min-height: 380px;
  background: linear-gradient(160deg, var(--azure-soft) 0%, #eef8fb 60%, var(--white) 100%);
}

/* ---------- map: live embed shown directly ------------------------------
   2026-09: client asked to drop the click-to-load step - this used to be a
   stylised SVG coastline with pinned locations, shown by default, with a
   "View live map" button that swapped in the real Google Maps iframe on
   demand. All of that (the illustration, its pins, the button, and the
   crossfade between the two) is gone; .map-embed now carries a real src
   from first paint and is simply the whole frame. */
.map-embed {
  position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%; border: 0; display: block;
}

/* ---------- contact ---------- */
.contact-card {
  background: var(--white); border-radius: var(--radius); padding: 30px;
  border: 1px solid var(--line); text-align: center; transition: all .3s var(--ease);
}
.contact-card:hover { transform: translateY(-6px); box-shadow: var(--shadow-md); border-color: var(--azure); }
.contact-card .ic { font-size: calc(30px * var(--ts)); margin-bottom: 12px; }
.contact-card h4 { font-size: calc(16px * var(--ts)); color: var(--midnight); margin-bottom: 5px; }
.contact-card .val { font-size: calc(17px * var(--ts)); font-weight: 800; color: var(--azure-deep); }
.contact-card small { display: block; font-size: calc(13px * var(--ts)); color: var(--muted); margin-top: 4px; }

/* ---------- app ---------- */
/* Enlarged per the client's note. The phones are flex:1 inside this
   column, so their max-width never binds - the column width is what
   actually sizes them. */
.app-grid { display: grid; grid-template-columns: 0.78fr 1.22fr; gap: 44px; align-items: center; }
.app-feats { list-style: none; margin: 26px 0 30px; }
.app-feats li { display: flex; align-items: center; gap: 12px; padding: 11px 0; font-size: calc(16px * var(--ts)); }
.app-feats .tick { width: 24px; height: 24px; border-radius: 50%; background: var(--azure); color: var(--white); display: flex; align-items: center; justify-content: center; font-size: calc(13px * var(--ts)); font-weight: 800; flex: 0 0 24px; }
.app-badges { display: flex; gap: 14px; flex-wrap: wrap; }
.app-badges img { height: 46px; width: auto; }
.app-shots { display: flex; gap: 20px; justify-content: center; }
.app-shot { flex: 1; max-width: 250px; aspect-ratio: 9 / 19; border-radius: 22px; overflow: hidden; border: 6px solid var(--midnight); background: var(--ice); position: relative; box-shadow: var(--shadow-lg); }
.app-shot img { width: 100%; height: 100%; object-fit: cover; }

/* ---------- footer ---------- */
.footer { background: #060a24; color: rgba(255, 255, 255, 0.7); padding: 68px 0 26px; }
.footer-grid { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1.2fr; gap: 40px; }
.footer h5 { font-size: calc(13px * var(--ts)); font-weight: 800; letter-spacing: 1.6px; text-transform: uppercase; color: var(--white); margin-bottom: 17px; }
.footer ul { list-style: none; }
.footer li { margin-bottom: 10px; font-size: calc(14.5px * var(--ts)); }
.footer a { color: rgba(255, 255, 255, 0.62); transition: color .2s, padding-left .2s; }
.footer a:hover { color: var(--azure); padding-left: 3px; }
.footer-brand img { height: 44px; margin-bottom: 16px; }
.footer-social { display: flex; gap: 10px; margin-top: 18px; }
.footer-social a {
  width: 38px; height: 38px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255, 255, 255, 0.07); border: 1px solid rgba(255, 255, 255, 0.14);
  color: rgba(255, 255, 255, 0.72); transition: all .25s var(--ease);
}
.footer-social a:hover { background: var(--azure); border-color: var(--azure); color: var(--white); transform: translateY(-3px); }
.footer-bottom {
  border-top: 1px solid rgba(255, 255, 255, 0.08); margin-top: 44px; padding-top: 22px;
  display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  font-size: calc(13.5px * var(--ts)); color: rgba(255, 255, 255, 0.42);
}
.footer-views { display: inline-flex; align-items: center; gap: 7px; }


/* ==========================================================================
   COLD CHAIN ATMOSPHERE
   Ambient effects that carry the cold-chain feel. All of them are
   pointer-events:none, sit under the nav, and switch off entirely under
   prefers-reduced-motion (see the bottom of this file).
   ========================================================================== */

/* ---------- ambient snowfall ---------- */
#snow {
  position: fixed; inset: 0; z-index: 700;
  pointer-events: none; opacity: .55;
}

/* ---------- hero frost orbs + drifting ice crystals ---------- */
@keyframes orbFloat {
  0%, 100% { transform: translateY(0) scale(1); opacity: .85; }
  50%      { transform: translateY(-30px) scale(1.08); opacity: 1; }
}

@keyframes crystalSpin { to { transform: rotate(360deg); } }

/* the hero copy has to sit above the atmosphere */
.hero .wrap { position: relative; z-index: 2; }


/* ---------- frost sheen sweeping across a card on hover ---------- */
.card { position: relative; }
.card::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none; opacity: 0; transition: opacity .4s var(--ease);
  background: linear-gradient(115deg, transparent 30%, rgba(255, 255, 255, 0.55) 48%,
              rgba(0, 183, 206, 0.18) 55%, transparent 72%);
  background-size: 260% 100%; background-position: 120% 0;
}
.card:hover::after { opacity: 1; animation: sheen 1.1s var(--ease); }
@keyframes sheen { from { background-position: 120% 0; } to { background-position: -60% 0; } }

/* ---------- delivery status strip: merged into the process flow ---------- */
/* 2026-09: replaces the old "lorry run" band (a flat navy box dropped
   between two var(--ice) sections, which the client circled as feeling
   disconnected). Rather than re-skin that same dark-box-with-truck idea,
   this drops the box entirely: same ice background as the sections on
   either side, a hairline rule picking up where the process steps end,
   and the final step's own copy carried straight through into a live
   status line instead of a separate illustrated band. */
.dlv-strip { background: var(--ice); }
.dlv-strip-inner {
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
  border-top: 1px solid var(--line); margin-top: 40px; padding: 28px 0 8px;
}
.dlv-strip-copy { flex: 0 1 auto; }
.dlv-strip-copy strong {
  display: block; font-family: var(--font-display); font-size: calc(18px * var(--ts));
  color: var(--midnight); letter-spacing: .2px; margin-bottom: 4px;
}
.dlv-strip-copy span { font-size: calc(13.5px * var(--ts)); color: var(--muted); }

.dlv-strip-flow {
  flex: 1 1 80px; min-width: 50px; height: 2px; align-self: center;
  background: repeating-linear-gradient(90deg, var(--azure) 0 8px, transparent 8px 16px);
  animation: dlvFlowDash 1s linear infinite;
}
@keyframes dlvFlowDash { to { background-position: 16px 0; } }

.dlv-strip-home {
  width: 36px; height: 36px; border-radius: 50%; flex: 0 0 auto;
  background: var(--azure-soft); color: var(--azure-deep);
  display: flex; align-items: center; justify-content: center;
}
.dlv-strip-home .bb-steam { animation: dlvSteamRise 2.6s ease-in-out infinite; transform-origin: 50% 100%; }
.dlv-strip-home .bb-steam2 { animation-delay: .4s; }
@keyframes dlvSteamRise {
  0%, 100% { opacity: .25; transform: translateY(2px) scale(.9); }
  50%      { opacity: 1;   transform: translateY(-1px) scale(1); }
}

.dlv-strip-chip { margin-top: 0; flex: 0 0 auto; }

/* ---------- live temperature readout ---------- */
.temp-chip {
  display: inline-flex; align-items: center; gap: 9px;
  background: rgba(0, 183, 206, .1); border: 1px solid rgba(0, 183, 206, .3);
  border-radius: 100px; padding: 8px 16px; margin-top: 18px;
  font-size: calc(13.5px * var(--ts)); font-weight: 700; color: var(--azure-deep);
}
.temp-chip .bead {
  width: 8px; height: 8px; border-radius: 50%; background: var(--azure);
  box-shadow: 0 0 0 0 rgba(0, 183, 206, .6); animation: beadPing 2s infinite;
}
@keyframes beadPing {
  0%   { box-shadow: 0 0 0 0 rgba(0, 183, 206, .55); }
  70%  { box-shadow: 0 0 0 11px rgba(0, 183, 206, 0); }
  100% { box-shadow: 0 0 0 0 rgba(0, 183, 206, 0); }
}
.temp-chip .val { font-variant-numeric: tabular-nums; min-width: 4.4em; }

/* ---------- scroll progress ---------- */

/* ---------- reveal ---------- */
/* Hidden only while JavaScript is running to reveal it again — see the
   bb-js flag in header.php. Without JS the page renders complete. */
.bb-js .rv { opacity: 0; transform: translateY(30px); transition: opacity .7s var(--ease), transform .7s var(--ease); }
.bb-js .rv.in { opacity: 1; transform: none; }

/* ---------- responsive ---------- */
@media (max-width: 1024px) {
  .grid-5, .grid-4 { grid-template-columns: repeat(2, 1fr); }
  .grid-3 { grid-template-columns: repeat(2, 1fr); }
  .hero-grid, .about-grid, .app-grid, .flix-grid, .loc-grid { grid-template-columns: 1fr; gap: 36px; }
  /* .loc-grid stacking loses the height-match with the address column - the
     map is on its own row now, full width and however tall min-height says.
     At this width that row is wide and short, and the map artwork is drawn
     portrait, so a short box would letterbox it with wide pale margins on
     both sides. Taller here keeps the graphic close to filling the row; the
     phone breakpoint below sets its own, shorter height, where the row is
     narrow enough that the same portrait artwork nearly fills it anyway. */
  .map-frame { height: 480px; }
  /* .process-grid belongs here too. Left out when it was split off from
     .about-grid, so between 721px and 1024px it stayed two-column and the
     photo shrank to 236px against an 814px column of steps — 57% dead space.
     Stacked, the portrait keeps its own ratio and is capped so it never
     dominates the section.
     width:100% before max-width, and justify-self instead of auto margins:
     auto margins on a grid item cancel the default stretch, and since the img
     inside is width:100%, the figure then had no width to resolve against and
     collapsed to 0 — the photo never even loaded. */
  .process-grid { grid-template-columns: 1fr; gap: 34px; }
  .process-video { position: static; width: 100%; }
  .flix-stage { min-height: 300px; }
  .nav-links { display: none; }
  .nav-toggle { display: block; }
  .lang-switch { margin-left: auto; margin-right: 10px; }
  .nav-links.open {
    display: flex; flex-direction: column; align-items: stretch; gap: 2px;
    position: absolute; top: 100%; left: 0; right: 0; background: var(--white);
    padding: 14px 22px 20px; box-shadow: var(--shadow-md); border-bottom: 2px solid var(--azure);
  }
  .nav-links.open a { padding: 12px 14px; }
}

@media (max-width: 720px) {
  body { font-size: calc(16px * var(--ts)); }
  .section { padding: 52px 0; }
  .wrap { padding: 0 20px; }
  .grid-5, .grid-4, .grid-3, .grid-2 { grid-template-columns: 1fr; }
  /* The generic card grid (services, process) stacks to one column here, and
     with seven full-photo cards in a row on the services grid a 46px lead-in
     plus a 22px gap between every card added up to real scroll before it
     even reached content. Tighter on a phone, where the columns aren't
     buying back any of that height. */
  .grid { margin-top: 30px; gap: 14px; }
  .svc-body { padding: 18px 20px 22px; }
  .footer-grid { grid-template-columns: 1fr; gap: 30px; }
  .track-box { padding: 26px 20px; }
  .cargo-custom { flex-direction: column; align-items: flex-start; text-align: left; padding: 26px 22px; gap: 18px; }
  .cargo-custom .btn { width: 100%; justify-content: center; }
  .cargo-strip { margin-top: 30px; }
  .map-frame { height: 280px; }
  .cert { flex-direction: column; align-items: flex-start; gap: 14px; }
  .team-strip { grid-template-columns: 1fr; }
  .mv-pair { grid-template-columns: 1fr; gap: 16px; }
  .about-mosaic { grid-template-rows: 180px 180px; }
  .dlv-strip-inner { margin-top: 30px; padding-top: 22px; gap: 14px; }
  .dlv-strip-flow { display: none; }
  /* App showcase: .app-grid already stacks (see the 1024px pass) with a 36px
     gap sized for a two-column layout. Under 720 that gap is just empty
     space above the phones, and the phones themselves used to wrap into
     three full-height rows - each one 250px wide at 9:19 is 527px tall, so
     stacked that's over 1500px of scroll for three screenshots. Sized to sit
     in a single row instead: three at 100px plus two 10px gaps is 320px,
     which clears the wrap's inner width on every phone this site supports. */
  .app-grid { gap: 22px; }
  .app-feats { margin: 20px 0 22px; }
  /* flex:1 with a max-width cap (the base rule) leaves the browser to
     redistribute grow-space across three aspect-ratio boxes with a 0%
     flex-basis - Chromium's third pass drops one of them to a 0-width box
     instead of holding it at the cap. A fixed flex-basis with grow and
     shrink both off sidesteps that redistribution: every phone is exactly
     100px, full stop. */
  .app-shots { flex-wrap: nowrap; gap: 10px; }
  .app-shot { flex: 0 0 100px; max-width: 100px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
  html { scroll-behavior: auto; }
  .bb-js .rv { opacity: 1; transform: none; }

  /* Atmosphere is decoration — remove it rather than freeze it mid-frame. */
  #snow, .frost-orb, .ice-crystal { display: none !important; }
  .card::after { display: none !important; }
  .dlv-strip-flow { background-position: 0 0 !important; }
}

/* ==========================================================================
   QUOTE / ENQUIRY PAGE
   ========================================================================== */
.quote-head { padding: calc(var(--nav-h) + 64px) 0 0; background: linear-gradient(170deg, var(--ice), #fff 70%); }
.quote-head .headline { margin-top: 4px; }
.quote-section { padding-top: 44px; }

.quote-grid { display: grid; grid-template-columns: 1fr 360px; gap: 32px; align-items: start; }

/* ---------- form blocks ---------- */
.q-block { border: 0; padding: 0 0 28px; margin: 0 0 28px; border-bottom: 1px solid var(--line); }
.q-block:last-of-type { border-bottom: 0; }
.q-block legend {
  font-family: var(--font-display); font-size: calc(22px * var(--ts)); font-weight: 800;
  text-transform: uppercase; color: var(--midnight); letter-spacing: .4px;
  padding: 0; margin-bottom: 16px;
}
.q-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.q-field { display: block; margin-bottom: 14px; }
.q-label { display: block; font-size: calc(14px * var(--ts)); font-weight: 700; color: var(--midnight); margin-bottom: 7px; }
.q-label b { color: var(--azure-deep); }

.q-field input, .q-field textarea {
  width: 100%; padding: 13px 15px; font-family: inherit; font-size: calc(15.5px * var(--ts));
  color: var(--ink); background: var(--white);
  border: 1.5px solid var(--line); border-radius: 12px;
  transition: border-color .2s, box-shadow .2s;
}
.q-field input:focus, .q-field textarea:focus {
  outline: none; border-color: var(--azure); box-shadow: 0 0 0 4px rgba(0, 183, 206, .14);
}
.q-field input.q-invalid { border-color: #d8443c; background: #fff6f5; }
.q-hidden { display: none; }

/* ---------- radio pills ---------- */
.q-choices { display: flex; gap: 10px; flex-wrap: wrap; }
.q-choices-wrap { margin-bottom: 4px; }
.q-choice input { position: absolute; opacity: 0; pointer-events: none; }
.q-choice span {
  display: block; padding: 11px 18px; border-radius: 50px; cursor: pointer;
  border: 1.5px solid var(--line); background: var(--white);
  font-size: calc(14.5px * var(--ts)); font-weight: 600; color: var(--muted); transition: all .2s var(--ease);
}
.q-choice span:hover { border-color: var(--azure); color: var(--midnight); }
.q-choice input:checked + span {
  background: var(--azure); border-color: var(--azure); color: var(--white);
  box-shadow: 0 4px 14px rgba(0, 183, 206, .3);
}
.q-choice input:focus-visible + span { outline: 3px solid var(--azure); outline-offset: 2px; }

/* ---------- box size picker ---------- */
.q-sizes { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.q-size input { position: absolute; opacity: 0; pointer-events: none; }
.q-size-card {
  display: block; height: 100%; padding: 16px 14px; cursor: pointer;
  border: 1.5px solid var(--line); border-radius: 14px; background: var(--white);
  transition: all .2s var(--ease);
}
.q-size-card:hover { border-color: var(--azure); transform: translateY(-2px); }
.q-size input:checked + .q-size-card {
  border-color: var(--azure); background: rgba(0, 183, 206, .07);
  box-shadow: 0 6px 18px rgba(0, 183, 206, .16);
}
.q-size-card b { display: block; font-family: var(--font-display); font-size: calc(30px * var(--ts)); color: var(--midnight); line-height: 1; }
.q-size-card em { display: block; font-style: normal; font-size: calc(12.5px * var(--ts)); font-weight: 700; color: var(--azure-deep); margin: 5px 0 8px; }
.q-size-card small { display: block; font-size: calc(11.5px * var(--ts)); color: var(--muted); line-height: 1.5; }
.q-size-none b { color: var(--line); }

/* Reference photo under the size picker. The supplied artwork carries its own
   pale background, so it gets a frame and inset padding rather than sitting
   raw on the white form — otherwise its edge reads as a rendering fault.
   Capped at 560px: the source is 886px wide, which keeps it sharp on 2x
   screens at that size but not at the full 784px column. */
.q-boxfig { margin: 18px 0 0; max-width: 560px; }
.q-boxfig img {
  width: 100%; height: auto; display: block;
  border: 1px solid var(--line); border-radius: 14px; background: var(--white);
}
.q-boxfig figcaption { font-size: calc(12.5px * var(--ts)); color: var(--muted); line-height: 1.6; margin-top: 9px; }

/* ---------- send ---------- */
.q-send { margin-top: 8px; }
.q-submit { font-size: calc(16.5px * var(--ts)); padding: 17px 34px; }
.q-error { color: #c0392b; font-weight: 700; font-size: calc(14.5px * var(--ts)); margin-bottom: 12px; }
.q-preview { margin-top: 16px; }
.q-preview summary { cursor: pointer; font-size: calc(13.5px * var(--ts)); font-weight: 700; color: var(--azure-deep); }
.q-preview pre {
  margin-top: 10px; padding: 16px; border-radius: 12px;
  background: var(--ice); border: 1px solid var(--line);
  font-family: ui-monospace, Menlo, Consolas, monospace; font-size: calc(12.5px * var(--ts));
  color: var(--ink); white-space: pre-wrap; word-break: break-word;
}

/* ---------- sidebar ---------- */
.quote-side { position: sticky; top: calc(var(--nav-h) + 18px); display: grid; gap: 18px; }
.q-panel { background: var(--white); border: 1px solid var(--line); border-radius: var(--radius); padding: 22px; box-shadow: var(--shadow-sm); }
.q-panel h3 {
  display: flex; align-items: center; gap: 9px;
  font-size: calc(15px * var(--ts)); font-weight: 800; color: var(--midnight);
  text-transform: uppercase; letter-spacing: .8px; margin-bottom: 14px;
}
.q-panel h3 svg { color: var(--azure-deep); }
.q-cov-empty p { font-size: calc(14px * var(--ts)); color: var(--muted); }
.q-cov-row { display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; padding: 9px 0; border-bottom: 1px dashed var(--line); font-size: calc(14px * var(--ts)); }
.q-cov-row:last-child { border-bottom: 0; }
.q-cov-row > span { color: var(--muted); flex: 0 0 auto; }
.q-cov-row > div { text-align: right; color: var(--ink); }
.q-price { font-size: calc(20px * var(--ts)); color: var(--azure-deep); }

.q-tag { display: inline-block; margin-top: 4px; padding: 3px 10px; border-radius: 50px; font-size: calc(11.5px * var(--ts)); font-weight: 700; }
.q-tag-ok  { background: rgba(0, 183, 206, .14); color: var(--azure-deep); }
.q-tag-alt { background: rgba(255, 168, 0, .16); color: #a86a00; }
.q-tag-no  { background: rgba(216, 68, 60, .12); color: #c0392b; }
/* East Malaysia: brand blue, not the red of a rejection — the network is
   being planned, and case-by-case shipments are still welcome. */
.q-tag-soon { background: rgba(25, 25, 112, .10); color: var(--midnight); }
.q-bad { color: #c0392b; }

.q-rates { width: 100%; border-collapse: collapse; font-size: calc(14px * var(--ts)); }
.q-rates th { text-align: left; font-size: calc(11.5px * var(--ts)); letter-spacing: .6px; text-transform: uppercase; color: var(--muted); padding-bottom: 8px; border-bottom: 1px solid var(--line); }
.q-rates td { padding: 10px 0; border-bottom: 1px solid var(--line); }
.q-rates tr:last-child td { border-bottom: 0; }
.q-rates td small { color: var(--muted); font-size: calc(11.5px * var(--ts)); margin-left: 4px; }
.q-rates tr.lit td { background: rgba(0, 183, 206, .09); }
.q-note { font-size: calc(12.5px * var(--ts)); color: var(--muted); line-height: 1.6; margin-top: 12px; }

/* ---------- terms ---------- */
.q-terms { margin-top: 44px; padding: 30px 32px; border-radius: var(--radius); background: var(--ice); border: 1px solid var(--line); }
.q-terms h3 { font-family: var(--font-display); font-size: calc(22px * var(--ts)); text-transform: uppercase; color: var(--midnight); margin-bottom: 14px; }
.q-terms ul { list-style: none; }
.q-terms li { position: relative; padding-left: 26px; margin-bottom: 10px; font-size: calc(15px * var(--ts)); color: var(--muted); }
.q-terms li::before { content: '!'; position: absolute; left: 0; top: 1px; width: 18px; height: 18px; border-radius: 50%; background: var(--azure); color: #fff; font-size: calc(11px * var(--ts)); font-weight: 800; display: flex; align-items: center; justify-content: center; }
.q-todo { border-left: 3px solid var(--azure); padding-left: 12px; }

@media (max-width: 1024px) {
  .quote-grid { grid-template-columns: 1fr; }
  .quote-side { position: static; }
  .q-sizes { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 720px) {
  .q-row { grid-template-columns: 1fr; }
  .quote-head { padding-top: calc(var(--nav-h) + 36px); }
  .q-terms { padding: 22px 20px; }
  .q-submit { width: 100%; justify-content: center; }
}

.q-from[hidden] { display: none; }
.q-from { display: inline-block; padding: 8px 16px; border-radius: 50px; background: var(--azure-soft); border: 1px solid rgba(0,183,206,.3); font-size: calc(14px * var(--ts)); color: var(--azure-deep); margin-bottom: 22px; }
.q-from b { color: var(--midnight); }

.q-route { display: block; font-size: calc(11.5px * var(--ts)); color: var(--muted); margin-top: 3px; }

/* ---------- delivery postcode type-ahead ---------- */
.q-typeahead { position: relative; }
.q-hint { display: block; font-size: calc(12.5px * var(--ts)); color: var(--muted); margin-top: 7px; }
.q-suggest {
  position: absolute; left: 0; right: 0; top: calc(100% - 18px); z-index: 30;
  max-height: 302px; overflow-y: auto; list-style: none; margin: 0; padding: 6px;
  background: var(--white); border: 1.5px solid var(--azure); border-radius: 14px;
  box-shadow: 0 18px 44px rgba(25, 25, 112, .18);
}
.q-suggest li {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 10px 12px; border-radius: 10px; cursor: pointer; font-size: calc(15px * var(--ts));
}
.q-suggest li:hover, .q-suggest li.on { background: var(--ice); }
.q-suggest li b { font-variant-numeric: tabular-nums; color: var(--midnight); min-width: 4.6em; }
.q-suggest li span { color: var(--ink); }
.q-suggest li small { margin-left: auto; font-size: calc(11.5px * var(--ts)); color: var(--muted); }
.q-suggest-none { color: var(--muted); font-size: calc(14px * var(--ts)); cursor: default; }
.q-suggest-none:hover { background: none; }

.q-note-soon { background: rgba(25,25,112,.06); border-left: 3px solid var(--midnight); padding: 9px 12px; border-radius: 8px; color: var(--ink); }

/* ---------- shipping guide (collapsed reference lists) ---------- */
.q-guide { margin-top: 34px; padding-top: 30px; border-top: 1px solid var(--line); }
.q-guide h3 { font-family: var(--font-display); font-size: calc(22px * var(--ts)); text-transform: uppercase; color: var(--midnight); letter-spacing: .4px; }
.q-guide-lede { font-size: calc(14.5px * var(--ts)); color: var(--muted); margin: 6px 0 18px; }
.q-guide-block { border: 1px solid var(--line); border-radius: 14px; margin-bottom: 10px; background: var(--white); }
.q-guide-block > summary {
  cursor: pointer; list-style: none; padding: 15px 18px;
  font-size: calc(15.5px * var(--ts)); font-weight: 700; color: var(--midnight);
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.q-guide-block > summary::-webkit-details-marker { display: none; }
.q-guide-block > summary::after { content: '+'; font-size: calc(20px * var(--ts)); font-weight: 400; color: var(--azure-deep); line-height: 1; }
.q-guide-block[open] > summary::after { content: '\2212'; }
.q-guide-block[open] > summary { border-bottom: 1px solid var(--line); }
.q-guide-body { padding: 16px 18px 20px; font-size: calc(14.5px * var(--ts)); color: var(--muted); }
.q-guide-body > p { margin-bottom: 12px; }
.q-guide-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 16px 22px; margin-bottom: 14px; }
.q-guide-ic { width: 46px; height: 46px; object-fit: contain; margin-bottom: 8px; display: block; }
.q-guide-cols h5 { font-size: calc(13px * var(--ts)); font-weight: 800; letter-spacing: .3px; text-transform: uppercase; color: var(--midnight); margin-bottom: 6px; }
.q-guide-cols ul { list-style: none; }
.q-guide-cols li { font-size: calc(13.5px * var(--ts)); line-height: 1.75; padding-left: 13px; position: relative; }
.q-guide-cols li::before { content: ''; position: absolute; left: 0; top: 10px; width: 5px; height: 5px; border-radius: 50%; background: var(--line); }
.q-guide-cols p { font-size: calc(13.5px * var(--ts)); line-height: 1.65; }
.q-guide-steps { margin: 0 0 12px 18px; }
.q-guide-steps li { font-size: calc(14px * var(--ts)); line-height: 1.7; padding-left: 4px; margin-bottom: 5px; }
.lang-zh .q-guide-cols h5 { font-size: calc(14px * var(--ts)); }
.q-note-flag { background: rgba(255,168,0,.10); border-left: 3px solid #ffa800; padding: 9px 12px; border-radius: 8px; color: var(--ink); }

/* quoted rate block — two clean rows instead of wrapped label/value pairs */
.q-quote { margin: 12px 0 4px; padding: 14px; border-radius: 12px; background: var(--ice); }
.q-quote-ttl { display: block; font-size: calc(12px * var(--ts)); font-weight: 700; letter-spacing: .6px;
  text-transform: uppercase; color: var(--muted); margin-bottom: 8px; }
.q-quote-ttl em { font-style: normal; color: var(--azure-deep); }
.q-quote-rows > div { display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; padding: 5px 0; }
.q-quote-rows span { font-size: calc(14px * var(--ts)); color: var(--ink); }
.q-quote-rows b { font-size: calc(21px * var(--ts)); color: var(--azure-deep); font-variant-numeric: tabular-nums; }

/* liability term — stands apart from the ordinary notes */
.q-terms li.q-term-warn {
  margin-top: 14px; padding: 12px 14px 12px 40px; border-radius: 10px;
  background: rgba(216, 68, 60, .07); border-left: 3px solid #d8443c;
  color: var(--ink); font-weight: 500;
}
.q-terms li.q-term-warn::before { left: 13px; top: 13px; background: #d8443c; content: '!'; }

/* ==========================================================================
   Reference documents (quote page)
   Cards, not a bullet list: each row carries an icon, a description and a
   page count, and a bare <a> list gives none of that room. The whole card
   is the link so the tap target on mobile is the full row.
   ========================================================================== */
.q-docs { margin-top: 34px; padding-top: 30px; border-top: 1px solid var(--line); }
.q-docs h3 {
  font-family: var(--font-display); font-size: calc(22px * var(--ts)); text-transform: uppercase;
  color: var(--midnight); letter-spacing: .4px;
}
.q-docs-grid { display: grid; gap: 10px; }

.q-doc {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 16px;
  padding: 15px 18px; border: 1px solid var(--line); border-radius: 14px;
  background: var(--white); text-decoration: none; color: inherit;
  transition: border-color .25s var(--ease), box-shadow .25s var(--ease), transform .25s var(--ease);
}
.q-doc:hover, .q-doc:focus-visible {
  border-color: var(--azure); box-shadow: var(--shadow-sm); transform: translateY(-2px);
}
.q-doc:focus-visible { outline: 2px solid var(--azure-deep); outline-offset: 3px; }

.q-doc-ic {
  display: flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; border-radius: 13px; flex: 0 0 46px;
  background: var(--azure-soft); color: var(--azure-deep);
}
.q-doc-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.q-doc-text strong {
  font-family: var(--font-display); font-size: calc(19px * var(--ts)); font-weight: 700;
  letter-spacing: .3px; color: var(--midnight); line-height: 1.2;
}
.q-doc-b { font-size: calc(13.5px * var(--ts)); line-height: 1.55; color: var(--muted); }
/* Muted, not azure: at 11.5px this is the smallest text on the card, and
   azure-deep on white only reaches 3.7:1. It is also a detail line, so it
   should not share a colour with the view pill next to it. */
.q-doc-meta {
  font-size: calc(11.5px * var(--ts)); font-weight: 700; letter-spacing: .7px; text-transform: uppercase;
  color: var(--muted); font-variant-numeric: tabular-nums; margin-top: 2px;
}

/* Midnight on ice (13.4:1) rather than azure-deep on ice (3.3:1), and midnight
   again on the azure hover fill (6.1:1) rather than white (2.4:1). The pill is
   the action on this card, so it is the one part that must never be marginal. */
.q-doc-dl {
  display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto;
  padding: 9px 15px; border-radius: 999px; background: var(--ice);
  font-size: calc(13px * var(--ts)); font-weight: 700; color: var(--midnight); white-space: nowrap;
  transition: background .25s var(--ease), color .25s var(--ease);
}
.q-doc:hover .q-doc-dl, .q-doc:focus-visible .q-doc-dl { background: var(--azure); color: var(--midnight); }

/* Chinese sets tighter than the condensed Latin face at the same size. */
.lang-zh .q-doc-text strong { font-size: calc(17px * var(--ts)); letter-spacing: 0; }

@media (max-width: 620px) {
  .q-doc { grid-template-columns: auto 1fr; gap: 12px; }
  /* Full-width row under the text, padded to a 44px touch target. */
  .q-doc-dl { grid-column: 1 / -1; justify-content: center; padding: 12px 15px; }
}
@media (prefers-reduced-motion: reduce) {
  .q-doc, .q-doc-dl { transition: none; }
  .q-doc:hover, .q-doc:focus-visible { transform: none; }
}

/* ==========================================================================
   Terms & Conditions page
   Two columns: a sticky contents list and the clauses. Thirteen sections is
   too many to scroll blindly — most visitors arrive looking for one of them.
   ========================================================================== */
.tc-head { padding-bottom: 0; }
.tc-head .lede { max-width: 68ch; }
.tc-meta {
  margin-top: 18px; font-size: calc(13px * var(--ts)); font-weight: 700; letter-spacing: .6px;
  text-transform: uppercase; color: var(--muted);
}
.tc-meta time { color: var(--azure-deep); font-variant-numeric: tabular-nums; }

.tc-body { padding-top: 34px; }
.tc-grid { display: grid; grid-template-columns: 268px 1fr; gap: 54px; align-items: start; }

.tc-toc { position: sticky; top: calc(var(--nav-h) + 22px); }
.tc-toc ol { list-style: none; counter-reset: toc; margin: 0; padding: 0; border-left: 2px solid var(--line); }
.tc-toc li { counter-increment: toc; }
.tc-toc a {
  display: block; padding: 7px 0 7px 16px; margin-left: -2px;
  border-left: 2px solid transparent; font-size: calc(13.5px * var(--ts)); line-height: 1.45;
  color: var(--muted); text-decoration: none;
  transition: color .2s var(--ease), border-color .2s var(--ease);
}
.tc-toc a::before {
  content: counter(toc) '.'; margin-right: 7px; font-weight: 700;
  color: var(--line); font-variant-numeric: tabular-nums;
}
.tc-toc a:hover, .tc-toc a:focus-visible { color: var(--midnight); border-left-color: var(--azure); }
.tc-toc a:hover::before, .tc-toc a:focus-visible::before { color: var(--azure-deep); }

.tc-content { min-width: 0; max-width: 74ch; }
.tc-sec { scroll-margin-top: calc(var(--nav-h) + 20px); padding-top: 30px; }
.tc-sec h2 {
  display: flex; align-items: baseline; gap: 13px;
  font-family: var(--font-display); font-size: calc(25px * var(--ts)); font-weight: 800;
  text-transform: uppercase; letter-spacing: .3px; color: var(--midnight);
  padding-bottom: 11px; border-bottom: 1px solid var(--line); margin-bottom: 4px;
}
.tc-num {
  flex: 0 0 auto; font-size: calc(15px * var(--ts)); font-weight: 800; color: var(--azure-deep);
  font-variant-numeric: tabular-nums;
}

.tc-clauses { list-style: none; margin: 0; padding: 0; }
.tc-clauses li {
  display: grid; grid-template-columns: 46px 1fr; gap: 4px;
  padding: 11px 0; font-size: calc(14.5px * var(--ts)); line-height: 1.72; color: var(--ink);
  border-bottom: 1px solid var(--line-soft, rgba(215, 233, 242, .55));
}
.tc-clauses li:last-child { border-bottom: 0; }
.tc-ref {
  font-size: calc(13px * var(--ts)); font-weight: 700; color: var(--azure-deep);
  font-variant-numeric: tabular-nums; padding-top: 1px;
}

/* The unfilled liability cap. Loud on purpose — it must not reach production
   unnoticed, so it is styled as a defect rather than as ordinary text. */
.tc-todo {
  background: rgba(216, 68, 60, .12); color: #a5251d; font-weight: 700;
  padding: 2px 8px; border-radius: 6px; border: 1px dashed #d8443c;
  font-size: calc(13px * var(--ts)); letter-spacing: .3px;
}

.tc-contact {
  margin-top: 44px; padding: 26px 28px; border-radius: var(--radius);
  background: var(--ice); border: 1px solid var(--line);
}
.tc-contact h3 {
  font-family: var(--font-display); font-size: calc(22px * var(--ts)); text-transform: uppercase;
  letter-spacing: .4px; color: var(--midnight); margin-bottom: 6px;
}
.tc-contact p { font-size: calc(14.5px * var(--ts)); color: var(--muted); margin-bottom: 18px; }
.tc-contact-links { display: flex; flex-wrap: wrap; gap: 12px; }
.tc-contact-links .btn { display: inline-flex; align-items: center; gap: 9px; }

.lang-zh .tc-sec h2 { font-size: calc(21px * var(--ts)); letter-spacing: 0; }
.lang-zh .tc-clauses li { line-height: 1.85; }

@media (max-width: 980px) {
  .tc-grid { grid-template-columns: 1fr; gap: 30px; }
  /* Contents becomes a plain list at the top — sticky is useless once it is
     no longer beside the text it indexes. */
  .tc-toc { position: static; }
  .tc-toc ol { columns: 2; column-gap: 26px; }
}
@media (max-width: 560px) {
  .tc-toc ol { columns: 1; }
  .tc-clauses li { grid-template-columns: 40px 1fr; }
  .tc-contact { padding: 22px 18px; }
  .tc-contact-links .btn { width: 100%; justify-content: center; }
}

/* Terms link in the copyright line — separated so it does not read as part
   of the company name. */
.footer-legal {
  margin-left: 14px; padding-left: 14px; border-left: 1px solid rgba(255,255,255,.22);
  color: rgba(255,255,255,.72); text-decoration: none;
}
.footer-legal:hover, .footer-legal:focus-visible { color: var(--white); text-decoration: underline; }
.q-terms-more { margin-top: 14px; font-size: calc(14px * var(--ts)); }
.q-terms-more a { color: var(--azure-deep); font-weight: 600; text-decoration: none; }
.q-terms-more a:hover, .q-terms-more a:focus-visible { text-decoration: underline; }

/* ==========================================================================
   Hero: the lorry drives in and brings the page with it
   The whole sequence lives inside prefers-reduced-motion: no-preference — the
   copy starts at opacity 0 only where it is going to be animated back in, so
   a reduced-motion visitor gets the finished hero immediately rather than a
   blank column.
   ========================================================================== */
@media (prefers-reduced-motion: no-preference) {

  /* The lorry faces left, so it enters from the right and settles. */
  @keyframes bb-truck-arrive {
    0%   { transform: translateX(52%); opacity: 0; }
    40%  { opacity: 1; }
    82%  { transform: translateX(-1.8%); }
    100% { transform: translateX(0); opacity: 1; }
  }
  /* Suspension, once it has parked. Tiny on purpose: a big float reads as a
     toy rather than a three-tonne reefer. */
  @keyframes bb-truck-settle {
    0%, 100% { transform: translateY(0) rotate(0deg); }
    50%      { transform: translateY(-6px) rotate(-.22deg); }
  }
  @keyframes bb-speed-lines {
    0%   { opacity: 0; transform: translateX(120px) scaleX(.35); }
    30%  { opacity: .5; }
    100% { opacity: 0; transform: translateX(-220px) scaleX(1.6); }
  }
  /* The lorry sweeps in from the right; the copy is dragged along in its
     wake, so it slides the same way rather than rising from below. A touch
     of vertical lift keeps it from reading as a mechanical slide. */
  @keyframes bb-hero-trail {
    from { opacity: 0; transform: translate(54px, 6px); }
    to   { opacity: 1; transform: none; }
  }
    @keyframes bb-road-wipe {
    from { opacity: 0; transform: scaleX(0); }
    to   { opacity: 1; transform: scaleX(1); }
  }

  /* Three motion streaks trailing the cab, gone once it has stopped. */
  /* A soft strip under the wheels so the lorry lands on something. */
  
  /* Copy follows the lorry in, one line at a time. */
  .bb-js .hero-copy > * { opacity: 0; animation: bb-hero-trail .68s cubic-bezier(.2, .8, .25, 1) forwards; }
  .bb-js .hero-copy > *:nth-child(1) { animation-delay: .42s; }
  .bb-js .hero-copy > *:nth-child(2) { animation-delay: .50s; }
  .bb-js .hero-copy > *:nth-child(3) { animation-delay: .58s; }
  .bb-js .hero-copy > *:nth-child(4) { animation-delay: .66s; }
  .bb-js .hero-copy > *:nth-child(5) { animation-delay: .74s; }
  .bb-js .hero-copy > *:nth-child(6) { animation-delay: .82s; }

}

/* ==========================================================================
   Phone pass
   Measured at 375px, not guessed: footer links were 17px tall (a line of text
   with no box to hit), the language pills 31px, and the About mosaic was still
   two columns — 146px is a swatch, not a photograph.
   ========================================================================== */
@media (max-width: 720px) {

  /* 17px -> 37px. The <li> gap shrinks so the column does not get longer. */
  .footer-grid ul a { display: inline-block; padding: 9px 0; line-height: 1.25; }
  .footer-grid ul li { margin-bottom: 0; }

  /* Language pills to a proper thumb target. The nav bar is 78px, so 40px
     fits without crowding the logo or the menu button. */
  .lang-switch a { min-height: 40px; display: inline-flex; align-items: center; padding: 5px 13px; }

  /* Lead photo goes full width on top, the two supporting frames sit under it. */
  .about-mosaic { grid-template-columns: 1fr 1fr; grid-template-rows: 208px 138px; }
  .about-mosaic .m-lead { grid-column: 1 / -1; grid-row: auto; }

  /* The hero was 844px against an 812px screen, so the trust strip never
     showed without a scroll. Trimming the padding brings it into view. */
  .hero { padding: 38px 0 30px; min-height: 0; }
  
  /* Four stages wrap to two rows; centre them so the rows line up. */
  .track-flow { justify-content: center; row-gap: 14px; }

  /* Contact and CTA buttons full width — half-width buttons on a phone are
     a miss waiting to happen. */
  .hero-actions .btn, .tc-contact-links .btn { width: 100%; justify-content: center; }

  /* The legal links in the copyright line were a 16px strip. Give them their
     own row with real padding instead of hanging off the end of a sentence. */
  .footer-legal {
    display: inline-block; margin: 10px 14px 0 0; padding: 10px 2px;
    border-left: 0;
  }
  .footer-bottom { text-align: center; }

  /* Contents list on the legal pages: 34px rows are hard to hit accurately
     when they sit 2px apart. */
  .tc-toc a { padding: 11px 0 11px 16px; }

  /* Quote page: the "what will be sent" toggle and the phone numbers inside
     the guide notes were 22px and 15px strips. */
  .q-preview > summary { padding: 11px 0; }
  .q-note a { display: inline-block; padding: 7px 0; }
}

@media (max-width: 420px) {
  /* Three app screenshots at ~100px each stop being readable; two is enough
     to show what the app looks like. */
  .app-shots > :nth-child(3) { display: none; }
  .app-shots { gap: 14px; }
}

/* Safety net for the hero entrance (see initHeroIntro in bluebox.js): after
   3.4s the finished state is forced, in case the sequence never ran. */
html.bb-intro-off .hero-copy > * { opacity: 1; animation: none; transform: none; }
/* Set by the reveal watchdog in bluebox.js when the IntersectionObserver has
   not done its job. !important and transition:none on purpose: this runs only
   when the normal path has already failed. */
html.bb-rv-off .rv {
  opacity: 1 !important; transform: none !important; transition: none !important;
}

/* ==========================================================================
   Footer contact column
   Three phone numbers, two mailboxes and an address used to sit in one
   undifferentiated list, with the label sometimes before the number and
   sometimes after. Grouped under labels, number first, so the eye runs down
   the digits and the role sits quietly beside them.
   ========================================================================== */
.ftr-label {
  margin: 18px 0 7px; font-family: var(--font-display); font-size: calc(12px * var(--ts));
  font-weight: 700; letter-spacing: 1.4px; text-transform: uppercase;
  color: rgba(255, 255, 255, 0.38);
}
.ftr-label:first-of-type { margin-top: 4px; }

.ftr-contact-list { list-style: none; margin: 0; padding: 0; }
.ftr-contact-list li { margin-bottom: 0; }
.ftr-contact-list a {
  display: flex; align-items: baseline; gap: 9px; padding: 5px 0;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* The role leads the line now, so it carries a little more weight than it did
   as a trailing note - but stays quieter than the number, which is the thing
   someone is actually reaching for. */
.ftr-contact-list a span {
  flex: 0 0 auto; min-width: 5.5em;
  font-size: calc(13px * var(--ts)); color: rgba(255, 255, 255, 0.55); white-space: nowrap;
}
.ftr-contact-list a:hover span { color: rgba(255, 255, 255, 0.7); }

.ftr-address {
  font-size: calc(13.5px * var(--ts)); line-height: 1.6; color: rgba(255, 255, 255, 0.5); max-width: 26ch;
}

@media (max-width: 720px) {
  .ftr-contact-list a { padding: 9px 0; }
  .ftr-label { margin-top: 20px; }
}

/* The C2C card's app screenshot, filling the frame.

   It was stood up like a device in the middle of the card, which left a wide
   band of empty ground either side. Filling means cropping - a 9:19 screen
   cannot cover a 4:3 frame otherwise - so the crop is anchored to the top,
   where the logo, the search bar and the parcel card are. The bottom of the
   screen is empty background in the screenshot anyway. */
.svc-photo-fill img {
  width: 100%; height: 100%; max-width: none;
  object-fit: cover; object-position: top center;
}
.svc:hover .svc-photo-fill img { transform: scale(1.05); }

/* ==========================================================================
   Hero sequence: arrive, then the copy follows
   The lorry keeps its side view and simply drives in and parks; the copy
   comes in behind it. Two richer versions were tried and dropped - turning
   the lorry to show its rear hid the side livery, and opening a tail door
   from a straight side view meant drawing an interior that a side view
   cannot actually see.
   ========================================================================== */
@media (prefers-reduced-motion: no-preference) {
  /* 0.00-0.85  drives in from the right and parks on the left */
  /* 1.10-1.85  the copy leaves through the opening, heading right */
  /* The lorry is back on the right, so the copy leaves it heading left. */
  @keyframes bb-hero-unload {
    from { opacity: 0; transform: translate(78px, 4px) scale(.95); }
    to   { opacity: 1; transform: none; }
  }
  .bb-js .hero-copy > * { animation-name: bb-hero-unload; animation-duration: .70s; }
  .bb-js .hero-copy > *:nth-child(1) { animation-delay: 1.10s; }
  .bb-js .hero-copy > *:nth-child(2) { animation-delay: 1.19s; }
  .bb-js .hero-copy > *:nth-child(3) { animation-delay: 1.28s; }
  .bb-js .hero-copy > *:nth-child(4) { animation-delay: 1.37s; }
  .bb-js .hero-copy > *:nth-child(5) { animation-delay: 1.46s; }
  .bb-js .hero-copy > *:nth-child(6) { animation-delay: 1.55s; }
}

/* Safety net, and reduced motion: the finished state - parked, door open. */
/* The arrive animation must be replaced, not just overridden: a running
   animation outranks a plain declaration, so its opacity:0 first frame
   would keep the lorry invisible however this rule is written. */
/* ==========================================================================
   Section rhythm
   Four sections in a row used to be the same shape: a centred heading over a
   three-column card grid. Two of them now read differently, so the page goes
   rows -> cards -> full-bleed -> split -> dark instead of one block five times.
   ========================================================================== */

/* ---------- why: rows, not cards ---------- */
.why-rows {
  list-style: none; margin: 46px 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px 54px;
}
.why-row {
  display: grid; grid-template-columns: auto 1fr; gap: 20px; align-items: start;
  padding: 24px 0; border-bottom: 1px solid var(--line);
}
/* The last row in each column has nothing under it to separate from. */
.why-row:nth-last-child(-n+2) { border-bottom: 0; }
.why-row-icon {
  display: flex; align-items: center; justify-content: center;
  width: 52px; height: 52px; flex: 0 0 52px; border-radius: 15px;
  background: var(--azure-soft); color: var(--azure-deep);
}
.why-row h3 {
  font-family: var(--font-display); font-size: calc(21px * var(--ts));
  font-weight: 700; letter-spacing: .3px; color: var(--midnight);
  line-height: 1.2; margin-bottom: 6px;
}
.why-row p { font-size: calc(14.5px * var(--ts)); line-height: 1.65; color: var(--muted); }

.why-cta {
  display: flex; align-items: center; justify-content: space-between; gap: 28px;
  flex-wrap: wrap; margin-top: 40px; padding: 34px 38px;
  background: var(--midnight); border-radius: var(--radius);
}
.why-cta h3 {
  font-family: var(--font-display); font-size: calc(26px * var(--ts));
  color: var(--white); line-height: 1.15; letter-spacing: .3px;
}
.why-cta p { font-size: calc(15px * var(--ts)); color: rgba(255, 255, 255, .72); margin-top: 6px; }

/* ---------- cargo: three photographs, edge to edge ---------- */
/* Breaks out of .wrap with a negative margin so the heading above stays on the
   page grid. The page already sets overflow-x: hidden, which is what keeps
   100vw from adding a scrollbar. */
.cargo-strip {
  margin: 48px calc(50% - 50vw) 0;
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 3px;
}
.cargo-panel {
  position: relative; margin: 0; overflow: hidden;
  /* 16:9, not 4:3. These panels are full-bleed, so at 4:3 each one is close
     to 500px tall on a wide screen and the three together run past a full
     screen - too much room for a supporting section. */
  aspect-ratio: 16 / 9; background: var(--midnight);
}
.cargo-panel img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform .8s var(--ease);
}
.cargo-panel:hover img { transform: scale(1.05); }
/* The scrim is what makes white type safe over an unknown photograph. */
.cargo-panel figcaption {
  position: absolute; inset: auto 0 0 0; padding: 30px 30px 28px;
  background: linear-gradient(to top, rgba(6, 9, 26, .92) 0%, rgba(6, 9, 26, .74) 46%, transparent 100%);
  color: var(--white);
}
.cargo-panel-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 42px; height: 42px; border-radius: 12px; margin-bottom: 12px;
  background: rgba(255, 255, 255, .14); color: var(--white);
  backdrop-filter: blur(4px);
}
.cargo-panel h3 {
  font-family: var(--font-display); font-size: calc(24px * var(--ts));
  text-transform: uppercase; letter-spacing: .6px; color: var(--white); margin-bottom: 6px;
}
.cargo-panel p { font-size: calc(14px * var(--ts)); line-height: 1.6; color: rgba(255, 255, 255, .84); max-width: 34ch; }
.lang-zh .cargo-panel h3 { font-size: calc(21px * var(--ts)); letter-spacing: 0; }

@media (max-width: 1024px) {
  .cargo-strip { grid-template-columns: 1fr; gap: 2px; }
  .cargo-panel { aspect-ratio: 16 / 9; }
}
@media (max-width: 720px) {
  .why-rows { grid-template-columns: 1fr; gap: 0; }
  .why-row:nth-last-child(-n+2) { border-bottom: 1px solid var(--line); }
  .why-row:last-child { border-bottom: 0; }
  .why-cta { padding: 26px 22px; }
  .why-cta .btn { width: 100%; justify-content: center; }
  .cargo-panel figcaption { padding: 22px 20px 20px; }
}

/* Scroll position, back where it was before the rail displaced it. */
.progress {
  position: fixed; top: 0; left: 0; height: 3px; width: 0;
  background: linear-gradient(90deg, var(--azure), var(--midnight));
  z-index: 950; transition: width .1s linear;
}

@media (prefers-reduced-motion: reduce) {
  .progress { transition: none; }
}

/* ==========================================================================
   Certification page
   ========================================================================== */

/* The opening paragraph does the work the facts table used to, so it needs
   room to be a paragraph rather than the narrow lede measure. */
.cert-lede { max-width: 74ch; }

/* The certificate itself.
   Capped rather than filling the column: A4 at full width would stand over a
   metre tall on a wide screen, and a document you have to scroll to see is a
   document you do not read. At 820px the whole sheet is about one screenful.
   The white page needs its own edge or it dissolves into the page background,
   hence the border and the shadow rather than a plain <img>. */
.cert-sheet {
  margin: 0 auto; max-width: 820px;
  border: 1px solid var(--line); border-radius: 14px; overflow: hidden;
  background: var(--white); box-shadow: var(--shadow-lg);
}
.cert-sheet img { display: block; width: 100%; height: auto; }

/* The certificate block on the home page is a link now. Without this it would
   pick up the underline and link colour from the base <a> rule. */
.cert-link { text-decoration: none; color: inherit; border-radius: 16px; transition: transform .25s var(--ease); }
.cert-link:hover { transform: translateY(-3px); }
.cert-link:focus-visible { outline: 2px solid var(--azure-deep); outline-offset: 6px; }
.cert-more {
  display: inline-block; margin-top: 9px;
  font-size: calc(13.5px * var(--ts)); font-weight: 700; letter-spacing: .3px;
  color: var(--azure-deep);
}
.cert-link:hover .cert-more { color: var(--midnight); }

/* ==========================================================================
   Round 20
   ========================================================================== */

/* A: the CTA band, one row.
   It was a heading over a sentence with the button off to the right, so on a
   wide screen there was a long empty gap between the copy and the button and
   the band grew two lines taller than it needed to be. On one line the band is
   a strip, and the gap becomes the space that pushes the button to the end. */
.why-cta-row {
  display: flex; align-items: center; gap: 26px; flex-wrap: wrap;
  padding: 24px 30px; text-align: left;
}
.why-cta-row h3 { margin: 0; flex: 0 0 auto; }
.why-cta-row p  { margin: 0; flex: 1 1 220px; min-width: 0; }
.why-cta-row .btn { flex: 0 0 auto; margin: 0; }

/* B: six reasons beside the service levels panel. */
/* text-align is inherited from the .center wrapper that centres the section
   heading. Running text centred on a ragged left edge is hard to read, so
   everything below the heading is set back to left. */
.why-split { display: grid; grid-template-columns: 1fr 340px; gap: 40px; align-items: start; margin-top: 42px; text-align: left; }
.why-split .why-rows { margin-top: 0; }

/* The panel is the only place on the page making a measurable promise, so it
   is dark - it should not read as a seventh reason. */
.svl {
  position: sticky; top: 96px;
  padding: 28px 26px; border-radius: 20px;
  background: var(--midnight); color: var(--white);
  box-shadow: var(--shadow-lg);
}
.svl h3 {
  font-family: var(--font-display); font-size: calc(13px * var(--ts));
  font-weight: 800; letter-spacing: 1.6px; text-transform: uppercase;
  color: #6FE9FA; margin-bottom: 16px;
}
.svl dl { margin: 0; }
.svl-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
  padding: 12px 0; border-bottom: 1px solid rgba(255, 255, 255, .12);
}
.svl-row:last-child { border-bottom: none; padding-bottom: 0; }
.svl-row dt { font-size: calc(14px * var(--ts)); color: rgba(255, 255, 255, .70); }
.svl-row dd {
  margin: 0; font-family: var(--font-display); font-size: calc(17px * var(--ts));
  font-weight: 800; letter-spacing: .3px; color: var(--white);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}

/* D: no video yet, so the steps take the whole width instead of holding an
   empty column open next to them. */
.process-grid-solo { grid-template-columns: 1fr; }
.process-grid-solo .steps { max-width: 780px; }

/* ── CJK typography ───────────────────────────────────────────────────────
   Chinese glyphs are full-width and full-height: they have no ascenders or
   descenders to give a line its air, and no side bearings to give a word its
   gap. Latin display settings applied to them - negative tracking to close up
   capitals, line-height near 1 - make characters collide and lines touch.
   Every Chinese heading needs the opposite of what the Latin ones want. */
.lang-zh h1, .lang-zh h2, .lang-zh h3, .lang-zh h4,
.lang-zh .headline {
  letter-spacing: .02em;
  line-height: 1.28;
}
/* Body copy too: at a given px size Chinese reads smaller than Latin because
   the glyphs fill their box, so it needs both size and leading. */
.lang-zh p, .lang-zh li, .lang-zh dd, .lang-zh dt { line-height: 1.85; }
.lang-zh .why-item p { font-size: calc(15.5px * var(--ts)); }

/* ── Hero ─────────────────────────────────────────────────────────────────
   One photograph, one headline, one sentence, one action, one strip of facts.
   Out: the pill badge, the frost orbs, the ice crystals, the horizontal rule
   and the second button. */
.hero {
  position: relative;
  /* A 16:9 frame, per the client's note — a widescreen window, not a band
     sized off the viewport height.

     No max-height cap on purpose. Capping to the screen is what keeps a hero
     entirely above the fold, but it also stops the box being 16:9 the moment
     the window is anything but 16:9 itself, and 16:9 is the instruction. On a
     1920x1080 screen the frame is 1080 tall under a 78px nav, so the last
     ~14% sits below the fold — normal for a hero banner, and the headline,
     the sentence and the call to action are all bottom-anchored, so every one
     of them is visible without scrolling.

     Expressed as a min-height rather than aspect-ratio: matching the actual
     photo (hero-shot.jpg, 1983x793 — 793/1983 = .4000) on purpose, and the
     difference is not cosmetic. aspect-ratio fixes the height outright, so
     copy taller than the frame overflows — and .hero is justify-content:
     flex-end with overflow: hidden, which means it overflows upward and is
     clipped. The Malay headline wraps to three lines and lost its eyebrow and
     the top of its first line that way. 40vw is .4 of the window, so this
     is the same ratio as the photo in the ordinary case (cover then has
     nothing to crop beyond rounding), but the box can still grow when a
     translation needs the room instead of cutting the words off.

     This used to be 56.25vw (16:9), sized for a since-replaced photo closer
     to that ratio. The new photo (see the .hero-bg img comment below) is a
     much wider 2.5:1 banner shot - keeping 16:9 here would force cover to
     crop nearly a quarter of its width at 1920px just to fill a frame far
     taller than the photo needs, eating into the open band on the left the
     client's own mock-up reserves for the copy. Matching the ratio removes
     that crop instead of fighting it with object-position.

     40vw measures the window including its scrollbar, so the frame runs
     about 1% tall on a scrolling page. That errs toward more room, which is
     the safe direction here.

     560px is the floor for narrow windows/phones, where a photo this wide
     would otherwise force a very short frame - the floor is what makes the
     phone hero portrait instead, at the cost of cover cropping the photo's
     width hard at that floor (see the phone-specific img rule further
     down). */
  min-height: max(560px, 40vw);
  /* stretch, not the align-items: center the old two-column hero left behind -
     on a column flex container that shrink-wraps .wrap and centres it, which
     pulled the copy off the page's left margin. */
  display: flex; flex-direction: column; justify-content: flex-end; align-items: stretch;
  /* Light on top: .hero still carries margin-top: var(--nav-h) from the base
     rule, so the nav is already cleared and a second 132px of padding only ate
     into the frame. */
  padding: clamp(40px, 6vh, 76px) 0 clamp(28px, 4.5vh, 52px); overflow: hidden;
  background: var(--midnight); color: var(--white);
}
.hero-bg { position: absolute; inset: 0; z-index: 0; }
.hero-bg img {
  width: 100%; height: 100%;
  /* Back to object-fit: cover, and this is not the old bug reappearing.
     cover + an artificial zoom-in transform was the "still too zoomed" bug
     from earlier - cover crops the photo to fill the box FIRST, and a
     transform after that only shrinks the already-cropped result, never
     revealing more of the photo. contain was the fix for that, but it
     bought "show the whole photo" at the cost of letterboxing it inside a
     matted card, which then drew its own complaint ("很丑" - a decorative
     dark ground with the photo floating in a box on top of it).

     hero-shot.jpg is a newer photo (the client sent it with a red-outlined
     mock-up of where the copy should sit - a band roughly 2-35% of the
     frame's width, the lorry's cab starting just past it around 37%) -
     1983x793, 2.5:1. The frame above is now sized to that same 2.5:1 (see
     the min-height comment), so cover has nothing left to crop at the
     widths that matter (1440/1920: under a pixel). Only the floor-limited
     range in between full-bleed and the phone breakpoint (roughly
     1081-1400px, where 560px's floor makes the frame taller than 793/1983
     calls for) still crops - see object-position below for how that's
     handled without touching the lorry. */
  object-fit: cover;
  /* 1.02, not .48 and not 1.0 flat - .48 was sized for contain's shrink,
     meaningless here; 1.0 leaves bluebox.js's scroll-parallax nothing to
     pan into without exposing an edge. 1.02 is the standing scroll-parallax
     margin, small enough not to trim any real content off the lorry at
     either edge (checked at 1920/1440/1081px).

     --hero-scale is the single source of truth: the parallax in bluebox.js
     rewrites this element's transform inline on every scroll, so it reads the
     value from here rather than carrying its own copy. */
  --hero-scale: 1.02;
  /* Pinned to the right edge - and getting here took a wrong turn worth
     recording, because the min-height comment above isn't the whole
     story.

     The 40vw floor only matches the photo's own ratio when the copy is
     short enough not to force the frame taller than that - and it isn't,
     at every width. The copy column is capped at min(24vw, 380px): wide
     open above ~1583px (380px flat, same wrap as 1920), but genuinely
     narrower at 1440 and 1081, which wraps the headline and lede onto
     more lines than the 40vw floor has room for. Measured, not assumed:
     the box actually renders at ~800px tall at 1920 (vs. a 768px floor),
     ~796px at 1440 (vs. 576px) and ~782px at 1081 (vs. 432px) - content,
     not the photo, is setting the height across most of this range, and
     a frame that tall next to the photo's 2.5:1 needs real cropping
     (roughly half the photo's width at 1081), not the "under a pixel"
     the 1920 case gets.

     First attempt here was object-position: 83% 50% flat at every width,
     picked to keep the lorry's true right edge in frame even at 1081.
     Rendered and checked against the actual copy column instead of just
     the lorry, that was wrong: at 1081/1440 the same 83% crops the SKY
     away almost entirely - there's so much less of the photo left once a
     ~782-800px-tall frame is done covering a 2.5:1 photo that the cab
     lands barely past the frame's own left edge, and the (unchanged,
     vw-sized) copy column runs 95-150px past it into the lorry itself.
     1920 alone had the room to make 83% look right.

     So this is 100%, not 83% - crop everything from the left before
     touching the lorry at all, at the ONE width (≥1600px-ish) where
     there's little enough to crop that a flat value can just mean "no
     crop, show it all". The 1081-1600px band needs a different number
     entirely, in its own rule further down - see the media query right
     after this one, and its comment, for why a single object-position
     can't cover both ranges. */
  object-position: 100% 50%;
  transform: scale(var(--hero-scale)); transform-origin: 50% 50%; will-change: transform;
}
/* The 1081-1600px band: measured against the copy column, not just the
   lorry's own edges, because 5.5.5's first pass at this only checked the
   latter and still overlapped.

   In this range the frame's content-driven height (~782-800px, see the
   comment on the rule above) is so much taller than what a 2.5:1 photo
   needs at these widths that cover has to crop roughly a quarter to a
   half of the photo's width just to fill it - far more than the ~1920px
   case, where there's barely anything to crop. 100% (crop everything
   from the left first) was tuned against that easy case; run at 1081 or
   1440 it crops so much of the open road away that the cab lands within
   a few pixels of the frame's own left edge - directly under the copy
   column, which is sized in vw and has no idea the crop moved.

   45% here isn't trying to show the whole lorry - at this frame height
   there isn't room to do that AND keep the copy clear of it, and the
   client's "see the whole lorry" photo swap was checked against the
   1920px case primarily (see functions.php's 5.5.5 entry). It's the
   number that keeps a real gap between the copy column's own right edge
   and wherever the cab lands, checked at both 1440 and 1081 against the
   actual rendered column width (not assumed) - the trade is showing
   less of the lorry's rear (the QR panel, "JAWATAN KOSONG") in exchange
   for the copy never sitting on top of it. */
@media (max-width: 1600px) {
  .hero-road:not(.hero-split):not(.hero-has-lorry) .hero-bg img {
    object-position: 45% 50%;
  }
}
/* Client's own description: left to right, a darker blue easing through a
   lighter blue into full transparency, not the near-black (rgba(6,10,32))
   this used to be - "从左边到右边是从较深的蓝慢慢变成浅蓝然后再慢慢变透明,
   不要有深蓝色的颜色". Both stops are the brand's own azure family (--azure-
   deep, --azure) at partial opacity, not navy and not black. Transparent by
   34% - short of where the lorry now starts (~37%, cover fitting the
   photo close to life-size, see the img rule above) - so the tint sits over
   the open road and skyline beside the lorry and never touches the lorry
   itself. Still two layers: the azure fade carries the legibility job left
   to right, and a light vertical fade at the foot (kept from the previous
   version, recoloured to match) stops the photo from floating against the
   bottom edge. */
.hero-bg::after {
  content: ''; position: absolute; inset: 0;
  background:
    linear-gradient(to right, rgba(0, 146, 166, .58) 0%, rgba(0, 183, 206, .30) 18%, rgba(0, 183, 206, 0) 34%),
    linear-gradient(to top, rgba(0, 92, 106, .32) 0%, rgba(0, 92, 106, 0) 40%),
    /* One more layer, added on top of the two above rather than touching
       them - "背后多一层深一点的颜色，不然看不到字，加多一层浅浅的颜色就好
       不要改其他东西". A flat, low-opacity wash (same stop colour twice, so
       it's uniform rather than a fade) over the whole frame, not just the
       zones the two gradients above already cover - still the azure-deep
       family, no navy. */
    linear-gradient(rgba(0, 40, 46, .18), rgba(0, 40, 46, .18));
}
/* ── The lorry, laid over the photograph ──────────────────────────────────
   Treated as a foreground cut-out, not as a vehicle standing on the road,
   and every value below follows from that decision.

   It has to be, because the two do not share a camera. The plate is a drone
   shot looking down a carriageway maybe three lanes wide at the bottom of the
   frame; the artwork is a flat side elevation. At the size the brief asks for
   the lorry is wider than the road it would be parked on, so any attempt to
   seat it in the scene reads as a mistake. Sized large, bled off the right
   edge and given its own shadow, it reads instead as what it is — the subject,
   in front of a landscape — which is a deliberate look rather than a failed
   composite.

   Replacing hero-road-clean.jpg with a photograph that has a real lorry in it
   (see the note in front-page.php) retires all of this automatically.

   z-index 1: over the photograph, under the copy. The two never meet at any
   width, but if a translation ever runs long the words win. */
.hero-lorry {
  position: absolute; z-index: 1;
  /* Bled off the right edge. The cab is what leaves the frame and the box
     body is what stays, which is the half that carries the mascot, the QR
     code and the phone number. */
  right: -4%; bottom: 4%;
  /* 812px is the artwork's own width, so the ceiling is set just above it —
     past that the QR code starts to soften and stops scanning reliably. The
     percentage is what governs below about 1700px; the cap only bites on a
     full-width 1920 screen, and then by 8%. */
  width: min(52%, 880px);
  pointer-events: none;
}
.hero-lorry img { width: 100%; height: auto; display: block;
  filter: drop-shadow(0 26px 34px rgba(0, 0, 0, .46)); }

/* Hold the copy clear of it. .hero-copy is a .wrap and cannot simply be
   narrowed — .wrap carries margin: 0 auto, so a max-width on it would centre
   the column instead of shortening it. The children take the limit. */
/* Note the percentage resolves against .wrap's 1176px content box, not the
   viewport — .hero-copy and .hero-meta are both inside a .wrap that is capped
   at --wrap and centred, so 52% is ~611px on any screen 1366 and wider, not a
   share of the window. Measured against the lorry's left edge it leaves 22px
   at 1366, which is the tightest case; everything wider has more.

   The trust strip runs to two rows at every width as a result. That is the
   honest outcome, not a compromise waiting to be tuned out: the three facts
   need about 770px side by side and the lorry never leaves that much, and the
   Chinese and Malay strings are longer again. Two rows survives all three
   languages; a cap wide enough for one row in English puts BM through the
   lorry's door. */
.hero-has-lorry .hero-copy > *,
.hero-has-lorry .hero-meta { max-width: min(52%, 660px); }

/* Two bugs in the first attempt at this, both from copying the .hero-split
   pattern without copying the reason it works. First, "%" resolves against
   .hero-copy's own box, which .wrap's max-width: 1240px caps and centres -
   at 1920px that box is 1140px wide, so "30%" was 30% of 1140px (~342px),
   not of the viewport the lorry's own position is measured against. Second,
   even fixed to vw, the centred wrap's left edge is not a fixed fraction of
   the viewport - it grows toward viewport/2 as the window widens past
   1240px, which is a different, more generous gutter than the lorry's own
   37%-of-viewport start, and no single vw cap fits both relationships.

   Same fix .hero-split already needed for the same underlying reason (see
   its own copy of this comment, above): stop measuring from the centred
   container and measure from the window instead. max-width: none plus a
   viewport-relative padding-left puts the column's start at a fixed vw
   gutter; the max-width below is then a vw fraction of the same viewport
   the photograph and the curve are already measured against, so the two
   stay in a fixed relationship at every width instead of drifting apart
   as the window resizes. Checked at 1920/1440/1281/1081px against the
   lorry's actual left edge (~37vw), not assumed. */
.hero-road:not(.hero-split):not(.hero-has-lorry) .hero-copy,
.hero-road:not(.hero-split):not(.hero-has-lorry) > .wrap:last-of-type {
  max-width: none; padding-left: clamp(22px, 5vw, 88px); padding-right: 0;
}
.hero-road:not(.hero-split):not(.hero-has-lorry) .hero-copy > *,
.hero-road:not(.hero-split):not(.hero-has-lorry) .hero-meta { max-width: min(24vw, 380px); }
/* "大一点" - client asked for the headline bigger. The first pass here
   (clamp(28px, .85rem+2.3vw, 60px)) was checked with canvas measureText and
   looked safe, but that method turned out to give false-safe readings -
   real DOM layout (span.scrollWidth vs the line's clientWidth, the only
   ground-truth check for this) showed "Dari Pengambilan" actually clipping
   by up to 37px at 1920 and 19px at 1081. Root cause: the column
   (min(24vw, 380px)) shrinks with the viewport, and at 1081/1920 that word
   was already right at the edge even before this change - the base clamp
   below is the largest step up that still clears EN at every width, and
   Malay gets its own smaller override just below since "Pengambilan" can't
   take the same increase without breaking. Chinese is untouched - it still
   runs on the separate body.lang-zh rule above. */
.hero-road:not(.hero-split):not(.hero-has-lorry) h1 { font-size: clamp(29px, .88rem + 2.4vw, 60px); }
/* Malay's longest word ("Pengambilan") is already at its column's edge at
   the pre-existing size - the base increase above overflows it, so it gets
   its own, smaller, DOM-verified-safe clamp instead of losing the "bigger"
   change entirely. */
html[lang^="ms"] .hero-road:not(.hero-split):not(.hero-has-lorry) h1 { font-size: clamp(26px, .79rem + 2.08vw, 53.2px); }

/* The headline comes down with the column. clamp(46px … 128px) was drawn for
   a hero the copy had to itself; asking it to also fit half the width is what
   turns a two-line English headline into a five-line Malay one — 587px of h1,
   and a frame that grew to 1114px to hold it. Sized for the split instead:
   82px at 1920 still carries the section, and every language lands in two or
   three lines.

   Doubled up as .hero.hero-has-lorry on purpose. Both classes are on the same
   element, so this reads as a redundant selector — it is not. `.hero h1`
   further down this file ties it on specificity at (0,1,1) and wins on order,
   which is how this rule silently did nothing at first. (0,2,1) settles it
   wherever either rule sits.

   Chinese is untouched — body.lang-zh .hero h1 is (0,3,1) and still outranks
   this. It already carries its own step, because CJK arrives at the same
   optical weight at a smaller size. */
.hero.hero-has-lorry h1 { font-size: clamp(38px, 1.2rem + 3.4vw, 82px); }

/* width:100% is load-bearing. .wrap carries margin: 0 auto, and an auto
   margin on a flex item's cross axis beats align-items: stretch - without
   a definite width the wrap shrink-wraps to the headline and floats in
   the middle of the hero instead of starting at the page margin. */
.hero > .wrap { position: relative; z-index: 2; width: 100%; }

.hero-label {
  display: inline-flex; align-items: center; gap: 12px;
  font-size: calc(12.5px * var(--ts)); font-weight: 600; letter-spacing: .18em;
  text-transform: uppercase; color: #8fe9f6;
}
.hero-label::before { content: ''; width: 28px; height: 1px; background: currentColor; opacity: .6; }

.hero h1 {
  font-size: clamp(46px, 1.8rem + 6vw, 128px); font-weight: 900;
  line-height: 1.06; letter-spacing: -.02em;
  margin: clamp(14px, 2.4vh, 24px) 0 clamp(18px, 3.4vh, 32px); max-width: 15ch; color: var(--white);
}
.lang-zh .hero h1 { line-height: 1.16; letter-spacing: .01em; }
.hero h1 .accent { color: #6FE9FA; }
.hero .hero-lede {
  font-size: clamp(19px, 1.05rem + .5vw, 24px); line-height: 1.6;
  /* ch is the width of a '0'; a Chinese glyph is about two of them, so 42ch
     measures ~636px in Chinese against ~380px in English. Capped in px too. */
  color: rgba(255,255,255,.86); max-width: min(42ch, 500px); opacity: 1;
}

/* A text link, not a filled pill. One action, stated once. */
.hero-cta {
  display: inline-flex; align-items: center; gap: 12px; margin-top: clamp(22px, 4vh, 40px);
  font-size: calc(14.5px * var(--ts)); font-weight: 600; letter-spacing: .04em;
  color: var(--white); text-decoration: none; padding-bottom: 10px;
  border-bottom: 1px solid rgba(255,255,255,.45);
  transition: border-color .5s var(--ease), gap .5s var(--ease);
}
.hero-cta:hover { border-color: #6FE9FA; gap: 20px; }
.hero-cta svg { transition: transform .5s var(--ease); }
.hero-cta:hover svg { transform: translateX(4px); }

.hero-meta {
  position: relative; z-index: 2; margin-top: clamp(22px, 4.4vh, 72px);
  border-top: 1px solid rgba(255,255,255,.22); padding-top: 20px;
  display: flex; flex-wrap: wrap; gap: 14px 48px;
  font-size: calc(12px * var(--ts)); letter-spacing: .12em; text-transform: uppercase;
  color: rgba(255,255,255,.62);
}
.hero-meta strong { color: var(--white); font-weight: 600; }

/* ══ Split hero ═══════════════════════════════════════════════════════════
   White copy panel left, photograph right, one curved edge between them.

   Everything here is scoped to .hero-split. The full-bleed treatment above is
   untouched and still the fallback: remove the class in front-page.php and the
   hero goes back to light type over a dark photograph, with nothing left to
   unpick here.

   Flat --midnight read as flat, not "layered" - client asked for a gradient
   with depth instead of the solid navy. Two layers, not one: a soft azure
   glow seated in the top-right corner (nearest the curve and the photo, so
   it reads as light spilling in from that side) over a diagonal navy-to-
   deep-azure sweep, darkest at the top-left corner where the headline
   starts. Kept dark through the whole left two-thirds on purpose — the
   glow is a top-right accent, not a wash over the panel, so the white
   type and the azure CTA button both still sit on a dark-enough ground to
   read clearly; a brighter overall gradient started to fight the button's
   own colour. Type flips to light-on-dark to match, the same direction as
   the full-bleed hero's own white-on-photo type below, so this reads as a
   variant of the same brand rather than a second theme. */
.hero-split {
  background:
    radial-gradient(130% 110% at 100% 0%, rgba(0, 183, 206, .38) 0%, rgba(0, 183, 206, 0) 55%),
    linear-gradient(150deg, #0c1030 0%, var(--midnight) 55%, var(--azure-deep) 100%);
  color: var(--white);
  /* Centred rather than bottom-anchored. The copy is a block in a panel now,
     not a caption sitting on a picture. */
  justify-content: center;
}

/* Client's own description: left to right, a darker blue easing through a
   lighter blue into full transparency - not the near-black (rgba(6,10,32))
   bottom-fade this used to be, and not navy either. Both stops are the same
   azure family as the rest of the brand (--azure-deep, --azure), just at
   partial opacity over the photograph, so the transition is blue easing to
   the photograph's own true colour rather than a dark wash. Transparent by
   45% on purpose - that is short of where the cab now sits (curve pulled
   back to clear ~46%, see front-page.php), so the tint never touches the
   lorry at all; it only ever sits over the open road and sky beside it. */
.hero-split .hero-bg::after {
  background: linear-gradient(to right,
    rgba(0, 146, 166, .55) 0%,
    rgba(0, 183, 206, .26) 22%,
    rgba(0, 183, 206, 0) 45%);
}

/* The panel background above is a gradient now, not a flat colour, so
   currentColor can no longer carry it - a solid fill on the curve would sit
   next to a gradient panel and show a seam right where the two meet. The
   path's own fill is a matching SVG linearGradient instead (defined once in
   front-page.php, id="heroSplitGrad"), same two colours and roughly the
   same angle as the CSS gradient's navy-to-azure sweep, so the curve reads
   as a continuation of the panel rather than a flat shape laid over it. The
   radial glow layer is not worth reproducing here — the curve only ever
   covers the panel's right-hand sliver, where the glow is at its faintest
   anyway. */
.hero-curve {
  position: absolute; inset: 0; z-index: 2;
  width: 100%; height: 100%; display: block;
  pointer-events: none;
}
.hero-split > .wrap { z-index: 3; }

/* ── Type, recoloured for a navy ground ── */
.hero-split .hero-label { color: #8fe9f6; }
.hero-split h1 { color: var(--white); }
.hero-split h1 .accent { color: #6FE9FA; }
/* Sized for the column, not inherited from the full-bleed rule below.
   .hero h1's clamp(46px … 128px) was drawn for a headline that owns the
   whole frame; it does not read var(--ts), so the site-wide scale pass
   above never touched it, and inside a narrow column at 128px "FROM
   PICKUP" ran to four lines and past the bottom of the hero entirely
   (visible below the fold with nothing scrolled). .hero-has-lorry already
   carries its own step for exactly this reason, tuned for its 660px
   column; this scales that same idea for this one. Bumped from clamp(28
   … 58px) alongside the column's own widening (460px cap to 540px, below)
   — a bigger column and the same headline size would have left the extra
   width unused instead of read as more breathing room. */
.hero-split h1 { font-size: clamp(30px, .9rem + 2.7vw, 64px); }
.hero-split .hero-lede { color: rgba(255,255,255,.82); }
/* The client's reference shows a solid button here, not a text link with an
   underline — the full-bleed .hero-cta (text + underline) read fine over a
   photograph but disappeared as a "quiet" element next to a solid colour
   panel with real weight behind it. Restyled as a proper button, same
   language as .btn-primary elsewhere on the page (azure fill, white text,
   the same lift-on-hover), so the split hero's CTA carries the same visual
   priority the reference gives it instead of reading as an afterthought. */
.hero-split .hero-cta {
  color: var(--white); background: var(--azure);
  padding: 15px 28px; border-radius: 999px; border-bottom: none;
  box-shadow: 0 8px 26px rgba(0, 183, 206, 0.28);
  transition: background-color .3s var(--ease), transform .3s var(--ease),
              box-shadow .3s var(--ease);
}
.hero-split .hero-cta:hover {
  background: var(--azure-deep); transform: translateY(-2px);
  box-shadow: 0 12px 32px rgba(0, 183, 206, 0.38);
}
.hero-split .hero-cta:hover svg { transform: translateX(4px); }
.hero-split .hero-meta {
  color: rgba(255,255,255,.7); border-top-color: rgba(255,255,255,.22);
}
.hero-split .hero-meta strong { color: var(--white); }

/* ── Room for the copy at a ~36/64 split ──
   The panel reaches ~36% at mid-height (see the curve's own comment in
   front-page.php for why it bows this much further in now — the client's
   own reference image, a bold rounded belly), and the copy is positioned
   independently of that number regardless.

   .wrap is capped at 1240px and centred, so its left gutter GROWS as the
   window widens while the panel edge stays a percentage of the window. At
   1280 that leaves 434px between the two; at 1920 it leaves 358px, and the
   wider the screen the worse it gets — the opposite of what you want. A
   headline at 82px does not live in 358px.

   So inside the split the copy measures from the window, not from the centred
   container. It starts at a viewport gutter and is capped in vw.

   Trade-off worth knowing: on wide screens the hero copy now begins further
   left than the sections below it, which align to the 1240px container. In a
   split hero that reads as intentional; if you would rather have the two line
   up, the panel has to go back to roughly half the width. */
.hero-split .hero-copy,
.hero-split > .wrap:last-of-type {
  max-width: none; padding-left: clamp(22px, 5vw, 88px); padding-right: 0;
}
/* Narrowed back from min(34vw, 540px) alongside the panel's own bowing-in
   (~44% to ~36% at mid-height, front-page.php) — a curve that reaches this
   much further left needs the text column to give ground back or the two
   collide at the narrower end of this layout's range. 27vw/460px checked
   with the real markup at 1081/1281/1440/1920px (1081 is the narrowest
   width this split runs at before the mobile stack takes over at 1080px):
   the panel edge clears the text block by 40–90px at every one of those,
   tightest at 1081 and only getting safer as the window widens. */
.hero-split .hero-copy > *,
.hero-split .hero-meta { max-width: min(27vw, 460px); }

/* ── The trust strip, tidied for the narrow column ──
   .hero-meta's base rule (above) is a wrapping flex row with a 48px column
   gap — tuned for the full-bleed hero, where the strip had roughly half the
   window to work with. The split panel gives it min(30vw, 460px) instead,
   which is not enough for three items with that much air between them: at
   a typical 1280–1440px window "Established 2017" and "Malaysia & Singapore"
   share the first line with only a few px of gap left between them, and
   "ISO 9001:2015 certified" drops to its own line, sized as if there were
   room it doesn't have — an uneven wrap rather than a set of three facts.

   Stacked instead: one fact per line, a small fixed gap, left-aligned. Reads
   as a short, deliberate list at every width from 1280 up, instead of a row
   that happens to wrap differently depending on how the words break. */
.hero-split .hero-meta {
  flex-direction: column; align-items: flex-start; gap: 10px;
}

/* ── The crop ──
   hero-shot.jpg is 1677x938 - 1.788:1, a hair wider than the hero frame's
   own 16:9 (1.778:1) - so object-fit: cover needs to trim only a sliver
   (well under 1%) off the sides to fill the frame; width and height are
   otherwise mapped close to 1:1 before scale() ever runs.

   That previous note here claimed a 2000x1125 photo and "the lorry's cab
   ... clear the panel edge with room to spare" checked at 1280-1920px -
   neither was true of this photo (measured directly: 1677x938, cab at
   roughly 35.5-44% of the frame width) and the claim didn't hold up once
   actually checked, per "背景照片我想要看到整辆卡车". --hero-scale dropped
   from 1.06 to 1.0 for exactly that reason: the old value's only job was
   scroll-parallax headroom (see the full-bleed .hero-bg img rule above),
   but it was also trimming ~2.8% off every edge before the curve pullback
   even got a chance to help, eating straight into the cab's margin. At 1.0
   bluebox.js's pan has less room to work with, but the cab staying fully
   in frame at rest matters more than a few extra pixels of parallax travel
   while scrolling past the hero. */
.hero-split .hero-bg img {
  /* object-fit: cover restated here on purpose: the full-bleed rule above
     switched the shared base .hero-bg img to contain (show the whole photo,
     never crop it) - the right fix for a photo spanning the entire viewport,
     where any crop was cutting real content out of frame. A split panel is
     the opposite situation: the panel's own edge already frames the shot,
     there is no "more of the photo" hiding off the edge of a viewport to
     reveal, and contain would letterbox (idle bars top and bottom) inside a
     column whose aspect ratio isn't the photo's own. cover fills the panel
     edge-to-edge, which is what "comfortably framed" means in a fixed panel
     rather than a full-bleed background. */
  object-fit: cover;
  --hero-scale: 1.0; transform-origin: 50% 50%;
}

/* ── The lorry, inside the photograph ──
   Smaller and higher than the full-bleed layout wants it. There it was the
   subject of the whole frame; here it is the subject of the right-hand half.

   right: -4% with width 40% puts the near edge at 104 - 40 = 64%, and the
   curve reaches 62% at its foot — so the box body clears the edge by two
   points instead of having its rear corner shaved off by it. That is the
   whole reason for the -4%: change the width and this has to move with it. */
/* ── The lorry, on the lane ─────────────────────────────────────────────
   Sized and placed from the photograph, not by eye.

   The tarmac was sampled off the plate row by row. The near carriageway runs
   0.580-0.719 of the source width at source y 0.85 — 0.139 wide, three lanes,
   so a lane is 0.046 and a lorry at 2.5m against a 3.5m lane is 0.033 of the
   source. The crop above shows a 0.385-wide window, so that lorry is
   0.033 / 0.385 = 8.6% of the frame. Not a preference: it is what a lorry
   measures on this road at this crop, and it is why the figure is odd.

   Horizontally, the middle lane's centre at that depth is source 0.6525,
   which the same window puts at 78.6% of the frame — hence left 74.3% for a
   box 8.6% wide.

   Change --hero-scale and all three of these have to be recomputed. The
   sampling script is in the transcript; the arithmetic is above. */
.hero-split .hero-lorry { width: 8.6%; left: 74.3%; right: auto; bottom: 38%; }

/* Grounded, not floating. The full-bleed layout gives the lorry a big soft
   drop shadow because it is a cut-out standing in front of a landscape; here
   it is meant to be ON the tarmac, and a cast shadow pooled under the axles
   is the single strongest cue for that. The drop shadow has to go with it —
   the two together read as a sticker with a halo. */
.hero-split .hero-lorry img { filter: none; }
.hero-split .hero-lorry::after {
  content: ''; position: absolute; z-index: -1;
  left: 50%; bottom: 1%; width: 112%; height: 7%;
  transform: translateX(-50%);
  background: radial-gradient(ellipse at 50% 50%,
              rgba(12,18,10,.52) 0%, rgba(12,18,10,.28) 45%, rgba(12,18,10,0) 72%);
}

/* ── Narrow: the split stops being a split ──────────────────────────────
   A vertical curve, a photograph pinned to one side, copy pinned to the
   other — all of that is a wide-screen idea, and below 1080 there is no
   second column left to justify it. Earlier this bred a compromise: the
   photograph demoted to a short dome-topped band wedged under the copy,
   position: absolute so it wouldn't push the copy down, with a hand-tuned
   padding-bottom on the copy so the two didn't print on top of each other.
   That is three moving parts standing in for what a phone screen actually
   wants, which is simpler than any of them: normal top-to-bottom flow.

   So this rewrites the layout as a real single column instead of faking
   one. Nav (unaffected, it's a separate element), then the copy — eyebrow,
   headline, lede, the quote link, then the trust strip — then the
   photograph, full width, in normal document flow. No absolute
   positioning and no reserved padding, because a block that is actually in
   flow doesn't need either: it simply takes the space it takes, and
   whatever comes after it (the trust strip, then the ticker section right
   below .hero in the markup) starts after that space ends.

   flex `order` is what reorders without touching the HTML: hero-bg is
   first in the markup (it has to stay there — above 1080px it is still
   the desktop split's absolutely-positioned photograph panel, painted
   behind everything else), so here it is given position: static, which
   both cancels that absolute positioning and puts it back in the flex
   flow, and order: 3 to move it visually after the copy and the trust
   strip, which keep their source order (1 and 2). */
@media (max-width: 1080px) {
  .hero-split {
    min-height: 0; justify-content: flex-start;
    /* Narrower than the desktop clamp(40px … 76px) top / clamp(28px … 52px)
       bottom on purpose — those were sized to centre a much shorter block
       of copy inside a 16:9 frame with a photograph filling the other half;
       stacked full-width, the photograph is its own block below the copy
       and doesn't need that frame to sit inside, so the extra air above
       and below only read as wasted space on a phone screen. */
    padding: 24px 0 32px;
  }
  .hero-split .hero-curve { display: none; }

  .hero-split .hero-copy { order: 1; }
  .hero-split > .wrap:last-of-type { order: 2; }
  .hero-split .hero-bg {
    order: 3; position: static; inset: auto;
    width: 100%; margin-top: 28px;
    /* 16:9, the photograph's own ratio (2000x1125) — see the desktop crop
       comment above for why that means object-fit: cover has nothing to
       crop here beyond the standing parallax margin. A plain rounded
       rectangle reads as "the next block in the page" the way the dome
       never could; var(--radius) matches every other rounded photo/card
       on the site rather than inventing a second radius just for this. */
    aspect-ratio: 16 / 9; border-radius: var(--radius); overflow: hidden;
  }
  .hero-split .hero-bg::after { background: none; }
  /* Same crop as the desktop panel now, not a mobile-specific one — the
     container is a plain 16:9 rectangle here too, not the short, wide dome
     band the old layout needed a separate crop to fit. object-fit: cover
     restated for the same reason as the desktop rule above: the shared base
     rule defaults to contain now, and this rounded card wants the photo
     filling it edge-to-edge, not letterboxed. */
  .hero-split .hero-bg img { object-fit: cover; --hero-scale: 1.06; transform-origin: 50% 50%; }

  /* The desktop measurements on .hero-copy and .hero-meta — a vw-measured
     gutter clear of the centred .wrap, children capped at 30vw — exist to
     keep the copy out of the photograph's half of a split screen. Stacked
     full-width there is no second half to stay clear of: the column is the
     whole screen, so it takes the ordinary .wrap gutter instead. */
  .hero-split .hero-copy,
  .hero-split > .wrap:last-of-type {
    max-width: none; padding-left: 20px; padding-right: 20px;
  }
  .hero-split .hero-copy > *,
  .hero-split .hero-meta { max-width: none; }

  /* The reserve grows to clear the band, not just the lorry. The dome is
     highest at the centre — where the lorry is — so it is the centre that
     has to be cleared. */
  .hero-split.hero-has-lorry {
    padding-bottom: calc(var(--lorry-w) * .595 + 120px);
  }

  /* Hand the lorry back to the stacked treatment.

     It does not get there on its own. The generic `.hero-lorry` rule that
     centres it lower down this file is a single class, and `.hero-split
     .hero-lorry` above is two — specificity beats source order, so the split
     layout's desktop sizing kept winning inside this breakpoint.

     Sized off the band rather than off --lorry-w. --lorry-w and the .595 that
     goes with it were derived from the SIDE artwork, which is 812x483 and
     lands on its side. This layout uses the rear elevation, 840x1110, which is
     taller than it is wide — at the same width it stands 2.3x higher, and a
     315px lorry came out 416px tall inside a 280px band, head and shoulders
     above the tarmac it was supposed to be driving on.

     A width in vw, capped, keeps it inside the band at every width.

     The lane arithmetic from the desktop rule is deliberately NOT applied
     here. A lane in this band measures about 26px across at 375 — a lorry to
     scale would be 18px, which is not a lorry, it is a speck. The band is
     scenery at this width and the lorry is a graphic standing on it. */
  .hero-split .hero-lorry {
    width: clamp(112px, 30vw, 180px);
    right: auto; left: 50%; transform: translateX(-50%);
    bottom: clamp(14px, 4vw, 28px);
  }
}

/* ── Ticker ───────────────────────────────────────────────────────────────
   The record, at the weight it deserves: a running tally you glance at, not a
   screen you stop for.

   The list is printed twice and the track translates by exactly -50%, so at
   the instant the animation resets the second copy is already where the first
   one was and the loop has no seam. */
/* 2026-09: client flagged the band as feeling like a separate box dropped
   between the hero and "why" sections rather than part of the page. Two
   things were doing that: the hard 1px border on its own bottom edge, and
   a flat, uniform --midnight fill meeting the hero's pale gradient and the
   plain white "why" background with no transition at all - light, dark,
   light, each cut clean. Border removed; the fill is now a short vertical
   gradient that eases from --ice (the hero's own bottom tone) down into
   --midnight over the top 14px, holds solid through the middle where the
   ticker-label pill and the running numbers read against it, then eases
   back out to --white (the "why" section's own background) over the last
   14px - the same band, just with soft seams instead of hard ones. */
.ticker {
  display: flex; align-items: stretch;
  background: linear-gradient(to bottom,
    var(--ice) 0, var(--midnight) 14px,
    var(--midnight) calc(100% - 14px), var(--white) 100%);
  color: var(--white);
}
.ticker-label {
  flex: 0 0 auto; display: flex; align-items: center; gap: 10px;
  padding: 0 22px; background: var(--azure); color: var(--white);
  font-family: var(--font-display); font-size: calc(12px * var(--ts));
  font-weight: 800; letter-spacing: .16em; text-transform: uppercase;
  white-space: nowrap;
}
.ticker-label::before {
  content: ''; width: 7px; height: 7px; border-radius: 50%;
  background: var(--white); flex: 0 0 7px;
}
@media (prefers-reduced-motion: no-preference) {
  .ticker-label::before { animation: bb-pulse 1.9s ease-in-out infinite; }
}
@keyframes bb-pulse { 0%,100% { opacity: 1; } 50% { opacity: .28; } }

.ticker-vp { flex: 1 1 auto; overflow: hidden; position: relative; }
/* Fade the ends so items enter and leave rather than being cut off. */
.ticker-vp::before, .ticker-vp::after {
  content: ''; position: absolute; top: 0; bottom: 0; width: 56px; z-index: 1; pointer-events: none;
}
.ticker-vp::before { left: 0; background: linear-gradient(90deg, var(--midnight), transparent); }
.ticker-vp::after { right: 0; background: linear-gradient(270deg, var(--midnight), transparent); }

.ticker-track { display: flex; width: max-content; }
@media (prefers-reduced-motion: no-preference) {
  .ticker-track { animation: bb-tick 42s linear infinite; }
  .ticker:hover .ticker-track { animation-play-state: paused; }
}
@keyframes bb-tick { to { transform: translateX(-50%); } }

.ticker-track ul { display: flex; list-style: none; margin: 0; padding: 0; }
.ticker-track li {
  display: flex; align-items: baseline; gap: 10px;
  padding: 14px 26px; white-space: nowrap;
  font-size: calc(13.5px * var(--ts)); color: rgba(255,255,255,.66);
}
.ticker-track li::after {
  content: ''; width: 4px; height: 4px; border-radius: 50%;
  background: rgba(255,255,255,.28); margin-left: 16px; align-self: center;
}
.ticker-track li b {
  font-family: var(--font-display); font-size: calc(19px * var(--ts));
  font-weight: 800; letter-spacing: .2px; color: var(--white);
  font-variant-numeric: tabular-nums;
}

/* ── Why: cards that turn over ────────────────────────────────────────────
   Front carries the index and the title, back carries the reason.

   The box is a fixed ratio because a card that flips cannot size itself to
   whichever face is forward - both faces are absolutely positioned, so the box
   has to be tall enough for the longer of the two before either is shown. */
.why-split { display: block; margin-top: 46px; text-align: left; }
.why-list {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px;
}
.why-item { padding: 0; border: 0; perspective: 1400px; }

.why-card {
  position: relative; display: block; width: 100%; aspect-ratio: 5 / 4;
  padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; color: inherit; text-align: left;
  transform-style: preserve-3d;
  transition: transform .7s var(--ease);
}
.why-item:hover .why-card,
.why-card[aria-expanded="true"] { transform: rotateY(180deg); }
.why-card:focus-visible { outline: 2px solid var(--azure-deep); outline-offset: 4px; border-radius: 18px; }

.why-face {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  padding: 26px 24px; border-radius: 18px;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
}
.why-front {
  background: var(--white); border: 1px solid var(--line);
  justify-content: space-between;
}

.why-back {
  background: var(--midnight); color: var(--white);
  transform: rotateY(180deg); justify-content: center; gap: 12px;
}

.why-top { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.why-i {
  font-size: calc(15px * var(--ts)); font-weight: 700; letter-spacing: .08em;
  color: var(--azure); font-variant-numeric: tabular-nums;
}
.why-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 54px; height: 54px; border-radius: 16px; flex: 0 0 54px;
  background: var(--azure-soft); color: var(--azure-deep);
  transition: background .35s var(--ease), transform .35s var(--ease);
}
.why-item:hover .why-icon { background: var(--azure); color: var(--white); transform: scale(1.05); }
.why-front .why-title {
  font-family: var(--font-body); font-size: calc(21px * var(--ts)); font-weight: 700;
  line-height: 1.3; color: var(--midnight); margin-top: auto;
}
.why-back .why-title {
  font-size: calc(13px * var(--ts)); font-weight: 700; letter-spacing: .14em;
  text-transform: uppercase; color: #6FE9FA;
}
.why-back .why-body { font-size: calc(15px * var(--ts)); line-height: 1.75; color: rgba(255,255,255,.82); }

.why-hint {
  margin-top: 14px; font-size: calc(11.5px * var(--ts)); letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted); opacity: .7;
}

/* 2026-09: client first asked for just card 3's text to sit centered, then
   asked for all six "why" cards to match - front face only, the back face
   (reason copy) reads better left-aligned so it is left as-is. Client then
   circled the titles in a screenshot and asked for them to move up into the
   card's vertical middle - as first centered, .why-front .why-title kept its
   own margin-top:auto (needed so a short title still sits at the bottom,
   right above the hint, when a neighbouring card's title wraps to a second
   line) with nothing balancing it below, so the title+hint pair sat glued to
   the bottom edge instead of centered. Giving .why-hint a matching
   margin-bottom:auto turns that into two auto margins - one above the title,
   one below the hint - so flexbox splits the leftover space evenly between
   them and the title+hint pair floats centered in the space below the
   icon row, instead of pinned to the bottom. */
.why-list > .why-item .why-front { align-items: center; text-align: center; }
.why-list > .why-item .why-top { justify-content: center; gap: 16px; }
.why-list > .why-item .why-front .why-title { margin-left: auto; margin-right: auto; }
.why-list > .why-item .why-hint { margin-bottom: auto; }

/* No rotation for anyone who has asked for less motion - the card simply
   shows both faces stacked, which is the flat list it replaced. */
@media (prefers-reduced-motion: reduce) {
  .why-card { aspect-ratio: auto; transform: none !important; transition: none; }
  .why-face { position: static; inset: auto; backface-visibility: visible; transform: none !important; }
  .why-front { border-bottom: 0; border-radius: 18px 18px 0 0; }
  .why-back { border-radius: 0 0 18px 18px; }
  .why-hint { display: none; }
}

@media (max-width: 900px) { .why-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 600px) { .why-list { grid-template-columns: 1fr; } }

/* Touch screens have no hover state, so the tap-to-flip affordance reads as
   broken there: a card looks inert until you happen to tap it, and tapping one
   leaves it flipped while its five neighbours are not, which is what the
   reduced-motion fallback above already turns into a stacked card - just with
   two leftovers that only made sense as a flip's reverse side, a duplicate
   uppercase title on the back and the "hover or tap" hint. Below the point
   where the grid itself goes to two columns, drop the flip the same way and
   remove those two leftovers, so every card is one calm block: icon and title
   up top, the reason directly under it, nothing hidden and nothing doubled. */
@media (max-width: 900px) {
  .why-item { perspective: none; }
  .why-card { aspect-ratio: auto; transform: none !important; transition: none; }
  .why-face {
    position: static; inset: auto; backface-visibility: visible; transform: none !important;
    padding: 20px 22px;
  }
  .why-front { border-bottom: 0; border-radius: 18px 18px 0 0; padding-bottom: 12px; }
  .why-front .why-title { margin-top: 10px; }
  .why-hint { display: none; }
  .why-back {
    background: var(--white); color: inherit; border: 1px solid var(--line); border-top: 0;
    border-radius: 0 0 18px 18px; justify-content: flex-start; gap: 0; padding-top: 0;
  }
  .why-back .why-title { display: none; }
  .why-back .why-body { color: var(--muted); font-size: calc(14.5px * var(--ts)); line-height: 1.65; }
}



/* The lorry stops fitting beside the copy long before the phone breakpoint, so
   it gets its own, earlier one. At 901px the copy column ended at 467px and the
   lorry began at 469 — two pixels of daylight, with the trust strip already
   grazing the box body. 1080 is the last width where side by side still has
   room to breathe. The rest of the tablet pass stays at 900: .why-list and the
   ticker have nothing to do with this. */
@media (max-width: 1080px) {
  /* Under the copy and centred. There is no column free beside the headline at
     this width, and the lorry has to stay big enough to make out the mascot and
     scan the code.

     The band it sits in is computed from the artwork rather than guessed:
     483/812 = .595, so the lorry's height at any width is that width times
     .595. A hand-picked padding was ~90px short and the trust strip printed
     straight across the roof.

     The percentage in --lorry-w resolves against the hero's width in both
     places it is used — padding percentages resolve against inline size, and
     .hero is the positioned ancestor — so the reserve and the lorry cannot
     drift apart when one of them is edited. */
  .hero-has-lorry {
    --lorry-w: min(84%, 460px);
    padding-bottom: calc(var(--lorry-w) * .595 + 46px);
  }
  .hero-has-lorry .hero-copy > *,
  .hero-has-lorry .hero-meta { max-width: none; }
  .hero-lorry {
    right: auto; left: 50%; transform: translateX(-50%);
    bottom: clamp(16px, 4vw, 30px); width: var(--lorry-w);
  }
  /* Stacked, the copy has the full width back, so the headline takes its
     original scale back with it. The smaller step exists only to fit the
     half-width column; holding it here would shrink the phone headline from
     51px to 38px for nothing. */
  .hero.hero-has-lorry h1 { font-size: clamp(46px, 1.8rem + 6vw, 128px); }

  /* Stacked, the copy takes the full width back and its original scale
     with it - full-bleed stays full-bleed at this width too (see the crop
     note just below), so there's no column to hold clear of anymore. */
  .hero-road:not(.hero-split):not(.hero-has-lorry) .hero-copy > *,
  .hero-road:not(.hero-split):not(.hero-has-lorry) .hero-meta { max-width: none; }
  .hero-road:not(.hero-split):not(.hero-has-lorry) h1 { font-size: clamp(46px, 1.8rem + 6vw, 128px); }

  /* Now that the copy is full width instead of a narrow left column, it
     sits directly over the lorry's own printed livery below (the shared
     ::after scrim above only shields the open road before ~34% of the
     frame - the desktop column never left that zone, but this one does).
     Two lightweight fixes rather than one heavy one: a text-shadow (works
     over whatever the crop lands on, doesn't hide any more of the lorry
     than the crop already does) plus a taller, stronger vertical fade on
     top of the existing one (still azure-deep family, no navy) so the
     band the text actually sits in - the bottom ~60% of the frame, where
     .hero-copy and .hero-meta live - has some help underneath it too. */
  .hero-road:not(.hero-split):not(.hero-has-lorry) .hero-bg::after {
    background:
      linear-gradient(to top, rgba(0, 40, 46, .62) 0%, rgba(0, 61, 71, .42) 32%, rgba(0, 92, 106, .18) 58%, rgba(0, 92, 106, 0) 80%),
      /* Same extra layer as the base rule above, restated here because
         this rule replaces the base ::after background rather than adding
         to it. The vertical fade above is 0 by 80% of the frame's height,
         which is short of where the headline actually sits (~14% down
         from the top, i.e. 86% up from the bottom) - this flat wash
         covers that gap uniformly instead of stretching the fade's own
         shape to reach it. */
      linear-gradient(rgba(0, 40, 46, .18), rgba(0, 40, 46, .18));
  }
  .hero-road:not(.hero-split):not(.hero-has-lorry) .hero-copy,
  .hero-road:not(.hero-split):not(.hero-has-lorry) .hero-meta {
    text-shadow: 0 2px 5px rgba(0, 16, 20, .6), 0 10px 26px rgba(0, 16, 20, .5);
  }

  /* This used to pin the crop to the photo's left edge and zoom to 1.85x
     specifically to push the lorry OUT of frame (old design: the copy sat
     on top of the photo full-width, so the lorry's own livery text would
     collide with the headline). That is now the opposite of what's wanted -
     the client asked explicitly to see the whole lorry ("我想要看到整辆卡车") -
     so a rule whose entire job was hiding it can't stay as-is at this
     breakpoint.

     The photo changed since this was last tuned (now 1983x793, 2.5:1 -
     see the base .hero-bg img comment above), and it makes the phone crop
     tighter, not looser. A portrait phone frame (390 wide, ~750 tall once
     the copy's own height is counted, so nowhere near the 560px floor)
     covering a 2.5:1 photo shows only about 21% of its width - narrower
     than the old photo's ~38%, because a wider source photo needs a
     bigger multiple to cover the same tall frame. The lorry spans roughly
     37% (cab) to 100% (rear doors) - 63 points - so a 21%-wide window
     was never going to hold the whole thing on a phone; the choice is
     which slice, not whether to crop.

     object-position-x: 52% lands that slice at roughly 41-62% of the
     photo - just past the cab, through the "BLUEBOX COLD CHAIN DELIVERY"
     wordmark and the mascot - the most recognisable single piece of the
     livery, rather than an arbitrary fifth of it. --hero-scale stays at
     1.0 here (not the base rule's 1.02): this crop has no margin to spare
     for parallax headroom without trimming into the wordmark on one side
     or the mascot on the other. */
  .hero-road:not(.hero-split):not(.hero-has-lorry) .hero-bg img {
    object-fit: cover;
    object-position: 52% 50%; --hero-scale: 1.0; transform-origin: 50% 50%;
  }
}

@media (max-width: 900px) {
  .why-list { grid-template-columns: 1fr; }

  /* No height override needed. 9/16 of a phone is 211px, so the 560px floor in
     the base rule wins here, and the copy plus the lorry band grow the box past
     it on their own — a phone hero is content-height, as it should be. */
  .hero { padding-top: 108px; }
  .hero-meta { gap: 10px 24px; margin-top: 48px; }

  .ticker-label { padding: 0 14px; }
}

/* Hero choreography. Gated on .bb-js, which header.php sets inline before the
   stylesheet loads - so without JavaScript none of this ever hides anything. */
.bb-js .hero [data-rise] { opacity: 0; transform: translateY(24px); }
.bb-js .hero [data-rise].in {
  opacity: 1; transform: none;
  transition: opacity .8s var(--ease), transform .8s var(--ease);
  transition-delay: var(--d, 0ms);
}
.hero .lines .ln { display: block; overflow: hidden; }
.bb-js .hero .lines .ln > span { display: block; transform: translateY(105%); }
.bb-js .hero .lines.in .ln > span {
  transform: none;
  transition: transform 1.05s var(--ease);
  transition-delay: calc(var(--i) * 90ms);
}
html.bb-intro-off .hero [data-rise],
html.bb-intro-off .hero .lines .ln > span {
  opacity: 1 !important; transform: none !important;
}
@media (prefers-reduced-motion: reduce) {
  .bb-js .hero [data-rise], .bb-js .hero .lines .ln > span {
    opacity: 1 !important; transform: none !important; transition: none !important;
  }
}

/* ── Services: the WhatsApp card, and some life in the six ────────────────*/

/* The seventh card. Dark, because it is an action rather than another thing
   to read, and it completes a last row that six cards leave half empty. */
.svc-wa {
  background: var(--midnight); border-color: var(--midnight);
  display: flex; align-items: center;
}
.svc-wa-body { padding: 30px 26px; }
.svc-wa-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 52px; height: 52px; border-radius: 16px; margin-bottom: 16px;
  background: rgba(255, 255, 255, .10); color: #6FE9FA;
  transition: background .35s var(--ease), transform .35s var(--ease);
}
.svc-wa:hover .svc-wa-icon { background: #25D366; color: var(--white); transform: scale(1.06); }
.svc-wa h3 {
  font-family: var(--font-display); font-size: calc(22px * var(--ts));
  text-transform: uppercase; letter-spacing: .3px; color: var(--white); margin-bottom: 10px;
}
.svc-wa p { font-size: calc(14.5px * var(--ts)); color: rgba(255, 255, 255, .70); }
.svc-wa-cta { color: #6FE9FA; margin-top: 16px; }
.svc-wa:hover { transform: translateY(-7px); border-color: #25D366; }

/* The six: the photograph warms and lifts, the tag slides in, the arrow
   travels. One easing, small distances - the same restraint as everywhere
   else on the page. */
.svc-photo img { transition: transform .6s var(--ease), filter .6s var(--ease); filter: saturate(.92); }
.svc:hover .svc-photo img { filter: saturate(1.06); }
.svc-tag { transition: transform .4s var(--ease), background .4s var(--ease); }
.svc:hover .svc-tag { transform: translateY(-2px); }
.svc-body h3 { transition: color .3s var(--ease); }
.svc:hover .svc-body h3 { color: var(--azure-deep); }

@media (max-width: 720px) {
  .svc-wa { display: block; }
  .svc-wa-body { padding: 26px 22px; }
}

/* ---------- mobile "feed" pass (2026-09) ----------------------------------
   Client asked for a fresh, more app-like phone layout after comparing three
   phone-only design directions and picking "Live Feed" — same copy and
   photographs throughout, no new sections, no new markup. Three groups that
   were collapsing to a plain single column at this width (the why-cards, the
   services grid, the cargo strip) become horizontal swipe-and-snap rows
   instead: each item is still wide enough to read on its own, with the next
   one peeking at the edge as the swipe affordance, the same full-bleed
   negative-margin technique .cargo-strip already used for its desktop
   edge-to-edge layout. The hero facts become a scrollable row of pill chips
   rather than a wrapping list. Nothing here needs new HTML - the existing
   <li>/<a>/<figure> markup just flows a different direction at this width.

   Placed at the end of the file on purpose: .why-list and .cargo-strip both
   have their own unconditional (non-media) base rules elsewhere that also
   set `display`, at equal selector specificity - CSS only lets a narrower
   @media block win a same-specificity tie by also being LATER in the
   stylesheet, so this has to be the last word, not just conditionally
   matched. */
@media (max-width: 720px) {
  .hero-meta {
    flex-wrap: nowrap; overflow-x: auto; gap: 10px; border-top: 0; padding-top: 0;
    margin: clamp(18px, 4vh, 40px) calc(50% - 50vw) 0; padding: 2px 20px 4px;
    scrollbar-width: none;
  }
  .hero-meta::-webkit-scrollbar { display: none; }
  .hero-meta span {
    flex: 0 0 auto; background: rgba(255, 255, 255, .1); border: 1px solid rgba(255, 255, 255, .22);
    border-radius: 100px; padding: 9px 16px; white-space: nowrap;
  }

  .why-list {
    display: flex; gap: 14px; overflow-x: auto; scroll-snap-type: x mandatory;
    margin: 28px calc(50% - 50vw) 0; padding: 4px 20px 10px; scrollbar-width: none;
  }
  .why-list::-webkit-scrollbar { display: none; }
  .why-item { flex: 0 0 80%; scroll-snap-align: start; }
  .why-face { box-shadow: var(--shadow-sm); }

  /* Scoped to #services so the contact grid three below (also .grid-3) stays
     a plain stack - enquiry details should all be visible at a glance, not
     hidden a swipe away. */
  #services .grid-3 {
    display: flex; gap: 14px; overflow-x: auto; scroll-snap-type: x mandatory;
    margin: 0 calc(50% - 50vw); padding: 4px 20px 10px; scrollbar-width: none;
  }
  #services .grid-3::-webkit-scrollbar { display: none; }
  #services .card.svc { flex: 0 0 80%; scroll-snap-align: start; }

  .cargo-strip {
    display: flex; gap: 10px; overflow-x: auto; scroll-snap-type: x mandatory;
    padding: 0 20px 4px; scrollbar-width: none;
  }
  .cargo-strip::-webkit-scrollbar { display: none; }
  .cargo-panel { flex: 0 0 78%; scroll-snap-align: start; aspect-ratio: 3 / 4; border-radius: 18px; }
}
