/* ---------------------------------------------------------------------------
   Vanson Engineering - site stylesheet
   THE ONE STYLESHEET FOR THE WHOLE SITE.

   ⚠️ IT IS THE HOME-V2 DESIGN NOW. Jacelyn, 2026-09-17: "remove the old home
   page. use .../home-v2/. change the other pages and tab to follow" it. So the
   palette, the type, the header, the footer and the buttons below are that
   design's, and the .v2-* section rules that used to live in their own
   home-v2.css were folded in at the bottom of this file. That file is gone -
   one site, one stylesheet, no second copy of the menu to keep in step.

   The palette is still Vanson's own logo orange, re-sampled by the new design a
   shade cooler (#F1662A, not #F47421), over a near-black #17202A.
--------------------------------------------------------------------------- */

:root {
  --orange: #F1662A;
  --orange-dark: #D9541C;
  --orange-soft: #FFF3EA;
  --amber: #FFC30D;
  --rule: #F1662A;
  --ink: #17202A;
  --ink-2: #17202A;
  --muted: #59636E;
  --line: #E4E8EC;
  --bg: #FFFFFF;
  --bg-soft: #F6F7F9;
  --dark: #1B2129;
  --radius: 10px;
  --radius-sm: 10px;
  --shadow: 0 1px 2px rgba(16,24,40,.05), 0 10px 26px rgba(23,32,42,.07);
  --shadow-lg: 0 24px 60px rgba(16,24,40,.13);
  --wrap: 1180px;
  --header-h: 74px;
}

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

html { scroll-behavior: smooth; }

body {
  margin: 0;
  /* ⛔ NO WEBFONT. This was Manrope from Google Fonts until the home-v2 design
     was adopted; that design was drawn and signed off in the system UI stack,
     and the site loads nothing from anyone else's server. */
  font-family: "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
  color: var(--muted);
  background: var(--bg);
  font-size: 16px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }
a { color: var(--orange-dark); text-decoration: none; }
a:hover { text-decoration: underline; }

h1, h2, h3, h4 { color: var(--ink); line-height: 1.15; margin: 0 0 .6em; font-weight: 700; }
h1 { font-size: clamp(2rem, 4.4vw, 3.05rem); letter-spacing: -.02em; }
.split h1 { font-size: clamp(1.7rem, 3vw, 2.4rem); }
h2 { font-size: clamp(1.5rem, 2.9vw, 2.1rem); letter-spacing: -.01em; }
h3 { font-size: 1.0625rem; }
h4 { font-size: 1rem; }
p  { margin: 0 0 1rem; }

.wrap { width: min(100% - 40px, var(--wrap)); margin-inline: auto; }

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 200;
  background: var(--ink); color: #fff; padding: 10px 16px; border-radius: 0 0 8px 0;
}
.skip:focus { left: 0; }

/* ------------------------------------------------------------------ header */
/* ⚠️ THE HOME-V2 HEADER, ON THE MARKUP THE WHOLE SITE ALREADY HAD. The design
   was rebuilt in CSS rather than by swapping in the mock-up's flat <nav>,
   because this markup is what carries the dropdowns and the mobile drawer that
   site.js drives. Same look, fifteen deep pages still one hover away. */

.site-header {
  position: sticky; top: 0; z-index: 100;
  background: #fff;
  border-bottom: 1px solid var(--line);
  transition: box-shadow .18s ease;
}
.site-header.is-stuck { box-shadow: 0 6px 22px rgba(23,32,42,.07); }
.header-in { display: flex; align-items: center; gap: 24px; min-height: var(--header-h); }

/* ⚠️ SIZED BY HEIGHT, NOT WIDTH. The file is 157x101 - a tall mark over a
   wordmark - so a width of 118px renders it 76px tall inside a 74px bar. */
.brand { display: flex; align-items: center; }
.brand img { height: 46px; width: auto; }

.nav { margin-left: auto; display: flex; align-items: center; gap: 1.55rem; }
.nav-list { list-style: none; display: flex; align-items: center; gap: 1.55rem; margin: 0; padding: 0; }
.nav-item > a {
  display: flex; align-items: center; gap: 6px; padding: .35rem 0;
  color: var(--ink); font-size: .9375rem; font-weight: 500; white-space: nowrap;
}
.nav-item > a:hover { color: var(--orange); text-decoration: none; }
.nav-item.is-active > a { color: var(--orange); }
/* The design marks the current page with a short orange rule under it. */
.nav-item.is-active > a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -2px; height: 2px;
  background: var(--orange);
}
.nav-item { position: relative; }

.caret {
  width: 8px; height: 8px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translateY(-2px); opacity: .55;
}

.sub-toggle { display: none; }

.sub {
  position: absolute; top: calc(100% + 6px); left: 0; min-width: 292px;
  list-style: none; margin: 0; padding: 8px;
  background: #fff; border: 1px solid var(--line); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
  opacity: 0; visibility: hidden; transform: translateY(6px);
  transition: opacity .16s ease, transform .16s ease, visibility .16s;
}
.has-sub:hover > .sub, .has-sub:focus-within > .sub { opacity: 1; visibility: visible; transform: translateY(0); }
.sub a {
  display: block; padding: 9px 12px; border-radius: 7px;
  color: var(--ink-2); font-size: .9rem; font-weight: 500; line-height: 1.35;
}
.sub a:hover { background: var(--orange-soft); color: var(--orange-dark); text-decoration: none; }

.burger { display: none; background: none; border: 0; padding: 10px; cursor: pointer; margin-left: auto; }
.burger span { display: block; width: 24px; height: 2px; background: var(--ink); border-radius: 2px; }
.burger span + span { margin-top: 5px; }

/* ------------------------------------------------------------------ buttons */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  padding: .78rem 1.35rem; border-radius: 999px; border: 1px solid transparent;
  font-size: .9375rem; font-weight: 600; line-height: 1; cursor: pointer; white-space: nowrap;
  transition: transform .15s ease, background .15s ease, color .15s ease, border-color .15s ease;
}
.btn:hover { text-decoration: none; transform: translateY(-1px); }
.btn-primary { background: var(--orange); color: #fff; }
.btn-primary:hover { background: var(--orange-dark); color: #fff; }
.btn-ghost { background: transparent; color: var(--ink); border-color: rgba(23,32,42,.2); }
.btn-ghost:hover { border-color: var(--orange); color: var(--orange-dark); }
.btn-light { background: #fff; color: var(--ink); }
.btn-outline-light { background: transparent; border-color: rgba(255,255,255,.75); color: #fff; }
.btn-outline-light:hover { background: #fff; color: var(--ink); }
/* ⛔ THE .btn-wa RULES WERE DELETED HERE (2026-09-21). They styled the green
   "WhatsApp Us" pill in the home hero, which Richie struck out; the round
   .wa-float button further down this file is the WhatsApp mark now and nothing
   else in the repo carries .btn-wa. */
.nav-cta { flex: 0 0 auto; padding: .62rem 1.15rem; }

/* ⛔ THE SLIDER AND TRUST-STRIP RULES WERE DELETED HERE (2026-09-17). They
   styled .hero / .slides / .slider-btn / .dots / .trust-*, which only the old
   slider home page ever used; that page was replaced by the home-v2 design and
   nothing in the repo carries those classes any more. The slider behaviour in
   assets/js/site.js went with them. */

/* ----------------------------------------------------------------- sections */

.section { padding: 70px 0; }
.section-soft { background: var(--bg-soft); }
.section-head { max-width: 760px; margin-bottom: 38px; }
.section-head.center { margin-inline: auto; text-align: center; }
.eyebrow {
  display: block; margin: 0 0 .5rem;
  font-size: .75rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: var(--orange);
}
/* ⛔ THE CLAIM THAT USED TO SIT HERE - "#F1662A on the hero's wash clears
   4.5:1; there is nothing to fix" - IS FALSE, AND WAS NEVER MEASURED.
   Sampled against the rendered pixels of the hero photograph at 390px it is
   1.34:1 with the old wash and 2.02:1 with the current, darker one. It needs
   4.5:1 (12px bold is "normal" text, not "large").
   It CANNOT be fixed by darkening. #F1662A sits at ~28% relative luminance,
   which is inside the photograph's own mid-tone range, so it never separates:
     even .70 wash -> 2.25:1      even .76 wash -> 2.85:1
   and .76 greys the picture out, which is the trade this site has twice
   refused. A lighter tint does not rescue it either (#FFA06B -> 3.54:1).
   White measures 6.34:1 on the same wash and is the only thing that passes.
   ⚠️ KEPT ORANGE ANYWAY, DELIBERATELY. Jacelyn, 2026-09-17, shown the measured
   numbers and the white-eyebrow comparison side by side (DP WIP Files
   #2460-#2464): "go for darker but keep the words colour". The brand orange
   matching the eyebrows on white further down the page was worth more to her
   than this one label's legibility. This is a decision, not an oversight -
   do not silently "fix" it to white. Reopen it with her.

   ⭐ SOLVED ON A PHONE, LATER THE SAME DAY, AND NOT BY CHANGING THE COLOUR.
   Everything above is about orange sitting ON the hero photograph. Design A
   (WIP #2468) stops it doing that below 900px - the picture is a band, the
   words are on flat --ink underneath - and the same #F1662A now measures
   5.33:1 there. See the SPLIT note in the max-width:900px block.
   ⚠️ THE DESKTOP HERO STILL OVERLAYS, AND IS STILL MARGINAL. The 90deg wash
   runs .88 at the left edge to .12 at the right, and this eyebrow is a long
   line that travels into the lighter half: sampled at 1400px it is 5.88:1
   against its darkest background and 4.05:1 against the mean along its own
   width. Not the 2.02:1 the phone had, but not a clean pass either. Untouched
   by the mobile work and still hers to decide. */
.eyebrow-on-dark { color: var(--orange); }
.section-head p { color: var(--muted); margin: 0; }

.split { display: grid; grid-template-columns: 1.05fr .95fr; gap: 56px; align-items: center; }
.split-top { align-items: start; }
.split-media { position: relative; }
.split-media img { border-radius: var(--radius); }
.split-media.plain img { border-radius: 0; box-shadow: none; }

.lead { font-size: 1.08rem; color: var(--muted); }

.ticks { list-style: none; margin: 0 0 24px; padding: 0; display: grid; gap: 10px; }
.ticks li { position: relative; padding-left: 30px; color: var(--ink-2); }
.ticks li::before {
  content: ""; position: absolute; left: 0; top: .45em;
  width: 16px; height: 9px; border-left: 2.5px solid var(--orange); border-bottom: 2.5px solid var(--orange);
  transform: rotate(-45deg);
}

/* -------------------------------------------------------------- card grids */

.grid { display: grid; gap: 20px; }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }

/* A row of fewer cards than the grid has columns, centred and capped so one
   card does not stretch to a third of the page in the left-hand column. --cols
   is set inline by whatever renders the row. */
.grid--centred { grid-template-columns: repeat(var(--cols, 3), minmax(0, 380px)); justify-content: center; }

.card {
  display: flex; flex-direction: column; gap: 10px;
  background: #fff; border: 1px solid var(--line); border-radius: var(--radius);
  padding: 26px 24px; transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
.card:hover { transform: translateY(-3px); box-shadow: var(--shadow); border-color: #D9DDE4; text-decoration: none; }
.card-icon {
  width: 62px; height: 62px; border-radius: 12px; background: var(--orange-soft);
  display: grid; place-items: center; margin-bottom: 6px;
}
.card-icon img { max-height: 38px; width: auto; }
.card h3 { margin: 0; color: var(--ink); font-size: 1.04rem; }
.card p { margin: 0; color: var(--muted); font-size: .92rem; }
.card-more { margin-top: auto; padding-top: 10px; color: var(--orange-dark); font-weight: 700; font-size: .88rem; }
.card-more::after { content: " \2192"; }

/* --------------------------------------------------------- service pages */
/* The v2 layout, approved 2026-09-17. One column down the middle at a readable
   measure - no sidebar, no split lists.
   ⚠️ NAMED .page-col, NOT .svc-col: the product pages were centred onto the same
   column on 2026-09-17 so the two page types line up, and a class called "svc"
   doing that job on a product page is the kind of thing nobody dares rename
   later. */
.page-col { max-width: 900px; margin-inline: auto; }
.page-lede { font-size: 1.15rem; line-height: 1.6; color: var(--ink); }
.svc-h { margin-top: 44px; }
/* A photograph between the copy and the detail, so a service page is not an
   unbroken wall of grey text. 21:9 so it reads as a band, not a hero. */
.svc-shot { margin: 34px 0 6px; }
.svc-shot img { width: 100%; aspect-ratio: 21 / 9; object-fit: cover; border-radius: var(--radius); }
.svc-shot figcaption { font-size: .8125rem; color: var(--muted); margin-top: .5rem; }
/* A list of short labels, as pills you can scan rather than a clause in prose. */
.pills { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 1rem; padding: 0; list-style: none; }
.pills li {
  border: 1px solid var(--line); background: #fff; border-radius: 999px;
  padding: .5rem 1rem; font-size: .9rem; font-weight: 600; color: var(--ink);
}

/* ------------------------------------------ product card with a photograph */
/* The "Other products" cards at the foot of a product page. Same shape as
   .card, but led by the product's own photograph instead of the icon tile -
   Jacelyn, 2026-09-17. The /products/ index keeps the icon cards.
   ⛔ NOT A VARIANT OF .card. That one carries 26px of padding, which a
   full-bleed photograph would have to be dragged back out of with negative
   margins; a sibling class is less to go wrong. */
.pcard {
  display: flex; flex-direction: column; overflow: hidden;
  background: #fff; border: 1px solid var(--line); border-radius: var(--radius);
  text-decoration: none;
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
.pcard:hover { transform: translateY(-3px); box-shadow: var(--shadow); border-color: #D9DDE4; text-decoration: none; }
/* ⚠️ A FIXED 16:10 WINDOW. These photographs run from 259x194 to 1600x1200 and
   two are portrait; left to size themselves the three cards in a row would
   start their headings at three different heights. */
.pcard__shot { display: block; aspect-ratio: 16 / 10; overflow: hidden; background: var(--bg-soft); }
.pcard__shot img { width: 100%; height: 100%; object-fit: cover; transition: transform .35s ease; }
.pcard:hover .pcard__shot img { transform: scale(1.04); }
/* Grows to fill the card so "Learn more" sits on the bottom edge of all three,
   whatever length the lead above it ran to. */
.pcard__body { display: flex; flex-direction: column; flex: 1 1 auto; gap: 10px; padding: 22px 24px 24px; }
.pcard h3 { margin: 0; color: var(--ink); font-size: 1.04rem; }
.pcard p { margin: 0; color: var(--muted); font-size: .92rem; }

/* ----------------------------------------------- product enquiry block */
/* The block at the foot of every product page. Jacelyn picked this out of the
   redesign proposal on 2026-09-17; it replaced a single small button that sat
   at the very bottom of a long spec page with nothing around it. */
.ask {
  margin-top: 44px; padding: 28px 30px; border-radius: var(--radius);
  background: var(--bg-soft); border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap;
}
.ask h3 { margin-bottom: .3rem; }
.ask p { margin: 0; font-size: .9rem; color: var(--muted); max-width: 52ch; }
.ask__do { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
/* ⚠️ THE NUMBER IS A tel: LINK, NOT DECORATION. Half the traffic on a page like
   this is a site manager on a phone; it has to be tappable. */
.ask__tel { font-weight: 700; color: var(--ink); white-space: nowrap; }
/* ⛔ THE 30px SIDE PADDING IS WHAT MADE THE PRODUCT PAGES SCROLL SIDEWAYS ON AN
   iPhone SE, and it went unnoticed because the service pages get away with it.
   .btn is white-space: nowrap, so the enquiry pill cannot shrink; measured at
   320px it is 272.8px wide on a product page against 267.3px on a service page,
   because "product" sets wider than "service" in this face. Add 31px of inset
   either side and only the product pages tip over - by 4px, which scrolls the
   whole document because .ask is as wide as its column.
   Trimming the inset buys 24px and fixes it without wrapping the pill onto two
   lines, which is what letting .btn wrap would have done. */
@media (max-width: 480px) {
  .ask { padding: 24px 18px; gap: 18px; }
}

/* ------------------------------------------------- where-we-work map band */
/* The /about/ regional map, beside the list of countries it marks. */
/* ⚠️ THE MAP COLUMN IS NOW THE NARROWER OF THE TWO, AND IT USED TO BE THE
   WIDER (.82fr 1.18fr). The map went portrait on 2026-09-21 when China and
   Australia were added - see tools/make-presence-map.py for why the frame
   cannot be anything else - and a 1.18fr portrait map rendered about 870px
   tall, which made this one band as long as the rest of the page. */
.map-band { display: grid; grid-template-columns: 1.2fr .8fr; gap: clamp(28px, 5vw, 56px); align-items: center; }
/* ⭐ NO MEASURE CAP ON THIS ONE HEADING, on Jacelyn's "Singapore, and across the
   region to 1 line" (2026-09-21). The cap was 16ch and the heading is 31
   characters, so it could only ever be two lines - the wrap was the cap, not the
   column, which had 618px for a 521px heading.
   ⚠️ THE 1.2fr ABOVE IS PART OF THE SAME FIX, not a separate tidy-up. Measured
   in Chrome across the font stack: the heading is 521px in Arial but 619px in
   the widest fallback, against a 618px column at 1.1fr - i.e. one font away from
   wrapping again. 1.2fr gives 674px, so it holds on one line in every font in
   the stack down to 940px, and the map column keeps 450px. Do not trim the copy
   column back without re-measuring the heading. */
.map-band__copy h2 { max-width: none; }
/* ⭐ NINE COUNTRIES GO IN TWO COLUMNS, and that is a HEIGHT decision, not a
   styling one. Jacelyn, 2026-09-21: "as in the section height for Where we work
   smaller." Capping the map alone could not do it - at five countries the map
   was the tall side of this band, but at nine the LIST was, so shrinking the
   map below the list's height bought nothing and just left a hole. */
.map-band__list { margin-top: 1.4rem; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 24px; }
.map-band__list li { font-weight: 600; color: var(--ink); }
/* The one qualifier - Singapore's factory - sits under its country, quietly. */
.map-band__list li span { display: block; font-weight: 400; font-size: .875rem; color: var(--muted); }
.map-band__map { margin: 0; }
/* ⚠️ height:auto IS DOING WORK HERE. The <img> carries width/height so the
   browser reserves the right box before the SVG arrives and the section does
   not jump; without this the intrinsic 1310px would then be forced on it.
   ⭐ max-height IS WHAT KEEPS THE BAND SHORT, and it is a HEIGHT cap because
   height is the thing that was wrong - capping the width would leave the band
   at the mercy of the next map's aspect ratio. width:auto + max-width:100% so
   the picture shrinks to match rather than letterboxing inside a wider box;
   margin-inline:auto centres what is left over in the column.
   On a phone the column is already narrower than 520/1.31, so this does
   nothing there and the map still fills the width. */
/* ⭐ 500px, UP FROM 430px - Jacelyn, 2026-09-21: "can the map be wider
   slightly?" It can, but ONLY by getting taller, and the arithmetic is worth
   writing down before the next person tries to widen it further.
   ⚠️ THE MAP IS PORTRAIT - 1000x1310, so w/h = 0.763. Its width is DERIVED from
   this cap, because the rule is width:auto + a max-HEIGHT. So every extra pixel
   of width costs 1.31px of height; there is no setting that buys one without
   the other, and widening the .8fr column on its own does nothing at all while
   the height cap is the binding constraint.
   ⚠️ THERE IS NO FREE HEADROOM EITHER. Measured at 1440px: the copy column is
   407px tall and the map was 430px, so the MAP was already the taller side and
   set the band height. Growing it does not fill slack - it lengthens the band
   1:1. 430 -> 500 takes the map from 328px wide to 382px (+16%) and the band
   from 430px to 500px (+70px).
   ⛔ THIS PARTLY GIVES BACK THE HEIGHT WON ON 2026-09-21 ("as in the section
   height for Where we work smaller"), and that was the deliberate trade. Do not
   "restore" 430px without asking her - and do not go to 589px, which is what it
   takes to fill the 450px column, because that is +159px on the band. */
.map-band__map img {
  width: auto; max-width: 100%; height: auto; max-height: 500px;
  display: block; margin-inline: auto; border-radius: var(--radius);
}

/* ------------------------------------------------------------------ gallery */

.gallery { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
.gallery a, .gallery figure { margin: 0; border-radius: var(--radius-sm); overflow: hidden; background: var(--bg-soft); }
.gallery img {
  width: 100%; aspect-ratio: 1 / 1; object-fit: cover;
  transition: transform .4s ease;
}
.gallery a:hover img, .gallery figure:hover img { transform: scale(1.05); }

.gallery-tall img { aspect-ratio: 3 / 4; }

/* --------------------------------------------------------- product page top */
/* ⭐ TWO APPROVED DESIGNS, ONE PER SIZE. Proposed as WIP #2481-#2484 and picked
   apart deliberately - Jacelyn, 2026-09-17: "i like proposal B for mobile. and
   proposal A for website."
     desktop  A - drawn card image BESIDE the words, in a split.
     phone    B - drawn card image as a full-bleed band ABOVE the words.
   It is one set of markup and one set of data; only the CSS forks, which is why
   there is no duplicated hero in product-page.php to drift out of sync.
   ⛔ IT REPLACED $page_banner ON THESE PAGES, not supplemented it. The old dark
   strip put a stock photograph and the product title over each other; keeping
   both would have meant two heroes stacked. */
.phero { padding-block: 34px 6px; }
.phero__crumbs { font-size: .8125rem; color: var(--muted); display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px; }
.phero__crumbs a { color: var(--muted); }
.phero__crumbs a:hover { color: var(--orange); }
.phero__crumbs span { opacity: .55; }
/* ⚠️ THE PICTURE COLUMN IS THE NARROWER ONE, AND THE HEIGHT IS CAPPED. At an
   even split the card art - 3:4 portrait - rendered 640x853, taller than the
   whole copy block, so the words floated in a column of white with nothing
   under them. 1.15/.85 plus the 520px ceiling lands both sides on roughly the
   same line and still shows 82% of the picture. */
.phero__split { display: grid; grid-template-columns: 1.15fr .85fr; gap: clamp(28px, 4vw, 56px); align-items: center; }
.phero h1 { font-size: clamp(1.7rem, 2.9vw, 2.35rem); margin-bottom: .6rem; }
.phero__lede { font-size: 1.0625rem; color: var(--muted); margin: 0 0 1.4rem; }
.phero__keys { list-style: none; margin: 0 0 1.6rem; padding: 0; display: grid; gap: 9px; }
.phero__keys li { position: relative; padding-left: 26px; font-size: .94rem; }
.phero__keys li::before {
  content: ''; position: absolute; left: 0; top: .34em; width: 15px; height: 9px;
  border-left: 2.5px solid var(--orange); border-bottom: 2.5px solid var(--orange);
  transform: rotate(-45deg);
}
.phero__cta { display: flex; gap: .7rem; flex-wrap: wrap; align-items: center; }
.phero__media img { width: 100%; height: 520px; object-fit: cover; border-radius: var(--radius); display: block; }

/* ------------------------------------------------ product page photo band */
.pshots__head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 18px; }
.pshots__head h2 { font-size: clamp(1.25rem, 2vw, 1.5rem); margin: 0; }
/* ⭐ THE CAPTION IS NOT DECORATION, IT IS THE POINT. The hero above is a drawn
   picture; these are photographs of jobs Vanson actually did and fire tests
   that actually happened. Demoting them below a generated image is only
   defensible while the page says plainly which is which. Do not drop this line
   to save space. */
.pshots__note { margin: 0; font-size: .85rem; color: var(--muted); }
/* The stage is the PHONE half of the design and is hidden on a desktop. */
.pshots__stage { display: none; position: relative; border-radius: var(--radius); overflow: hidden; background: var(--bg-soft); margin: 0 0 8px; }
.pshots__stage img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; display: block; }
.pshots__stage figcaption {
  position: absolute; inset: auto 0 0 0; padding: 14px 16px; color: #fff; font-size: .875rem;
  background: linear-gradient(0deg, rgba(10,14,19,.86), rgba(10,14,19,0));
}
.pshots__list { display: flex; gap: 14px; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; padding-bottom: 4px; }
.pshots__list::-webkit-scrollbar { display: none; }
.pshots__item {
  flex: 0 0 216px; scroll-snap-align: start; padding: 0; margin: 0; border: 0;
  background: none; text-align: left; font: inherit; color: inherit; cursor: zoom-in;
}
.pshots__item img { width: 100%; aspect-ratio: 3 / 4; object-fit: cover; border-radius: var(--radius-sm); display: block; }
.pshots__cap { display: block; margin-top: .5rem; font-size: .8rem; color: var(--muted); line-height: 1.35; }
/* ⚠️ A GALLERY OF ONE SHOWS THE STAGE ON A DESKTOP TOO. The stage is normally
   the phone half and is display:none above 860px, so a product left with a
   single photograph and no thumbnail row (see inc/product-page.php) would
   otherwise draw an EMPTY band on a desktop. Capped so one picture does not
   stretch the full width of the wrap. */
.pshots--one .pshots__stage { display: block; max-width: 720px; margin-inline: auto; }

@media (max-width: 860px) {
  /* ── the phone half: design B ─────────────────────────────────────────── */
  /* ⛔ FULL BLEED, SO THE PICTURE TOUCHES BOTH EDGES. `margin-inline: calc(50%
     - 50vw)` is what breaks it out of the centred .wrap. It is safe here only
     because phones draw overlay scrollbars, so 100vw and the client width are
     the same number; on a desktop it would hang a scrollbar off the page,
     which is the other reason this is inside the query. */
  .phero { padding-block: 0 4px; }
  .phero__split { grid-template-columns: 1fr; gap: 0; }
  .phero__media { order: -1; margin-inline: calc(50% - 50vw); margin-bottom: 26px; }
  .phero__media img { height: auto; aspect-ratio: 3 / 2; border-radius: 0; }
  .phero__crumbs { order: 0; }
  .phero__body { padding-top: 2px; }
  /* Three tick lines are desktop furniture; on a phone they push the enquiry
     button below the fold for no gain. The same three are in the panels below. */
  .phero__keys { display: none; }

  .pshots__stage { display: block; }
  /* The list stops being the filmstrip and becomes the stage's thumbnail row. */
  .pshots__list { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; overflow: visible; }
  .pshots__item { flex: none; cursor: pointer; border: 2px solid transparent; border-radius: 6px; overflow: hidden; }
  .pshots__item[aria-current="true"] { border-color: var(--orange); }
  .pshots__item img { aspect-ratio: 1 / 1; border-radius: 4px; }
  .pshots__cap { display: none; }
}

/* ------------------------------------------------------------- certificates */

/* ⚠️ FOUR ACROSS SINCE 2026-09-20, NOT THREE - ISO 45001 joined the row. The
   max-width went up with the column count so each certificate is the same size
   it was; at 900px in four columns they would have shrunk by a third on the one
   page whose whole job is showing them. */
/* ⭐ FLEX, NOT grid-template-columns, SINCE THE FIFTH CERTIFICATE (UL Test
   Certification, Richie 2026-09-21). Four columns of a grid put certificate
   five alone against the left margin with three empty cells beside it. Flex
   with justify-content:center centres whatever is left over, so the shelf reads
   as a shelf at any count.
   ⭐ FIVE ACROSS, ONE ROW, on Jacelyn's "certificate to 1 row" (2026-09-21).
   The basis was (100% - 3 gaps) / 4, which put the fifth certificate on a row of
   its own; it is now (100% - 4 gaps) / 5 so all five sit on one shelf.
   ⚠️ THE BASIS AND THE NUMBER OF CERTIFICATES ARE TIED TOGETHER NOW. A sixth
   certificate added to certificates() in inc/site.php will wrap to a second row
   and land back where the fifth was. Change the divisor and the gap count
   together when the list grows - they are 5 and 4 because there are five. */
.certs {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 22px; max-width: 1120px; margin-inline: auto;
}
.cert {
  flex: 0 1 calc((100% - 4 * 22px) / 5);
  margin: 0;
  background: #fff; border: 1px solid var(--line); border-radius: var(--radius);
  padding: 18px; text-align: center; box-shadow: var(--shadow);
}
.cert img { width: 100%; height: auto; border: 1px solid var(--line); border-radius: 6px; }
.cert p { margin: 12px 0 0; font-size: .86rem; font-weight: 600; color: var(--muted); }

/* The blur disclaimer. It replaced the "click to view full size" line in the
   same spot, so it reads the same way - a quiet note under the grid. */
.cert-note { text-align: center; margin-top: 26px; color: var(--muted); font-size: .92rem; }

/* BCA, in the text form its own letter permits - see certifications/index.php. */
.cert-reg {
  max-width: 760px; margin: 44px auto 0; padding: 26px 30px;
  background: var(--bg-soft); border: 1px solid var(--line); border-radius: var(--radius);
}
.cert-reg h2 { margin: 0 0 14px; font-size: 1.15rem; }
.cert-reg ul { margin: 0 0 14px; padding-left: 20px; }
.cert-reg li { margin-bottom: 6px; font-weight: 600; }
.cert-reg p { margin: 0; font-size: .92rem; color: var(--muted); }
/* The validity line under the list came off on Richie's mark-up (2026-09-21),
   which left the ul's 14px and the last li's 6px propping up a bottom edge with
   nothing on it - the box read bottom-heavy against its own 26px padding. */
.cert-reg ul:last-child { margin-bottom: 0; }
.cert-reg ul:last-child li:last-child { margin-bottom: 0; }

/* --------------------------------------------------------------- page banner */

/* ⚠️ THE INNER PAGES' VERSION OF THE HOME HERO. Same photograph-under-a-wash
   treatment, same gradient direction, same white headline - so arriving at
   /about/ from the home page does not feel like arriving at a different site.
   It is shorter, because these pages have a job to get on with. */
.page-banner {
  background: var(--ink) url("/assets/img/bg/banner.jpg") center/cover no-repeat;
  color: #fff; padding: clamp(48px, 6vw, 70px) 0 clamp(42px, 5vw, 60px); position: relative;
}
.page-banner::before {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, rgba(14,19,25,.88) 0%, rgba(14,19,25,.72) 38%,
                                     rgba(14,19,25,.46) 100%);
}
.page-banner .wrap { position: relative; }
.page-banner h1 { margin: 0; color: #fff; }
.page-banner-sub { margin: 12px 0 0; max-width: 660px; color: rgba(255,255,255,.9); font-size: 1.0625rem; }
.crumbs { font-size: .8125rem; color: rgba(255,255,255,.62); margin-bottom: 12px; display: flex; gap: 8px; flex-wrap: wrap; }
.crumbs a { color: rgba(255,255,255,.9); }
.crumbs span { opacity: .6; }

/* ---------------------------------------------------------------- prose bits */

.prose h2 { margin-top: 1.6em; }
.prose h2:first-child { margin-top: 0; }

.panel {
  background: #fff; border: 1px solid var(--line); border-left: 4px solid var(--orange);
  border-radius: var(--radius-sm); padding: 20px 24px; margin: 0 0 20px;
}
.panel h3 { margin: 0 0 8px; color: var(--ink); }
.panel p:last-child, .panel ul:last-child { margin-bottom: 0; }

.note {
  background: var(--orange-soft); border-radius: var(--radius-sm);
  padding: 16px 20px; font-size: .92rem; color: var(--ink-2);
}

.spec-group { margin-bottom: 26px; }
.spec-group > h3 {
  font-size: 1.02rem; color: #fff; background: var(--ink-2);
  display: inline-block; padding: 6px 16px; border-radius: 999px; margin-bottom: 14px;
}
.spec-card {
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 16px 20px; margin-bottom: 12px; background: #fff;
}
.spec-card h4 { margin: 0 0 8px; font-size: .98rem; color: var(--orange-dark); }
.spec-card ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.spec-card li { font-size: .9rem; color: var(--muted); }

.table-scroll { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius-sm); }
table.data { width: 100%; border-collapse: collapse; font-size: .86rem; min-width: 640px; background: #fff; }
table.data th, table.data td { padding: 10px 14px; text-align: left; border-bottom: 1px solid var(--line); vertical-align: top; }
table.data thead th { background: var(--bg-soft); font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
table.data tbody tr:last-child td { border-bottom: 0; }

/* -------------------------------------------------------------------- forms */

.form-grid { display: grid; gap: 16px; }
.field label { display: block; font-size: .85rem; font-weight: 700; margin-bottom: 6px; color: var(--ink-2); }
.field input, .field textarea {
  width: 100%; padding: 12px 14px; font: inherit; font-size: .95rem;
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: #fff; color: var(--ink);
}
.field input:focus, .field textarea:focus { outline: 2px solid var(--orange); outline-offset: 1px; border-color: var(--orange); }
.field textarea { min-height: 150px; resize: vertical; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.hp { position: absolute; left: -9999px; }

.alert { padding: 14px 18px; border-radius: var(--radius-sm); margin-bottom: 20px; font-size: .93rem; }
.alert-ok { background: #E9F7EF; border: 1px solid #BFE5CD; color: #1E5B36; }
.alert-err { background: #FDECEA; border: 1px solid #F5C6C0; color: #8B2318; }

.contact-cards { display: grid; gap: 14px; }
.contact-card {
  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 18px 20px; background: #fff;
}
.contact-card h3 { margin: 0 0 4px; font-size: .84rem; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
.contact-card p { margin: 0; font-weight: 600; }

/* ── the WhatsApp row, contact page card stack (Richie, 2026-09-20) ───────── */
/* ⚠️ GREEN ON THE GLYPH ONLY. The link text keeps the site's own colour so the
   Telephone / WhatsApp / Email rows read as one column; the mark carries the
   recognition. One of three places the icon is drawn - footer, the round float,
   here - and all three use wa_svg() from inc/site.php. */
.contact-wa { display: inline-flex; align-items: center; gap: .55rem; }
.contact-wa svg { flex: none; color: #25D366; }
.contact-wa:hover svg, .contact-wa:focus-visible svg { color: #1ebe5b; }

/* ----------------------------------------------------------------- CTA band */

/* The home-v2 CTA: a photograph under a heavy wash, not a flat dark strip. */
.cta-band { position: relative; isolation: isolate; overflow: hidden; color: #fff; }
.cta-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: -2; }
.cta-band::before { content: ""; position: absolute; inset: 0; z-index: -1; background: rgba(15,20,26,.86); }
.cta-in {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1.5rem; flex-wrap: wrap; padding-block: clamp(36px, 5vw, 54px);
}
.cta-band h2 { margin: 0 0 .35rem; color: #fff; }
.cta-band p { margin: 0; color: rgba(255,255,255,.82); max-width: 620px; font-size: .9375rem; }
.cta-actions { display: flex; gap: 12px; flex-wrap: wrap; }

/* ------------------------------------------------------------------- footer */

/* The home-v2 footer. Same four columns the site already had - keeping every
   product and service one click from the bottom of any page was Jacelyn's call
   on 2026-09-17, against the mock-up's two short columns. */
.site-footer { background: var(--dark); color: rgba(255,255,255,.72); font-size: .875rem; padding: clamp(36px, 5vw, 52px) 0 0; }
.footer-in { display: grid; grid-template-columns: 1.4fr 1fr 1fr .8fr; gap: 32px; padding-bottom: 36px; }
.footer-brand img { height: 52px; width: auto; margin-bottom: 14px; }
.footer-tag { color: #fff; font-weight: 700; margin: 0 0 10px; }
.footer-addr { margin: 0; line-height: 1.9; }

/* ── the WhatsApp icon, footer brand column (Richie, 2026-09-20) ──────────── */
/* ⚠️ A PILL, NOT A BARE GLYPH. The footer is dark and this sits under a block
   of muted grey text; a 22px outline on its own reads as decoration rather than
   something to tap. It also gives it a real 40px-plus target on a phone.
   ⚠️ BRAND GREEN ONLY ON HOVER/FOCUS. At rest it takes the footer's own muted
   white so the bottom of the page stays one colour, which is the "refine the
   footer" work this was asked on top of. */
.footer-social { margin: 16px 0 0; }
.footer-wa {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 9px 16px 9px 13px; border-radius: 999px;
  border: 1px solid rgba(255,255,255,.22);
  font-weight: 600; line-height: 1;
  transition: background .2s ease, border-color .2s ease, color .2s ease;
}
.footer-wa svg { flex: none; }
.footer-wa:hover, .footer-wa:focus-visible {
  background: #25D366; border-color: #25D366; color: #0b2e18;
  text-decoration: none;
}
.site-footer h3 { color: #fff; font-size: .75rem; text-transform: uppercase; letter-spacing: .12em; margin: 0 0 14px; }
.site-footer ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.site-footer a { color: rgba(255,255,255,.72); }
.site-footer a:hover { color: #fff; text-decoration: none; }
.footer-bottom {
  border-top: 1px solid rgba(255,255,255,.12); padding: 16px 0;
  display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  font-size: .8125rem; color: rgba(255,255,255,.55);
}
.footer-bottom p { margin: 0; }
/* The phone-only half of footer design F2 - see the note in inc/footer.php.
   Hidden by default so the desktop footer is exactly what it always was; the
   620px query below turns these on and the two deep lists off. */
.footer--m { display: none; }
.footer-act { display: none; }

/* -------------------------------------------------------------- responsive */

@media (max-width: 1080px) {
  .grid-4 { grid-template-columns: repeat(3, 1fr); }
  .footer-in { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 900px) {
  .burger { display: block; }
  .nav {
    position: fixed; inset: 0 0 0 auto; width: min(86vw, 360px);
    background: #fff; border-left: 1px solid var(--line);
    flex-direction: column; align-items: stretch; gap: 0;
    padding: 20px; overflow-y: auto;
    transform: translateX(100%); transition: transform .25s ease;
    box-shadow: var(--shadow-lg);
  }
  .nav.is-open { transform: translateX(0); }
  .nav-list { flex-direction: column; align-items: stretch; gap: 2px; margin-bottom: 4px; }
  .nav-item > a { padding: 13px 10px; font-size: 1rem; }
  .nav-item.is-active > a::after { display: none; }
  .caret { display: none; }
  .sub-toggle {
    display: block; position: absolute; right: 4px; top: 6px;
    width: 38px; height: 38px; border: 0; background: none; cursor: pointer;
  }
  .sub-toggle::after {
    content: ""; position: absolute; left: 13px; top: 13px;
    width: 9px; height: 9px; border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted);
    transform: rotate(45deg); transition: transform .2s ease;
  }
  .sub-toggle[aria-expanded="true"]::after { transform: rotate(-135deg); }
  .sub {
    position: static; opacity: 1; visibility: visible; transform: none;
    border: 0; box-shadow: none; padding: 0 0 6px 10px; min-width: 0;
    display: none; border-left: 2px solid var(--line);
  }
  .sub.is-open { display: block; }
  /* ⭐ GET A QUOTE IN THE BAR, LEFT OF THE BURGER - see the note in
     inc/header.php. The margin-top that used to be here spaced it under the
     last menu item inside the drawer; it is not in the drawer any more.
     ⚠️ THE ORDER PAIR IS WHAT PUTS IT LEFT OF THE BURGER. In the markup the
     button comes after the burger, because that is what keeps the desktop
     right - nav, then button, no order needed. On a phone `order` swaps the
     two without touching the desktop's DOM sequence.
     ⚠️ AND THE BAR GAP DROPS TO 10px. Logo, pill and burger at the desktop's
     24px gap measured 283px of a 280px bar at 320px - four pixels of sideways
     scroll on an iPhone SE, which is the exact fault that was fixed on the home
     page and the product pages earlier today. */
  .header-in { gap: 10px; }
  .nav-cta {
    order: 2; margin: 0 0 0 auto;
    padding: .5rem .85rem; font-size: .8125rem; white-space: nowrap;
  }
  .burger { order: 3; margin-left: 0; }
  .split { grid-template-columns: 1fr; gap: 30px; }
  /* ⛔ THE ABOUT PAGE'S DOOR CUTAWAY COMES OFF THE PHONE ENTIRELY - Jacelyn,
     2026-09-17: "can you hide the door at the about page? just only for
     mobile."
     It earns its place in the two-column layout above this breakpoint, where it
     sits beside the copy and balances it. The line directly above is what takes
     that away: at one column the drawing stops being a companion to the text
     and becomes 500px of technical diagram stranded at the foot of six
     paragraphs, which is where it reads as a loose offcut.
     ⚠️ ON THE MEDIA BOX, NOT THE img. Hiding the image alone would leave the
     .split-media wrapper behind as an empty grid row, still collecting the
     30px gap above it. */
  .split-media--hide-sm { display: none; }
  /* The map goes above the list rather than beside it - at this width a 1000px
     map in the right-hand column is 380px wide and the country labels stop
     being readable. */
  .map-band { grid-template-columns: 1fr; }
  .map-band__map { order: -1; }
  .grid-3, .grid-4 { grid-template-columns: repeat(2, 1fr); }
  .gallery { grid-template-columns: repeat(3, 1fr); }
  /* Four certificates on a tablet is 2x2, not a squeezed single row. */
  .certs { gap: 18px; max-width: 620px; }
  .cert { flex-basis: calc((100% - 18px) / 2); }
  .section { padding: 52px 0; }
}

@media (max-width: 620px) {
  .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
  .gallery { grid-template-columns: repeat(2, 1fr); }
  /* ⭐ THREE THEN TWO ON PHONES, NOT ONE PER ROW - Jacelyn, 2026-09-21: "for
     mobile the about page. the certification, show it like the home page
     mobile. 3 in first row, 2 in second row."
     ⚠️ THIS MATCHES .v2-certs__row ON THE HOME PAGE DELIBERATELY. That is the
     point of the instruction - the same five certificates were 3 + 2 there and
     a single 320px-wide column here, five cards tall, which made the about page
     band roughly three times the height for the same content.
     justify-content:center on .certs is what centres the two on the second row;
     it is already set on the base rule, so the odd row does not sit left.
     ⚠️ THE PADDING HAS TO COME DOWN WITH THE WIDTH. .cert is a bordered card
     with 18px of padding - fine at a third of 1120px, but at a third of a 350px
     phone the cell is ~110px and 36px of that would be padding, leaving the
     certificate itself under 75px. 8px keeps the card and gives the picture the
     width. The caption drops with it or three words take five lines.
     ⛔ min-width:0 IS LOAD-BEARING, NOT TIDYING. A flex item defaults to
     min-width:auto, which refuses to shrink past its longest word - here
     "Registration" - and .cert is a bordered card, so its padding and borders
     count toward that floor too. Without it the third card cannot fit and the
     row falls back to 2 + 2 + 1.
     ⛔ max-width MUST BE RESET TO none. The 860px block above caps .certs at
     620px for the two-up tablet layout. That cap is still in force here and,
     left alone, it is not what breaks the row - but the gap:18px beside it is,
     so both are overridden together rather than relying on one. */
  .certs { max-width: none; gap: 10px; }
  .cert { flex-basis: calc((100% - 22px) / 3); padding: 8px; min-width: 0; }
  .cert p { margin-top: 8px; font-size: .72rem; }
  .field-row { grid-template-columns: 1fr; }
  .footer-in { grid-template-columns: 1fr; gap: 26px; }

  /* ── footer design F2 - see the note in inc/footer.php ─────────────────── */
  /* ⛔ THE TWO DEEP LISTS COME OFF THE PHONE. Fifteen product and service links
     stacked single file is most of the 1130px this footer used to run, and
     every one of those pages is already in the menu two taps away. */
  .footer-col--list { display: none; }
  .footer--m { display: block; }
  li.footer--m { display: list-item; }
  .footer--d { display: none; }
  /* ⚠️ THE TEL AND EMAIL LINES GO, THE BUTTONS REPLACE THEM. Do not simply
     delete this rule to "put the email back" - that would print the number
     twice, once as text and once as the Call us button directly beneath it. */
  .footer-contact { display: none; }
  /* One button, full width - it was a 1fr 1fr pair until the Get a Quote half
     came out. See the note in inc/footer.php for why that half went, and why it
     sits last rather than up under the address.
     ⚠️ NO margin-top. It is a grid cell of .footer-in now, so the 26px grid gap
     already separates it from the Explore list above; a margin here would stack
     on top of that and float the button away from the footer. */
  .footer-act { display: grid; gap: 10px; }
  .footer-act a {
    display: flex; align-items: center; justify-content: center;
    padding: .72rem 1rem; border-radius: 999px; font-weight: 600; font-size: .875rem;
  }
  .footer-act__call { background: rgba(255,255,255,.12); color: #fff; }
  .footer-act__call:hover { background: rgba(255,255,255,.2); color: #fff; }
  /* Six links read better in two short columns than one tall one. */
  .footer-col--company ul { grid-template-columns: 1fr 1fr; gap: 10px 14px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
}

/* ── the floating WhatsApp button (Richie, 2026-09-20) ────────────────────── */
/* "watsapp icon shall be round in shape here and when scroll down the page
   shall always be there" - so: a circle, position:fixed, no scroll listener and
   nothing to fade it in. It is simply always on screen.
   ⚠️ z-index 90 KEEPS IT UNDER the sticky header (100) and the lightbox (200).
   ⚠️ WHITE GLYPH ON THE GREEN HERE, unlike the hero pill which uses ink. There
   is no text beside it to keep legible, so the contrast pairing that governs
   the pill does not apply - and the white-on-green disc is the mark people
   recognise without reading.
   ⚠️ SMALLER AND TIGHTER INTO THE CORNER ON A PHONE, because this is the
   bottom-right of every page - the objection that removed the old floating call
   button. 52px still clears a 44px tap target. */
.wa-float {
  position: fixed; right: 22px; bottom: 22px; z-index: 90;
  width: 58px; height: 58px; border-radius: 50%;
  display: grid; place-items: center;
  background: #25D366; color: #fff;
  box-shadow: 0 8px 22px rgba(11, 46, 24, .32);
  transition: background .2s ease, transform .2s ease, box-shadow .2s ease;
}
.wa-float:hover, .wa-float:focus-visible {
  background: #1ebe5b; color: #fff; text-decoration: none;
  transform: translateY(-2px); box-shadow: 0 10px 26px rgba(11, 46, 24, .40);
}
.wa-float svg { flex: none; }

@media (max-width: 620px) {
  .wa-float { right: 14px; bottom: 14px; width: 52px; height: 52px; }
}

@media print { .wa-float { display: none; } }

/* ----------------------------------------------------------------- lightbox */

.lightbox {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(12,14,17,.9);
  display: none; place-items: center; padding: 40px;
}
.lightbox.is-on { display: grid; }
.lightbox img { max-width: min(92vw, 1100px); max-height: 86vh; width: auto; border-radius: 8px; }
.lightbox-close {
  position: absolute; top: 18px; right: 22px;
  width: 42px; height: 42px; border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(255,255,255,.14); color: #fff; font-size: 26px; line-height: 1;
}
.lightbox-close:hover { background: rgba(255,255,255,.26); }

/* Per-page banner artwork. Defaults to bg/banner.jpg in .page-banner above. */
/* ⚠️ THESE ARE 1600x375 AND THAT IS NOT ARBITRARY. The band renders 1440x288 on
   a laptop with center/cover, so a taller picture is thrown away and a shorter
   one gets scaled up. Crop new artwork to the strip before it goes in.
   ⚠️ THE LEFT THIRD IS UNDER rgba(14,19,25,.88) - see .page-banner::before - so
   anything worth seeing has to be right of centre, and the left has to be quiet
   enough for a white headline. A busy left edge just becomes black. */
.page-banner.bn-about { background-image: url("/assets/img/bg/banner-2.jpg"); }
.page-banner.bn-cert  { background-image: url("/assets/img/bg/cert-bg.jpg"); }
.page-banner.bn-portfolio { background-image: url("/assets/img/bg/banner-portfolio.jpg"); }
/* ⭐ GENERATED ARTWORK, NOT A VANSON PHOTOGRAPH - Jacelyn, 2026-09-21: "create a
   picture for the top section background picture." It is the only banner on the
   site that is not a real photograph, and the only one showing the work being
   done rather than a finished building, which is the point on a Services page.
   ⚠️ IT IS ALREADY CROPPED TO 1600x375 to the rule above. The generator returned
   1942x809; the centre band was taken and scaled, NOT the whole frame squashed.
   Regenerating means cropping again - do not drop a raw 3:2 render in here. */
.page-banner.bn-services { background-image: url("/assets/img/bg/banner-services.jpg"); }
/* ⭐ ALSO GENERATED, same 2026-09-21 run and the same caveat as bn-services -
   it is artwork, not a Vanson photograph. Deliberately a tight product detail
   (the laminated glass edge at the frame) rather than another building or
   lobby: Portfolio already carries a tower and Services carries installers, and
   a third wide architectural shot would have made the three banners
   interchangeable. Cropped 1916x821 -> centre band -> 1600x375. */
.page-banner.bn-products { background-image: url("/assets/img/bg/banner-products.jpg"); }

/* ===========================================================================
   HOME PAGE SECTIONS
   These were assets/css/home-v2.css until 2026-09-17, when that design became
   the site and the separate file was deleted. They are still prefixed .v2-*
   because the home page's markup is, and renaming both at once for tidiness is
   a diff nobody can review. Everything now reads the tokens at the top of this
   file, so a palette change lands here too.
   ========================================================================= */

/* ── hero ────────────────────────────────────────────────────────────────── */
.v2-hero { position: relative; isolation: isolate; color: #fff; overflow: hidden; }
.v2-hero__bg {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: 60% 50%; z-index: -2;
}
/* ⚠️ A GRADIENT, NOT A FLAT WASH. The headline sits on the left third and the
   photograph's detail is on the right; a flat overlay dark enough to read
   against would grey out the glass, which is the thing being sold. */
.v2-hero::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(90deg, rgba(14,19,25,.88) 0%, rgba(14,19,25,.72) 38%,
                                     rgba(14,19,25,.28) 66%, rgba(14,19,25,.12) 100%);
}
.v2-hero__in { padding-block: clamp(68px, 9vw, 116px) clamp(72px, 9vw, 120px); }
.v2-hero h1 { color: #fff; margin-bottom: 1rem; }
.v2-hero__lead { max-width: 46ch; color: rgba(255,255,255,.9); font-size: 1.0625rem; }
.v2-hero__btns { display: flex; gap: .75rem; flex-wrap: wrap; margin: 1.9rem 0 0; }

/* The vertical words at the right edge of the mock-up's hero. Decorative. */
.v2-hero__side {
  position: absolute; right: 26px; top: 50%; transform: translateY(-50%);
  color: rgba(255,255,255,.82); font-size: .8125rem; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase; text-align: right; line-height: 1.5;
}

/* ── stats strip ─────────────────────────────────────────────────────────── */
.v2-stats { background: var(--bg-soft); border-bottom: 1px solid var(--line); }
/* ⚠️ NOT FOUR EQUAL COLUMNS - THE RATIO IS LOAD-BEARING. Jacelyn, 2026-09-21:
   the country list had to come down to two lines. At repeat(4, 1fr) each cell
   is 295px and the text well only ~220px once the icon and gap are taken off,
   which is not enough for the eight countries, so they ran to three.
   ⛔ DO NOT "TIDY" THIS BACK TO repeat(4, 1fr), and do not nudge column 1 up.
   The widths were measured, not guessed, and the four are in tension: giving
   column 1 enough to keep "Over 30 years in Singapore" on one line takes it
   from column 3, whose "Exclusive Distributor & Appointed Vetrotech & Dorma
   Distributor" then breaks to THREE lines - trading the problem, not fixing
   it. .82/1.02/1.0/1.16 is the only split tested that puts the countries on
   two lines while leaving every other cell no worse.
   ⚠️ THE RATIO ONLY WORKS WITH THE 12px CELL PADDING SET BELOW - the two were
   tuned together and neither survives alone. Equal columns were re-tested at
   12px and still put "Singapore & Southeast Asia" on two lines and the country
   list on three, which is why the ratio is still here.
   Only the >900px grid is touched; the 2-column mobile grid is set below. */
.v2-stats__in {
  display: grid; grid-template-columns: .82fr 1.02fr 1fr 1.16fr; padding-block: 22px;
}
/* ⚠️ TOP-ALIGNED, NOT CENTRED, SINCE 2026-09-20. Richie's rewrite of the third
   and fourth items took them to three lines while "Since 1991" stayed at two,
   and centring each cell independently then put the four orange icons at four
   different heights across the strip. */
/* ⚠️ 12px, NOT 18px - THIS IS A MEASUREMENT, NOT A STYLE PREFERENCE. Dropping
   6px a side returns 12px of text width to every cell, and that is exactly what
   buys "Singapore & Southeast Asia" onto one line and "Over 30 years in
   Singapore" back onto one line, without taking anything from column 3. Tried
   at 10px and 8px too: both go further than needed and start orphaning "Tested"
   off the end of column 2's second line, so 12px is the floor, not the start of
   a range. Raising it back to 18px re-breaks both of those cells. */
.v2-stat { display: flex; align-items: flex-start; gap: .8rem; padding-inline: 12px; }
.v2-stat + .v2-stat { border-left: 1px solid var(--line); }
.v2-stat__ico { color: var(--orange); display: flex; flex: 0 0 auto; }
.v2-stat__txt { display: flex; flex-direction: column; line-height: 1.3; }
.v2-stat__txt strong { color: var(--ink); font-size: 1.0625rem; }
.v2-stat__txt span { font-size: .8125rem; }

/* ── sections ────────────────────────────────────────────────────────────── */
.v2-sec { padding: clamp(52px, 7vw, 82px) 0; }
.v2-sec--soft { background: var(--bg-soft); }
.v2-sec__lead { max-width: 62ch; margin-bottom: 2.2rem; }
.v2-sec__head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 1.5rem; margin-bottom: 2rem; flex-wrap: wrap;
}
.v2-sec__head h2 { margin-bottom: 0; }
.v2-more { color: var(--orange); font-weight: 600; font-size: .9375rem; white-space: nowrap; }
.v2-more:hover { text-decoration: underline; }

/* ── about ───────────────────────────────────────────────────────────────── */
/* ⭐ WIP #2608, picked by Jacelyn 2026-09-21 out of three mock-ups of this band.
   Three changes, all of them arrangement - not one word of the copy moved. */

/* ⚠️ align-items IS stretch, NOT center, AND THAT IS THE FIX FOR A REAL BUG.
   The rule below has always said height:100%, but a grid item under
   `align-items: center` is sized to its content, so 100% resolved against an
   auto height and did nothing. The photograph therefore rendered at its own
   aspect ratio - about 250px against 570px of text - and floated in the middle
   of a half-empty column. Putting it back to `center` re-breaks it. */
.v2-about { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(28px, 5vw, 64px); align-items: stretch; }
.v2-about__media { min-height: 100%; }
/* ⚠️ object-position IS 85%, NOT THE DEFAULT 50%, AND IT MATTERS MORE THAN IT
   LOOKS. vanson-systems.jpg is 1920x849 - very wide - so filling a
   portrait-ish column takes a narrow VERTICAL slice out of it and throws the
   rest away. Centred, that slice lands on the empty white sky in the middle of
   the frame and the band reads as a blank rectangle. At 85% it lands on the
   curtain-wall glass, which is the part of the photograph that is about what
   Vanson sells, while still keeping the Singapore skyline in the bottom-left.
   ⚠️ THE SECOND VALUE DOES NOTHING HERE and is left at 50% honestly: at this
   column size the scaled image is already only as tall as the box, so there is
   no vertical slack to position. It starts mattering the moment the picture is
   swapped for a taller one - which is also when 85% stops being right. */
.v2-about__media img {
  width: 100%; height: 100%; min-height: 440px; max-height: 620px;
  object-fit: cover; object-position: 85% 50%; border-radius: var(--radius);
}
/* ⭐ TWO LINES, NOT THREE - Jacelyn, 2026-09-21: "Trusted Partner in
   Architectural & Fire Safety Solutions in 2 lines instead of 3."
   ⚠️ THE FONT-SIZE IS WHAT DOES IT, AND THE max-width HAD TO GO FIRST. The cap
   was 18ch, which forced the break after "Trusted Partner in" on its own; but
   removing the cap alone still gives three lines, because at the inherited
   2.1rem the heading is about 960px of text in a 558px column. At 1.85rem it
   fits in two. Do not put the 18ch cap back - the two rules fight, and the cap
   wins.
   ⚠️ IT IS A CLAMP, SO CHECK BOTH ENDS. The floor is 1.4rem for phones, where
   the column is the full width and the heading breaks where it likes anyway. */
.v2-about__copy h2 { max-width: none; font-size: clamp(1.4rem, 2.25vw, 1.85rem); text-wrap: balance; }

/* ⛔ THE ORANGE-RULE CALL-OUT IS GONE, AND ON PURPOSE. It lived here from the
   #2608 mock-up as .v2-about__claim - a 4px orange left border with the
   sentence at 1.19rem/600. Jacelyn took it off in two steps on 2026-09-21:
   "remove the orange line, change the text to 16 px". The sentence is an
   ordinary 16px paragraph again, so the only rule it still needs is the colour
   on its one <strong>. Do not bring the border or the larger size back. */
.v2-about__copy strong { color: var(--orange-dark); font-weight: 700; }

/* ⛔ THERE IS NO DISTRIBUTOR-LIST STYLING HERE, AND THAT IS DELIBERATE. The
   #2608 mock-up set the five brand names as chips, then as labelled hairline
   rows; Jacelyn kept the orange rule above and sent the rest back to prose
   ("as in back to original words"). The distributorships are a paragraph in
   index.php and need no rules of their own. */

/* ── product rail ────────────────────────────────────────────────────────── */
/* ⭐ THE #2439 TILE IN A ROW THAT SCROLLS, on Jacelyn's instruction (2026-09-17):
   "can it be #2439 style but 9. if 1 screen doesnt fit, maybe put an arrow at
   the side." Five are in view at a time on a desktop. */
.v2-rail { position: relative; }
.v2-prods {
  display: flex; gap: 16px;
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory; scroll-behavior: smooth;
  /* ⚠️ THE SCROLLBAR IS HIDDEN, AND THAT IS ONLY SAFE BECAUSE THE ARROWS EXIST.
     A hidden scrollbar on a track with no other control is a row of content
     nobody can reach with a mouse. */
  scrollbar-width: none; -ms-overflow-style: none;
  /* ⛔ NO PADDING ON THE LEFT OR RIGHT OF THIS TRACK, EVER. With `scroll-snap-type:
     mandatory` the snapport starts at the padding box, so a 4px padding-left put
     the first tile's snap point at 4px and the browser parked the row there on
     load - scrollLeft never read 0, so the left arrow never knew it was at the
     start and sat there permanently. Vertical padding is harmless. */
  padding-bottom: 6px; margin-bottom: -6px;
}
.v2-prods::-webkit-scrollbar { display: none; }
.v2-prod {
  position: relative; display: block; overflow: hidden;
  border-radius: var(--radius); text-decoration: none; aspect-ratio: 3 / 4;
  background: var(--ink);
  /* Five across the visible width, and they do not shrink to fit nine in. */
  flex: 0 0 calc((100% - 4 * 16px) / 5);
  scroll-snap-align: start;
}
.v2-prod img { width: 100%; height: 100%; object-fit: cover; transition: transform .35s; }
.v2-prod:hover img { transform: scale(1.05); }
/* ⚠️ THE FOCUS RING DRAWS INSIDE THE TILE. Outside it, the first and last tiles
   would have it clipped by the track's own overflow. */
.v2-prod:focus-visible { outline: 3px solid var(--orange); outline-offset: -3px; }
/* The caption sits on a gradient so it stays readable whatever the photo does. */
.v2-prod__cap {
  position: absolute; inset: auto 0 0 0; display: flex; align-items: flex-end;
  justify-content: space-between; gap: .6rem; padding: 14px;
  color: #fff; font-size: .875rem; font-weight: 600; line-height: 1.3;
  background: linear-gradient(0deg, rgba(10,14,19,.86) 0%, rgba(10,14,19,.45) 55%, rgba(10,14,19,0) 100%);
}
.v2-prod__arw { flex: 0 0 auto; font-size: 1rem; }

/* ⚠️ THE ARROWS SIT OUTSIDE THE TILES, NOT ON TOP OF THEM. The section is
   1180px inside a wider band, so there is white space either side to put them
   in and nothing has to cover a photograph. Below 1280px there is no such
   margin and they move to overlap the edge tiles - see the media query. */
.v2-rail__btn {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  width: 44px; height: 44px; border-radius: 50%; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  background: #fff; color: var(--ink); border: 1px solid var(--line);
  box-shadow: 0 6px 20px rgba(23,32,42,.12);
  transition: background .15s, color .15s, opacity .2s, visibility .2s;
}
.v2-rail__btn:hover { background: var(--orange); color: #fff; border-color: var(--orange); }
.v2-rail__btn--prev { left: -22px; }
.v2-rail__btn--next { right: -22px; }
/* ⛔ HIDDEN AT THE ENDS, NOT GREYED OUT. A button that cannot do anything should
   not be tabbable either, which `visibility` handles and `opacity` does not. */
.v2-rail__btn[disabled] { opacity: 0; visibility: hidden; }

/* ── service cards ───────────────────────────────────────────────────────── */
.v2-caps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.v2-cap {
  display: block; padding: 24px; text-decoration: none;
  border: 1px solid var(--line); border-radius: var(--radius); background: #fff;
  transition: border-color .15s, box-shadow .15s, transform .15s;
}
.v2-cap:hover { border-color: #D6DBE1; box-shadow: var(--shadow); transform: translateY(-2px); }
.v2-cap .card-icon { margin-bottom: 16px; }
.v2-cap h3 { margin-bottom: .35rem; }
.v2-cap p { margin: 0; font-size: .875rem; color: var(--muted); }

/* ── projects mosaic ─────────────────────────────────────────────────────── */
/* ⚠️ THE MOCK-UP'S SHAPE: one tall tile on the left, four in a 2x2 beside it. */
.v2-projs {
  display: grid; gap: 16px;
  grid-template-columns: 1.15fr 1fr 1fr;
  grid-template-rows: 1fr 1fr;
}
/* ⚠️ THE TILE HEIGHT IS SET HERE, NOT BY THE PHOTOGRAPH. Richie, 2026-09-20:
   "overall the picture size smaller by 20%". Every small tile used to take its
   height from its own image - and the four he was sent are 3:4 portraits off a
   phone, so a 364px-wide tile rendered 486px tall and the band came to 988px,
   most of a laptop screen for one section. Pinning the ratio makes the tile a
   fixed fraction of that at every width, because it is a ratio and not a pixel
   height. object-fit:cover on the img means the photographs are cropped a
   little tighter, not squashed.
   ⭐ 3/2 SINCE 2026-09-21, WAS 15/16 - Jacelyn, on the laptop view: "the section
   height is too big". 15/16 is a near-square tile, so Richie's 20% cut above
   still left the band 793px and the whole section 1056px - 1.38 screens on a
   1366x768 laptop, which cannot be seen in one view at any scroll position.
   3/2 takes the tile to 243px, the band to 502px and the section to 764px,
   inside a laptop viewport, and it is a standard photographic landscape crop
   rather than an arbitrary number. Measured in Chrome at 1280x800, 1366x768 and
   1440x900 - the band is the same height at all three, because the mosaic is
   capped by --wrap and not by the viewport.
   ⚠️ CHECK THE CROP BEFORE GOING SHORTER. Two of the five photographs are 3:4
   portraits (1200x1600) and cover crops them from the centre, so past 3/2 the
   tops of the doors start going. */
.v2-proj {
  position: relative; display: block; overflow: hidden;
  border-radius: var(--radius); text-decoration: none; background: var(--ink);
  min-height: 190px; aspect-ratio: 3 / 2;
}
/* The big tile spans both rows, so its height is those two rows plus the gap -
   it must not carry a ratio of its own or it fights the row sizing. */
.v2-proj--big { grid-row: span 2; aspect-ratio: auto; }
.v2-proj img { width: 100%; height: 100%; object-fit: cover; transition: transform .35s; }
.v2-proj:hover img { transform: scale(1.04); }
.v2-proj__cap {
  position: absolute; inset: auto 0 0 0; display: flex; align-items: flex-end;
  justify-content: space-between; gap: .6rem; padding: 16px; color: #fff;
  background: linear-gradient(0deg, rgba(10,14,19,.88) 0%, rgba(10,14,19,.42) 58%, rgba(10,14,19,0) 100%);
}
.v2-proj__cap > span:first-child { display: flex; flex-direction: column; }
.v2-proj__cap strong { font-size: .9375rem; }
.v2-proj__cap span span { font-size: .75rem; color: rgba(255,255,255,.82); }

/* ── certifications band ─────────────────────────────────────────────────── */
/* ⭐ OPTION B OFF WIP #2611 - Jacelyn, 2026-09-21: "change the certificate to
   option B." The band is NO LONGER A TWO-COLUMN GRID. It was
   `.85fr 1.15fr` with the copy on the left, which is what forced five portrait
   certificates to wrap 2 + 2 + 1 inside the right-hand half and made the
   section 1300px tall. Header above, one full-width row under it: 736px. */
/* ⚠️ TWO DIFFERENT WIDTHS, AND THEY ARE BOTH MEASUREMENTS. The block is 900px
   because "Recognised for Quality and Compliance" sets at about 715px at
   2.1rem, and anything under ~740px breaks it after "and" - #2611 draws it on
   one line. The paragraph is pulled back in to 620px because a centred lead
   running the full 900px reads as a loose single line instead of the tidy
   two-line block in the mock-up.
   ⛔ NO 16ch CAP ON THE HEADING. That cap existed to keep it off the thumbnails
   beside it in the old split band; here it would only break the line again. */
.v2-certs__head { text-align: center; max-width: 900px; margin: 0 auto 36px; }
.v2-certs__head h2 { margin-bottom: .5rem; }
.v2-certs__head p { max-width: 620px; margin: 0 auto .6rem; }
/* ⚠️ 2x2, NOT A ROW OF FOUR. This band sits in the 1.15fr half of a two-column
   section, so a fourth certificate across took every thumbnail down to about
   140px - too small to read as a certificate at all, which is the only thing
   they are here to do. */
/* ⭐ 20% SMALLER, NOT 20% NARROWER. Richie, 2026-09-20: "i want the presentation
   of the 4 certificates to be 20% diagonal smaller in size, look too big on the
   webpage." Capping the row at 80% and scaling the gap with it (18 -> 14) takes
   each thumbnail from 314x419 to 251x335 at 1440px - 80% on BOTH sides, which is
   what "diagonal" asks for; the aspect-ratio on the img does the height. The gap
   has to come down too, or the leftover 18px pads the cells back up.
   Centred, not left-aligned: the 130px it gives up would otherwise all pile on
   the right and leave the band hanging off the page's right margin. */
/* ⭐ FLEX SINCE THE FIFTH ONE (UL Test Certification, Richie 2026-09-21). Two
   columns of a grid left certificate five hard against the left edge with an
   empty cell beside it, directly under the pair above - it read as a mistake.
   Centring the odd one out is the whole reason this is flex; the basis is the
   old 2-column width written out, so the four already here did not move and the
   20% cap above still does exactly what it did. */
/* ⭐ FIVE ACROSS, FULL WIDTH, SINCE OPTION B (WIP #2611, 2026-09-21).
   ⚠️ THE 80% CAP IS GONE AND RICHIE'S REQUEST IS STILL HONOURED. He asked on
   2026-09-20 for the certificates "20% diagonal smaller"; the cap was how that
   was done inside the old narrow column, where it took each thumbnail from
   314px to 251px. Across the full 1180px, five of them come out at about
   225px - smaller again than what he asked for - so re-adding the cap would
   now shrink them a second time, to roughly 180px, and that is below the size
   at which a certificate reads as a certificate. */
.v2-certs__row {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 14px;
}
.v2-cert { flex: 0 1 calc((100% - 4 * 14px) / 5); margin: 0; text-align: center; }
.v2-cert img {
  width: 100%; aspect-ratio: 3 / 4; object-fit: contain; background: #fff;
  border: 1px solid var(--line); border-radius: 6px; padding: 8px;
}
.v2-cert figcaption { margin-top: .55rem; font-size: .75rem; line-height: 1.4; }

/* ── home page responsive ────────────────────────────────────────────────── */
/* ⚠️ THE RAIL ARROWS COME INSIDE BEFORE THE PAGE DOES. The wrap is 1180px plus
   a 44px button either side, so below about 1280px they would hang off the
   viewport and put a horizontal scrollbar on the whole page. */
@media (max-width: 1279px) {
  .v2-rail__btn--prev { left: 8px; }
  .v2-rail__btn--next { right: 8px; }
}
@media (max-width: 1080px) {
  .v2-prod { flex-basis: calc((100% - 3 * 16px) / 4); }
  .v2-caps { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 900px) {
  /* Three tiles in view, not five - below this the six-word product names
     start breaking mid-word inside the caption. */
  .v2-prod { flex-basis: calc((100% - 2 * 16px) / 3); }

  .v2-stats__in { grid-template-columns: repeat(2, 1fr); gap: 18px 0; }
  .v2-stat + .v2-stat { border-left: 0; }
  .v2-stat:nth-child(n+3) { border-top: 1px solid var(--line); padding-top: 18px; }
  /* ⛔ min-width:0 IS WHAT STOPS THE WHOLE PAGE SCROLLING SIDEWAYS AT 320px,
     and it is not cosmetic. A grid item and a flex item both default to
     min-width:auto, which refuses to shrink below the content's min-content
     width - here the word "distributor" and the string "Singapore & ASEAN".
     Two columns that each insist on ~124px inside a 320px viewport is 40px of
     overflow, and because the strip is full-bleed the overflow scrolls the
     document, not the strip: measured on iPhone SE, right column clipped
     mid-word (WIP #2455). Both levels need it - .v2-stat is the grid item,
     .v2-stat__txt is the flex item inside it - or the inner one holds the
     width open on its own.
     The narrower padding is what buys "Since 1991" back onto one line at
     390px; 18px each side was costing 36px of a 195px column. */
  .v2-stat { padding-inline: 12px; min-width: 0; }
  .v2-stat__txt { min-width: 0; }

  /* ⚠️ .v2-certs IS NOT IN THIS RULE ANY MORE. It stopped being a grid when the
     band went to Option B (WIP #2611) - header above, one row under - so there
     are no columns left here to collapse. */
  .v2-about { grid-template-columns: 1fr; }
  .v2-hero__side { display: none; }
  /* Five across needs about 1180px to stay legible. On a tablet the row drops
     to three and wraps 3 + 2, which the centred flex row already handles. */
  .v2-cert { flex-basis: calc((100% - 2 * 14px) / 3); }
  /* ⚠️ THE 16ch CAP IS A TWO-COLUMN RULE AND HAS NO JOB HERE. Up on the
     desktop grid the copy sits in the narrow .85fr column, and 16ch is what
     keeps the heading off the certificate thumbnails beside it. The line above
     collapses that grid to one column, so the cap stops shaping anything and
     just squeezes a full-width heading into three lines. Jacelyn, 2026-09-17:
     "can it be in 2".
     Measured at 390px: uncapped it breaks "Recognised for Quality" / "and
     Compliance", longest line 303px in 350px of column. Same at 360px. Any cap
     of 20ch or more gives the identical break, so there is nothing to tune -
     the cap is simply wrong below 900px, not mis-set.
     ⛔ 320px STILL TAKES THREE LINES AND NO max-width FIXES IT. The column is
     280px there and the two-line break needs 303px, so the third line is the
     type size, not the cap - and shrinking a section heading to buy a line
     break is the wrong trade. Left as is, deliberately.
     ⛔ THE RULE THIS NOTE DESCRIBES IS GONE, AND THE NOTE IS KEPT ON PURPOSE.
     It was `.v2-certs__copy h2 { max-width: none }`, undoing a 16ch cap that no
     longer exists - Option B (WIP #2611) took the heading out of a narrow
     column and centred it full width, so there is nothing to uncap. Read the
     above as the history of why no cap belongs on this heading at any width. */

  /* ⭐ DESIGN A - SPLIT. Proposed as WIP #2466-#2468; Jacelyn, 2026-09-17:
     "i like #2468, live it."
     ON A PHONE THE PHOTOGRAPH AND THE WORDS STOP SHARING A RECTANGLE. Every
     version before this one tried to make white text readable ON the picture
     in a portrait frame, and each round paid for the text with the picture:
     .82/.66/.78 crushed a sunlit photograph to flat grey; the .62/.42/.66 that
     replaced it measured 2.79:1 under the h1 and failed outright; the even
     .66/.64/.70 that replaced THAT passed, but only by dimming the photograph
     to 27% mean brightness and 14% saturation.
     Splitting them ends the trade instead of re-pricing it. The photograph
     runs full width as a landscape band with no wash on it at all - see
     --hero-band for the height and why it is what it is - and the words sit
     underneath on flat --ink, where they are on a known colour instead of on
     whatever pixel happens to fall behind them.
     SAMPLED ON THE RENDERED PAGE AT 390px, not taken from the mock-up:
       headline 16.78:1, lead 13.05:1, eyebrow 5.33:1
       photograph 48% mean brightness, 31% mean saturation (was 27% / 14%)
     The proposal predicted 18.7 / 13.9 / 5.93 and 43% / 28%; the measured
     numbers are what is written here, and every one of them still passes.
     ⭐ THE EYEBROW LINE IS THE POINT - see the long note at .eyebrow. Brand
     orange could not clear 4.5:1 anywhere on that wash at any opacity, and
     Jacelyn kept it orange knowing it measured 2.02:1. Nothing about the
     colour changes here; it stopped sitting on a photograph. */
  /* ⚠️ 170px, DOWN FROM THE 280px THIS SHIPPED AT. Jacelyn, 2026-09-17: "the
     image is taking too much space, maybe lesser flooring? meaning the
     gradient and words shift up abit."
     ⛔ AND THE HEIGHT IS THE ONLY LEVER THAT DOES IT - object-position alone
     cannot. `cover` scales by max(390/1738, H/905), so at any band 203px or
     taller the HEIGHT drives the scale, the photograph fits its box exactly
     top to bottom, and there is no vertical overflow for object-position to
     move: every pixel of floor is on screen no matter what y you write. Only
     below 203px does the WIDTH drive instead, and the 203-H difference is the
     vertical crop. At 170 that is 33px, and `50% 15%` spends 28 of them on the
     bottom - about a third of the flooring - and 5 on the sky.
     The other half of the win is composition. At 280px the photograph was
     scaled to 538px wide and lost 148px off its sides: no trees, no sunset, no
     skyline, just floor and glass. At 170px it renders at exactly 390 and the
     whole frame is back. */
  .v2-hero { background: var(--ink); --hero-band: 170px; }
  .v2-hero__bg {
    position: static; z-index: auto;
    width: 100%; height: var(--hero-band); object-position: 50% 15%;
  }
  /* ⛔ THE FADE COVERS THE PHOTOGRAPH'S FOOT ONLY, NOT THE WHOLE HERO. The
     desktop rule is `inset: 0`, which here would put a wash straight back over
     the words - the one thing this layout exists to remove. It is there only
     so the band's bottom edge melts into the panel instead of ruling a hard
     line across the page.
     ⚠️ IT READS --hero-band RATHER THAN REPEATING THE NUMBER. These two
     heights have to be identical; written out twice, the next person to retune
     the band changes one of them and leaves a strip of ungraded photograph or
     a bar of gradient hanging under it.
     z-index goes POSITIVE because the photograph is now in normal flow at
     `auto`, not parked at -2 behind everything. */
  .v2-hero::before {
    inset: 0 0 auto 0; height: var(--hero-band); z-index: 1; pointer-events: none;
    background: linear-gradient(180deg, rgba(23,32,42,0) 68%, rgba(23,32,42,.94) 100%);
  }
  .v2-hero__in { padding-block: 30px 40px; }

  /* The 3-across project mosaic becomes 2-across; the big tile stops spanning. */
  .v2-projs { grid-template-columns: 1fr 1fr; grid-template-rows: none; }
  .v2-proj--big { grid-row: auto; grid-column: 1 / -1; min-height: 240px; }
}
/* ⛔ THE STATS STRIP GOES TO ONE COLUMN ON SMALL PHONES, AND THIS IS WHAT KEEPS
   WIP #2455 FIXED. Jacelyn, 2026-09-21, required "Blast-Resistant" to stay whole
   on one line ("Blast is not in 2nd line with resistent"), which is why index.php
   now joins it with a non-breaking hyphen. That makes it a single 132px word that
   can no longer be broken to fit. In the two-column grid the text well is only
   (100vw - 40)/2 - 63, so it stops fitting below about 430px viewport - measured
   clipped past the right edge at 320px and 390px, and because the strip is
   full-bleed that overflow scrolls the whole document, which is exactly the bug
   #2455 recorded. One column gives the cell the full width and the word fits with
   room to spare. 480px is the threshold with margin on the 430px measurement.
   ⛔ DO NOT RAISE THE NON-BREAKING HYPHEN'S PRIORITY OVER THIS RULE, and do not
   drop this block while that hyphen is still in index.php - the two go together.
   If the hyphen ever goes back to a plain "-", this block can go too. */
@media (max-width: 480px) {
  .v2-stats__in { grid-template-columns: 1fr; }
  /* The 2-col rule put the divider above items 3 and 4 only; stacked, every
     item after the first needs one. Same specificity, so order decides. */
  .v2-stat:nth-child(n+3) { border-top: 0; padding-top: 0; }
  .v2-stat + .v2-stat { border-top: 1px solid var(--line); padding-top: 18px; }
}

@media (max-width: 640px) {
  /* ⭐ THE ABOUT PICTURE IS LANDSCAPE ON PHONES - Jacelyn, 2026-09-21: "the home
     page about vanson, the picture is too big. make it shorter. just for mobile."
     ⚠️ min-height:440px IS THE THING THAT MADE IT TALL, not the image. On the
     desktop two-column grid that floor is what stops the picture collapsing
     shorter than the copy beside it, and there it is right. Once .v2-about goes
     to one column the picture is full-width, so on a 390px phone that same 440px
     floor renders a 350x440 PORTRAIT block out of a 1920x849 LANDSCAPE
     photograph - taller than it is wide, and cropped down to a narrow vertical
     slice of a wide picture. Releasing the floor and pinning 16/9 gives roughly
     350x197: about 250px shorter, and it finally shows the building as shot.
     ⚠️ object-position COMES BACK TO CENTRE, and that is not a stylistic tweak.
     The 85% up on the base rule exists because a narrow vertical slice centred
     on this photograph lands on the empty white sky in its middle. That reason
     dies here - a 16/9 crop keeps about four fifths of the frame's width, so
     there is no narrow slice to aim, and 85% would now push the composition
     hard against its right edge for no reason.
     ⛔ PHONES ONLY. Do not lift this into the 900px block: at tablet widths the
     column is ~860px wide, so 440px tall is already a 2:1 landscape and fine. */
  .v2-about__media img {
    height: auto; min-height: 0; max-height: none;
    aspect-ratio: 16 / 9; object-position: 50% 50%;
  }

  /* ⚠️ ONE AND A BIT, NOT ONE. A tile that exactly fills a phone screen looks
     like the end of the row; the sliver of the next one is what says swipe. */
  .v2-prod { flex-basis: 62%; }
  .v2-caps { grid-template-columns: 1fr; }
  /* Three across on a phone is already small; the gap comes in to 10px so the
     thumbnails keep as much of the width as possible. (The 80% cap this note
     used to undo went with Option B - see .v2-certs__row.) */
  .v2-certs__row { gap: 10px; }
  .v2-cert { flex-basis: calc((100% - 2 * 10px) / 3); }

  /* ── featured projects - design P3, hero + 2-up grid ──────────────────── */
  /* ⭐ Mocked up as WIP #2492; Jacelyn, 2026-09-17: "for mobile featured
     project, change to P3."
     ⛔ WHAT WAS HERE COLLAPSED THE BAND TO ONE COLUMN - `.v2-projs {
     grid-template-columns: 1fr }` - and that single line is what made it 1292px
     tall: five full-width tiles, over three phone screens for one section.
     Holding two columns puts all five on roughly one screen, and the big tile
     still spans both so the band keeps a lead image rather than flattening into
     five equal thumbnails.
     ⚠️ THESE RULES HAVE TO LIVE HERE, NOT IN THE 620px BLOCK FURTHER UP THE
     FILE. Media queries add no specificity, so whichever matching rule comes
     LAST in the file wins - and this one does. Written up there they were
     silently overridden by the very line they were meant to replace. */
  .v2-projs { grid-template-columns: 1fr 1fr; gap: 10px; }
  .v2-proj { min-height: 0; aspect-ratio: 1 / 1; }
  .v2-proj--big { grid-column: 1 / -1; aspect-ratio: 16 / 10; min-height: 0; }
  /* Each caption now has half the width it had, so it is set down to match. */
  .v2-proj__cap { padding: 10px; }
  .v2-proj__cap strong { font-size: .82rem; }
  .v2-proj__cap span span { font-size: .7rem; }
  .v2-proj--big .v2-proj__cap strong { font-size: 1.05rem; }
  .v2-proj--big .v2-proj__cap span span { font-size: .78rem; }
}
@media (prefers-reduced-motion: reduce) {
  /* The arrows jump the row rather than gliding it. */
  .v2-prods { scroll-behavior: auto; }
}
