/* ==========================================================================
   Prebuild Material Sdn Bhd

   Colour strategy: Restrained. Tinted neutrals carry the page; the accent
   appears on roughly 5% of the surface (rules, links, one button style).
   Neutrals are tinted toward the brand hue (H 30-45) rather than pure grey,
   so nothing is #000 or #fff.

   The logo red (#ED1B23, oklch 0.58 0.226 27) is left alone in the logo
   artwork but never used as a UI colour: at that chroma it dominates any
   surface it touches. The UI accent is the same hue pulled down to
   oklch(0.505 0.150 29), which reads as the same brand red without shouting.

   Every value below is generated from the OKLCH triplet in its comment.
   Contrast verified: body 8.1:1, muted 5.2:1, accent 6.0:1 on paper.
   Do not place muted text on paper-3 (4.4:1, fails AA).

   Radius: 0 for surfaces, --r-pill for the segmented control and the
   conveyor arrows. Two values, both deliberate. Do not add a third.
   ========================================================================== */

:root{
  --paper:        #FCF9F8;  /* oklch(0.985 0.003 45) */
  --paper-2:      #F5F1F0;  /* oklch(0.962 0.005 45) */
  --paper-3:      #ECE6E4;  /* oklch(0.928 0.007 42) */
  --line:         #DDD6D3;  /* oklch(0.880 0.009 40) */
  --field-border: #8E8380;  /* oklch(0.620 0.014 36) */
  --muted:        #746764;  /* oklch(0.525 0.017 35) */
  --body:         #564A47;  /* oklch(0.420 0.018 32) */
  --ink:          #2C1F1D;  /* oklch(0.255 0.020 30) */
  --ink-deep:     #1A100E;  /* oklch(0.185 0.017 30) */
  --accent:       #AA392E;  /* oklch(0.505 0.150 29) */
  --accent-hi:    #94231A;  /* oklch(0.440 0.150 29) */
  --accent-soft:  #FBE8E4;  /* oklch(0.945 0.022 32) */

  --wrap:1180px;
  /* Height of the sticky header. The home page video fills the screen below
     it and the phone menu drops from under it, so both read this instead of a
     number of their own, and a change here moves all three together. */
  --header-h:88px;
  --gutter:24px;

  --display:"Archivo","Segoe UI",system-ui,sans-serif;
  --body-face:"IBM Plex Sans","Segoe UI",system-ui,sans-serif;

  --ease:cubic-bezier(.22,1,.36,1);

  /* Shape: square everywhere except the segmented control, which is a pill.
     Two values, both deliberate. Do not add a third. */
  --r-pill:999px;

  /* How far each timeline block rides up into the one above it.
     Blocks two apart sit on the SAME side of the spine, and the gap between
     them works out as cardHeight - 2 x overlap. So the overlap has a hard
     ceiling of half a card height, past which they collide in one column.
     Cards run about 570px, so the ceiling is roughly 17.8rem. 16rem leaves
     about 60px of clear air between the first and third blocks, which is
     close to as tight as this can go. Raise it and check that same-side gap,
     do not just eyeball the adjacent pair. */
  --tl-overlap:16rem;
  /* (The cards now fill their half of the page, so they are taller on a wide
     screen than a narrow one and one overlap cannot suit both. Wide screens
     get their own value, just below this block.) */

  /* How long a timeline block takes to slide in. Tune here, not in three
     places: the spine fill and the node dot are both derived from it.
     Anything under about 0.8s reads as a flicker while scrolling. */
  --tl-speed:2.3s;
}

*,*::before,*::after{box-sizing:border-box}
body{
  margin:0;
  font-family:var(--body-face);
  font-size:17px;
  line-height:1.68;
  color:var(--body);
  background:var(--paper);
  -webkit-font-smoothing:antialiased;
}
img{max-width:100%;display:block}
a{color:inherit}

.wrap{max-width:var(--wrap);margin:0 auto;padding:0 var(--gutter)}
.narrow{max-width:66ch}

/* ---------- type ----------------------------------------------------------
   Headings are Title Case, applied by titlecase.py rather than typed by hand.
   They are never set in all caps: at display size that reads as shouting, which
   is what made the first pass feel aggressive. Full uppercase is reserved for
   the small labels, where it works as a size cue.                        */
h1,h2,h3,h4{font-family:var(--display);font-weight:700;color:var(--ink);margin:0;letter-spacing:-.022em}
h1{font-size:clamp(2.3rem,4.6vw,3.65rem);line-height:1.04}
h2{font-size:clamp(1.85rem,3vw,2.6rem);line-height:1.08}
h3{font-size:1.28rem;line-height:1.22;letter-spacing:-.012em}
h4{font-size:1.02rem;line-height:1.3;letter-spacing:-.008em}
p{margin:0 0 1.15em;max-width:70ch}
.lede{font-size:1.16rem;line-height:1.6;color:var(--ink)}
.small{font-size:.93rem}
.muted{color:var(--muted)}

/* Used sparingly. Budget: one per three sections. */
.eyebrow{
  font-family:var(--body-face);font-size:.735rem;font-weight:600;
  letter-spacing:.16em;text-transform:uppercase;color:var(--accent);
  margin:0 0 1rem;
}

/* ---------- buttons ---------- */
.btn{
  display:inline-flex;align-items:center;justify-content:center;
  font-family:var(--body-face);font-weight:600;font-size:.92rem;
  text-decoration:none;white-space:nowrap;
  padding:.82rem 1.6rem;border:1.5px solid transparent;cursor:pointer;
  transition:background .15s var(--ease),color .15s var(--ease),border-color .15s var(--ease);
}
.btn-primary{background:var(--accent);color:var(--paper)}
.btn-primary:hover{background:var(--accent-hi)}
.btn-line{border-color:var(--line-strong,var(--field-border));color:var(--ink)}
.btn-line:hover{border-color:var(--ink);background:var(--ink);color:var(--paper)}
.btn-onink{border-color:rgba(252,249,248,.5);color:var(--paper)}
.btn-onink:hover{background:var(--paper);color:var(--ink);border-color:var(--paper)}

.arrow-link{
  display:inline-block;font-weight:600;font-size:.95rem;color:var(--accent);
  text-decoration:none;border-bottom:1.5px solid var(--accent-soft);padding-bottom:2px;
  transition:border-color .15s var(--ease);
}
.arrow-link:hover{border-bottom-color:var(--accent)}

/* ---------- header ---------- */
.site-header{position:sticky;top:0;z-index:60;background:var(--ink-deep)}
.site-header .wrap{display:flex;align-items:center;justify-content:space-between;height:var(--header-h);gap:2rem}
/* an in-page link such as about.html#certifications lands below the header, not under it */
html{scroll-padding-top:var(--header-h)}
/* the logo never gives up width to the menu: squeezed, it went from 260px
   wide to 97px while staying 40px tall */
.brand{flex:0 0 auto}
.brand img{height:40px;width:auto;display:block}
.nav{display:flex;align-items:center;gap:1.9rem;list-style:none;margin:0;padding:0}
.nav a{
  font-size:1rem;font-weight:500;color:#C3B8B5;text-decoration:none;
  padding:.35rem 0;position:relative;transition:color .15s var(--ease);
}
.nav a:hover{color:var(--paper)}
.nav a[aria-current="page"]{color:var(--paper)}
.nav a[aria-current="page"]::after{
  content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--accent)
}
.nav-toggle{display:none;background:none;border:0;color:var(--paper);font-size:1.45rem;cursor:pointer;padding:.2rem .4rem;line-height:1}

/* ---------- hero ---------- */
/* Fills the screen below the header, so on a standard 16:9 display the
   hero and the header together occupy exactly one viewport. */
.hero{position:relative;min-height:calc(100dvh - var(--header-h));display:flex;align-items:flex-end;overflow:hidden;background:var(--ink-deep)}
.hero > img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:.46}
.hero::after{content:"";position:absolute;inset:0;background:linear-gradient(to top,rgba(26,16,14,.93),rgba(26,16,14,.5) 55%,rgba(26,16,14,.3))}
.hero .wrap{position:relative;z-index:2;padding-top:4.5rem;padding-bottom:3.4rem;width:100%}
.hero h1{color:var(--paper);max-width:17ch}
.hero p{max-width:50ch;font-size:1.1rem;color:#D9CFCC;margin-top:1.15rem}
.hero-actions{display:flex;flex-wrap:wrap;gap:.85rem;margin-top:2rem}

/* ---------- video hero ---------------------------------------------------
   The <video> sits under the same scrim as the still hero. If the file is
   missing or the browser blocks autoplay, the poster image shows and the
   section is unchanged, so the page never depends on the video loading.   */
.hero video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:.46}
/* Shown only when the phone would not start the video by itself: a round
   play button, bottom right of the hero, clear of the headline. */
.hero-play{position:absolute;right:var(--gutter);bottom:1.4rem;z-index:3;
  width:52px;height:52px;border-radius:var(--r-pill);border:1px solid rgba(252,249,248,.55);
  background:rgba(26,16,14,.55);color:var(--paper);cursor:pointer;
  display:flex;align-items:center;justify-content:center}
.hero-play[hidden]{display:none}
.hero-play svg{width:22px;height:22px;fill:currentColor;margin-left:3px}
.hero-play:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.hero .brandline{
  font-size:1.02rem;font-weight:600;letter-spacing:.17em;text-transform:uppercase;
  color:#D2C7C4;margin:0 0 1.15rem;
}
.hero-tagline h1{font-size:clamp(2.05rem,3.9vw,3.3rem);max-width:28ch}
}

/* ---------- figures ------------------------------------------------------
   Three headline numbers, separated by rules rather than boxed. Numerals sit
   in ink, not the accent, so the band does not become another red surface. */
.figures{border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.figures .wrap{display:grid;grid-template-columns:repeat(3,1fr);gap:0}
.figures div{padding:3rem 2rem;border-left:1px solid var(--line);text-align:center}
.figures div:first-child{border-left:0}
.figures .fig{
  font-family:var(--display);font-weight:700;color:var(--ink);
  font-size:clamp(2.8rem,5.2vw,4.1rem);line-height:1;letter-spacing:-.03em;
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1;
}
.figures .fig sup{font-size:.42em;vertical-align:super;font-weight:600;margin-left:.06em}
.figures .unit{font-size:.5em;font-weight:600;color:var(--muted);margin-left:.15em;letter-spacing:0}
.figures .fig-label{
  margin:.85rem 0 0;font-size:.78rem;font-weight:600;letter-spacing:.13em;
  text-transform:uppercase;color:var(--muted);
}
.figures .fig-note{margin:.4rem 0 0;font-size:.88rem;color:var(--muted)}

/* ---------- horizontal product carousel ---------- */
.carousel-head{display:flex;align-items:flex-end;justify-content:space-between;gap:2rem;margin-bottom:2rem}
.carousel-nav{display:flex;gap:.5rem;flex:0 0 auto}
.carousel-nav button,.belt-nav button,.shuffle-nav button,.hspine-nav button{
  width:46px;height:46px;border:1px solid var(--field-border);background:transparent;
  border-radius:var(--r-pill);
  color:var(--ink);cursor:pointer;display:flex;align-items:center;justify-content:center;
  transition:background .15s var(--ease),color .15s var(--ease),border-color .15s var(--ease);
}
.carousel-nav button:hover:not(:disabled),.belt-nav button:hover,.shuffle-nav button:hover,.hspine-nav button:hover:not(:disabled){background:var(--ink);border-color:var(--ink);color:var(--paper)}
.carousel-nav button:disabled{opacity:.3;cursor:default}
.carousel-nav button:focus-visible,.belt-nav button:focus-visible,.shuffle-nav button:focus-visible,.hspine-nav button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.carousel-nav svg,.belt-nav svg,.shuffle-nav svg,.hspine-nav svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2}

/* ---------- photo conveyor -------------------------------------------------
   Site photographs on the product belt's conveyor, 16:9 and uncaptioned. A
   photo under the pointer lifts well clear of the belt:
   rises over the photos either side and throws a shadow. It grows by a
   third, so the padding above and below is 3.8rem, clear of the 55px it rises. The belt clips
   whatever leaves it, so it carries padding above and below for the lifted
   photo to rise into, and the belt already stops drifting under the pointer,
   so a photo never slides away while it is up. Pointer devices only; on a
   touch screen :hover sticks after a tap. */
.carousel-photos{padding:3.8rem 0}
.belt-nav + .carousel-photos{margin-top:-1.2rem}
.carousel-photos .carousel-track > *{flex:0 0 560px;position:relative}
/* .carousel.carousel-photos: the product belt's 4:3 rule is as specific as a
   single class and would otherwise win, cropping the drone shots to 4:3 */
.carousel.carousel-photos img{aspect-ratio:16/9}
@media (hover:hover){
  .carousel-photos figure:hover{z-index:4}
  .carousel-photos img{transition:transform .45s var(--ease),box-shadow .45s var(--ease)}
  .carousel-photos figure:hover img{transform:scale(1.35);
    box-shadow:0 26px 56px rgba(26,16,14,.38),0 8px 18px rgba(26,16,14,.2)}
}
@media (prefers-reduced-motion:reduce){
  .carousel-photos figure:hover img{transform:none}
}

/* ---------- conveyor belt -------------------------------------------------
   The track drifts continuously and is driven by `transform`, not by
   scrollLeft. Scroll snapping was what made the stepped version stutter: the
   browser kept snapping mid-animation and fighting the easing. There is no
   snapping here and nothing animates a layout property.
   The slide set is duplicated in script so the loop never shows a seam.     */
/* pan-y: the browser keeps vertical swipes for scrolling the page and hands
   sideways ones to the belt's own swipe code */
.carousel{overflow:hidden;position:relative;touch-action:pan-y}
.carousel-track{display:flex;gap:1.6rem;will-change:transform}
.carousel-track > *{flex:0 0 520px;margin:0}
.carousel-products .carousel-track > *{flex:0 0 620px}
.carousel img{width:100%;aspect-ratio:4/3;object-fit:cover;background:var(--paper-3)}
.carousel figcaption{margin-top:.95rem}
.carousel figcaption b{display:block;font-weight:600;color:var(--ink);font-size:1.16rem}
.carousel figcaption span{display:block;margin-top:.3rem;font-size:.96rem;line-height:1.5;color:var(--muted)}

/* service slides: a rule and an icon, not a boxed card */
.carousel-svc .carousel-track > *{flex:0 0 330px}
.carousel-svc article{border-top:2px solid var(--ink);padding-top:1.5rem}
.carousel-svc h3{margin:.2rem 0 .7rem;font-size:1.24rem}
.carousel-svc p{margin:0;font-size:.96rem;color:var(--body)}
.svc-icon{display:block;width:40px;height:40px;color:var(--accent);margin-bottom:1.1rem}
.split .svc-icon{width:44px;height:44px;margin-bottom:1.2rem}
/* Vision and mission as two cards on the tinted band, the same height as each
   other, lifting when the pointer rests on one. Paper on the tint so the card
   has a surface for its shadow to fall from; square, as every surface here is.
   The lift is a transform and the shadow a box-shadow, so nothing around the
   card reflows while it moves. */
/* .split.vm-cards, not .vm-cards: the general .split rule sits later in this
   file and would otherwise win, centring the two cards at different heights. */
.split.vm-cards{align-items:stretch;gap:1.8rem}
.vm-cards > div{background:var(--paper);border:1px solid var(--line);padding:2.4rem 2.2rem;
  transition:transform .35s var(--ease),box-shadow .35s var(--ease),border-color .35s var(--ease)}
.vm-cards p:last-child{margin-bottom:0}
@media (hover:hover){
  .vm-cards > div:hover{transform:translateY(-8px);border-color:transparent;
    box-shadow:0 18px 40px rgba(26,16,14,.14),0 4px 10px rgba(26,16,14,.08)}
}
@media (prefers-reduced-motion:reduce){
  .vm-cards > div:hover{transform:none}
}
.svc-icon svg{width:100%;height:100%;display:block}

/* page banner for inner pages */
.banner{position:relative;overflow:hidden;background:var(--ink-deep);padding:4.2rem 0 3.4rem}
.banner .wrap{position:relative;z-index:2}
.banner h1{color:var(--paper)}
/* 58ch clipped the About subtitle by ~28px and dropped one word onto a second
   line. Wide enough to hold it on one line, and balanced so that any banner
   subtitle that does wrap splits evenly instead of orphaning a word. */
.banner p{color:#D9CFCC;max-width:92ch;margin-top:1rem;font-size:1.08rem;text-wrap:balance}

/* A banner laid over a photograph: deeper, with a scrim heavy enough that
   contrast holds wherever the image is bright. The photo is set inline on the
   section so the stylesheet stays page-agnostic. */
.banner-media{
  display:flex;align-items:center;
  min-height:min(74vh,700px);
  padding:6rem 0;
  background-size:cover;background-position:center 45%;background-repeat:no-repeat;
}
.banner-media .wrap{width:100%}
/* Logo-centred banner. The PreBUILD sign sits 22.3% down the photograph, so
   at `cover` it can never reach the middle of a deep banner: there is only
   214px of image above it and centring needs 311px. Rendering the image at
   224% of the banner height and pinning it to the top puts the sign at
   0.223 x 2.24 = 0.50 of the height, dead centre, at any banner size.
   The cost is a 1.56x upscale and a tighter crop, both hidden by the scrim. */
.banner-zoom{background-size:auto 224%;background-position:60% top}

.banner-media::after{
  content:"";position:absolute;inset:0;
  background:linear-gradient(to bottom,rgba(26,16,14,.75),rgba(26,16,14,.88));
}

/* A band in the middle of a page that borrows the page banner's treatment: the
   photograph runs the full width and the words sit on it behind the same
   scrim. Shallower than a page banner, because it carries a paragraph rather
   than a title, and the measure is held in so a line of body copy is not read
   across the whole window over a photograph.

   Contrast over this photograph, scrim included: heading 8.0:1 at worst,
   body 5.5:1. Swap the picture and measure again before shipping it. */
.band{min-height:min(56vh,520px);padding:4.8rem 0}
/* wide enough to hold the heading on one line; it wraps on a phone anyway */
.band h2{color:var(--paper);max-width:38ch}
/* Justified: the measure is held to 64ch, which is narrow enough that the
   word spacing stays even. Turned off on a phone, where a 40ch line justified
   opens gaps you can read down the middle of. */
.band p{color:#D9CFCC;max-width:64ch;text-align:justify}
.band .lede{color:var(--paper)}

/* ---------- sections ---------- */
.section{padding:5rem 0}
.section-sm{padding:3.2rem 0}
.tint{background:var(--paper-2)}
.tint-3{background:var(--paper-3)}
.head{margin-bottom:2.8rem}
/* Wide enough that a heading of about thirty characters stays on one line;
   anything genuinely longer still wraps rather than running the full page. */
.head h2{max-width:32ch}
/* A heading that is a whole question reads badly broken in the middle, so this
   one runs the full width of the wrap. Only used where the line fits. */
.head-wide h2{max-width:none}
/* a lede that has to hold one line, where the usual 70ch measure is short */
.lede-line{max-width:none}
/* For a heading that has to hold one line and is long enough that the normal
   size cannot. h2 keeps growing with the window, but the wrap stops at 1180px,
   so a 65 character heading overshoots it on an ordinary laptop. Here the size
   follows the width actually available instead: the line measures about 30em,
   and dividing the content width by 31 leaves a little air. Below 1.5rem it
   stops shrinking and wraps, which on a phone it has to. */
.head-fit h2{font-size:clamp(1.5rem,calc((min(100vw,var(--wrap)) - var(--gutter) * 2) / 31),2.3rem)}
/* A centred heading block. The paragraphs keep their measure and are centred
   as blocks as well as line by line, and the row that follows it is centred to
   match, so it does not hang off to the left under a centred title. */
.head-center{text-align:center}
.head-center p{margin-left:auto;margin-right:auto}
.head-center + .vtiles{margin-left:auto;margin-right:auto}
.head p{margin-top:.9rem}

/* ---------- credential bar -----------------------------------------------
   Deliberately not the big-number hero-metric template. Values sit at
   body scale so the band reads as a specification line, not a scoreboard. */
.creds{border-top:1px solid var(--line);border-bottom:1px solid var(--line);background:var(--paper-2)}
.creds .wrap{display:grid;grid-template-columns:repeat(4,1fr);gap:0}
.creds div{padding:1.45rem 1.6rem 1.5rem;border-left:1px solid var(--line)}
.creds div:first-child{border-left:0;padding-left:var(--gutter)}
.creds dt{font-size:.7rem;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin-bottom:.4rem}
.creds dd{margin:0;font-size:1.02rem;font-weight:600;color:var(--ink);line-height:1.35}

/* ---------- split (prose + image) ---------- */
/* centred single-column block, used where there is no supporting image */
/* The block is wide enough for the headline to break only where its <br> says,
   while the paragraphs stay at a readable measure inside it. */
.stack-center{max-width:860px;margin:0 auto;text-align:center}
.stack-center h2{margin-bottom:1.3rem;max-width:none}
/* Second line of the headline. Set apart from the first: smaller, lighter,
   in the accent, with open tracking. The gap does the separating that the
   deleted hyphen used to do. */
.stack-center h2 .h2-line2{
  display:block;
  margin-top:.62em;
  font-size:.66em;
  font-weight:600;
  color:var(--accent);
  letter-spacing:.055em;
  line-height:1.2;
}
.stack-center p{max-width:62ch;margin-left:auto;margin-right:auto}
.stack-center .arrow-link{margin-top:.4rem}

.split{display:grid;grid-template-columns:1fr 1fr;gap:3.5rem;align-items:center}
.split.wide-left{grid-template-columns:1.15fr .85fr}
.split img,.split video{width:100%;height:100%;min-height:360px;object-fit:cover;display:block}
/* A clip in a split column keeps its own proportions rather than stretching to
   the height of the text beside it, and plays clean with no overlay. */
.split video{height:auto;min-height:0;aspect-ratio:4/5;background:var(--paper-3)}

/* ---------- indexed rows (replaces identical card grids) ------------------
   Capabilities are rows separated by rules, not boxes. No numbered
   eyebrows, no icon-heading-text card repetition.                        */
.rows{border-top:1px solid var(--line)}
.rows > div{
  display:grid;grid-template-columns:minmax(0,7.5rem) minmax(0,1fr);
  gap:1.5rem 2.5rem;padding:1.9rem 0;border-bottom:1px solid var(--line);
}
.rows h3{margin:0;grid-column:1 / -1}
.rows .r-label{grid-column:1;font-size:.78rem;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);padding-top:.3rem}
.rows .r-body{grid-column:2}
.rows .r-body p:last-child{margin-bottom:0}
@media (min-width:860px){
  .rows > div{grid-template-columns:minmax(0,9rem) minmax(0,1fr)}
  .rows h3{grid-column:2;margin-bottom:.55rem}
  .rows .r-label{padding-top:.35rem}
}

/* The heading and line that close the enquiry spine. The gap above belongs to
   the heading, so the note sits tight under it and ends the section. */
.flow-title{margin:2.6rem 0 .5rem;font-size:1.5rem}
.flow-note{margin:0;max-width:52rem;color:var(--ink)}

/* ---------- portrait video tiles -----------------------------------------
   Site footage shot on a phone, held upright. Three across at 9:16, capped so
   a tile does not run taller than a screen. A tile with no clip yet is a plain
   frame of the same shape, so the row already sits at its finished size and
   adding the footage later moves nothing around it.
   ------------------------------------------------------------------------ */
.vtiles{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1.4rem;max-width:52rem}
.vtile{aspect-ratio:9/16;overflow:hidden;background:var(--paper-3)}
.vtile video{width:100%;height:100%;object-fit:cover;display:block}

/* ---------- project tiles ------------------------------------------------
   Projects side by side, three across: photograph, name, and the same three
   facts on every card. A still grid rather than a moving belt, so a reader can
   compare the jobs at a glance.
   ------------------------------------------------------------------------ */
/* Three across until a phone. Two across would leave the third card alone on
   a row of its own, which every other grid on the site is built to avoid. */
.ptiles{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:2.4rem 1.6rem}
.ptile img{display:block;width:100%;aspect-ratio:16/10;object-fit:cover;
  background:var(--paper-3);margin-bottom:1.1rem}
.ptile h3{margin:0 0 .7rem;font-size:1.22rem}
/* term and value, compact, the same look the project entries use */
.card-facts{display:grid;grid-template-columns:minmax(0,6.2rem) minmax(0,1fr);
  gap:.35rem 1rem;margin:0;font-size:.95rem;line-height:1.5}
.card-facts dt{color:var(--muted)}
/* Balanced, so a value that runs to a second line splits evenly instead of
   leaving one word alone on it ("Estates / Limited", "Precast / Components").
   The project names above them get the same treatment. */
.card-facts dd{margin:0;color:var(--ink);text-wrap:balance}
.hs h3{text-wrap:balance}

/* Footnote marks: a small raised asterisk against an item, and the note it
   refers to set small and centred beneath the row it belongs to. */
.fn{font-size:.62em;vertical-align:super;line-height:0;margin-left:.12em;color:var(--accent);font-weight:600}
.footnote{margin:1.6rem 0 0;font-size:.86rem;color:var(--muted);text-align:center}
.footnote .fn{font-size:1em;vertical-align:baseline;margin:0 .1em 0 0}

/* ---------- tap to enlarge ---------------------------------------------------
   On a laptop a picture lifts in place when the pointer rests on it. On a
   phone the picture is already the width of the screen and cannot grow in
   place, so a tap lifts it out of the page into this full-screen view instead:
   it rises from slightly smaller, over a dark ground, and a tap anywhere or the
   close button puts it back. Touch screens only; the script checks. */
.lightbox{position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;
  padding:0;background:rgba(26,16,14,0);transition:background .26s var(--ease)}
.lightbox.is-open{background:rgba(26,16,14,.93)}
/* edge to edge: held upright, a wide picture can be no wider than the screen,
   so it takes all of it; turned sideways it fills the height instead */
.lightbox img,.lightbox video{display:block;width:100%;height:auto;max-height:90vh;object-fit:contain;
  box-shadow:0 26px 60px rgba(26,16,14,.6);transform:scale(.9);opacity:0;
  transition:transform .3s var(--ease),opacity .3s var(--ease)}
.lightbox.is-open img,.lightbox.is-open video{transform:none;opacity:1}
.lightbox-close{position:absolute;top:calc(env(safe-area-inset-top,0px) + .8rem);right:.8rem;
  width:46px;height:46px;border-radius:var(--r-pill);border:1px solid rgba(252,249,248,.5);
  background:rgba(26,16,14,.6);color:var(--paper);font-size:1.7rem;line-height:1;cursor:pointer}
.lb-lock,.lb-lock body{overflow:hidden}
/* a wide picture on an upright phone gets a line saying how to see it larger;
   turned sideways, the line has done its job and goes */
.lightbox-hint{position:absolute;left:0;right:0;bottom:calc(env(safe-area-inset-bottom,0px) + 1.3rem);
  margin:0;text-align:center;font-size:.86rem;color:rgba(252,249,248,.72)}
@media (orientation:landscape){.lightbox-hint{display:none}}

/* ---------- segmented control and panels ---------------------------------
   Press a category, the set below swaps. Square rather than pill shaped
   because the whole site runs on a 0 radius system.                       */
.segmented{
  display:inline-flex;flex-wrap:wrap;border:1px solid var(--field-border);
  border-radius:var(--r-pill);overflow:hidden;margin-bottom:3rem;
}
.segmented button{
  font:inherit;font-weight:600;font-size:.98rem;color:var(--muted);
  background:none;border:0;border-right:1px solid var(--field-border);
  padding:.95rem 1.7rem;cursor:pointer;white-space:nowrap;
  transition:background .16s var(--ease),color .16s var(--ease);
}
.segmented button:last-child{border-right:0}
.segmented button:hover{color:var(--ink)}
.segmented button[aria-selected="true"]{background:var(--ink);color:var(--paper)}
.segmented button:first-child{padding-left:2rem}
.segmented button:last-child{padding-right:2rem}
.segmented button:focus-visible{outline:2px solid var(--accent);outline-offset:2px;position:relative}

.panel[hidden]{display:none}
.panel{animation:panel-in .34s var(--ease) both}
@keyframes panel-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.panel .lede{margin-bottom:2.4rem}
.panel .facts{margin-top:2.6rem}

/* ---------- card shuffle -------------------------------------------------
   A stack of photographs. Pressing next throws the top card out to the side,
   drops it to the back of the pack, and lets it settle back in, so the stack
   cycles rather than sliding like a carousel.

   Each card's place in the pack is a single custom property, --pos, and the
   offset, rotation and scale are all derived from it. The script only has to
   renumber the cards; the CSS does the arranging.
   ------------------------------------------------------------------------ */
.shuffle{touch-action:pan-y;position:relative;width:100%;aspect-ratio:4/3}
.shuffle-card{
  position:absolute;top:0;left:0;margin:0;overflow:hidden;
  width:calc(100% - 46px);height:calc(100% - 40px);
  background:var(--paper-3);
  /* square to the frame: offset and scale only, no rotation */
  transform:
    translate(calc(var(--pos) * 24px), calc(var(--pos) * 21px))
    scale(calc(1 - var(--pos) * 0.035));
  transition:transform .6s var(--ease),box-shadow .6s var(--ease);
  box-shadow:0 12px 34px rgba(44,31,29,.20);   /* tinted to ink, never black */
}
.shuffle-card img{width:100%;height:100%;object-fit:cover;display:block}
/* thrown clear before it drops to the back */
.shuffle-card.is-leaving{transform:translateX(116%) scale(.97)}
.shuffle-card.is-entering{transform:translateX(-116%) scale(.97)}

.shuffle-nav{display:flex;justify-content:flex-end;gap:.5rem;margin-top:1.4rem}

/* the image column gets the extra width, since the stack is the point here */
.split.wide-right{grid-template-columns:.86fr 1.14fr}

/* ---------- spine timeline ------------------------------------------------
   One line runs the full height of the section. Three blocks hang off it,
   alternating left, right, left, and each slides in from its own side as it
   reaches the viewport. The accent fill climbs the spine a third at a time as
   they land, so the line reads as progress rather than decoration.

   The node and its connector stub sit on the static wrapper, not on the card
   that moves, otherwise the dot would drift off the line mid-animation.

   The hidden start state is armed by script and only by script, so a script
   failure leaves the section readable rather than blank.
   ------------------------------------------------------------------------ */
/* At the full page width the PPVC cards run about 620px against 575px on a
   tablet, so the same 16rem overlap left 132px between the first and third
   blocks up there and 66px down here. 18.2rem brings the wide layout back to
   about 60px. Below 1180px the page is narrower than the wrap and 16rem still
   fits; 18.2rem there would put the first and third blocks into each other. */
@media (min-width:1180px){:root{--tl-overlap:18.2rem}}
.branch-head{text-align:center;max-width:64ch;margin:0 auto 3.4rem}
.branch-head .lede{margin-top:1rem}

.tl{position:relative}
.tl-spine{
  position:absolute;left:50%;top:0;bottom:0;width:2px;margin-left:-1px;
  background:var(--line);
}
.tl-fill{
  position:absolute;left:0;top:0;width:100%;height:100%;background:var(--accent);
  transform:scaleY(0);transform-origin:top;
  transition:transform calc(var(--tl-speed) * 1.1) var(--ease);
}

.tl-item{position:relative;display:grid;grid-template-columns:1fr 1fr}
/* Compact: each block is pulled up into the one above it, so the left and
   right sides interleave instead of stacking. Each sits in its own grid row
   and its own column, so they overlap vertically without ever colliding.
   Keep the overlap well under a card height or a block would ride above the
   node of the one before it. */
.tl-item + .tl-item{margin-top:calc(var(--tl-overlap) * -1)}
.tl-item .tl-card{grid-column:1;padding-right:2.8rem}
.tl-item.from-right .tl-card{grid-column:2;padding-right:0;padding-left:2.8rem}

/* the stub joining card to spine, and the node sitting on it */
.tl-item::before{
  content:"";position:absolute;top:1.6rem;height:2px;width:2.8rem;background:var(--line);
}
.tl-item.from-left::before{right:50%}
.tl-item.from-right::before{left:50%}
.tl-node{
  position:absolute;left:50%;top:calc(1.6rem - 6px);width:14px;height:14px;margin-left:-7px;
  border-radius:var(--r-pill);background:var(--paper-2);border:2px solid var(--line);
  transition:background calc(var(--tl-speed) * .45) var(--ease),border-color calc(var(--tl-speed) * .45) var(--ease),transform calc(var(--tl-speed) * .45) var(--ease);
}
.tl-item.is-in .tl-node{background:var(--accent);border-color:var(--accent);transform:scale(1.15)}

.tl-card{
  max-width:none;                       /* fills its half of the page; the
                                           padding is the reach to the spine */
  transition:opacity var(--tl-speed) var(--ease),transform var(--tl-speed) var(--ease);
}
.tl-item.from-left .tl-card{margin-left:auto}   /* hug the spine from the left */
.tl-card img{width:100%;aspect-ratio:4/3;object-fit:cover;background:var(--paper-3);margin-bottom:.95rem}
/* The picture lifts under the pointer: it grows by a fifth, rises over what
   is beside it and throws a shadow. A fifth, not the third the site photos
   use, so it stays on its own side of the spine. The raised z-index is on the
   picture itself, because a later block down the page would otherwise paint
   over it. Pointer devices only. */
.tl-card img{position:relative}
@media (hover:hover){
  .tl-card img{transition:transform .45s var(--ease),box-shadow .45s var(--ease)}
  .tl-card img:hover{transform:scale(1.2);z-index:5;
    box-shadow:0 22px 48px rgba(26,16,14,.32),0 6px 14px rgba(26,16,14,.18)}
}
@media (prefers-reduced-motion:reduce){
  .tl-card img:hover{transform:none}
}
.tl-card h3{margin:0 0 .55rem;font-size:1.14rem}
.tl-card > p{font-size:.93rem;margin-bottom:.85em}

/* points: a short accent dash, not a stripe down the side of the block */
.tl-points{list-style:none;margin:0;padding:.95rem 0 0;border-top:1px solid var(--line)}
.tl-points li{position:relative;padding-left:1.45rem;margin-bottom:.58rem;font-size:.88rem;line-height:1.5}
.tl-points li:last-child{margin-bottom:0}
.tl-points li::before{
  content:"";position:absolute;left:0;top:.62em;width:14px;height:2px;background:var(--accent);
}

.tl.is-armed .tl-card{opacity:0}
.tl.is-armed .from-left .tl-card{transform:translateX(-110px)}
.tl.is-armed .from-right .tl-card{transform:translateX(110px)}
.tl-item.is-in .tl-card{opacity:1;transform:none}

@media (prefers-reduced-motion:reduce){
  .tl-card{opacity:1 !important;transform:none !important}
  .tl-fill{transform:scaleY(1) !important}
}

/* ---------- horizontal process spine --------------------------------------
   The five production stages read left to right along one rule. Every picture
   hangs above the rule and every description below it, so the images line up
   as one band and the text as another.

   Nothing here positions the rule by eye. Each stage is a three-row grid
   whose first row is exactly --hs-media-h, so the rule is simply that height
   plus half the band below it, and the pictures cannot drift off it.
   ------------------------------------------------------------------------ */
.hspine{
  --hs-w:500px;                                 /* stage column width */
  --hs-gap:2rem;
  --hs-media-h:calc(var(--hs-w) * 9 / 16);      /* every picture is 16:9 */
  --hs-stem:2.5rem;                             /* band the rule runs through */
  --hs-travel:58px;                             /* how far a part rides in from */
  --hs-speed:1.3s;
  --hs-nav:3.4rem;                              /* room kept clear for the arrows */
  position:relative;margin-top:2.4rem;
}
.hspine-view{overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}
.hspine-view::-webkit-scrollbar{display:none}

.hspine-track{
  position:relative;display:flex;align-items:start;gap:var(--hs-gap);
  width:max-content;list-style:none;margin:0;
  /* The wrap is --wrap wide including its own gutter, so the text inside it
     starts a gutter in from the wrap's edge. The track has to add that gutter
     too, or the first card sits 24px to the left of the heading above it. */
  padding:0 calc(max(0px,(100% - var(--wrap)) / 2) + var(--gutter)) var(--hs-nav);
}
/* the rule, and the accent that draws along it */
.hspine-track::before,.hspine-track::after{
  content:"";position:absolute;left:0;right:0;height:1px;
  top:calc(var(--hs-media-h) + var(--hs-stem) / 2);
}
.hspine-track::before{background:var(--line)}
.hspine-track::after{background:var(--accent);transform-origin:left center}

.hs{flex:0 0 var(--hs-w);display:grid;
  grid-template-rows:var(--hs-media-h) var(--hs-stem) auto}
.hs-media{grid-row:1;overflow:hidden;background:var(--ink-deep)}
.hs-media img,.hs-media video{width:100%;height:100%;object-fit:cover;display:block}
.hs-node{grid-row:2;justify-self:center;align-self:center;
  width:11px;height:11px;border-radius:var(--r-pill);
  background:var(--paper);border:2px solid var(--line);z-index:2;
  transition:border-color .5s var(--ease)}
.hs-text{grid-row:3}
.hs .r-label{margin:0;font-size:.78rem;font-weight:600;letter-spacing:.12em;
  text-transform:uppercase;color:var(--muted)}
.hs h3{margin:.4rem 0 .6rem;font-size:1.34rem}
.hs-text p:last-child{margin:0;font-size:.99rem;line-height:1.62;color:var(--body)}

/* The arrows ride with the reader. Sticky is measured against .hspine, so
   they are pulled up off the bottom of the section while any of it is still
   on screen and settle into place at its end. The track keeps --hs-nav clear
   underneath the last line of text for them to sit in, and the negative
   margin here spends exactly that, so the arrows add no height of their own. */
.hspine-nav{
  position:sticky;bottom:.7rem;z-index:6;pointer-events:none;
  display:flex;justify-content:flex-end;gap:.55rem;
  margin:calc(var(--hs-nav) * -1 + .9rem) calc(max(0px,(100% - var(--wrap)) / 2) + var(--gutter)) 0 0;
}
.hspine-nav button{pointer-events:auto;background:var(--paper)}
/* an arrow pointing past the end of a bounded run goes dead rather than
   pointing at nothing */
.hspine-nav button:disabled{opacity:.3;cursor:default}


/* Start states are set only once the script has added .is-armed, so a page
   with no JavaScript shows five finished stages rather than five empty gaps.
   The pictures drop in and the text rises, both closing on the rule. */
.hspine.is-armed .hs-media,.hspine.is-armed .hs-text{
  opacity:0;transition:opacity var(--hs-speed) var(--ease),
  transform var(--hs-speed) var(--ease)}
.hspine.is-armed .hs-media{transform:translateY(calc(var(--hs-travel) * -1))}
.hspine.is-armed .hs-text{transform:translateY(var(--hs-travel))}
.hspine.is-armed .hspine-track::after{transform:scaleX(0);transition:transform 2.2s var(--ease)}
.hspine.is-armed .hs-node{border-color:var(--line)}

.hspine.is-armed.is-in .hs-media,.hspine.is-armed.is-in .hs-text{opacity:1;transform:none}
.hspine.is-armed.is-in .hspine-track::after{transform:scaleX(1)}
.hspine.is-armed.is-in .hs-node{border-color:var(--accent)}
/* They arrive one after another, not all at once. The delay comes from --i on
   the column, not from counting position in CSS: the belt appends a second
   copy of the run, and a count would give the copies the wrong turn. */
.hspine.is-armed.is-in .hs-media,
.hspine.is-armed.is-in .hs-text{transition-delay:calc(var(--i,0) * .17s)}
.hspine.is-armed.is-in .hs-node{transition-delay:calc(var(--i,0) * .17s + .25s)}

/* A still row: every card in view at once, centred, nothing to scroll and no
   arrows. The cards take a third of the window each, less the gutters and the
   gaps and a little for the scrollbar, and stop growing at 500px, the size a
   moving spine uses. The rule still runs the full width underneath. */
.hspine.still{--hs-w:min(500px,calc((100vw - var(--gutter) * 2 - var(--hs-gap) * 2 - 20px) / 3))}
.hspine.still .hspine-view{overflow:visible}
.hspine.still .hspine-track{width:auto;justify-content:center;
  padding:0 var(--gutter)}
/* Every spine picture pops out when the pointer rests on it, on Projects and
   on Services alike: the picture grows past its own edges, lifts above the
   cards beside it and throws a shadow, as if it came up off the page. The
   BIM clip does the same as the photographs.

   Only where there is a real pointer. On a touch screen a tap leaves :hover
   stuck on, which would leave a picture enlarged, and an enlarged picture
   near the screen edge would add a sideways scroll to the page.

   Each frame is its own layer so the one under the pointer can be raised over
   its neighbours and over the rule. The transform is on the picture, not on
   .hs-media: the frame carries the arrival animation, whose slow timing and
   stagger would make the pop lag. */
.hs-media{position:relative;z-index:1}
@media (hover:hover){
  .hspine .hs-media{overflow:visible}
  .hspine .hs-media:hover{z-index:5}
  .hspine .hs-media img,.hspine .hs-media video{
    transition:transform .45s var(--ease),box-shadow .45s var(--ease)}
  .hspine .hs-media:hover img,.hspine .hs-media:hover video{transform:scale(1.12);
    box-shadow:0 22px 48px rgba(26,16,14,.34),0 6px 14px rgba(26,16,14,.18)}
  /* A spine that scrolls sideways clips its top edge too: a box cannot scroll
     one way and let content spill the other. So the view is given headroom
     above the pictures, and pulled up by the same amount so nothing moves. */
  .hspine:not(.still) .hspine-view{padding-top:1.6rem;margin-top:-1.6rem}
}
/* A drawing is shown whole on paper inside its frame, so the paper has to
   travel with it, or the lifted picture is a drawing floating in an outline. */
.hspine.docs .hs-media img{background:var(--paper)}
@media (prefers-reduced-motion:reduce){
  .hspine .hs-media:hover img,.hspine .hs-media:hover video{transform:none}
}

/* A spine of drawings rather than photographs. A drawing cropped to fill its
   frame is a drawing with its plan cut off, so these are shown whole on paper
   inside the frame, and the column is wider because a framing plan needs the
   room. */
.hspine.docs{--hs-w:600px}
.hspine.docs .hs-media{background:var(--paper);border:1px solid var(--line)}
.hspine.docs .hs-media img{object-fit:contain}

@media (prefers-reduced-motion:reduce){
  .hs-media,.hs-text{opacity:1 !important;transform:none !important}
  .hspine-track::after{transform:scaleX(1) !important}
  .hs-node{border-color:var(--accent) !important}
}

/* ---------- product belt -------------------------------------------------
   Every tile is the same size and the same 4:3 ratio, which matches the source
   photographs so nothing is cropped. The name sits on a permanent gradient so
   the belt reads at a glance; hovering brings up the full overlay with the
   specification. Devices without hover get the full overlay all the time,
   otherwise their users could never reach the spec at all.              */
.belt-nav{display:flex;justify-content:flex-end;gap:.5rem;margin-bottom:1.2rem}

.p-tile{position:relative;overflow:hidden;background:var(--paper-3)}
.p-tile img{width:100%;aspect-ratio:4/3;object-fit:cover;display:block}

.p-tile .p-name{
  position:absolute;left:0;right:0;bottom:0;margin:0;padding:2.6rem 1.3rem 1.05rem;
  font-family:var(--display);font-weight:700;font-size:1.22rem;letter-spacing:-.01em;
  color:var(--paper);
  background:linear-gradient(to top,rgba(26,16,14,.9),rgba(26,16,14,0));
  transition:opacity .28s var(--ease);
}
.p-tile .p-over{
  position:absolute;inset:0;display:flex;flex-direction:column;justify-content:flex-end;
  padding:1.6rem 1.5rem;background:rgba(26,16,14,.86);
  opacity:0;transition:opacity .28s var(--ease);
}
.p-tile .p-over b{
  display:block;font-family:var(--display);font-weight:700;font-size:1.3rem;
  letter-spacing:-.01em;color:var(--paper);margin-bottom:.5rem;
}
.p-tile .p-over span{display:block;font-size:.97rem;line-height:1.55;color:#E2D9D6}

.p-tile:hover .p-over,.p-tile:focus-within .p-over{opacity:1}
.p-tile:hover .p-name,.p-tile:focus-within .p-name{opacity:0}

/* On a touch screen there is no hover, so the overlay and description come up
   on a tap instead (the script adds .is-open) and go on a second tap or a tap
   elsewhere. The name stays on its strip until then, as it does on a laptop.
   The overlay used to be shown permanently on phones, which buried every
   photograph under text. */
.p-tile.is-open .p-over{opacity:1}
.p-tile.is-open .p-name{opacity:0}

/* ---------- photo mosaic -------------------------------------------------
   A 12 column grid composed row by row rather than left to right, so every
   row fills exactly and nothing is orphaned at the end:

     row A   wide 7  +  narrow 5
     row B   third 4 +  third 4  +  third 4
     row C   narrow 5 + wide 7          (row A mirrored)
     row D   third 4 +  third 4  +  third 4

   The aspect ratios are not arbitrary. A 7 column tile is 7/5 wider than a
   5 column tile, so for the two to finish at the same height the wide ratio
   has to be (span7 width / span5 width) x 4/3. Gaps do not scale with the
   columns, so the true figure is 1.893 rather than the naive 7/5 x 4/3 =
   1.867, which left the two tiles 5px apart and the captions misaligned.   */
.mosaic{display:grid;grid-template-columns:repeat(12,1fr);gap:1.9rem 1.4rem}
.mosaic figure{margin:0;min-width:0}
.mosaic img{width:100%;object-fit:cover;background:var(--paper-3);display:block}
.m-wide{grid-column:span 7}
.m-narrow{grid-column:span 5}
.m-third{grid-column:span 4}
.m-full{grid-column:span 12}
.m-wide img{aspect-ratio:1.893}
.m-narrow img{aspect-ratio:4/3}
.m-third img{aspect-ratio:5/4}
.m-full img{aspect-ratio:21/9}

.mosaic figcaption{margin-top:.75rem}
.mosaic figcaption b{display:block;font-weight:600;color:var(--ink);font-size:1.04rem}
.mosaic figcaption span{display:block;margin-top:.22rem;font-size:.88rem;line-height:1.5;color:var(--muted)}

/* ---------- project entries (rows, not cards) ---------- */
.entries{border-top:1px solid var(--line)}
.entry{
  display:grid;grid-template-columns:minmax(0,22rem) minmax(0,1fr);
  gap:2.4rem;padding:2.2rem 0;border-bottom:1px solid var(--line);align-items:start;
}
.entry img{width:100%;aspect-ratio:16/10;object-fit:cover;background:var(--paper-3)}
.entry h3{margin-bottom:.75rem}
.entry dl{margin:0;display:grid;grid-template-columns:minmax(0,7rem) minmax(0,1fr);gap:.4rem 1.4rem;font-size:.95rem}
.entry dt{color:var(--muted)}
.entry dd{margin:0;color:var(--ink)}
.status{
  display:inline-block;font-size:.72rem;font-weight:600;letter-spacing:.1em;
  text-transform:uppercase;color:var(--accent);margin-bottom:.5rem;
}

/* ---------- grouped record (replaces the long hairline table) ------------ */
.record-group{margin-bottom:2.6rem}
.record-group > h3{
  font-family:var(--body-face);font-size:.76rem;font-weight:600;letter-spacing:.14em;
  text-transform:uppercase;color:var(--muted);padding-bottom:.7rem;border-bottom:1px solid var(--line);margin-bottom:1.1rem;
}
.record-group ul{list-style:none;margin:0;padding:0}
.record-group li{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:.3rem 2rem;padding:.55rem 0}
.record-group li b{font-weight:600;color:var(--ink)}
.record-group li span{color:var(--muted);font-size:.95rem}

/* ---------- definition grid (company details, cert details) ---------- */
.facts{display:grid;grid-template-columns:minmax(0,13rem) minmax(0,1fr);gap:0 2.5rem}
.facts dt{padding:.85rem 0;color:var(--muted);font-size:.95rem;border-top:1px solid var(--line)}
.facts dd{padding:.85rem 0;margin:0;color:var(--ink);border-top:1px solid var(--line)}
.facts dt:first-of-type,.facts dd:first-of-type{border-top:0}

/* ---------- grade marks ---------- */
.grades{display:grid;grid-template-columns:repeat(3,1fr);gap:0;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.grades div{padding:1.8rem 1.7rem;border-left:1px solid var(--line)}
.grades div:first-child{border-left:0;padding-left:0}
.grades .code{font-family:var(--display);font-size:1.7rem;font-weight:700;color:var(--accent);letter-spacing:-.02em}
.grades .what{font-weight:600;color:var(--ink);margin-top:.35rem}
.grades .note{font-size:.92rem;color:var(--muted);margin-top:.15rem}

/* ---------- contact ---------- */
.contact-grid{display:grid;grid-template-columns:1.1fr .9fr;gap:4rem}
.field{margin-bottom:1.25rem}
.field label{display:block;font-size:.85rem;font-weight:600;color:var(--ink);margin-bottom:.4rem}
.field input,.field textarea,.field select{
  width:100%;padding:.8rem .95rem;border:1px solid var(--field-border);
  font:inherit;font-size:.97rem;background:var(--paper);color:var(--ink);border-radius:0;
}
.field input:focus,.field textarea:focus,.field select:focus{outline:2px solid var(--accent);outline-offset:1px;border-color:var(--accent)}
.field textarea{min-height:145px;resize:vertical}
.field .hint{font-size:.85rem;color:var(--muted);margin:.35rem 0 0}

/* no side stripes: contact details are a definition list with rules */
.details dt{font-size:.76rem;font-weight:600;letter-spacing:.13em;text-transform:uppercase;color:var(--muted);padding-top:1.1rem;border-top:1px solid var(--line)}
.details dd{margin:.35rem 0 1.1rem;color:var(--ink)}
.details dt:first-of-type{border-top:0;padding-top:0}
.details a{color:var(--accent);font-weight:600;text-decoration:none;border-bottom:1px solid var(--accent-soft)}
.details a:hover{border-bottom-color:var(--accent)}

/* ---------- closing band ---------- */
.closer{background:var(--ink-deep);padding:4.2rem 0}
.closer h2{color:var(--paper);max-width:22ch}
.closer p{color:#D9CFCC;max-width:52ch;margin:.9rem 0 1.9rem}

/* ---------- footer ---------- */
.site-footer{background:var(--ink-deep);color:#A49793;font-size:.92rem;padding:3.4rem 0 1.8rem;border-top:1px solid rgba(252,249,248,.1)}
.site-footer .cols{display:grid;grid-template-columns:1.8fr 1fr 1.5fr;gap:3rem;padding-bottom:2.2rem}
.site-footer img{height:26px;margin-bottom:1.1rem}
.site-footer h4{font-family:var(--body-face);font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:var(--paper);margin:0 0 .9rem;font-weight:600}
.site-footer p{max-width:38ch;margin-bottom:.8rem}
.site-footer ul{list-style:none;margin:0;padding:0}
.site-footer li{margin-bottom:.5rem}
.site-footer a{color:#A49793;text-decoration:none;transition:color .15s var(--ease)}
.site-footer a:hover{color:var(--paper)}
.legal{padding-top:1.5rem;border-top:1px solid rgba(252,249,248,.1);display:flex;justify-content:space-between;gap:1rem;flex-wrap:wrap;font-size:.85rem;color:#8A7C78}

/* ---------- header, collapsed -----------------------------------------------
   The 40px logo and the six links need about 900px side by side, so below
   940px the menu folds behind the button and the bar slims down. This used to
   happen only on phones; at the larger size a tablet needs it too. */
@media (max-width:940px){
  .nav{
    display:none;position:absolute;top:var(--header-h);left:0;right:0;background:var(--ink-deep);
    flex-direction:column;align-items:stretch;gap:0;padding:.4rem 0 1rem;
    border-bottom:1px solid rgba(252,249,248,.14);
  }
  .nav.open{display:flex}
  /* larger and roomier, for a thumb */
  .nav{padding:.7rem 0 1.4rem}
  .nav a{display:block;padding:1.05rem var(--gutter);font-size:1.2rem;font-weight:600}
  .nav li + li a{border-top:1px solid rgba(252,249,248,.09)}
  .nav a[aria-current="page"]::after{display:none}
  .nav-toggle{display:block}
  :root{--header-h:66px}
  .brand img{height:32px}
}

/* ---------- responsive ---------- */
@media (max-width:1000px){
  /* narrower columns, so the pictures still lead at a smaller window */
  .hspine{--hs-w:400px}
  .hspine.docs{--hs-w:470px}
  .split,.split.wide-left{grid-template-columns:1fr;gap:2.2rem}
  .m-wide,.m-narrow,.m-third{grid-column:span 6}
  .m-full img{aspect-ratio:16/9}
  .m-wide img,.m-narrow img,.m-third img{aspect-ratio:4/3}
  .split img{min-height:0;height:auto;aspect-ratio:3/2}
  /* stacked: the clip is centred at a sane width instead of filling the
     column, where a 4:5 crop would be taller than the viewport */
  .split video{max-width:400px;margin:0 auto}
  .creds .wrap{grid-template-columns:repeat(2,1fr)}
  .creds div:nth-child(3){border-left:0;padding-left:var(--gutter)}
  .creds div{border-top:1px solid var(--line)}
  .creds div:nth-child(1),.creds div:nth-child(2){border-top:0}
  .contact-grid{grid-template-columns:1fr;gap:2.6rem}
  .entry{grid-template-columns:1fr;gap:1.3rem}
  .site-footer .cols{grid-template-columns:1fr 1fr;gap:2rem}
  /* the three CIDB grades stay side by side, small, instead of three tall blocks */
  .grades{grid-template-columns:repeat(3,minmax(0,1fr))}
  .grades div{padding:1rem .7rem}
  .grades div:first-child{padding-left:0}
  .grades .code{font-size:1.3rem}
  .grades .what{font-size:.84rem;line-height:1.3}
  .grades .note{font-size:.76rem;line-height:1.35}
  .figures div{padding:2.2rem 1.2rem}
  .carousel-track > *{flex-basis:380px}
  .carousel-products .carousel-track > *{flex-basis:440px}
  .carousel-svc .carousel-track > *{flex-basis:300px}
}
/* Below this the spine has nowhere to run: a rule with cards above and
   below it needs width the phone does not have. The stages stack instead,
   which is the same sequence read top to bottom. */
@media (max-width:720px){
  .vtiles{grid-template-columns:none;grid-auto-flow:column;grid-auto-columns:62%;
    overflow-x:auto;scrollbar-width:none;max-width:none}
  .ptiles{grid-template-columns:minmax(0,1fr)}
  .carousel-photos .carousel-track > *{flex-basis:82vw}
  .band p{text-align:left}
  .hspine-nav{display:none}
  .hspine-view{overflow:visible}
  .hspine-track{display:block;width:auto;padding:0 var(--gutter)}
  .hspine-track::before,.hspine-track::after{display:none}
  .hs{display:block;max-width:32rem;margin-bottom:2.8rem}
  .hs-node{display:none}
  /* the looping belt keeps a hidden second copy of its stages; stacked into a
     list on a phone, that copy showed the whole process twice */
  .hs[aria-hidden="true"]{display:none}
  .hs-media{aspect-ratio:16/9;margin-bottom:1.1rem}
  body{font-size:16px}
  .section{padding:3.4rem 0}
  .creds .wrap{grid-template-columns:1fr}
  .creds div{border-left:0;padding-left:var(--gutter);border-top:1px solid var(--line)}
  .creds div:first-child{border-top:0}
  /* label and value side by side, tighter, instead of stacked one over the
     other: the company and certificate details were two screens long */
  .facts{grid-template-columns:minmax(0,8.6rem) minmax(0,1fr);gap:0 1rem}
  .facts dt,.facts dd{padding:.55rem 0;font-size:.9rem}
  .record-group li{grid-template-columns:1fr;gap:.15rem}
  .entry dl{grid-template-columns:minmax(0,6rem) minmax(0,1fr)}
  .banner-media{min-height:52vh;padding:4.5rem 0}
  /* too tight a crop on a phone: fall back to the whole building, sharp */
  .banner-zoom{background-size:cover;background-position:60% 25%}
  /* the spine moves to the left edge and every block hangs off the same
     side, all entering from the left */
  .split.wide-right{grid-template-columns:1fr}
  .shuffle-card{width:calc(100% - 34px);height:calc(100% - 30px)}
  .tl-spine{left:0}
  .tl-item{grid-template-columns:1fr}
  .tl-item .tl-card,.tl-item.from-right .tl-card{grid-column:1;padding-left:3.2rem;padding-right:0}
  .tl-item::before,.tl-item.from-left::before,.tl-item.from-right::before{left:0;right:auto;width:2.4rem}
  .tl-node{left:1px}
  .tl{--tl-overlap:0rem}
  .tl-item + .tl-item{margin-top:2.8rem}
  .tl.is-armed .from-left .tl-card,
  .tl.is-armed .from-right .tl-card{transform:translateX(-32px)}
  .m-wide,.m-narrow,.m-third{grid-column:span 12}
  /* three equal buttons in one row; a long name wraps inside its own button */
  .segmented{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));width:100%}
  .segmented button,.segmented button:first-child,.segmented button:last-child{
    padding:.8rem .35rem;font-size:.84rem;line-height:1.25;white-space:normal}
  .site-footer .cols{grid-template-columns:1fr}
  .rows > div{grid-template-columns:1fr;gap:.5rem}
  .rows .r-label{grid-column:1}
  .rows .r-body{grid-column:1}
  .figures .wrap{grid-template-columns:1fr}
  .figures div{border-left:0;border-top:1px solid var(--line);padding:2.1rem 1rem}
  .figures div:first-child{border-top:0}
  .carousel-head{flex-direction:column;align-items:flex-start;gap:1.2rem}
  .carousel-track > *{flex-basis:82vw}
  .carousel-products .carousel-track > *{flex-basis:84vw}
  .carousel-svc .carousel-track > *{flex-basis:82vw}
}

@media (prefers-reduced-motion:reduce){
  *{transition-duration:.01ms !important;animation-duration:.01ms !important}
}
