/******* Do not edit this file *******
Simple Custom CSS and JS - by Silkypress.com
Saved: Oct 30 2026 | 06:12:42 */
@import url("https://use.typekit.net/opw5hnz.css");

/* --- tokens --- */
:root {
    --ink: #1b2429;
    --white: #ffffff;
    --off-white: #fdfdfe;
    /* #FF3E15 sitewide since 2026-09-24 (was #EE0000): the three exports of
       that date draw it, and the client asked for every button to match */
    --red: #ff3e15;
    --red-brand: #c41425;
    --line: #ebebeb;
    --line-soft: #d9d9d9;
    --grey-text: #939393;

    --gutter: 4.861vw;
    --radius: 1.111vw;
    --radius-sm: 0.556vw;

    /* The type scale, 2026-09-27 (client spec, verified against every
       export in pages-pdf/). Five styles, all Urbane 600, sized in vw against
       the 1440 artboard: design px / 14.4. Letter-spacing is the spec's px
       over its font size, so it scales with the type.
       Reduced by 2px sitewide 2026-10-01 (client, 2026-09-30 review:
       "too bold and oversized"), and another 2px 2026-10-12 (client).
       2026-10-13 (client): each token is `max(Nvw, Npx)` so the vw drives
       the type at or above the 1440 artboard, and the design value pins as
       a floor on narrower desktop viewports. Mobile pins to px in the
       media override below. */
    /* 2026-10-27 (client): the section type scale is dropped — .h1 → 40px,
       .h2 → 30px, and every other token lands within ±2px of its original
       design value. Each token is `clamp(min, Npvw, max)` so the vw drives
       around the 1440 artboard and px floors/ceilings kick in only outside
       the band where it would deviate more than 2px. Target values at a
       1440 viewport: h1 40, h2 30, h3 20, h4 16, p 12.

       The HERO title keeps its original big size through its own token,
       `--fs-hero`, which the hero rules reference directly. See the two
       hero rules in this file (.crrc-home .section-hero__title and
       .page-hero h1). */
    --fs-hero: max(4.2vw, 60.48px);         /* unchanged — hero only */
    --fs-h1: clamp(36px, 2.6778vw, 40px);   /* 38.56px at 1440 (2026-10-29, client) */
    --lh-h1: 0.929;
    --ls-h1: -0.06em;
    --fs-h2: clamp(28px, 2.083vw, 32px);    /* 30px at 1440 (2026-10-29, client) */
    --lh-h2: 1;         /* 2026-10-12: unitless, not vw */
    --ls-h2: -1.6px;    /* 2026-10-12: px, not em */
    --fs-h3: clamp(18px, 1.389vw, 22px);    /* 20px at 1440 */
    --lh-h3: 1.083;
    --ls-h3: -0.03em;
    --fs-h4: clamp(14px, 1.111vw, 18px);    /* 16px at 1440 */
    --lh-h4: 1.3;       /* 2026-10-12: unitless */
    --ls-h4: -0.4px;    /* 2026-10-12: px, not em */
    --fs-p:  clamp(10px, 0.833vw, 14px);    /* 12px at 1440 */
    --lh-p: 1.167;
    --ls-p: -0.01em;

    --grey-bg: #f6f6f6;
    --body-grey: #939393;
    --hairline: #e5e5e5;
    --page-hero-h: 51.6vw;

    /* Core's flow layout puts `margin-block-start: var(--wp--style--block-gap)`
       between the children of every `.wp-block-group.is-layout-flow` (which is
       every text wrapper on the site: .loc-panel__copy, .abt-ops__copy, the
       hero copy columns and so on). The theme sets --wp--style--block-gap to
       1.2rem (19.2px) -- a FIXED gap that does not scale with the vw type.
       Pin it to vw on desktop so paragraph spacing scales with the font
       (1.333vw = 19.2px at 1440, the theme's own value). Mobile pins to px
       to match the rest of the mobile scale -- rem is not used anywhere else
       on this project (2026-10-13, on request). */
    --wp--style--block-gap: 1.333vw;

    /* mobile: the type scale in plain px. 2026-10-27 (client) sync with the
       new desktop scale: section h1 pins at 40 and section h2 at 30 (was 66
       and 36). The HERO keeps its original mobile 66px size through
       --fs-hero. */
    @media (max-width: 1024px) {
        --fs-hero: 66px;    /* hero only — unchanged */
        --fs-h1: 40px;      /* was 66px */
        --fs-h2: 30px;      /* 2026-10-29 (client): matches new desktop target */
        --fs-h3: 20px;
        --fs-h4: 16px;
        --fs-p: 12px;
        --gutter: 27px;
        --radius: 14px;
        --radius-sm: 7px;
        /* was 19.2px -- bumped (2026-10-20, client) so stacked text / button /
           module blocks have room to breathe on narrow screens. */
        --wp--style--block-gap: 24px;
    }
}

/* 2026-10-27: the earlier `.crrc-home-fv2` preview scope (duplicated home
   page 1251) that staged these token changes is retired — the overrides
   are now in :root above and apply site-wide. Page 1251 can be trashed
   from wp-admin whenever (it still carries the extra class in markup;
   harmless, no rules match it any more). */

/* --- base --- */
/* Anchor jumps and `scrollIntoView` land content below the fixed header
   instead of behind it (2026-10-28). The header is ~73px on tablet/mobile
   (padding 20 + content + border) and ~60px on desktop; approximating with
   a flat 90px with the admin bar height on top covers both. */
html {
    scroll-padding-top: calc(var(--wp-admin--admin-bar--height, 0px) + 90px);
}

body {
    font-family: "urbane", "Century Gothic", "Avenir Next", sans-serif;
    font-weight: 600;
    color: var(--ink);
    background: var(--white);
    -webkit-font-smoothing: antialiased;
}

:is(.crrc-home, .crrc-page),
:is(.crrc-home, .crrc-page) * {
    margin: 0;
    box-sizing: border-box;
}

:is(.crrc-home, .crrc-page) {
    overflow-x: clip;
}

.crrc-home img,
.crrc-page img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.crrc-home figure,
.crrc-page figure {
    margin: 0;
}

.crrc-home a,
.crrc-page a {
    color: inherit;
    text-decoration: none;
}

/* --- type --- */
/* 2026-10-27 (client): h1 and h2 split so .h1 → 40 and .h2 → 30 take
   their own tokens. Was grouped on the previous bigger scale where both
   displayed at the same ~60px size. Sections that used an explicit
   font-size: var(--fs-h2) override on an h2 (ets-intro, section-about,
   sol-card, abt-op, etc.) now get the same size as the base — those
   overrides are harmless duplicates and can be swept in a later tidy. */
.crrc-home .h1,
.crrc-home h1,
.crrc-page .h1,
.crrc-page h1 {
    font-size: var(--fs-h1);
    font-weight: 600;
    line-height: var(--lh-h1);
    letter-spacing: var(--ls-h1);
    text-transform: uppercase;

    /* 2026-10-27: the mobile --fs-h1 is now 40 (was 66), well within the
       390vp gutter, so the long-word clamp this used to carry is no
       longer needed on the base rule. Per-element hero overrides keep
       their own clamp (section-hero__title, page-hero h1, loc-panel
       card h2, abt-miles head) because the hero still runs at ~66px
       through --fs-hero. */
}

.crrc-home .h2,
.crrc-home h2,
.crrc-page .h2,
.crrc-page h2 {
    font-size: var(--fs-h2);
    font-weight: 600;
    line-height: var(--lh-h2);
    letter-spacing: var(--ls-h2);
    text-transform: uppercase;
}

.crrc-home .h3,
.crrc-home h3,
.crrc-page .h3,
.crrc-page h3 {
    font-size: var(--fs-h2);
    font-weight: 600;
    line-height: var(--lh-h2);
    letter-spacing: var(--ls-h2);
    text-transform: none;
}

.crrc-home p,
.crrc-page p {
    font-size: var(--fs-p);
    letter-spacing: var(--ls-p);
    font-weight: 600;
    line-height: var(--lh-p);
    color: var(--grey-text);
}

.crrc-home p.lead,
.crrc-page p.lead {
    font-size: var(--fs-h4);
    letter-spacing: var(--ls-h4);
    line-height: var(--lh-h4);
}

:is(.crrc-home, .crrc-page) .eyebrow,
.crrc-footer .eyebrow {
    display: block;
    width: fit-content;
    align-self: flex-start;
    position: relative;
    font-size: var(--fs-p);
    font-weight: 600;
    line-height: var(--lh-p);
    letter-spacing: var(--ls-p);
    text-transform: uppercase;
    color: #bebebe;
    padding-bottom: 0.42vw;
    /* The rule is drawn by the ::after below, not by this border, so that it can
       be wiped in from the left (10.14). The border stays, TRANSPARENT, because
       the box height has to remain exactly what it was -- dropping it would take
       every eyebrow on the site 1px shorter. */
    border-bottom: 1px solid transparent;

    @media (max-width: 1024px) {
        padding-bottom: 6px;
    }
}

/* bottom: -1px lands it exactly on the border it replaces: an absolutely
   positioned child resolves against the PADDING box, and the border is the 1px
   below that. */
:is(.crrc-home, .crrc-page) .eyebrow::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -1.5px;
    /* Rounded dashes, 2px thick, 5 on and 3 off (2026-09-24, on request): a
       1px `dotted` border read as a faint grey line at this size. Painted in
       currentColor through a repeating pill mask, so every eyebrow keeps its
       own colour. It overhangs the transparent border by half a pixel either
       side, so the eyebrow's box height does not move. */
    height: 2px;
    background: currentColor;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='2'%3E%3Crect width='5' height='2' rx='1'/%3E%3C/svg%3E") left center / 8px 2px repeat-x;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='2'%3E%3Crect width='5' height='2' rx='1'/%3E%3C/svg%3E") left center / 8px 2px repeat-x;
}

/* --- button -----------------------------------------------------------
   2026-10-12: "rolling arrow" hover (per lib.mydemobb.com/portfolio-item/
   rolling-arrow-button-animation/). Label + outgoing arrow tile slide
   right together, while a second arrow tile — a ::before pseudo on
   .crrc-btn — stays put at the left and rotates into view, giving the
   feel of an arrow rolling across the button. Letter-spacing hover is
   gone. The ::before is a square red tile with the arrow glyph as a
   background-image + a rotate(-90deg → 0) transform. */
:is(.crrc-home, .crrc-page) .crrc-btn,
.crrc-footer .crrc-btn {
    --btn-tile: 2.08vw;
    --btn-gap:  0.28vw;
    position: relative;
    display: inline-flex;
    /* A grid or flex parent BLOCKIFIES inline-* children (CSS Display L3), so
       `inline-flex` becomes `flex` here and the button stretches to fill its
       column. That leaves empty space on the right of the pill, so when the
       hover translation slides the arrow tile right it stays inside the
       container's overflow: hidden box and never gets clipped -- the "arrow
       stays visible on hover" bug (client, 2026-10-15). `width: fit-content`
       pins the container to its content width regardless of parent layout. */
    width: fit-content;
    align-items: stretch;
    gap: var(--btn-gap);
    overflow: hidden;
    isolation: isolate;
    font-size: var(--fs-p);
    line-height: var(--lh-p);
    font-weight: 600;
    letter-spacing: var(--ls-p);
    text-transform: uppercase;
    color: var(--white);

    @media (max-width: 1024px) {
        --btn-tile: 35px;
        --btn-gap:  5px;
        gap: 5px;
    }
}

/* Give the pill button its own breathing room from any preceding content on
   mobile (2026-10-20, client). The flow-layout block-gap puts 28px above by
   default; a button after copy needs more than regular text spacing. */
@media (max-width: 1024px) {
    :is(.crrc-home, .crrc-page, .crrc-footer) :is(.wp-block-buttons, .crrc-btn):not(:first-child) {
        margin-top: 18px;
    }
}

/* The incoming arrow tile. Lives at left:0, same size as the tile, starts
   rotated -90deg around its bottom-right so the arrow is "lying on its
   side"; on hover it rotates back to 0 as the .wp-block-button slides past
   it, so the arrow appears to roll into place. z-index below the pill so
   it is hidden at rest. */
.crrc-btn::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    width: var(--btn-tile);
    height: var(--btn-tile);
    background: var(--red)
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2.4'%3E%3Cpath d='M7 17 17 7'/%3E%3Cpath d='M8.6 7H17v8.4'/%3E%3C/svg%3E")
        center / 52% no-repeat;
    border-radius: var(--radius-sm);
    transform-origin: 100% 100%;
    transform: rotate(-90deg);
    transition: transform 0.45s cubic-bezier(.4, 0, .2, 1);
    z-index: 0;

    /* Hover-only effect; on touch it never animates in, and in some states
       (text-reveal transforms + overflow interactions) the rotated tile has
       been seen escaping the container's overflow clip. Just hide it. */
    @media (max-width: 1024px) {
        display: none;
    }
}

.crrc-btn:hover::before,
.crrc-btn:focus-visible::before,
.crrc-btn:focus-within::before {
    transform: rotate(0deg);
}

/* .crrc-btn is a core Buttons block (`.wp-block-buttons` wrapper); the
   `.wp-block-button` child wraps the link (whose ::after is the arrow
   tile). On hover the whole `.wp-block-button` slides right by
   (tile + gap), exposing the incoming ::before tile at the left. The
   pre-Oct-14 sub-elements (`.crrc-btn__label` / `.crrc-btn__icon`) are
   retired. */
.crrc-btn .wp-block-button {
    position: relative;
    z-index: 1;
    display: flex;
    margin: 0;
    transition: transform 0.45s cubic-bezier(.4, 0, .2, 1);
}

.crrc-btn .wp-block-button__link {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    height: var(--btn-tile);
    min-width: 7.5vw;
    margin-right: calc(var(--btn-tile) + var(--btn-gap));
    padding: 0 0.63vw;
    border: 0;
    border-radius: var(--radius-sm);
    background: var(--red);
    color: var(--white);
    font: inherit;
    letter-spacing: var(--ls-p);
    text-transform: inherit;
    text-decoration: none;

    @media (max-width: 1024px) {
        height: auto;
        min-width: 0;
        margin-right: calc(35px + 5px);
        padding: 12px 23px;
        justify-content: center;
    }
}

.crrc-btn .wp-block-button__link::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: calc(100% + var(--btn-gap));
    width: var(--btn-tile);
    border-radius: var(--radius-sm);
    background: var(--red) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2.4'%3E%3Cpath d='M7 17 17 7'/%3E%3Cpath d='M8.6 7H17v8.4'/%3E%3C/svg%3E") center / 52% no-repeat;

    @media (max-width: 1024px) {
        left: calc(100% + 5px);
        width: 35px;
    }
}

.crrc-btn:hover .wp-block-button,
.crrc-btn:focus-visible .wp-block-button,
.crrc-btn:focus-within .wp-block-button {
    transform: translateX(calc(var(--btn-tile) + var(--btn-gap)));
}

/* --- site header --- */
.crrc-header *,
.crrc-footer * {
    margin: 0;
    box-sizing: border-box;
}

.crrc-header a,
.crrc-footer a {
    color: inherit;
    text-decoration: none;
}

/* The template-part wrapper is the positioned box, not .crrc-header inside
   it: positioning the inner element left the wrapper 0px tall, which is what
   DevTools showed when you inspected the header (2026-09-27). The transform
   slides the whole wrapper off (2026-10-01, client review). The ease is
   `cubic-bezier(.22,1,.36,1)` (expo-out) — matches Lenis's own glide and reads
   as a single motion rather than two steps. */
.wp-site-blocks > header.wp-block-template-part {
    position: fixed;
    inset: var(--wp-admin--admin-bar--height, 0px) 0 auto;
    z-index: 20;
    transform: translate3d(0, 0, 0);
    transition: transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
    will-change: transform;
    backface-visibility: hidden;
}

.wp-site-blocks > header.wp-block-template-part.is-hidden {
    transform: translate3d(0, -110%, 0);
}

.crrc-header {
    position: relative;
    z-index: 20;
    display: flex;
    align-items: center;
    padding: 1.11vw 1.74vw;
    color: var(--white);
    background: transparent;
    border-bottom: 1px solid transparent;
    transform: translateY(0%);
    transition: background-color 1s ease, backdrop-filter 1s ease, border-color 1s ease, transform 1s ease;

    @media (max-width: 1024px) {
        gap: 12px;
        padding: 20px var(--gutter);
    }
}

/* Hamburger menu open: solid chrome regardless of scroll position, since the
   panel sits over the page. */
body.crrc-menu-open .crrc-header,
.crrc-header.is-menu-open {
    background: rgba(15, 15, 15, 0.92);
    -webkit-backdrop-filter: blur(24px) saturate(110%);
    backdrop-filter: blur(24px) saturate(110%);
}

body.crrc-menu-open {
    overflow: hidden;
}

body.not-top .crrc-header {
    background: rgba(15, 15, 15, 0.68);
    -webkit-backdrop-filter: blur(24px) saturate(110%);
    backdrop-filter: blur(24px) saturate(110%);
    border-bottom-color: rgba(255, 255, 255, 0.08);
}

.crrc-header.is-scrolling-down {
    transform: translateY(-100%);
}

/* Pin the header in place while the hamburger panel is open — otherwise a
   scroll would run the scroll-down hide and take the close button off-screen. */
.crrc-header.is-menu-open,
.crrc-header.is-menu-open.is-scrolling-down,
.wp-site-blocks > header.wp-block-template-part:has(.crrc-header.is-menu-open) {
    transform: translateY(0%) !important;
}

.crrc-header__logo {
    width: 9.44vw;
    max-width: 170px;
    flex: none;

    @media (max-width: 1024px) {
        width: 117px;
    }
}

.crrc-header__logo svg,
.crrc-header__logo img {
    width: 100%;
    height: auto;
    display: block;
    color: var(--white);
}

.crrc-header__nav {
    margin-inline: auto;
    display: flex;
    gap: 2.15vw;
}

@media (max-width: 1024px) {
    .crrc-header__nav {
        /* The containing block is the template-part wrapper (it carries a
           transform, 10.something), not the viewport, so `inset: … 0` would
           bound this to the wrapper's auto height — i.e. one item tall.
           Give the panel an explicit viewport height instead. */
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        height: 100dvh;
        margin-inline: 0;
        flex-direction: column;
        align-items: stretch;
        gap: 14px;
        padding: calc(var(--wp-admin--admin-bar--height, 0px) + 74px + 20px) var(--gutter) 40px;
        background: rgba(15, 15, 15, 0.96);
        -webkit-backdrop-filter: blur(24px) saturate(110%);
        backdrop-filter: blur(24px) saturate(110%);
        overflow-y: auto;
        transform: translateX(100%);
        visibility: hidden;
        transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1), visibility 0s linear 0.45s;
    }

    .crrc-header.is-menu-open .crrc-header__nav {
        transform: none;
        visibility: visible;
        transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1);
    }
}

/* The hamburger button: three bars that fold into an X when open. Hidden on
   desktop; replaces the wrapped-nav mobile layout since 2026-10-20. */
.crrc-header__burger {
    display: none;
    margin: 0;
    padding: 0;
    background: none;
    border: 0;
    color: inherit;
    cursor: pointer;

    @media (max-width: 1024px) {
        display: flex;
        flex-direction: column;
        justify-content: center;
        gap: 5px;
        width: 32px;
        height: 32px;
        margin-left: auto;
    }
}

.crrc-header__burger span {
    display: block;
    width: 24px;
    height: 2px;
    background: currentColor;
    border-radius: 1px;
    transform-origin: center;
    transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.2s ease;
}

.crrc-header.is-menu-open .crrc-header__burger span:nth-child(1) {
    transform: translateY(7px) rotate(45deg);
}

.crrc-header.is-menu-open .crrc-header__burger span:nth-child(2) {
    opacity: 0;
}

.crrc-header.is-menu-open .crrc-header__burger span:nth-child(3) {
    transform: translateY(-7px) rotate(-45deg);
}

.crrc-header__nav > a,
.crrc-header__drop > :is(a, button) {
    font-size: var(--fs-p);
    line-height: var(--lh-p);
    font-weight: 600;
    letter-spacing: var(--ls-p);
    text-transform: uppercase;
    position: relative;
    padding-bottom: 0.28vw;
    border-bottom: 1px solid transparent;

    @media (max-width: 1024px) {
        font-size: 15px;
        line-height: 1.2;
        letter-spacing: 0.04em;
        padding: 14px 0;
        border-bottom: 1px solid rgba(255, 255, 255, 0.08);
        width: 100%;
    }
}

/* 2px dotted red underline, drawn on hover/focus and for the current page
   (2026-10-05, client review -- dotted brought back after the 2026-10-01 removal). */
.crrc-header__nav > a::before,
.crrc-header__drop > :is(a, button)::before {
    content: "";
    position: absolute;
    inset: auto 0 -2px;
    border-bottom: 2px dotted var(--red);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 0.35s cubic-bezier(0.65, 0, 0.35, 1);
    pointer-events: none;

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

.crrc-header__nav > a:is(:hover, :focus-visible)::before,
.crrc-header__drop:is(:hover, :focus-within) > :is(a, button)::before,
.crrc-header__nav .is-current::before,
.crrc-header__drop:has(.is-current)::before {
    transform: none;
}

/* Solutions carries a dropdown (the 2026-09-24 exports draw a caret on it and
   nothing under it, 12.22): its three categories. Opens on hover and on
   keyboard focus; on a touch screen the word itself still goes to /solutions/. */
/* flex, not block: an inline-flex link in a block wrapper sits on a line box
   and dropped ~10px below the other items */
.crrc-header__drop {
    position: relative;
    display: flex;

    @media (max-width: 1024px) {
        flex-direction: column;
        align-items: stretch;
        width: 100%;
        gap: 2px;
        padding-bottom: 10px;
        border-bottom: 1px solid rgba(255, 255, 255, 0.08);
    }
}

/* Inside the mobile panel the dropdown trigger loses its own border so the
   submenu reads as one unit with it; the group boundary is on the parent. */
@media (max-width: 1024px) {
    .crrc-header__drop > :is(a, button) {
        border-bottom-color: transparent;
    }
}

.crrc-header__drop > :is(a, button) {
    display: flex;
    align-items: center;
    gap: 0.35vw;
}

.crrc-header__drop > :is(a, button) svg {
    width: 0.55vw;
    min-width: 8px;
    height: auto;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    transition: transform 0.3s ease;

    @media (max-width: 1024px) {
        display: none;
    }
}

.crrc-header__drop:is(:hover, :focus-within, .is-open) > :is(a, button) svg {
    transform: rotate(180deg);
}

.crrc-header__menu {
    position: absolute;
    top: 100%;
    left: -1.1vw;
    z-index: 30;
    display: flex;
    flex-direction: column;
    min-width: 12vw;
    margin-top: 0.8vw;
    padding: 0.5vw;
    background: var(--white);
    border-radius: 0.556vw;
    box-shadow: 0 0.8vw 2.4vw rgba(0, 0, 0, 0.18);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-0.4vw);
    transition: opacity 0.25s ease, transform 0.25s ease, visibility 0s linear 0.25s;

    /* Inside the mobile panel the submenu is laid out inline, not floated as
       a card. White links, indented, no background. */
    @media (max-width: 1024px) {
        position: static;
        display: flex;
        min-width: 0;
        margin: 0 0 0 16px;
        padding: 0;
        background: transparent;
        box-shadow: none;
        border-radius: 0;
        opacity: 1;
        visibility: visible;
        transform: none;
        transition: none;
    }
}

/* No-op placeholder -- the real mobile override is .crrc-header__nav .crrc-header__menu a
   below the base rule, since the base rule for .crrc-header__menu a comes after
   this block in source order and would otherwise win on ties. */

/* Careers has no page of its own (12.6): its word is a button that only opens
   the menu of the three subsidiaries' JobStreet pages (2026-09-28, client doc).
   A tap toggles `.is-open` (the header script), because iOS does not focus a
   tapped button, so :focus-within alone would never open it there. */
.crrc-header__drop > button {
    margin: 0;
    padding-top: 0;
    padding-inline: 0;
    font-family: inherit;
    color: inherit;
    background: none;
    border-width: 0 0 1px;
    border-style: solid;
    border-color: transparent;
    border-radius: 0;
    cursor: pointer;
}

/* Careers has no page of its own and leads to the three JobStreet links.
   Inside the hamburger panel it renders inline like the other dropdown. */
.crrc-header__drop-careers .crrc-header__menu {
    @media (max-width: 1024px) {
        left: auto;
        right: auto;
    }
}

/* a bridge over the gap, so the pointer can travel from the word to the menu */
.crrc-header__menu::before {
    content: "";
    position: absolute;
    inset: -0.8vw 0 100%;

    @media (max-width: 1024px) {
        display: none;
    }
}

.crrc-header__drop:is(:hover, :focus-within, .is-open) .crrc-header__menu {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: opacity 0.25s ease, transform 0.25s ease;
}

.crrc-header__menu a {
    padding: 0.6vw 0.7vw;
    border-radius: 0.35vw;
    font-size: var(--fs-p);
    line-height: var(--lh-p);
    font-weight: 600;
    letter-spacing: var(--ls-p);
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--ink);
    transition: background 0.2s ease, color 0.2s ease;
}

.crrc-header__menu a:hover,
.crrc-header__menu a:focus-visible {
    background: #f3f3f3;
    color: var(--red);
}

/* Mobile hamburger panel — the submenu text is white over the dark panel.
   Specificity (0,2,1) wins over the base rule above at (0,1,1), so this
   does not need to be after the base in a tied race. */
@media (max-width: 1024px) {
    .crrc-header__nav .crrc-header__menu a {
        padding: 10px 0;
        font-size: 13px;
        letter-spacing: 0.04em;
        color: #ffffff;
        background: transparent;
    }

    .crrc-header__nav .crrc-header__menu a:hover,
    .crrc-header__nav .crrc-header__menu a:focus-visible {
        background: transparent;
        color: var(--red);
    }
}

@keyframes crrc-ln-draw-x {
    from { transform: scaleX(0); }
}

.crrc-header__spacer {
    width: 9.44vw;
    max-width: 170px;
    flex: none;

    @media (max-width: 1024px) {
        display: none;
    }
}

/* --- 1. hero --- */
.section-hero {
    position: relative;
    /* Above the About routes, which now run up behind the banner (see
       `.section-about__routes`). Unconditional, not just under `.crrc-anim`:
       the art is drawn at rest too, and `.section-about` is later in the
       document, so without this it paints over the hero whenever the animation
       is not running -- reduced motion, or the CDN blocked. */
    z-index: 10;
    height: 50vw;
    min-height: 520px;
    color: var(--white);
    isolation: isolate;

    @media (max-width: 1024px) {
        height: auto;
        min-height: 88vh;
    }
}

/* the cover block carries the photo and the headline as one unit, so the headline
   shrinks with the picture instead of sitting on top of it */
.section-hero__cover {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    display: block;
    min-height: 0;
    padding: 0;
    overflow: hidden;
    z-index: 0;

    /* The hero background is a video. This still is the SAME upload the About card
       uses, so it costs no extra request -- it paints instantly while the video is
       still decoding and stands in permanently if the video never loads at all.
       It is set here rather than as the video's `poster` attribute because `poster`
       is not part of core's cover save output and would flag the block as invalid
       in the editor. */
    background-image: url("/wp-content/uploads/2026/09/hero-kl-aerial.webp");
    background-size: cover;
    background-position: center;
}

/* core already cover-fits the video background, but this cover overrides enough of
   the block's defaults that it is worth pinning explicitly */
.section-hero__cover .wp-block-cover__video-background {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    max-width: none;
    max-height: none;
    object-fit: cover;
    z-index: 0;
}

.section-hero__cover .wp-block-cover__inner-container {
    position: absolute;
    inset: 0;
    max-width: none;
}

/* Scoped `.crrc-home .section-hero__title` (0,2,0) so the mobile clamp beats
   the base `.crrc-home h1` rule at (0,1,1) -- the font-size trap in 10.8b.
   Reads --fs-hero rather than --fs-h1 so the hero stays at its original
   big-display size after the 2026-10-27 section-type reduction (h1 → 40px). */
.crrc-home .section-hero__title {
    position: absolute;
    z-index: 1;
    /* centred on the hero's height (2026-09-28, on request); was 25.1vw up
       from the bottom, which sat low on anything taller than 16:10 */
    inset: 50% 3.47vw auto;
    transform: translateY(-50%);
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    gap: 4vw;
    font-size: var(--fs-hero);
    letter-spacing: var(--ls-h1);
    line-height: var(--lh-h1);

    @media (max-width: 1024px) {
        inset: auto var(--gutter) 12vh;
        transform: none;
        flex-direction: column;
        align-items: flex-start;
        gap: 8px;
        /* 2026-10-10 feedback: the fixed --fs-h1 at mobile overflows on
           anything narrower than ~620px -- "Advancing"/"Connecting"/
           "Communities" each run past the right gutter on a 390px phone.
           Clamp the hero title down so the longest word fits, while keeping
           the mobile --fs-h1 as the cap for the widest mobile viewports. */
        font-size: clamp(32px, 11.5vw, 66px);
    }
}

.section-hero__title span:last-child {
    text-align: right;

    @media (max-width: 1024px) {
        text-align: left;
    }
}

/* --- hero -> about shrink --- */
/* html.crrc-anim is set by the page script only once GSAP + ScrollTrigger are
   loaded and the viewport is >768px, so the page renders unchanged without JS. */
.crrc-anim .section-hero {
    height: 100vh;
    min-height: 520px;
    /* The About section scrolls up behind the pinned hero, so the hero must paint
       above it -- `z-index: 10` now lives on the base rule, because the routes
       run up behind the banner at rest too. What is left here is that the pinned
       hero covers the whole viewport for the length of the pin and must not
       swallow clicks meant for the section underneath. */
    pointer-events: none;

    @media (max-width: 1024px) {
        height: auto;
        min-height: 88vh;
        z-index: auto;
        pointer-events: auto;
    }
}

/* the cover is the element that travels. Its box is animated rather than
   transformed, so object-fit re-crops the photo and the 2.16 hero can become the
   1.80 card with no distortion -- and so the landing frame is pixel-identical to
   the About card, which is what makes the hand-over invisible */
.crrc-anim .section-hero__cover {
    box-shadow: 0 1.1vw 2.2vw rgba(27, 36, 41, 0.16);
    /* the banner itself is hit-testable (and inspectable), only the empty
       viewport around it while it shrinks lets clicks through */
    pointer-events: auto;
}

/* The headline is laid out against a box that stays viewport-sized and is scaled
   by JS, so the whole composition shrinks as a unit. Scaling the cover itself
   instead would need a non-uniform scale (the aspect changes) and would squash
   both the type and the photo. */
.crrc-anim .section-hero__cover .wp-block-cover__inner-container {
    inset: 0 auto auto 0;
    width: 100vw;
    height: 100vh;
    transform-origin: 0 0;
}

/* --- 2. about --- */
.section-about {
    position: relative;
    background: var(--white);
    /* clear space under the intro heading (2026-09-24, on request); the two
       routes that reach the bottom run on through it to the section edge */
    padding-bottom: 4.5vw;
    /* Clips the BOTTOM edge only, so the route art stops exactly on the
       section's edge at any width -- the section's height is not pure vw
       (the heading's clamp()), so no stated box height lands on it. Top and
       sides are left 200vw open: the cards overhang the board there and the
       art runs up behind the hero. */
    clip-path: inset(-200vw -200vw 0 -200vw);

    @media (max-width: 1024px) {
        display: none;
    }
}

.section-about__map {
    position: relative;
    /* 67.1vw, not 70.5: the design puts the ABOUT US eyebrow 967pt below the
       section top and the board is what holds it down. The section total is
       unchanged -- the 3.4vw comes off here and goes onto the section's own
       padding-bottom, which the design draws as 60pt of clear space under the
       heading. */
    height: 67.1vw;

    @media (max-width: 1024px) {
        /* mobile: the 1440 scatter has no artboard to shrink to; stack the
           seven photos as a flex column so the About section is not visually
           empty between heading and dark Solutions band. */
        height: auto;
        display: flex;
        flex-direction: column;
        gap: 16px;
        padding: 0 var(--gutter);
    }
}

/* The dashed connector routes. THEY TRAVEL WITH THE CARDS THEY RUN UNDER -- see
   the parallax block below for why that is the whole constraint on this section.

   Measured against the four routes' own dash coordinates: route 1 runs under card
   4 and continues under card 7, route 2 under card 1 and on under card 6. So the
   art is two long lines, each joined to two cards, and the two lines sit in
   disjoint halves of the board -- the left one spans x 93-333, the right one
   x 1026-1355. That is what makes this cheap: the same file is drawn twice and
   each copy clipped to its own half, with the whole middle of the board for the
   clips to fall in, so neither can cut art. The second <img> is the first one out
   of cache, so it is still one request.

   THE WRAPPER IS THE WHOLE SECTION, NOT THE BOARD. Measured off Home.pdf on
   2026-09-09: the four routes span design y 720.5-2068.5 -- they run past the
   cards and turn to frame the intro copy underneath. `about-connectors.svg` is
   a 1440x1349 viewBox of exactly that region, and an <img> with both dimensions
   set is `object-fit: fill`, so pinning it to the 70.5vw board squashed every
   route to 75.2% of its height and put the frame inside the scatter instead of
   around the copy. At 93.68vw the box is 1440 x 1349 and the art is at its own
   aspect.

   THE ROUTES DO NOT PARALLAX, AND THE ART RUNS UP BEHIND THE HERO. Both were
   asked for on 2026-09-10, in that order, and the second is why the first is
   free.

   What the client saw: the dashed lines not reaching the top of the screen. Two
   separate causes, and the travel was both of them. The box used to be exactly
   the art with `overflow: hidden`, so a line that had travelled had its leading
   end CUT -- measured 109.3px off the top at the start of the board's passage.
   Giving the box the travel as clearance fixed that, and left the second cause
   showing: at the start of the passage the right-hand line is translated DOWN
   6.4vw, so its top END sits 92.16px below the top of the section with nothing
   above it. No clip box can fix that one -- the art simply stops there.

   With `--par: 0` on both lines there is no travel, so neither cause exists: no
   clearance is needed and the box is the art again. What it costs is that the
   lines no longer hold their registration with the cards they run under, which
   10.9 called the whole constraint on this section. It is survivable HERE
   because both routes are continuous from the top edge to the bottom edge --
   they have no terminus in the middle of the composition to be exposed (checked
   against the path data: the four paths join in pairs at 333,875 and 1026,820,
   so the art is two lines, each running edge to edge). Cards now slide over a
   fixed line rather than with it. That is a design call the client made; it is
   not one to make silently on their behalf if the routes are ever redrawn with
   an end under a card.

   THE ART IS 1440 UNITS TALLER THAN THE REGION IT DRAWS -- `about-connectors-full.svg`
   (media 169), which is the same file with the two vertical routes continued
   straight up as a stroked `stroke-dasharray: 2 2` above the old viewBox top.
   The dash phase joins exactly: rendered and measured across the seam, ink 2 /
   gap 2 unbroken through y=0 on both lines. That extension is what puts the
   lines BEHIND THE BANNER FROM THE FIRST FRAME, so the flight uncovers them as
   it shrinks instead of revealing an empty white field with the routes starting
   somewhere below the fold.

   --routes-top is that extension expressed in vw: the SVG is scaled to the box
   width, so 1440 of its units is exactly 100vw. It covers a viewport of height
   up to 100vw, i.e. any landscape window; the routes are `display: none` under
   768px anyway. `.section-hero` carries `z-index: 10` unconditionally for it
   (not just under `.crrc-anim`), or the art -- which is later in the document --
   would paint straight over the banner. */
.section-about__routes {
    --routes-top: 100vw;
    position: absolute;
    /* As tall as the art; `.section-about`'s bottom-only clip-path is what
       ends it on the section's edge (2026-09-24). A stated height anchored
       to the board could not: the board is not the section, and the section
       is not pure vw. */
    inset: calc(var(--routes-top) * -1) 0 auto;
    height: calc(var(--routes-top) + 99.236vw);
    overflow: hidden;
    pointer-events: none;
    z-index: 0;

    @media (max-width: 1024px) {
        display: none;
    }
}

/* (0,2,0), and it has to be: the base `.crrc-home img` rule is (0,1,1) and sets
   `height: 100%`, which a plain class here loses to. It did not matter while
   this rule also said 100%; the moment the two stopped agreeing it did -- the
   art stretched to the full box and `object-fit: cover` quietly cropped it,
   which measured as 254.8px off the bottom. 10.8b's trap, on this file, again. */
.section-about__routes .section-about__connectors {
    position: absolute;
    inset: 0;
    width: 100%;
    /* 99.236vw: 1349 units of the export's art plus 80 added below it
       (2026-09-24), where the two routes that reach the bottom -- x 183 and
       x 1216 -- are continued in their own dash phase so they meet the
       section's edge through the new clear space. The box clips the rest. */
    height: calc(var(--routes-top) + 99.236vw);
}

/* `--par: 0` -> the parallax loop skips them outright (it returns on a zero
   amplitude), so this is the whole of "do not parallax the connectors": no
   script change, no attribute, no tween. */
.section-about__connectors--left  { clip-path: inset(0 65% 0 0); --par: 0; }
.section-about__connectors--right { clip-path: inset(0 0 0 65%); --par: 0; }

.section-about__map figure {
    --par-img: 2.6vw;
    position: absolute;
    border-radius: var(--radius);
    overflow: hidden;
    box-shadow: 0 1.1vw 2.2vw rgba(27, 36, 41, 0.16);

    @media (max-width: 1024px) {
        /* Back into the grid flow -- the scatter's absolute slots are a 1440
           composition. The parallax is gated off here too (home-blocks.html):
           the cards are in flow at this width, and the largest amplitude is
           wider than the 16px grid gap, so a drifting card would run into its
           neighbour. */
        position: static;
        width: auto !important;
        aspect-ratio: 4 / 3 !important;
        box-shadow: 0 12px 23px rgba(27, 36, 41, 0.16);
    }
}

/* The hero video's last frame, painted into the centre card by the page script at
   the moment the shrink hands over (see home-blocks.html). Opacity is toggled
   rather than `display`, so the paint is already composited when it is revealed.
   Without this the hand-over cuts from a video frame to a visibly different still. */
.section-about__freeze {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
}

/* The slot each card sits in, and how far the whole card drifts from it.
   `--par` is the parallax amplitude in EACH direction -- see the block below for
   what shares an amplitude with what, and why card 3 is 0.

   Cards 1 and 6 carry the same figure as `--connectors--right`, and cards 4 and 7
   the same as `--connectors--left`, because a card and the dashed line running
   under it have to stay registered to the pixel. Cards 2, 3 and 5 touch no route
   and are free.

   THE SIGN IS THE POINT. A positive amplitude lags the scroll (a far layer), a
   negative one leads it (a near layer), and what the eye reads as parallax is the
   difference between two layers, not the absolute travel. Cards 2 and 3 are 12.7vw
   and -16vw, so they separate by 28.7vw across the range -- 413px at a 1440 board.
   Everything at the same sign and a similar rate reads as the whole board lagging
   slightly, which is to say as nothing at all: that was the third build,
   0.06-0.11 px per px on every card, and the client's word for it was "completely
   no parallax".

   EVERY CARD IS NOW ON ITS CEILING (2026-09-10, "more parallax"), and the ceiling
   is the board's own edges, not the gaps between cards. Worked out from the slots:
   card 2 may not exceed 12.7vw or it rises out of the top of the board into the
   intro copy; group B (cards 4 and 7) may not exceed 9.2vw or card 7 drops past
   the section's bottom edge into the dark Solutions band; group A (cards 1 and 6)
   may not exceed 6.4vw for the same reason at card 6 -- both of those land on the
   same 70.4vw boundary, which is what confirms it is the section edge doing the
   binding. Card 3 overlaps nothing and has geometric room for 20vw; it is held at
   16.

   CARD 5 IS THE ONE THAT IS NOT ON A BOARD EDGE. It is bounded by card 6, 9.02vw
   below it and sharing its x range: the gap closes by (--par of 6) - (--par of 5)
   across the run, so amp5 <= amp6 + 8.02 = 1.62vw if 1vw of that gap is to
   survive. It went from -3vw to +1.5vw for exactly that reason -- pushing it to
   the far side of the scroll doubles its contrast against card 6 (4vw of relative
   travel before, 7.9vw now) where making it MORE negative would have parked it on
   top of its neighbour's rate and shown nothing.

   Card-to-card collisions do not bind anywhere else: 2 and 7 share a 1.8vw strip
   of x and keep 24.9vw of their 28.4vw gap, and cards that share a connector line
   are at zero relative displacement by construction.

   THE PICTURE INSIDE EACH CARD MOVES TOO -- see --par-img below. That is where
   the depth added on 2026-09-10 actually comes from: the board-edge ceilings cap
   the cards at about +15% on the numbers before it, where an image drifting
   inside its own clipped frame is bounded by nothing but its own oversize. */
.section-about__map figure:nth-of-type(1) { left: 85.63%; top: -0.83vw;  width: 15.56%; aspect-ratio: 1;         --par: -6.4vw; }
.section-about__map figure:nth-of-type(2) { left: 16.74%; top: 12.71vw;  width: 10.07%; aspect-ratio: 1;         --par: 12.7vw; }
.section-about__map figure:nth-of-type(3) { left: 37.36%; top: 20.35vw;  width: 25.28%; aspect-ratio: 364 / 202; --par: -16vw;  --par-img: 0; }
.section-about__map figure:nth-of-type(4) { left: -1.18%; top: 27.85vw;  width: 15.56%; aspect-ratio: 1;         --par: 9.2vw;  }
.section-about__map figure:nth-of-type(5) { left: 75.56%; top: 29.38vw;  width: 10.07%; aspect-ratio: 1;         --par: 1.5vw;  }
.section-about__map figure:nth-of-type(6) { left: 80.63%; top: 48.47vw;  width: 15.56%; aspect-ratio: 1;         --par: -6.4vw; }
.section-about__map figure:nth-of-type(7) { left:  8.47%; top: 51.18vw;  width: 10.07%; aspect-ratio: 1;         --par: 9.2vw;  }

/* Parallax depth for the scatter (home-blocks.html drives it).

   THE WHOLE CARD MOVES, and each dashed connector line moves with it. The four
   routes are not a backdrop the cards float over -- they run *under* the frames
   and, on the right-hand line, one of them terminates under card 1. A card that
   drifted while its route stayed put would pull that route out from underneath
   itself: a dash-end pokes past a corner that used to hide it and the
   rail-network motif comes apart. Nothing errors; it just looks wrong, the same
   failure class as 10.2c.

   So the amplitudes are grouped, and the grouping is the reason the numbers in
   the table above repeat:

       card 2                        12.7vw     touches no route
       left  line + card 4 + card 7   9.2vw
       card 5                         1.5vw      touches no route
       right line + card 1 + card 6  -6.4vw
       card 3                        -16vw       touches no route

   Registration is exact rather than approximate because one ScrollTrigger drives
   everything (see the script): elements that share an amplitude share a progress,
   so they can never disagree by even a subpixel. Two triggers with matching
   amplitudes would not do -- ScrollTrigger normalises each to its own element's
   passage through the viewport, and a card's passage is not the board's.

   The tween runs across the board's passage from `top top` to `bottom top`, NOT
   from `top bottom` -- see home-blocks.html. Under the older start, 44% of every
   layer's travel was spent behind the pinned hero before the board was revealed,
   so the amplitudes above would have shown just over half of themselves.

   Card 3 travels too, even though it is the slot the hero cover shrinks onto
   (10.5). That is safe because the flight interpolates against the card's LIVE
   rect on every frame rather than a precomputed landing spot, so the cover simply
   follows the parallaxed card and still lands on it to within a quarter of a
   pixel -- measured, and re-measured for mid-flight overshoot, which stays under
   half a pixel. It was left at 0 in the first two builds out of caution and that
   was the most visible thing on the board holding still. The freeze canvas is
   inside the card, so it travels with it and the hand-over is unaffected.

   The script still skips any element whose `--par` is 0, so anchoring one is a
   one-word change here and needs no change there.

   `--par` is the ONE place an amplitude is written -- the script reads it back off
   the computed style rather than keeping a second copy that can drift out of step
   with this one. Registering it is what makes that readable: getComputedStyle
   resolves a registered <length> to px, where an unregistered custom property
   would hand back the literal "3.82vw". */
@property --par { syntax: "<length>"; inherits: true; initial-value: 0px; }

/* INNER-IMAGE PARALLAX. `--par` moves the card; `--par-img` moves the PICTURE
   inside it, and the two compose -- a card that leads the scroll with a picture
   that lags inside it is two layers of depth out of one box.

   This is the lever that actually bought "more parallax" on 2026-09-10. The card
   amplitudes above are pinned to the board's edges and had roughly 15% left in
   them; an image drifting inside a frame that is `overflow: hidden` is bounded by
   nothing but its own oversize, so it can be as strong as it needs to be and
   costs no layout at all.

   IT IS WRITTEN ON THE FRAME AND READ OFF THE IMAGE. `inherits: true` is what
   makes that work: the amplitude is declared once on the element that clips, the
   <img> inherits it, and the script reads a resolved px value off the image's own
   computed style -- the same single-source-of-truth arrangement as `--par`, and
   the same reason for registering it (an unregistered custom property hands back
   the literal "2.6vw" and parseFloat would silently give a 2.6px animation).
   Anything at 0 is skipped, so a frame opts out from the stylesheet alone.

   Every frame carrying `--par-img` is listed with its own section rule below.
   Three kinds of image deliberately have none: the world map and the About
   connectors (line art, and the connectors already travel on `--par`), the CKM
   name lockup on Contact (the one image with alpha -- it is a wordmark, not a
   photograph), and the hero cover video, which has the flight instead.

   `--par-img: 0` on card 3 is a fourth, and it is not a style choice: card 3 is
   the slot the hero shrinks onto, and after the hand-over what fills it is the
   freeze canvas painted from the video (10.5), sized and cropped to the CARD box
   and sitting `inset: 0` over the still. The still underneath is not visible
   again, so an inner drift there would animate nothing; making the canvas drift
   instead would need it painted at the oversized box, whose cover-crop of a
   1600x900 video is 27% larger than the card's -- which is exactly the visible
   cut the freeze exists to remove. */
@property --par-img { syntax: "<length>"; inherits: true; initial-value: 0px; }

/* The attribute is set by the script, never in the markup, so with JS blocked or
   under reduced motion every picture renders exactly as it did before. (0,2,0)
   because `.page-hero img` and the base `.crrc-page img` rule are both (0,1,1).

   `bottom` is left `auto` and the height stated outright. `inset: calc(...) 0`
   alone would over-constrain an absolutely positioned REPLACED element, which
   resolves it by taking the picture's own intrinsic ratio and dropping `bottom`
   -- the image then letterboxes inside the frame instead of filling it, and
   `object-fit: cover` does nothing because the box already matches the source.
   That cost a build in 10.9 and it is the same trap here. */
:is(.crrc-home, .crrc-page) [data-par-img] {
    position: absolute;
    inset: calc(var(--par-img) * -1) 0 auto;
    width: 100%;
    height: calc(100% + var(--par-img) * 2);
}

/* On mobile the per-frame --par-img amplitudes are set in vw (2-3.6vw), which
   at 420px wide works out to 8-15px of travel -- barely visible. Floor the
   amplitude on phones so the parallax reads (2026-10-20). */
@media (max-width: 1024px) {
    :is(.crrc-home, .crrc-page) [data-par-img] {
        --par-img: 36px;
    }
}

.section-about__intro {
    /* The routes wrapper overhangs the board into this block, so without a
       stacking position of its own the art would paint over the copy. */
    position: relative;
    z-index: 1;
    display: grid;
    grid-template-columns: 31.4vw 1fr;
    column-gap: 3.6vw;
    padding-left: 22.5%;
    padding-right: 24.3%;

    @media (max-width: 1024px) {
        grid-template-columns: 1fr;
        gap: 0;
        padding: 47px var(--gutter) 16px;
    }
}

/* the eyebrow gets a row of its own, so the copy column starts level with
   the heading rather than with the eyebrow (2026-09-27, on request). A
   subgrid, not display: contents, so the head keeps a box of its own
   (contents leaves DevTools nothing to highlight). */
.section-about__intro-head {
    grid-column: 1;
    grid-row: 1 / span 2;
    display: grid;
    grid-template-rows: subgrid;
}

.section-about__intro-copy {
    grid-column: 2;
    grid-row: 2;
    margin-top: 1.1vw;

    @media (max-width: 1024px) {
        grid-column: auto;
        grid-row: auto;
        margin-top: 23px;
    }
}

.section-about__intro h2 {
    margin-top: 1.1vw;
    font-size: var(--fs-h2);
    letter-spacing: var(--ls-h2);
    line-height: var(--lh-h2);
    text-transform: none;

    @media (max-width: 1024px) {
    }
}

.section-about__intro p {
    width: 18.2vw;

    @media (max-width: 1024px) {
        width: auto;
    }
}

.section-about__intro .crrc-btn {
    margin-top: 1.7vw;
}

/* --- 3. solutions --- */
.section-solutions {
    background: var(--ink);
    color: var(--white);
    overflow: clip;
    height: 100vh;
    padding: 3.26vw var(--gutter) 4.6vw;
    display: grid;
    grid-template-columns: 28.2vw 1fr;
    gap: 1.8vw;
    box-sizing: border-box;

    @media (max-width: 1024px) {
        /* On mobile the wheel is replaced by stacked cards; no need for
           100vh height, which left a screenful of dead space between the
           tabs and the visible card. */
        height: auto;
        min-height: 0;
        grid-template-columns: minmax(0, 1fr);
        gap: 31px;
        padding: 47px var(--gutter);
    }
}

.section-solutions__head {
    display: flex;
    flex-direction: column;
}

.section-solutions__head h2 {
    margin-top: 1.3vw;

    @media (max-width: 1024px) {
    }
}

.section-solutions__head > p:not(.eyebrow) {
    margin-top: auto;
    color: #c8ced1;
    font-size: var(--fs-h4);
    letter-spacing: var(--ls-h4);
    line-height: var(--lh-h4);
    width: 19.5vw;

    @media (max-width: 1024px) {
        width: auto;
        margin-top: 27px;
    }
}

/* Panel 69-249 x 2309-2407 in the export: 12.5 x 6.81vw, with the rail inset
   1.04vw from the left rather than sitting on the panel edge, and the three rows
   27.5pt apart. Padded rows put it at 125 tall with 33.5pt rows. */
.section-solutions__tabs {
    position: relative;
    margin-top: 1.67vw;
    width: 12.5vw;
    background: rgba(255, 255, 255, 0.09);
    border-radius: var(--radius-sm);
    padding: 0.5vw 0 0.5vw 1.04vw;

    /* Hidden on mobile: all three cards stack as a column (2026-10-20),
       so the tabs no longer have anything to switch between. */
    @media (max-width: 1024px) {
        display: none;
    }
}

/* The dashed rail runs the height of the list, behind all three rows -- one line
   in the export (x 85-86, y 2321-2391), not a border repeated per row. */
.section-solutions__tabs::before {
    content: "";
    position: absolute;
    left: 1.04vw;
    top: 0.83vw;
    bottom: 1.11vw;
    border-left: 2px dotted #bebebe;
    --ln-mask: var(--ln-y);

    @media (max-width: 1024px) {
        display: none;
    }
}

.section-solutions__tab {
    position: relative;
    display: block;
    width: 100%;
    text-align: left;
    background: none;
    border: 0;
    line-height: var(--lh-p);
    padding: 0.55vw 1.25vw 0.55vw 1.11vw;
    font-family: inherit;
    font-size: var(--fs-p);
    font-weight: 600;
    letter-spacing: var(--ls-p);
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.55);
    cursor: pointer;
    transition: color 0.25s ease, border-color 0.25s ease;

    @media (max-width: 1024px) {
        flex: 1;
        text-align: center;
        border-bottom: 3px solid transparent;
        padding: 12px 4px;
        background: rgba(255, 255, 255, 0.09);
    }
}

/* The active marker is 3 x 18 and centred on its row -- a border would run the
   whole 27.5 and read as a much heavier rule than the design draws. */
.section-solutions__tab::before {
    content: "";
    position: absolute;
    left: 0;
    top: 50%;
    width: 0.21vw;
    height: 1.25vw;
    translate: 0 -50%;
    background: var(--red);
    opacity: 0;
    transition: opacity 0.25s ease;

    @media (max-width: 1024px) {
        display: none;
    }
}

.section-solutions__tab:focus {
    outline: none;
}

.section-solutions__tab:focus-visible {
    outline: 2px solid var(--red);
    outline-offset: 2px;
}

.section-solutions__tab[aria-selected="true"] {
    color: var(--white);

    @media (max-width: 1024px) {
        border-bottom-color: var(--red);
    }
}

.section-solutions__tab[aria-selected="true"]::before {
    opacity: 1;
}

/* The card stack is a VERTICAL CAROUSEL: picking a tab slides the open card up
   and out of the top and brings the next one up from underneath, so the clip has
   to be the card's own box. The export stacks its two card states 236 right and
   574 down of each other, which is a Figma state stack rather than a layout --
   the travel here is pure vertical. */
.section-solutions__cards {
    position: relative;
    align-self: start;
    margin-top: 3vw;
    border-radius: var(--radius);
    aspect-ratio: 859 / 553;

    @media (max-width: 1024px) {
        margin-top: 31px;
        aspect-ratio: auto;
        display: flex;
        flex-direction: column;
        gap: 48px;
    }
}

.section-solutions__card {
    position: absolute;
    inset: 0;
    display: grid;
    grid-template-columns: minmax(0, 375fr) minmax(0, 427fr);
    grid-template-rows: auto auto minmax(0, 1fr);
    gap: 0 1.11vw;
    background: var(--white);
    color: var(--ink);
    border-radius: var(--radius);
    padding: 1.74vw 1.25vw 1.39vw 1.6vw;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.45s ease;

    @media (max-width: 1024px) {
        position: relative;
        inset: auto;
        display: grid;
        grid-template-columns: 1fr;
        grid-template-rows: auto auto;
        gap: 20px;
        padding: 20px;
        opacity: 1;
        visibility: visible;
    }
    /* a size container below 1440, so the big EXPLORE can measure the copy
       column it sits in (see .crrc-btn below) */
    @media (min-width: 1025px) and (max-width: 1439px) {
        container-type: inline-size;
    }
}

/* --- the wheel (2026-09-24, on request) ---
   The cards are pinned to a circle whose centre is off to the right; the
   open card sits at 9 o'clock and --d (set by the script) is each card's
   offset from it in steps round the rim. The export puts the next state
   236pt right and 574pt down of the open one, which fixes the geometry:
   step = 2 * atan(236 / 574) = 44.8deg, radius = 574 / sin(44.8deg) = 816pt
   = 56.7vw from the card's centre.

   translate(R) rotate(a) translate(-R) is a rotation about the circle's
   centre, and because the transition interpolates the ANGLE the card travels
   along the arc, not across the chord. The trailing rotate() cancels the
   circle's own rotation exactly (--wheel-keep: 0), so the cards ride the
   rim like gondolas and never tilt -- the client's call: "rotation
   animation correct, but the cards themselves should not rotate". */
.section-solutions__card {
    --wheel-r: 56.7vw;
    --wheel-step: -44.8deg;
    --wheel-keep: 0;
    transform:
        translateX(var(--wheel-r))
        rotate(calc(var(--d, 0) * var(--wheel-step)))
        translateX(calc(var(--wheel-r) * -1))
        rotate(calc(var(--d, 0) * var(--wheel-step) * (var(--wheel-keep) - 1)));
    transition: transform 0.9s cubic-bezier(0.65, 0, 0.35, 1),
                opacity 0.5s ease, visibility 0s linear 0.9s;
    box-shadow: 0 1vw 3vw rgba(0, 0, 0, 0.35);

    @media (max-width: 1024px) {
        transform: none;
    }

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

.section-solutions__card[data-near] {
    opacity: 1;
    visibility: visible;
    cursor: pointer;
    transition: transform 0.9s cubic-bezier(0.65, 0, 0.35, 1),
                opacity 0.5s ease, visibility 0s;
}

/* a neighbour is a picture of the next card, not a control -- its own links
   do not take the pointer, the card does (and spins round to the front) */
.section-solutions__card[data-near] * {
    pointer-events: none;
}

.section-solutions__card[data-active] {
    opacity: 1;
    visibility: visible;
    transition: transform 0.9s cubic-bezier(0.65, 0, 0.35, 1),
                opacity 0.5s ease, visibility 0s;

    @media (max-width: 1024px) {
        display: grid;
    }
}

/* No inset. The export puts the body copy and the big EXPLORE flush to the
   column's own dashed rule (text on 534, rule on 534-535), and the 23pt padding
   that was here squeezed the 355-wide button down to 326. */
.section-solutions__card-body {
    position: relative;
    grid-column: 1 / -1;
    grid-row: 1 / -1;
    display: grid;
    grid-template-columns: subgrid;
    grid-template-rows: subgrid;
    /* the visible line is the heading's ::before; this box clips its right
       leg at the foot of the card */
    clip-path: inset(0);

    @media (max-width: 1024px) {
        grid-column: auto;
        grid-row: auto;
        display: flex;
        flex-direction: column;
        gap: 9px;
    }
}

.section-solutions__card-body > h3 {
    grid-column: 1 / -1;
}

.section-solutions__card-body > p {
    grid-column: 1;
}

.section-solutions__card h3 {
    position: relative;
    padding-bottom: 1.2vw;
    border-bottom: 2px dotted transparent;
    text-transform: uppercase;
    font-size: var(--fs-h1);
    letter-spacing: var(--ls-h1);
    line-height: var(--lh-h1);
    text-wrap: balance;

    @media (max-width: 1024px) {
        padding-bottom: 12px;
        /* "LOCOMOTIVES" is one word: at the phone h1 (70px) it is wider
           than the screen and cannot wrap */
        font-size: min(var(--fs-h1), 10.5vw);
    }
}

.section-solutions__card h3::before {
    /* One dotted bracket, as the design draws it (2026-09-27): along under
       the heading, round a soft top-right corner and down the right side to
       the foot of the card. No line on the left. It starts on the heading's
       bottom spacer and runs long; the card body clips it. */
    content: "";
    position: absolute;
    top: 100%;
    left: 0;
    /* the heading spans the card; the bracket turns down at the copy
       column's right edge: 375 of the 802fr left after the column gap */
    right: calc(100% - (100% - 1.11vw) * 375 / 802);
    height: 100vw;
    box-sizing: border-box;
    border-top: 2px dotted #bebebe;
    border-right: 2px dotted #bebebe;
    border-top-right-radius: 0.7vw;
    pointer-events: none;

    @media (max-width: 1024px) {
        right: 0;
        height: 0;
        border-right: 0;
    }
}

.section-solutions__card p {
    margin-top: 1.5vw;
    width: 12.3vw;
    font-size: var(--fs-p);
    letter-spacing: var(--ls-p);
    line-height: var(--lh-p);

    @media (max-width: 1024px) {
        width: auto;
        margin-top: 16px;
    }
}

/* The one large EXPLORE on the page: pill 534-791 x 2760-2842 and tile 807-889,
   with the label 9 in / 11 down and the arrow at the tile's top right, all
   measured off the export. Everywhere else the button is the 108 x 30 pill. */
/* 2026-10-14 feedback: the Intercity / Urban / Locomotives EXPLOREs now
   use the standard home .crrc-btn (small pill, rolling-arrow hover) --
   same as every other EXPLORE on the home page.
   2026-10-27 feedback (restored): the SIZE and TEXT POSITION go back to
   the pre-Oct-14 oversized version (design pill 257x82, tile 82x82,
   gap 16, label at the pill's TOP-LEFT corner and arrow at the tile's
   TOP-RIGHT corner at a 1440 artboard), but the hover is still the
   rolling-arrow swap. Scoped to the home Solutions cards; every other
   EXPLORE on the site keeps the small .crrc-btn size.
   The vw values and paddings are copied verbatim from the Sept-10
   build's `.crrc-btn__label` / `.crrc-btn__icon` rules, adapted onto
   the current `.wp-block-button__link` + `::after` structure. */
.section-solutions__card .crrc-btn {
    grid-column: 1;
    grid-row: 3;
    margin-top: auto;
    align-self: flex-start;
    --btn-tile: 5.69vw;
    --btn-gap:  1.11vw;
}

.section-solutions__card .crrc-btn .wp-block-button__link {
    /* Fixed width so the design's 3.14:1 pill-to-tile ratio is preserved
       regardless of content. Label is a SMALL caps word at the pill's
       TOP-LEFT corner, matching the "Group 4919" reference the client
       sent 2026-10-27 -- not centred, and not at h4 size. The pill is
       mostly empty space; only the label is drawn in the top-left. */
    width: 17.85vw;
    min-width: 0;
    align-items: flex-start;
    justify-content: flex-start;
    padding: 0.78vw 0 0 0.63vw;
    font-size: var(--fs-p);
    letter-spacing: 0.08em;

    @media (max-width: 1024px) {
        width: auto;
        align-items: center;
        padding: 12px 23px;
    }
}

/* Arrow at the TOP-RIGHT of the tile, small (1.08vw ≈ 16px at 1440,
   matching the "Group 4919" reference's thin diagonal arrow). BOTH tiles
   have to match: the ::after is the arrow in the resting tile on the
   right, and the ::before is the incoming hover tile that slides in from
   the left -- the Group 4919 reference shows them as the same small
   top-right glyph, not the big centred one the sitewide `.crrc-btn` uses. */
.section-solutions__card .crrc-btn .wp-block-button__link::after,
.section-solutions__card .crrc-btn::before {
    background-position: top 0.63vw right 0.9vw;
    background-size: 1.08vw;

    @media (max-width: 1024px) {
        background-position: center;
        background-size: 52%;
    }
}

@media (max-width: 1024px) {
    .section-solutions__card .crrc-btn {
        --btn-tile: 35px;
        --btn-gap:  5px;
    }
}

/* until the script picks a card, the first one is the open one */
.section-solutions__cards:not(:has([data-active])) > .section-solutions__card:first-child {
    opacity: 1;
    visibility: visible;

    @media (max-width: 1024px) {
        display: grid;
    }
}

.section-solutions__card figure {
    --par-img: 2vw;
    position: relative;
    grid-column: 2;
    grid-row: 2 / 4;
    min-height: 0;
    border-radius: var(--radius-sm);
    overflow: hidden;

    @media (max-width: 1024px) {
        grid-column: auto;
        grid-row: auto;
        aspect-ratio: 16 / 10;
    }
}

/* the photo fills whatever the title leaves; absolute so its own
   intrinsic height cannot size the row (the parallax rule, when the
   script tags it, outranks this) */
.section-solutions__card figure img {
    position: absolute;
    inset: 0;

    @media (max-width: 1024px) {
        position: static;
    }
}

/* --- 4. fleet mro --- */
/* The export's gutters are not uniform: this section's copy sits on 64 and the
   rail's right edge on 1369, so the padding is asymmetric and the first column
   carries the difference. The rail still starts at 510. */
.section-mro {
    background: var(--off-white);
    padding: 3.33vw 4.93vw 9.65vw 4.44vw;
    display: grid;
    grid-template-columns: 29.17vw 1fr;
    gap: 1.8vw;

    @media (max-width: 1024px) {
        grid-template-columns: 1fr;
        gap: 31px;
        padding: 47px var(--gutter);
    }
}

.section-mro__head {
    display: flex;
    flex-direction: column;
}

.section-mro__head > p:not(.eyebrow) {
    margin-top: auto;
    width: 19.5vw;
    font-size: var(--fs-h4);
    letter-spacing: var(--ls-h4);
    line-height: var(--lh-h4);

    @media (max-width: 1024px) {
        width: auto;
        margin-top: 20px;
    }
}

/* One number drives the whole accordion -- each card's width and its panel -- so the
   open/closed state is decided in one place instead of being duplicated across a
   width rule and an opacity rule. Registered with @property so it interpolates; an
   unregistered custom property changes discretely and nothing would transition.
   Without @property support it still works, just without the ease. */
@property --mro-open {
    syntax: "<number>";
    inherits: true;
    initial-value: 0;
}

.section-mro__rail {
    display: flex;
    gap: 1.18vw;
    height: 42.85vw;

    @media (max-width: 1024px) {
        flex-direction: column;
        gap: 16px;
        height: auto;
        overflow: visible;
    }
}

.section-mro__rail::-webkit-scrollbar { display: none; }

/* grow ratios, not fixed widths: the rail width is split 41.67 : 16.81, which is the
   pair of card widths the design draws */
.section-mro__card {
    --par-img: 2.2vw;
    --mro-open: 0;
    /* The opening is a sequence, and these four numbers are all of it: the card
       widens (--mro-dur), 0.3s later the white panel wipes up from its bottom
       edge (--mro-panel-dur), and once the panel is up the text replays
       (10.14b). Each step is written against the one before so they cannot
       drift; --tr-wait is registered <time>, which is what resolves the calc()
       to plain seconds for the script to read. */
    --mro-dur: 0.55s;
    /* 0.1s, on request 2026-09-24: waiting for the card to finish widening
       (0.85s) made the panel feel late when switching cards */
    --mro-panel-delay: 0.1s;
    --mro-panel-dur: 0.5s;
    --tr-wait: calc(var(--mro-panel-delay) + var(--mro-panel-dur));
    position: relative;
    flex-basis: 0;
    flex-shrink: 1;
    flex-grow: calc(16.81 + 24.86 * var(--mro-open));
    min-width: 0;
    height: 100%;
    border-radius: var(--radius);
    overflow: hidden;
    transition: --mro-open var(--mro-dur) cubic-bezier(0.4, 0, 0.2, 1);

    @media (max-width: 1024px) {
        --mro-open: 1;
        --tr-wait: 0s;
        flex: 0 0 auto;
        width: 100%;
        height: 304px;
    }

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

/* Two mechanisms, and only one of them is live at a time.

   WITHOUT JS this is the original CSS-only accordion: the first card is open at
   rest and yields the moment the pointer or focus picks another. `:not(:hover)`
   on the yield rule keeps it from ever fighting the hover rule, which is
   otherwise less specific and would lose.

   WITH JS the rail carries `.is-toggle` and the state LATCHES: the card the
   pointer last touched stays open after the pointer leaves, which is what the
   client asked for -- "toggle after hover, not just when hovering". Every hover
   rule is scoped `:not(.is-toggle)` rather than being outranked, because
   `:has()` takes the specificity of its argument and the yield rule above is
   (0,5,0) -- nothing a plain descendant selector can beat. */
.section-mro__rail:not(.is-toggle) .section-mro__card:first-child:not(:hover):not(:focus-within) {
    --mro-open: 1;
}

.section-mro__rail:not(.is-toggle):has(.section-mro__card:hover) .section-mro__card:first-child:not(:hover),
.section-mro__rail:not(.is-toggle):has(.section-mro__card:focus-within) .section-mro__card:first-child:not(:focus-within) {
    --mro-open: 0;
}

.section-mro__rail:not(.is-toggle) .section-mro__card:hover,
.section-mro__rail:not(.is-toggle) .section-mro__card:focus-within {
    --mro-open: 1;
}

/* Desktop only: under 768px every card is open (there is no hover there), and
   this would otherwise close the ones the pointer never visited. */
@media (min-width: 1025px) {
    .section-mro__rail.is-toggle .section-mro__card[data-open] {
        --mro-open: 1;
    }
}

/* the photo is an Image block; its figure fills the card, as the bare
   <img> did */
.section-mro__card > .wp-block-image {
    height: 100%;
}

/* Fixed width, anchored left, so a closing card CLIPS the panel behind its own
   rounded corner instead of squashing the heading and button into a sliver. */
.section-mro__panel {
    position: absolute;
    inset: auto auto 1.1vw 1.46vw;
    width: 39.06vw;
    background: var(--white);
    border-radius: var(--radius-sm);
    /* 531-1094 x 3457-3577 in the export, with "Fleet MRO" on x 567. The
       export ran the dashed rule to 1092 (2 from the edge); since 2026-09-28,
       on request, it stops as far from the right edge as the copy sits from
       the left, 36. */
    padding: 0.78vw 2.5vw 0.9vw;
    opacity: var(--mro-open);

    @media (max-width: 1024px) {
        inset: auto 16px 16px 16px;
        width: auto;
        padding: 16px;
    }
}

/* The panel wipes up from its bottom edge once its card is fully open. A clip,
   not `height`: the panel is anchored to the card's bottom with content of fixed
   size, so an inset clip growing up from the bottom is visually the same box
   going from height 0 to full, and it costs no layout on every frame.

   Only with the latch script (`.is-toggle`) and only on desktop, where cards
   actually open and close -- with JS blocked the CSS-only accordion keeps its
   plain fade, and under 768px every panel is always shown. The TRANSITION is
   split by direction: opening waits for the card (--mro-panel-delay), closing
   drops the panel back down at once, so a card being left does not hold a
   white box up while it narrows. */
@media (min-width: 1025px) and (prefers-reduced-motion: no-preference) {
    .section-mro__rail.is-toggle .section-mro__panel {
        clip-path: inset(100% 0 0 0 round var(--radius-sm));
        transition: clip-path 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }

    .section-mro__rail.is-toggle .section-mro__card[data-open] .section-mro__panel {
        clip-path: inset(0 0 0 0 round var(--radius-sm));
        transition: clip-path var(--mro-panel-dur) var(--tr-ease) var(--mro-panel-delay);
    }
}

.section-mro__panel h3 {
    position: relative;
    padding-bottom: 0.43vw;
    border-bottom: 2px dotted transparent;
    text-transform: none;
}

.section-mro__panel h3::before {
    content: "";
    position: absolute;
    inset: auto 0 -2px;
    border-bottom: 2px dotted #bebebe;
    pointer-events: none;
}

.section-mro__panel .crrc-btn {
    margin-top: 1.04vw;
}

/* --- 5. localization --- */
/* The localization band HOLDS while the global-network section slides up over
   it, like a card sliding off the top of a deck. Sticky rather than a
   ScrollTrigger pin: it needs no spacer, no z-index bookkeeping against the
   pinned hero, and it still works with the CDN blocked. `.crrc-home` uses
   `overflow-x: clip`, which does NOT establish a scroll container, so sticky
   survives it; `overflow-x: hidden` there would kill this.

   The offset holds the band's BOTTOM at the foot of the viewport -- it must not
   stick until the whole photograph has been seen. `bottom: 0` is the obvious way
   to write that and it does not engage here (measured: the band scrolls straight
   past while `top: 0` on the same element pins immediately), so the same
   position is expressed as a top offset instead: the band is 55.21vw tall, being
   a 1440 x 795 photo at full bleed. On a viewport taller than that the offset is
   positive and the band sits proud of the fold; on a shorter one it goes
   negative and the top runs off the screen, which is the behaviour wanted. */
.section-localization {
    position: sticky;
    top: calc(100vh - 55.21vw);
    aspect-ratio: 1440 / 795;
    color: var(--ink);
    isolation: isolate;

    /* Mobile: a plain flex column with ONE consistent padding on the
       section. The photo background sits at the TOP (242px) via its own
       absolute positioning, with content stacked below it. */
    @media (max-width: 1024px) {
        position: relative;
        top: auto;
        aspect-ratio: auto;
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        gap: 20px;
        padding: 282px var(--gutter) 47px;
    }
}

.section-localization__bg {
    --par-img: 3.2vw;
    overflow: hidden;
    position: absolute;
    inset: 0;
    z-index: -1;
    /* 2026-10-14 feedback (round 2): the photo was meeting the surrounding
       white sections with a hard horizontal cut (sky-against-white at the
       top; pavement-against-white at the bottom). A top-and-bottom alpha
       mask bleeds the photo into whatever sits above/below -- the client's
       "gradient effect at the beginning and end." Fade bands are ~14% of
       the band's own height each; the middle 72% stays fully opaque so no
       meaningful photo content is lost. */
    mask-image: linear-gradient(to bottom,
                                transparent 0%,
                                #000 14%,
                                #000 86%,
                                transparent 100%);
    -webkit-mask-image: linear-gradient(to bottom,
                                        transparent 0%,
                                        #000 14%,
                                        #000 86%,
                                        transparent 100%);

    @media (max-width: 1024px) {
        inset: 0 0 auto;
        height: 242px;
        /* photo sits at the TOP of the stacked section (content pushed down
           by the section's padding-top); fade both edges so it bleeds into
           the section above AND into the white content area below. */
        mask-image: linear-gradient(to bottom,
                                    transparent 0%,
                                    #000 10%,
                                    #000 85%,
                                    transparent 100%);
        -webkit-mask-image: linear-gradient(to bottom,
                                            transparent 0%,
                                            #000 10%,
                                            #000 85%,
                                            transparent 100%);
    }
}

.section-localization__eyebrow {
    position: absolute;
    left: 4.44vw;
    top: 5.7vw;
    color: rgba(27, 36, 41, 0.45);

    @media (max-width: 1024px) {
        position: static;
        display: block;
        padding: 0;
    }
}

/* the copy is the paragraph itself (no wrapper group); .crrc-home puts it
   above the base `p` rule, 10.8b */
.crrc-home .section-localization__copy {
    color: var(--ink);
    font-size: var(--fs-h3);
    letter-spacing: var(--ls-h3);
    line-height: var(--lh-h3);
    position: absolute;
    left: 42.5%;
    top: 5.9vw;
    width: 25.2%;

    @media (max-width: 1024px) {
        position: static;
        width: auto;
        padding: 0;
    }
}

.section-localization .crrc-btn {
    position: absolute;
    right: 9.4%;
    top: 5.7vw;

    @media (max-width: 1024px) {
        position: static;
        margin: 0;
    }
}

/* --- 6. global network --- */
/* z-index 1 so it paints OVER the sticky band above it. A sticky element is
   positioned, so with both at `auto` the localization would win on DOM order and
   the slide-over would happen behind it. The white background is what makes the
   card read as solid on the way up. */
.section-network {
    position: relative;
    z-index: 1;
    aspect-ratio: 1440 / 720;
    background: var(--white);

    /* On mobile the map is zoomed and goes full-bleed (0 side/top padding) so
       the China/Malaysia centre reads as big as the viewport allows. The
       heading + note that follow carry their own gutter via nested rules. */
    @media (max-width: 1024px) {
        aspect-ratio: auto;
        padding: 0 0 55px;
    }
}

/* Layout from the client's 2026-09-28 reference: the map centred behind
   everything, the heading over its top-left, the note bracketed bottom-left. */
.section-network__map {
    position: absolute;
    left: 9.86%;
    top: 2.29vw;
    width: 80.07%;

    /* On mobile the full world map renders ~340px wide and the China/Malaysia
       pills end up ~8px tall — illegible. Zoom into the pins + pills on mobile:
       the container becomes a square window, the SVG is enlarged to ~367% so
       the pill text lands roughly at --fs-p (12px), and the SVG is positioned
       so that the midpoint of the two pills (712.5, 242.5 in viewBox units =
       61.8% x, 38.8% y) sits at the container's centre. See .section-network__map
       .globe below. */
    @media (max-width: 1024px) {
        position: relative;
        left: auto;
        top: auto;
        width: 100%;
        margin-inline: 0;
        padding-inline: 0;
        overflow: hidden;
        aspect-ratio: 1 / 1;
    }
}

.section-network__map img,
.section-network__map svg {
    width: 100%;
    height: auto;
    object-fit: contain;
}

/* --- the globe animates on scroll enter (10.19, on request) -----------------
   TWO stages: the network dots land, then the China-Malaysia link traces.

   A third stage came first -- a conic sweep that drew the land and graticule in
   from 12 o'clock -- and was dropped on request. What is left of it is the
   `.globe__world` wrapper in the markup, which costs nothing and is where the
   mask went; `build-globe.py`'s comment has the one line that pairs with it.

   The svg is INLINE because none of this can address the inside of an <img>;
   `build-globe.py` takes the client's 577 KB file to 91 KB and regroups it by
   role. It enrols with text-reveal.js as `--ln: group` -- the observer, the
   batch cascade and `is-tr-in` for free, and no mask of its own (10.16).

   EVERY hidden state is keyed on [data-ln], never on a bare class, because
   text-reveal.js REMOVES data-ln when it is finished (its `finish()`); with the
   attribute gone neither rule matches and the globe is left as plain markup.
   That is also what makes a blocked script, a thrown error or reduced motion
   render the finished globe rather than an invisible one -- the attribute is
   only ever added by the script.

   `--ln-dur` is what `armFinish()` waits before doing that, so it has to cover
   the whole choreography, not one stage. */
.section-network__map .globe {
    --ln: group;
    --ln-dur: 2.6s;
    --g-dots: 0.55s;   /* the beat before the dots start */
    --g-link: 1.6s;    /* the link, after the last dot has landed */
    display: block;

    /* Mobile zoom: enlarge SVG to 367% of container (capped at 1400px so the
       pills don't balloon on wider mobile widths), position its top-left at
       container centre, then translate by the midpoint's own fractional
       coordinates so that the pill-pin midpoint lands exactly at the
       container centre. Container overflow:hidden crops the rest.
       Width 367% picks a scale where 11 svg-units (pill text cap height) ≈
       --fs-p (12px) at the common 342px mobile container.

       Midpoint maths: the globe artwork is wrapped in <g transform="translate(131 89)">
       inside the 1153 x 625 viewBox (see build-globe.py, CLAUDE.md 10.19), so the
       China/Malaysia pin+pill bbox at local coords x[662,763] y[184,301] renders at
       viewBox x[793,894] y[273,390]. The centroid is (843.5, 331.5) = (73.16%, 53.04%). */
    @media (max-width: 1024px) {
        position: absolute;
        left: 50%;
        top: 50%;
        width: min(367%, 1400px);
        height: auto;
        max-width: none;
        object-fit: unset;
        transform: translate(-73.16%, -53.04%);
    }
}

.globe__dot,
.globe__city,
.globe__pill {
    transform-box: fill-box;
    transform-origin: center;
}

/* the dots pop west to east. --i is written per dot by build-globe.py in
   longitude order, so this reads as a cascade across the map rather than 29
   things arriving at once. The overshoot is the "slight bounce":
   cubic-bezier(.34,1.56,.64,1) passes 1 and settles back. */
.globe[data-ln] .globe__dot {
    transform: scale(0);
}

.globe[data-ln].is-tr-in .globe__dot {
    transform: none;
    transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1)
                calc(var(--tr-d, 0s) + var(--g-dots) + var(--i, 0) * 26ms);
}

/* the two cities land first, then the dashed line traces from China down to
   Malaysia, then the labels arrive.

   The line is DASHED, so stroke-dashoffset is unavailable -- it would march the
   dashes along rather than reveal them. `clip-path: inset()` on the wrapping <g>
   resolves against that group's own fill-box, which is the line's 26 x 89
   bounding box, so closing the bottom inset wipes it top-to-bottom: China first,
   Malaysia last, which is the direction the design's arrow implies. */
.globe[data-ln] .globe__city {
    transform: scale(0);
}

.globe[data-ln].is-tr-in .globe__city {
    transform: none;
    transition: transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1)
                calc(var(--tr-d, 0s) + var(--g-link));
}

.globe[data-ln] .globe__trace {
    clip-path: inset(0 0 100% 0);
}

.globe[data-ln].is-tr-in .globe__trace {
    clip-path: inset(0 0 0 0);
    transition: clip-path 0.75s cubic-bezier(0.4, 0, 0.2, 1)
                calc(var(--tr-d, 0s) + var(--g-link) + 0.12s);
}

/* 2026-10-10 feedback: the pew path is stroked with #pew-grad (a
   linearGradient aligned China -> Malaysia, defined inline on the SVG), and
   the two mid-stops' `offset` attributes are driven by SMIL <animate>
   elements inside the gradient. The offset walks 0 -> 1 over the first 4s
   of an 8s cycle, then holds, reproducing the "line draws from China down
   to Malaysia at full opacity, then stays" effect WITHOUT needing a
   separate overlay/outline or opacity change. CSS cannot drive a <stop>'s
   `offset` -- that attribute is not exposed as a CSS property; the CSS
   `offset:` shorthand resolves to CSS Motion Path's `offset-position`
   instead and the actual SVG offset stays at 0 (measured 2026-10-10).

   Reduced-motion handling + the .is-live gate stay below. */

.globe[data-ln] .globe__pill {
    transform: scale(0.6);
    opacity: 0;
}

.globe[data-ln].is-tr-in .globe__pill {
    transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1)
                    calc(var(--tr-d, 0s) + var(--g-link) + 0.58s),
                opacity 0.3s ease calc(var(--tr-d, 0s) + var(--g-link) + 0.58s);
}

.globe__city {
    transform-origin: center;
    transform-box: fill-box;
}

/* 2026-10-06: pulse + pew ran only while [data-ln].is-tr-in matched, so
   text-reveal.js's finish() stripped data-ln and the animations stopped
   dead -- the live globe sat static. An inline script in home-blocks.html
   now flips `.is-live` on the globe once the reveal is done, which keeps
   the pulse and pew running for the rest of the session. */
/* 2026-10-06 feedback: PINS pulsate (scale + fade). A STATIC copy of each
   pin is painted on top -- cloned by the inline script in home-blocks.html
   and inserted BEFORE the original in SVG DOM order (SVG paints in DOM
   order, so the clone draws behind). The pill LABELS stay static. */
.globe__city-ping {
    transform-origin: center;
    transform-box: fill-box;
}

.globe.is-live .globe__city-ping {
    animation: crrc-globe-pulse 1.8s ease-in-out infinite;
}

/* The gradient reveal runs on SMIL inside the SVG; no CSS animation needed
   on the pew itself. Hidden by default until .is-live (so the pew doesn't
   peek through during the scroll-in reveal of the other globe elements). */
.globe__pew { opacity: 0; }
.globe.is-live .globe__pew { opacity: 1; transition: opacity 0.3s; }

/* The ping clones draw a growing, fading halo behind the static pin. Scale
   runs from 1 to ~2.6 and opacity fades out so the clone looks like a
   radar echo. */
@keyframes crrc-globe-pulse {
    0%   { opacity: 0.7; transform: scale(1); }
    80%  { opacity: 0;   transform: scale(2.6); }
    100% { opacity: 0;   transform: scale(2.6); }
}

@media (prefers-reduced-motion: reduce) {
    .globe.is-live .globe__city-ping { animation: none; }
    .globe__city-ping { display: none; }
    /* SMIL inside the SVG doesn't honor prefers-reduced-motion; hide the
       pew path entirely so the gradient reveal doesn't play. The 1px
       trace underneath still carries the semantic connector line. */
    .globe__pew { display: none; }
}

.section-network h2 {
    position: absolute;
    left: 4.44vw;
    top: 3.9vw;
    z-index: 1;

    @media (max-width: 1024px) {
        position: static;
        margin-top: 31px;
        padding-inline: var(--gutter);
    }
}

/* the note is the paragraph itself (no wrapper group) */
.crrc-home .section-network__note {
    font-size: var(--fs-p);
    letter-spacing: var(--ls-p);
    line-height: var(--lh-p);
    position: absolute;
    left: 4.31vw;
    top: 37.22vw;
    /* a little clear space inside the bracket (2026-09-28, on request); the
       width grew by the same 0.55vw so the copy wraps where it did */
    width: 13.26vw;
    z-index: 1;
    padding: 0.9vw 0 0.9vw 0.83vw;
    border-top: 2px dotted #bebebe;
    border-left: 2px dotted #bebebe;
    border-top-left-radius: 0.7vw;

    @media (max-width: 1024px) {
        position: static;
        width: auto;
        border: 0;
        margin-top: 23px;
        padding-inline: var(--gutter);
    }
}

/* The theme's block gap puts 1.2rem of white between the page and the footer
   part. Invisible while the footer was white; a visible strip now it is dark. */
.wp-site-blocks > footer.wp-block-template-part {
    margin-block-start: 0;
}

/* --- footer curtain reveal (added 2026-09-14) ---
   The footer holds at the foot of the viewport the whole way down the page, and
   the page slides up off it at the end like a curtain going up.

   `position: sticky; bottom: 0` on the footer, NOT `fixed` plus a matching
   margin on <main>: the footer stays in flow, so the document height and every
   ScrollTrigger start/end are unchanged, and nothing has to know how tall the
   footer is. It is the last child of .wp-site-blocks, so its natural place is
   always below the fold until the end of the page, and it sticks until then.

   The page covers it by being a stacking context ABOVE it (`z-index: 1`), not
   by pushing the footer to `z-index: -1`. A negative footer paints behind every
   in-flow block's box, and in Chrome those boxes still take the hit test even
   with no background -- the links would render and not click. The header is
   z-index 20 outside <main> and nothing inside <main> goes above 10, so making
   <main> a stacking context reorders nothing. `background` because any
   transparent gap in the page would otherwise show the footer through it.

   Desktop only, and not under reduced motion, like the rest of the site's
   scroll-linked movement (10.3). The width gate is also load-bearing: the
   phone footer is taller than the screen, and a sticky box taller than the
   viewport never shows its own top. The aspect-ratio gate is the same guard for
   a short, wide desktop window -- the footer is ~27vw tall. */
/* 2026-10-14 feedback (dropdown round 2): the sticky "curtain reveal" is
   retired. With `position: sticky; bottom: 0` the footer's bottom was
   pinned to the viewport, so a growing quicklinks column (dropdown open)
   made the top edge rise up into the page -- reading as the footer
   "pushing the page up". The footer is now a plain end-of-document block
   again, so dropdown growth extends the footer DOWNWARD and the user
   scrolls a touch to see the extra items. The `<main>` background still
   paints its own white so no mixed content shows through transparent gaps. */
.wp-site-blocks > main {
    position: relative;
    z-index: 1;
    background: var(--white);
}

/* --- site footer ---
   Rebuilt 2026-09-14 from pages-pdf/footer.pdf (1440 x 383), which settles
   12.13: the footer is the DARK one, on every page. Measured off the export's own
   paint ops (pdf-geom.py); sizes in design pt / 14.4 as everywhere else. The word
   widths match the old white footer's to the hundredth -- and measured live, the
   old sizes set every word 20-42% WIDER than that. The four sizes below are
   matched on width (10.8g), which is why they are their own and not the shared
   --fs-* tokens: "Company" 73.3, "committed to shaping the" 236.4, "Level 7,
   Menara Khuan Choo" 175.4, "QUICKLINKS" 74.2. */
.crrc-footer {
    --footer-grey: #bebebe;
    --fs-footer-tagline: 1.021vw;
    --fs-footer-link: 0.903vw;   /* 13 / 30, -0.6 */
    --fs-footer-sm: 0.674vw;
    --fs-footer-eyebrow: 0.58vw;
    background: var(--ink);
    padding-top: 3.333vw;

    @media (max-width: 1024px) {
        padding-top: 47px;
    }
}

/* Content on x 83 on the left and ending on x 1362 on the right -- the dotted
   rules run 2pt past the tiles, which end on 1360. Not the 70 site gutter. */
.crrc-footer__inner {
    display: grid;
    grid-template-columns: minmax(0, 23.61vw) minmax(0, 20.14vw);
    justify-content: space-between;
    padding: 0 5.417vw 4.1vw 5.764vw;

    @media (max-width: 1024px) {
        grid-template-columns: minmax(0, 1fr);
        padding: 0 var(--gutter) 47px;
        gap: 39px;
    }
}

.crrc-footer__logo {
    width: 10.56vw;
    max-width: 190px;
    color: var(--white);

    @media (max-width: 1024px) {
        width: 133px;
    }
}

.crrc-footer__logo svg,
.crrc-footer__logo img {
    width: 100%;
    height: auto;
    display: block;
}

.crrc-footer p {
    color: var(--footer-grey);
    font-size: var(--fs-p);
    letter-spacing: var(--ls-p);
    font-weight: 600;
    line-height: var(--lh-p);
}

/* The bracket: a dashed rule along the top and down the left, meeting in a
   rounded corner, open on the other two sides. Box 81-421 x 101-179 in the
   export, copy inset to x 92. The box hangs 2pt left of the logo, which is on 83. */
.crrc-footer .crrc-footer__tagline {
    margin: 0.972vw 0 0 -0.139vw;
    padding: 0.7vw 0 0.444vw 0.694vw;
    position: relative;
    border-top: 2px dotted transparent;
    border-left: 2px dotted transparent;
    border-top-left-radius: 0.694vw;
    color: var(--white);
    font-size: var(--fs-footer-tagline);
    line-height: 1.2;

    @media (max-width: 1024px) {
        font-size: 16px;
        margin: 23px 0 0;
        padding: 9px 0 9px 11px;
        border-top-left-radius: 11px;
    }
}

/* The bracket itself, over the transparent border, so it can trace (10.16):
   up the left side, round the corner, along the top. */
.crrc-footer .crrc-footer__tagline::before {
    content: "";
    position: absolute;
    inset: -2px 0 0 -2px;
    box-sizing: border-box;
    border-top: 2px dotted #bebebe;
    border-left: 2px dotted #bebebe;
    border-radius: inherit;
    pointer-events: none;
    --ln-mask: var(--ln-bracket);
}

.crrc-footer .crrc-footer__address {
    margin-top: 4.393vw;
    font-size: var(--fs-p);
    letter-spacing: var(--ls-p);
    line-height: var(--lh-p);

    @media (max-width: 1024px) {
        margin-top: 31px;
    }
}

/* No rule under QUICKLINKS in this export -- the shared ::after is not applied
   to the footer's eyebrow. */
.crrc-footer .eyebrow {
    margin-top: 0.689vw;
    padding-bottom: 0.28vw;
    font-size: var(--fs-p);
    line-height: var(--lh-p);
    letter-spacing: var(--ls-p);
    color: var(--footer-grey);

    @media (max-width: 1024px) {
    }
}

.crrc-footer__links {
    list-style: none;
    padding: 0;
    margin-top: 0.1vw;
}

/* Rule to rule is 52-53pt: 15 of space, the 30 tile, 6.5 under it, the rule. */
.crrc-footer__links li + li {
    margin-top: 1.088vw;

    @media (max-width: 1024px) {
        margin-top: 12px;
    }
}

/* The rule is 2-on-1 dots, not a CSS `dotted` border (1-on-1). px, like every
   other hairline on the site.

   The word sits LOW against its tile -- baseline 24.5pt below the tile's top, not
   centred on it -- so the row is bottom-aligned and the line-height sets how far
   up from the tile's bottom edge the baseline lands. */
.crrc-footer__links a {
    display: flex;
    align-items: flex-end;
    line-height: 2;
    letter-spacing: -0.04em;
    justify-content: space-between;
    gap: 1vw;
    position: relative;
    padding: 0 0.139vw 0.45vw 0;
    font-size: var(--fs-footer-link);
    font-weight: 600;
    color: var(--white);

    @media (max-width: 1024px) {
        padding-bottom: 12px;
        font-size: 13px;
    }
}

/* the dotted rule, on a ::before so it can draw (10.16) -- 2px dotted
   #BEBEBE like every dotted line on the site (2026-09-27) */
.crrc-footer__links a::before {
    content: "";
    position: absolute;
    inset: auto 0 0;
    border-top: 2px dotted #bebebe;
    pointer-events: none;
}

.crrc-footer__links span {
    display: grid;
    place-items: center;
    flex: none;
    width: 2.083vw;
    aspect-ratio: 1;
    box-sizing: border-box;
    /* an outlined tile on the dark footer (2026-09-27); fills red on hover */
    background: transparent;
    border: 1.5px solid var(--footer-grey);
    border-radius: var(--radius-sm);
    color: var(--white);
    transition: background 0.25s ease, border-color 0.25s ease, transform 0.25s ease;

    @media (max-width: 1024px) {
        width: 35px;
    }
}

/* 8pt of arrow in the 30 tile -- the same glyph and ratio as the pill
   `::after` arrow. */
.crrc-footer__links svg {
    width: 52%;
    height: auto;
    fill: none;
    stroke: var(--white);
    stroke-width: 2.4;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.crrc-footer__links a:hover span,
.crrc-footer__links a:focus-visible span {
    background: var(--red);
    border-color: var(--red);
    transform: translate(0.1vw, -0.1vw);
}

/* Solutions dropdown in the footer (2026-10-01, client review): same row look
   as the links -- the <button> inherits .crrc-footer__links a's styles via
   the shared selector below, and the submenu opens on hover, focus or click. */
.crrc-footer__drop {
    position: relative;
}

.crrc-footer__links .crrc-footer__drop > button {
    appearance: none;
    background: none;
    border: 0;
    /* 2026-10-05 feedback: the "Solutions" label was rendering in --ink
       on the --ink footer background and so read as empty. The button
       inherited color from its li, which the block editor's flow layout
       sets to --ink; the sibling anchor links override it to --white and
       the button did not. Set the full type stack explicitly to match
       .crrc-footer__links a. */
    /* 2026-10-14 feedback: without font-family: inherit the button falls
       back to the UA system font and "Solutions" reads visibly narrower
       than the Urbane anchor labels either side of it. */
    font-family: inherit;
    font-size: var(--fs-footer-link);
    font-weight: 600;
    letter-spacing: -0.04em;
    color: var(--white);
    cursor: pointer;
    width: 100%;
    text-align: left;
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 1vw;
    padding: 0 0.139vw 0.45vw 0;
    line-height: 2;
    position: relative;

    @media (max-width: 1024px) {
        padding-bottom: 12px;
        font-size: 13px;
    }
}

.crrc-footer__links .crrc-footer__drop > button::before {
    content: "";
    position: absolute;
    inset: auto 0 0;
    border-top: 2px dotted #bebebe;
    pointer-events: none;
}

.crrc-footer__links .crrc-footer__drop > button:hover span,
.crrc-footer__links .crrc-footer__drop > button:focus-visible span,
.crrc-footer__drop:hover > button span,
.crrc-footer__drop:focus-within > button span {
    background: var(--red);
    border-color: var(--red);
    transform: translate(0.1vw, -0.1vw);
}

/* 2026-10-14 feedback: push-down menu (Maserati footer pattern) -- sits
   below its button in flow so opening it pushes the next quicklink down
   rather than overlaying it. Was position:absolute until 2026-10-14 and
   floated over the Localisation/Careers/Contact rows below. The row-
   stacking parent (.crrc-footer__links li + li { margin-top }) already
   gives the open menu air without a background panel. */
/* Timings (2026-10-14 round 3): max-height was burning through the actual
   content height (~120px) in the opening fast portion of the ease curve,
   so even at 1.4s only the first 15% of the easing was ever VISIBLE and
   the pop felt snappy. grid-template-rows: 0fr → 1fr animates against the
   real content height, so the full long-tail settle is seen end to end.
   The children collapse under grid tracks (min-height: 0, overflow: hidden)
   so the row size genuinely controls the visible height. */
.crrc-footer__menu {
    display: grid;
    grid-template-rows: 0fr;
    opacity: 0;
    visibility: hidden;
    transition: grid-template-rows 0.9s cubic-bezier(.22, 1, .36, 1),
                opacity 0.45s ease 0s,
                visibility 0s linear 0.9s;
}

/* The inner wrapper IS the single grid row -- without it, each anchor would
   be its own implicit row and only the first would collapse under 0fr. */
.crrc-footer__menu-inner {
    overflow: hidden;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

.crrc-footer__drop:hover > .crrc-footer__menu,
.crrc-footer__drop:focus-within > .crrc-footer__menu,
.crrc-footer__drop.is-open > .crrc-footer__menu {
    grid-template-rows: 1fr;
    opacity: 1;
    visibility: visible;
    transition: grid-template-rows 0.9s cubic-bezier(.22, 1, .36, 1),
                opacity 0.55s ease 0.2s,
                visibility 0s;
}

.crrc-footer__menu a {
    display: block;
    padding: 0.5vw 1.1vw;
    color: var(--white);
    font-size: var(--fs-footer-link);
    letter-spacing: -0.04em;
    font-weight: 600;
    white-space: nowrap;

    @media (max-width: 1024px) {
        padding: 10px 18px;
        font-size: 13px;
    }
}

/* The dropdown anchors are nested inside .crrc-footer__links, so they inherit
   the dotted underline rule from .crrc-footer__links a::before. Hide it on the
   last anchor only, so the panel's bottom reads clean against its border and
   top/bottom padding stays balanced -- rules between items are kept. */
.crrc-footer__menu a:last-child::before {
    display: none;
}

.crrc-footer__menu a:hover,
.crrc-footer__menu a:focus-visible {
    color: var(--red);
}

/* A white bar, 38pt, with a 1pt grey rule inside its top edge; copy on x 83,
   centred. */
.crrc-footer .crrc-footer__copyright {
    display: flex;
    align-items: center;
    position: relative;
    height: 2.639vw;
    border-top: 1px solid transparent;
    padding: 0.472vw 0 0 5.764vw;
    background: var(--white);
    font-size: var(--fs-p);
    line-height: var(--lh-p);
    letter-spacing: var(--ls-p);

    @media (max-width: 1024px) {
        height: auto;
        padding: 16px var(--gutter);
    }
}

.crrc-footer .crrc-footer__copyright::before {
    content: "";
    position: absolute;
    inset: -1px 0 auto;
    border-top: 1px solid var(--footer-grey);
    pointer-events: none;
}

/* =====================================================================
   INTERIOR PAGES  (Company / Solutions / Services / Localization / Contact)
   Added 2026-09-07 from pages-pdf/{About,solutions,services,Our Localization,
   Contact}.pdf. Same 1440 artboard and the same vw convention as the home
   page: design px / 14.4. Section names are prefixed per page so nothing
   here can reach the home page's unscoped .section-* rules.
   ===================================================================== */

/* --- splide: structural CSS, written out rather than loaded ---
   splide-core.css is a stylesheet, and the only thing this site's markup
   needs from it is the flex track. Writing it here keeps the sliders working
   if a CSP ever restricts style-src, and removes a render-blocking request.
   Only the JS comes from the CDN. */
.splide {
    visibility: hidden;
}

.splide.is-initialized,
.splide.is-rendered {
    visibility: visible;
}

.splide__track {
    position: relative;
    z-index: 0;
    overflow: hidden;
}

.splide__list {
    display: flex;
    margin: 0 !important;
    padding: 0 !important;
    height: 100%;
    backface-visibility: hidden;
    list-style: none;
}

.splide__slide {
    position: relative;
    flex-shrink: 0;
    box-sizing: border-box;
    margin: 0;
    list-style-type: none !important;
    backface-visibility: hidden;
}

.splide--draggable > .splide__track {
    user-select: none;
}

/* a ttb slider stacks its slides; Splide sizes the track and moves the
   list with translateY, so the list must be a plain block */
.splide--ttb > .splide__track > .splide__list {
    display: block;
}

/* Splide's live region ("2 of 2"). splide-core.css hides it and that sheet is
   not loaded, so the first move put a 31px line of text under every list --
   which is the Company card stack's arrow slabs growing and twitching on each
   click (2026-09-25). It must stay in the accessibility tree, so it is
   visually hidden rather than display: none. */
.splide__sr {
    position: absolute !important;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* The theme outlines every :focus inside .wp-site-blocks, so a mouse click
   left a white box on buttons (the Services layer tabs, 2026-09-25). The ring
   is kept for the keyboard, which is what :focus-visible is. */
:is(.crrc-home, .crrc-page, .crrc-header, .crrc-footer) :focus:not(:focus-visible) {
    outline: none;
}

/* --- page hero (all five interior pages) --- */
.page-hero {
    --par-img: 3.6vw;
    position: relative;
    height: var(--page-hero-h);
    min-height: 460px;
    overflow: hidden;
    color: var(--white);
    isolation: isolate;

    @media (max-width: 1024px) {
        height: auto;
        /* was 78vh — on a short landscape tablet (~500px tall) that ate
           most of the viewport and pushed the next section off the fold.
           60vh scaled with a 320px floor keeps a hero-sized hero without
           hogging short screens. 2026-10-28 */
        min-height: max(60vh, 320px);
    }
}

/* the figure fills the hero too, so it has a box of its own */
.page-hero > .wp-block-image {
    position: absolute;
    inset: 0;
    margin: 0;
    z-index: 0;
}

.page-hero img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: 0;
}

/* The moving photo (PAGE_JS heromotion()): a WebGL canvas over the <img>,
   placed on the img's own box by the script. It fades in on its first frame,
   so until then -- and wherever it never runs -- the still shows. */
.page-hero__depth {
    position: absolute;
    z-index: 0;
    display: block;
    opacity: 0;
    transition: opacity 0.6s ease;
    pointer-events: none;

    &.is-on {
        opacity: 1;
    }
}

/* Calibrated against the About export, the cleanest of the five: the photo
   reads at ~75% of source brightness at the top of the frame, untouched a
   third of the way down, and ~55% at the very bottom -- a scrim that keeps the
   nav legible over sky and the headline legible over whatever is at the
   bottom, without flattening the middle of the picture. */
.page-hero::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    background: linear-gradient(180deg,
        rgba(0, 0, 0, 0.28) 0%,
        rgba(0, 0, 0, 0) 30%,
        rgba(0, 0, 0, 0.1) 60%,
        rgba(0, 0, 0, 0.48) 100%);
}

.page-hero__inner {
    position: absolute;
    inset: 0;
    z-index: 2;

    @media (max-width: 1024px) {
        position: relative;
        display: flex;
        flex-direction: column;
        justify-content: flex-end;
        gap: 16px;
        padding: 40vh var(--gutter) 47px;
    }
}

/* the eyebrow is centred on its rule, not left-aligned to it: in the Solutions
   and Our Localization exports the word's centre is constant at x 58 while its
   left edge moves with the word's length.

   The three exports re-issued 2026-09-20 (About, Contact, services) moved it to
   a plain LEFT alignment at x 71, so those three override it below. The two
   stale exports keep the centred position -- the client's call, so that every
   page still matches its own export rather than guessing that the move is
   global. Revisit when Solutions and Our Localization are re-exported. */
.page-hero__eyebrow {
    position: absolute;
    left: 4.03vw;
    top: 37.71vw;
    transform: translateX(-50%);
    color: var(--white);

    @media (max-width: 1024px) {
        position: static;
        top: auto;
        left: auto;
        transform: none;
        /* the base `.eyebrow` rule (0,2,0) sets position: relative, and that
           keeps the desktop `top: 37.71vw` offset pushing the eyebrow below
           the h1. Explicitly reset to zero so DOM order wins (eyebrow above
           h1), user image 2026-10-28. */
    }
}

/* 2026-09-24: the Solutions and Our Localization re-exports moved it to x71
   too, so every interior page now takes the left alignment. */
:is(.crrc-page-about, .crrc-page-contact, .crrc-page-services,
    .crrc-page-solutions, .crrc-page-localization) .page-hero__eyebrow {
    left: 4.931vw;
    transform: none;

    @media (max-width: 1024px) {
        left: auto;
    }
}

/* the hero is a different height on each export; 51.6vw is Services' and the
   two stale ones', so only the two that differ are named */
.crrc-page-contact { --page-hero-h: 53.333vw; }
.crrc-page-about { --page-hero-h: 51.389vw; }

.page-hero h1 {
    position: absolute;
    left: 24.444vw;
    top: 36.92vw;
    margin: 0;
    width: 66vw;
    /* reads --fs-hero so the hero keeps its original big-display size after
       the 2026-10-27 section-type reduction (base h1 → 40px). An explicit
       font-size is needed here because the base `.crrc-page h1` rule now
       sets 40px, which would otherwise shrink the hero title. */
    font-size: var(--fs-hero);

    @media (max-width: 1024px) {
        position: static;
        width: auto;
        /* 66px mobile hero is wider than a 390vp gutter box for
           "INTEGRATED", "LOCALISATION", "COMMITMENT" -- cuts off against
           overflow-x: clip. Same clamp the home hero uses. */
        font-size: clamp(32px, 11.5vw, 66px);
    }
}

/* --- shared interior-page furniture --- */
.crrc-page {
    background: var(--white);
}

.crrc-page .body-sm {
    /* 2026-10-12: client spec — Urbane 600, 12/14 at 1440 as vw, letter
       spacing in px (not em). Mobile keeps the floor at 12px.
       2026-10-27: clamp pattern matches the --fs-p token in :root —
       0.833vw = 12px at 1440; ±2px bounds (10–14 font, 12–16 line) so
       type deviates no more than 2px from the design value. */
    font-family: Urbane, sans-serif;
    font-size: clamp(10px, 0.833vw, 14px);
    font-weight: 600;
    line-height: clamp(12px, 0.972vw, 16px);
    letter-spacing: -0.1px;
    color: var(--body-grey);

    @media (max-width: 1024px) {
        font-size: 12px;
        line-height: 14px;
    }
}

.crrc-page .body-sm + .body-sm {
    margin-top: 1em;
}

.crrc-page h2.h2-mid,
.crrc-page .h2-mid {
    font-size: var(--fs-h2);
    line-height: var(--lh-h2);
    letter-spacing: var(--ls-h2);
    text-transform: none;
}

.crrc-page .h4 {
    font-size: var(--fs-h4);
    font-weight: 600;
    line-height: var(--lh-h4);
    letter-spacing: var(--ls-h4);
    color: var(--ink);
}

/* a paragraph set off by a hairline down its left edge -- used for the lead
   sentence in About, Localization and the MRO statement.

   The three exports re-issued 2026-09-20 draw this as a top-LEFT BRACKET
   instead: a rule along the top, round a ~10pt corner, down the left. Measured
   on About.pdf -- overview x122-415 y970-1097, milestones x69-414 y1738-1947,
   Malaysia operations x850-1190 y2557-2664. The two stale exports still draw
   the plain left rule, so the bracket is scoped to the re-exported pages,
   matching the hero-eyebrow call above. */
.crrc-page .rule-lead {
    position: relative;
    border-left: 2px dotted transparent;
    padding-left: 1.389vw;
    font-size: var(--fs-h4);
    letter-spacing: var(--ls-h4);
    line-height: var(--lh-h4);
    color: var(--ink);

    @media (max-width: 1024px) {
        padding-left: 16px;
    }
}

/* when .rule-lead is a wrapper around paragraphs (used on the About page's
   milestones intro since 2026-10-12), its children need the h4 size too --
   `.crrc-page p` is (0,1,1) and beats inheritance from the wrapper. */
.crrc-page .rule-lead > p {
    font-size: var(--fs-h4);
    letter-spacing: var(--ls-h4);
    line-height: var(--lh-h4);
    color: var(--ink);
}

.crrc-page .rule-lead > p + p {
    margin-top: 1em;
}

/* the sitewide 2px dotted (10.29) since 2026-09-28, on request: the curved
   brackets were the last solid hairlines */
.crrc-page .rule-lead::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 -2px;
    border-left: 2px dotted #bebebe;
    pointer-events: none;
    --ln-mask: var(--ln-y);
}

:is(.crrc-page-about, .crrc-page-contact, .crrc-page-services) .rule-lead {
    border-top: 2px dotted transparent;
    border-top-left-radius: 0.694vw;
    padding-top: 1.389vw;

    @media (max-width: 1024px) {
        padding-top: 16px;
        border-top-left-radius: 11px;
    }
}

:is(.crrc-page-about, .crrc-page-contact, .crrc-page-services) .rule-lead::before {
    inset: -2px 0 0 -2px;
    box-sizing: border-box;
    border-top: 2px dotted #bebebe;
    border-radius: inherit;
    --ln-mask: var(--ln-bracket);
}

/* used by the milestone years: the century is shown as text on the current
   year only, so the full year still reaches a screen reader on every item */
.u-sr {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* the pill the design uses for ISO marks, fleet names and years. 2026-10-01
   (client review): orange-red --red, not dark --red-brand, to match the rest
   of the accent colour sitewide. */
.crrc-page .tag {
    display: inline-block;
    background: var(--red);
    color: var(--white);
    font-size: var(--fs-p);
    font-weight: 600;
    letter-spacing: var(--ls-p);
    text-transform: uppercase;
    line-height: var(--lh-p);
    /* 20pt tall in the re-exported About.pdf (the ISO pills 98x20, the
       milestone year pill 40x20); it was rendering 24 */
    padding: 0.313vw 0.7vw;
    border-radius: 0.35vw;

    @media (max-width: 1024px) {
        padding: 6px 9px;
        border-radius: 5px;
    }
}

/* --- CONTACT ------------------------------------------------------- */
/* Re-measured 2026-09-20 against the re-exported `pages-pdf/Contact.pdf`
   (1440 x 1825, was 1767). The band grew 595 -> 669pt and BOTH columns moved
   53pt left: the copy column from x122 to x69 and the photo from x712 to x659,
   which takes the photo's right margin from 21pt to 74pt. See CLAUDE.md 10.17. */
/* 2026-10-20, client: a 2x2 grid -- offices + nav stacked in the left column
   (nav pushed to the bottom), photo in the right column spanning both rows.
   No absolute positioning for layout; the nav sits in flow at the band's
   bottom-left. */
.contact-band {
    position: relative;
    background: var(--grey-bg);
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: minmax(0, 1fr) auto;
    column-gap: 2vw;
    row-gap: 1.5vw;
    padding: 8vw 4.792vw;

    @media (max-width: 1024px) {
        display: flex;
        flex-direction: column;
        gap: 23px;
        padding: 47px var(--gutter);
        /* Order on tablet (2026-10-28, on request): photo on top, then
           the office lockup+address, then the nav buttons on the LEFT. */
    }
}

/* Tablet re-order: photo first, offices (lockup+address) second, nav last.
   Base indexes are set here so each child's visual order is explicit.
   The nav's base rule uses `align-self: end; justify-self: start` for the
   desktop grid; in the tablet flex-column, align-self is the horizontal
   axis, so `end` was pushing it to the RIGHT. Override to flex-start. */
@media (max-width: 1024px) {
    .contact-band > .contact-office__photo { order: 1; }
    .contact-band > .contact-offices { order: 2; }
    .contact-band > .contact-band__nav {
        order: 3;
        align-self: flex-start;
        justify-content: flex-start;
    }
}

/* 2026-10-12 feedback: contact is a plain cross-fade. The four .contact-office
   children stack by grid overlap (same grid cell) instead of absolute+inset:0,
   so .contact-offices sizes to the tallest office naturally. */
.contact-offices {
    display: grid;
    grid-column: 1;
    grid-row: 1;
    min-width: 0;
    align-self: stretch;

    @media (max-width: 1024px) {
        display: block;
    }
}

.contact-office {
    grid-area: 1 / 1;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    gap: 2.6vw;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.6s ease, visibility 0s 0.6s;

    @media (max-width: 1024px) {
        display: none;
        gap: 23px;
    }
}

.contact-office.is-active {
    opacity: 1;
    visibility: visible;
    transition: opacity 0.6s ease, visibility 0s;

    @media (max-width: 1024px) {
        display: flex;
    }
}

.contact-office__lockup {
    /* 2026-10-28: fixed container box so every office's lockup takes the
       SAME slot. Images render at their natural aspect via object-fit:
       contain -- the four lockups the client supplied share a 623x68 canvas
       but their ink has different aspects (LANDAS is a 2-line lockup at
       full canvas height, the others are single-line taking ~40px), so a
       plain 100%/100% stretch would distort and clip LANDAS. Left-aligned
       so the CRRC mark lines up across offices. */
    width: 100%;
    height: 4.7vw;

    @media (max-width: 1024px) {
        height: 60px;
    }
}

.contact-office__lockup figure.wp-block-image {
    display: block;
    margin: 0;
    line-height: 0;
    width: 100%;
    height: 100%;
}

.contact-office__lockup img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    object-position: left center;
}

.contact-office__address {
    /* Lays out in .contact-office's flex column now; the lockup sits above
       with a 2.6vw gap. */
}

.contact-office__photo {
    /* `position: relative` is load-bearing -- img-parallax.js makes the
       inner <img> `position: absolute; width: 100%`, which would otherwise
       resolve against the band and render at full viewport width. */
    --par-img: 2.2vw;
    position: relative;
    grid-column: 2;
    grid-row: 1 / span 2;
    width: 39.667vw;
    height: 29vw;
    border-radius: 0.417vw;
    overflow: hidden;

    @media (max-width: 1024px) {
        /* was `width: 100%` which stretched the photo edge-to-edge of the
           band and read as the "slide" being too wide against the narrower
           address text above. Cap it so the pair reads as a card.
           2026-10-28 */
        width: 100%;
        max-width: 460px;
        height: 220px;
        border-radius: var(--radius);
    }
}

.contact-band__nav {
    /* Sits in the band's grid row 2, column 1 -- bottom-left of the
       offices column, in flow (2026-10-20). */
    grid-column: 1;
    grid-row: 2;
    align-self: end;
    justify-self: start;
    display: flex;
    gap: 1.319vw;
    z-index: 3;

    @media (max-width: 1024px) {
        margin-top: 31px;
        gap: 16px;
    }
}

/* 2026-10-12 feedback: rounded-square red button -- same size and stroke
   as the Solutions / Company slider arrows. */
.contact-band__nav button {
    display: grid;
    place-items: center;
    width: 2.083vw;
    height: 2.083vw;
    min-width: 26px;
    min-height: 26px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: var(--red);
    color: var(--white);
    cursor: pointer;
    /* 2026-10-14 feedback: hover no longer darkens to --red-brand. The pill
       stays orange-red and the arrow slides a few px in its facing
       direction, as a tactile cue. */
    transition: transform 0.25s cubic-bezier(.4, 0, .2, 1), opacity 0.25s ease;

    @media (max-width: 1024px) {
        width: 35px;
        height: 35px;
        border-radius: 7px;
    }
}

.contact-band__nav button svg {
    width: 42%;
    height: auto;
    fill: none;
    stroke: currentColor;
    stroke-width: 2.4;
    transition: transform 0.25s cubic-bezier(.4, 0, .2, 1);
}

.contact-band__nav button[data-contact-go=">"]:hover svg,
.contact-band__nav button[data-contact-go=">"]:focus-visible svg {
    transform: translateX(0.25vw);
}

.contact-band__nav button[data-contact-go="<"]:hover svg,
.contact-band__nav button[data-contact-go="<"]:focus-visible svg {
    transform: translateX(-0.25vw);
}

.contact-band__nav button:disabled {
    opacity: 0.3;
    cursor: default;
}

/* --- SOLUTIONS -----------------------------------------------------
   Rebuilt 2026-09-24 from the re-exported `pages-pdf/solutions.pdf` (10.20).
   The intro and the cards share one dark band (design y 743-2001). */
.sol-intro {
    position: relative;
    padding: 4.306vw 0 6.389vw;
    text-align: center;
    background: var(--ink);

    @media (max-width: 1024px) {
        padding: 55px var(--gutter) 47px;
    }
}

.crrc-page .sol-intro > p {
    width: 37.153vw;
    margin: 0 auto;
    font-size: var(--fs-h4);
    letter-spacing: var(--ls-h4);
    line-height: var(--lh-h4);
    color: var(--white);

    @media (max-width: 1024px) {
        width: auto;
    }
}

.sol-intro strong {
    color: var(--white);
}

/* the dashed S-curve that links the intro to the first card; read straight
   out of the content stream, so it is the design's own path */
.sol-intro__connector,
.sol-group__connector {
    display: block;
    width: 7.153vw;
    margin: 2.639vw 0 0 46.42vw;
    color: #bebebe;

    @media (max-width: 1024px) {
        display: none;
    }
}

.sol-intro__connector svg,
.sol-group__connector svg {
    display: block;
    width: 100%;
    height: auto;
}

/* --- solutions: the card deck ----------------------------------------
   Base (JS blocked, reduced motion, phones): a plain column of cards.
   `.is-deck` (PAGE_JS deck(), desktop + motion only): all four stacked in a
   sticky box, card 1 in front and each card behind it one 96pt (6.667vw)
   step lower, which is exactly the export's resting state -- the numbered
   tabs step down the left edge 1, 2, 3, 4. Scrolling through the section
   lifts the front card off (--out 0 -> 1) and closes the rest up one step
   (--slot k -> k-1). Both are written per frame from the scroll position;
   nothing here transitions, so the deck is on Lenis's clock. */
.sol-cards {
    --deck-card: 31.736vw;
    --deck-step: 6.667vw;
    /* the deck sits inside the centred intro band */
    text-align: left;
    display: flex;
    flex-direction: column;
    padding: 3.264vw 0 0;

    @media (max-width: 1024px) {
        padding: 39px 0 0;
    }
    /* below 1440 the copy is floored px type; the card keeps a px floor
       too, so it still holds its copy (2026-10-13) */
    @media (min-width: 1025px) and (max-width: 1439px) {
        --deck-card: max(31.736vw, 360px);
    }
}

.sol-cards__pin {
    display: flex;
    flex-direction: column;
    gap: 1.944vw;

    @media (max-width: 1024px) {
        /* horizontal swipe-slider on mobile instead of a vertical stack */
        flex-direction: row;
        gap: 16px;
        overflow-x: auto;
        overflow-y: hidden;
        scroll-snap-type: x mandatory;
        scroll-padding: var(--gutter);
        padding: 4px var(--gutter);
        margin: 0 calc(var(--gutter) * -1);
        scrollbar-width: none;
        -ms-overflow-style: none;
    }
}

@media (max-width: 1024px) {
    .sol-cards__pin::-webkit-scrollbar {
        display: none;
    }
}

.sol-cards.is-deck {
    /* the stack itself plus 260vh of pinned scroll: three lifts of ~70vh,
       each after a hold (deck()'s DWELL), then 50vh with card 4 alone */
    height: calc(var(--deck-card) + 3 * var(--deck-step) + 260vh);
    /* the export's 96pt step makes a 745pt stack, taller than a short
       laptop window; the step gives way first, and the tabs (one step tall)
       shrink with it so they still tile the left edge */
    --deck-top: calc(var(--wp-admin--admin-bar--height, 0px) + var(--crrc-header-h, 0px));
    /* the front card is centred, so the three steps behind it have to fit
       in the half of the leftover height below it */
    --deck-step: clamp(2.5vw, calc((100vh - var(--deck-top) - var(--deck-card) - 4vw) / 6), 6.667vw);
}

.sol-cards.is-deck .sol-cards__pin {
    position: sticky;
    /* the FRONT card is centred in the part of the window below the fixed
       header (10.33), and the cards behind step down below it (2026-09-28;
       it used to be the whole stack that was centred, which put the front
       card high) */
    top: calc(var(--deck-top) + max(1vw, (100vh - var(--deck-top) - var(--deck-card)) / 2));
    display: block;
    height: calc(var(--deck-card) + 3 * var(--deck-step));
}

.sol-cards.is-deck .sol-card {
    position: absolute;
    top: 0;
    left: 0;
    z-index: calc(10 - var(--k));
    /* the front card sits at the pin's top, which is centred; each card
       behind it is --slot steps lower. (--deck-p re-centred the stack as it
       emptied until 2026-09-28; with the front card centred it is not used.) */
    transform: translateY(calc(var(--slot, var(--k)) * var(--deck-step) - var(--out, 0) * 70vh));
    /* opaque while it lifts -- a fading card shows the one behind through
       its own copy -- and gone over the last 40% of the lift */
    opacity: calc(1 - clamp(0, (var(--out, 0) - 0.6) * 2.5, 1));
}

/* a card behind the front one shows only its photo strip and tab, as the
   export draws it; its copy comes up as it reaches the front */
.sol-cards.is-deck .sol-card__body {
    opacity: clamp(0, 1 - var(--slot, var(--k)) * 1.6, 1);
}

/* Below 1025 the deck's pin+absolute+clipped-height layout breaks: cards
   are taller than --deck-card (narrow viewports wrap the body heavily), so
   content overflows the card AND the next card lifts in behind it, making
   both unreadable during the transition. On tablet/mobile just undo the
   deck entirely -- cards stack vertically at auto height, scrolled through
   like any other section. The .is-deck class stays (deck() keeps setting
   it), so JS still writes --slot/--out etc, but every geometry/opacity rule
   above is overridden here. */
@media (max-width: 1024px) {
    .sol-cards.is-deck {
        height: auto;
    }
    .sol-cards.is-deck .sol-cards__pin {
        position: static;
        top: auto;
        display: flex;
        flex-direction: column;
        gap: 24px;
        height: auto;
        padding: 0;
        margin: 0;
        overflow: visible;
    }
    .sol-cards.is-deck .sol-card {
        position: relative;
        top: auto;
        left: auto;
        right: auto;
        width: 100%;
        /* flex: initial resets the swipe-slider's flex-basis — in a row that
           was the card WIDTH, but the pin is flex-column here so the basis
           was pinning each card's HEIGHT at `100vw - 2*gutter` (436px at
           490vp) and clipping the taller content. */
        flex: initial;
        /* !important beats the inline --slot/--out transforms that deck()
           continues to write; and any residual height from the base clamp */
        height: auto !important;
        min-height: 0;
        max-height: none;
        transform: none !important;
        opacity: 1 !important;
        overflow: visible;
    }
    .sol-cards.is-deck .sol-card__body {
        opacity: 1;
    }
}

/* each card's place in the deck -- a block cannot carry the inline --k */
.sol-card:nth-child(1) { --k: 0; }
.sol-card:nth-child(2) { --k: 1; }
.sol-card:nth-child(3) { --k: 2; }
.sol-card:nth-child(4) { --k: 3; }

.sol-card {
    position: relative;
    margin-left: 19.965vw;
    width: 60.069vw;
    /* was `min-height: var(--deck-card)`. Clamp instead so the card has
       both a px floor (360) and a vh ceiling (92), while scaling at
       31.736vw between those bounds. */
    height: clamp(360px, 31.736vw, 92vh);
    display: grid;
    /* `minmax(0, 1fr)` so the text column can shrink below its min-content
       instead of pushing the photo column (28.889vw) past the card's own
       edge. At narrow widths "Comprehensive" is wider than the fair 1fr
       share and a bare `1fr` ends up `minmax(min-content, 1fr)`, which
       swelled column 1 and spilled column 2 (image #27, 2026-10-27). */
    grid-template-columns: minmax(0, 1fr) 28.889vw;
    gap: 1.5vw;
    background: var(--white);
    color: var(--ink);
    border: 1px solid transparent;
    border-radius: 0 var(--radius-sm) var(--radius-sm) var(--radius-sm);
    box-shadow: 0 0.7vw 2vw rgba(0, 0, 0, 0.18);
    padding: 0.694vw;

    @media (max-width: 1024px) {
        /* a swipe-slider on mobile (2026-10-21, on request): each card is a
           full-width scroll-snap target inside .sol-cards__pin's horizontal
           overflow. margin is 0 -- the pin's own padding handles the gutter.
           Height auto so the card grows to fit its content on tablet/mobile
           (the clamp floor of 360px clipped the body text and image on
           narrow viewports, 2026-10-28). */
        flex: 0 0 calc(100vw - 2 * var(--gutter));
        margin: 0;
        width: calc(100vw - 2 * var(--gutter));
        height: auto;
        min-height: 0;
        grid-template-columns: minmax(0, 1fr);
        gap: 23px;
        border-radius: var(--radius);
        padding: 31px 23px;
        overflow: hidden;
        scroll-snap-align: center;
        scroll-snap-stop: always;
    }
}

.sol-card::before {
    content: "";
    position: absolute;
    inset: -1px;
    box-sizing: border-box;
    border: 1px solid var(--hairline);
    border-radius: inherit;
    pointer-events: none;
    --ln-mask: var(--ln-x);
}

/* the card's top-left corner steps out into a narrow tab that holds the
   number. The tab paints white over the card's own left border for its
   height, so the two outlines read as one stepped edge. It is one deck step
   tall, which is why the tabs of a stacked deck tile the left edge. */
.sol-card__tab {
    position: absolute;
    left: -2.049vw;
    top: -1px;
    width: 2.083vw;
    height: var(--deck-step);
    background: var(--white);
    border: 1px solid transparent;
    border-right: 0;
    border-radius: var(--radius-sm) 0 0 var(--radius-sm);

    @media (max-width: 1024px) {
        display: none;
    }
}

.sol-card__tab::before {
    content: "";
    position: absolute;
    inset: -1px 0 -1px -1px;
    box-sizing: border-box;
    border: 1px solid var(--hairline);
    border-right: 0;
    border-radius: inherit;
    pointer-events: none;
}

/* a button since 2026-09-24: in the deck the tabs jump to their card */
.sol-card__num {
    position: absolute;
    left: -1.667vw;
    top: 0.556vw;
    display: grid;
    place-items: center;
    width: 1.528vw;
    height: 1.528vw;
    padding: 0;
    background: var(--white);
    border: 1px solid transparent;
    border-radius: 50%;
    font: inherit;
    font-size: var(--fs-p);
    line-height: var(--lh-p);
    letter-spacing: var(--ls-p);
    font-weight: 600;
    color: var(--ink);
    cursor: pointer;

    @media (max-width: 1024px) {
        display: none;
    }
}

.sol-card__num:hover,
.sol-card__num:focus-visible {
    background: var(--ink);
    color: var(--white);
}

.sol-card__num::before {
    content: "";
    position: absolute;
    inset: -1px;
    box-sizing: border-box;
    border: 1px solid #d5d5d5;
    border-radius: inherit;
    pointer-events: none;
    --ln-mask: var(--ln-sweep);
}

/* no max-width on the column: "Manufacturing" is 298pt in the export's face
   and ~337 in Urbane (12.17), wider than the old 24vw cap less its padding */
.sol-card__body {
    padding: 1.25vw 0 0 2.118vw;

    @media (max-width: 1024px) {
        padding: 0;
        max-width: none;
    }
}

.sol-card__body .eyebrow {
    margin-bottom: 1.1vw;

    @media (max-width: 1024px) {
        margin-bottom: 20px;
    }
}

.sol-card__body .body-sm {
    width: 20.5vw;

    @media (max-width: 1024px) {
        width: auto;
    }
}

.crrc-page .sol-card__body h2 {
    font-size: var(--fs-h2);
    line-height: var(--lh-h2);
    letter-spacing: var(--ls-h2);
    text-transform: none;
    color: var(--ink);
    margin-bottom: 1.2vw;

    @media (max-width: 1024px) {
        margin-bottom: 20px;
    }
}

.sol-card figure {
    --par-img: 2vw;
    position: relative;
    border-radius: 0.42vw;
    overflow: hidden;

    @media (max-width: 1024px) {
        /* 2026-10-28: no fixed height. 242px clipped the image on narrow
           viewports (user image), leaving the top of the train/platform
           cropped. The figure keeps a 768/501 aspect box, overflow is
           hidden so the border-radius crops the image cleanly, and the
           img inside fills it via object-fit: cover (see rule below).
           --par-img: 0 keeps img-parallax from oversizing the picture. */
        --par-img: 0;
        border-radius: var(--radius);
        height: auto;
        aspect-ratio: 768 / 501;
        overflow: hidden;
        /* The theme's block flow layout gives this figure the card's full
           width ignoring the padding on narrow phones, so the figure
           overflows the right edge by ~22px. Pin it to the content box. */
        width: 100%;
        max-width: 100%;
    }
}

.sol-card figure img {
    @media (max-width: 1024px) {
        /* img-parallax.js writes inline position:absolute + inset for the
           parallax; `!important` overrides those inline styles so the img
           sits in flow and fills the figure cleanly. */
        position: static !important;
        inset: auto !important;
        width: 100% !important;
        height: 100% !important;
        object-fit: cover;
        display: block;
    }
}

/* the workforce photo's two people sit right of centre in the 1440x804
   source; the default 50% crop hides the right one. Shift to show the
   handshake centred. */
.sol-card img[src*="sol-workforce"] {
    object-position: 68% 50%;
}

/* --- solutions: "discover our trains" ---------------------------------
   Three categories, each under a giant grey marquee word, each holding one
   or more slider groups: a numbered model list, the photo, an info panel and
   two arrows. A floating category card rides down the left the whole way. */
.sol-trains {
    padding: 2.778vw 0 7.5vw;
    overflow-x: clip;

    @media (max-width: 1024px) {
        padding: 55px 0;
    }
}

.crrc-page .sol-trains h2 {
    text-align: center;
    font-size: var(--fs-h1);
    letter-spacing: var(--ls-h1);
    line-height: var(--lh-h1);
    text-transform: uppercase;

    @media (max-width: 1024px) {
    }
}

.sol-trains__cats {
    position: relative;
}

/* the card rides the whole section without taking up any room in it: an
   absolutely positioned rail the height of the section, with the card sticky
   inside it. (It was a zero-height sticky element, which inspected as a
   0px line, 2026-09-27.) The rail lets clicks through; the card takes them. */
.sol-catnav {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 14.375vw;
    z-index: 5;
    pointer-events: none;

    @media (max-width: 1024px) {
        display: none;
    }
}

.sol-catnav nav {
    position: sticky;
    top: 6vw;
    margin-left: 1.875vw;
    width: 12.5vw;
    pointer-events: auto;
    display: flex;
    flex-direction: column;
    gap: 0.49vw;
    padding: 0.83vw 0 0.83vw 2.15vw;
    background: var(--white);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-sm);
    box-shadow: 0 0.3vw 1.2vw rgba(27, 36, 41, 0.06);
}

/* the dotted rail every row hangs off, as on the home page's tab list */
.sol-catnav nav::before {
    content: "";
    position: absolute;
    left: 1.11vw;
    top: 0.9vw;
    bottom: 0.9vw;
    border-left: 2px dotted #bebebe;
}

.sol-catnav a {
    position: relative;
    font-size: var(--fs-p);
    font-weight: 600;
    line-height: var(--lh-p);
    letter-spacing: var(--ls-p);
    text-transform: uppercase;
    color: #bebebe;
    transition: color 0.3s ease;
}

.sol-catnav a[aria-current="true"],
.sol-catnav a:hover {
    color: var(--ink);
}

.sol-catnav a::before {
    content: "";
    position: absolute;
    left: -1.1vw;
    top: 50%;
    width: 0.208vw;
    height: 1.25vw;
    background: var(--red);
    border-radius: 1px;
    transform: translateY(-50%) scaleY(0);
    transition: transform 0.3s ease;
}

.sol-catnav a[aria-current="true"]::before {
    transform: translateY(-50%) scaleY(1);
}

.sol-cat {
    scroll-margin-top: 4vw;
}

/* parent-qualified: the theme's flow layout resets a group's first/last
   child margin at (0,2,0), which a lone class loses to (10.30) */
.sol-cat > .sol-cat__marquee {
    margin: 4.5vw 0 3.2vw;
    overflow: hidden;
    user-select: none;

    @media (max-width: 1024px) {
        margin: 39px 0 31px;
    }
}

.sol-cat + .sol-cat .sol-cat__marquee {
    margin-top: 9vw;

    @media (max-width: 1024px) {
        margin-top: 62px;
    }
}


.sol-cat__track {
    display: flex;
    align-items: center;
    width: max-content;
    font-size: 14.295vw;  /* 205.846 / 193, -12.471; pure vw, it is a graphic (2026-10-13) */
    font-weight: 600;
    line-height: 0.929;
    letter-spacing: -0.06em;
    text-transform: uppercase;
    color: #e5e5e5;
    white-space: nowrap;
    animation: sol-marquee 48s linear infinite;

    @media (max-width: 1024px) {
        font-size: 206px;
    }
}

.sol-cat__marquee.is-rev .sol-cat__track {
    animation-direction: reverse;
}

.sol-cat__track i {
    display: block;
    width: 6.458vw;
    margin: 0 3vw;
    border-top: 2px dotted #bebebe;
}

@keyframes sol-marquee {
    to { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
    .sol-cat__track { animation: none; transform: translateX(-12%); }
}

.sol-group {
    padding-bottom: 1vw;
}

.sol-group__head {
    text-align: center;
    margin-bottom: 2.9vw;
}

.crrc-page .sol-group__cat {
    font-size: var(--fs-p);
    letter-spacing: var(--ls-p);
    font-weight: 600;
    line-height: var(--lh-p);
    text-transform: uppercase;
    color: #bebebe;
}

.crrc-page .sol-group__name {
    margin-top: 0.4vw;
    font-size: var(--fs-h1);
    letter-spacing: var(--ls-h1);
    line-height: var(--lh-h1);
    text-transform: uppercase;

    @media (max-width: 1024px) {
    }
}

.sol-group__body {
    display: grid;
    /* 2026-10-12 feedback: list column restored (dropped 2026-10-10 #18) */
    grid-template-columns: 11.875vw 44.583vw 17.431vw;
    padding-left: 15.833vw;
    align-items: start;

    @media (max-width: 1024px) {
        grid-template-columns: minmax(0, 1fr);
        row-gap: 20px;
        padding: 0 var(--gutter);
    }
}

/* parent-qualified: the theme's flow layout resets a group's first/last
   child margin at (0,2,0), which a lone class loses to (10.30) */
.sol-group__body > .sol-group__list {
    list-style: none;
    padding: 0;
    margin: 0.25vw 0 0 -1.25vw;
    display: flex;
    flex-direction: column;
    gap: 0.4vw;

    @media (max-width: 1024px) {
        order: 2;
        flex-direction: row;
        flex-wrap: wrap;
        gap: 12px 20px;
        margin: 0;
    }
    /* 2026-10-27: at narrow desktop the row gap shrunk below the floored
       type and the list read as a cramped stack. Floor it. */
    @media (min-width: 1025px) and (max-width: 1439px) {
        gap: max(0.4vw, 10px);
    }
}

.sol-group__list button {
    display: flex;
    align-items: center;
    gap: 0.5vw;
    padding: 0;
    background: none;
    border: 0;
    font: inherit;
    font-size: var(--fs-p);
    line-height: var(--lh-p);
    letter-spacing: var(--ls-p);
    font-weight: 600;
    text-transform: uppercase;
    color: var(--ink);
    cursor: pointer;

    @media (max-width: 1024px) {
        gap: 8px;
    }
}

.sol-group__list button span {
    display: grid;
    place-items: center;
    width: 1.528vw;
    height: 1.528vw;
    border: 1px solid #d5d5d5;
    border-radius: 50%;
    font-size: 0.9em;
    transition: background 0.3s ease, color 0.3s ease, border-color 0.3s ease;

    @media (max-width: 1024px) {
        width: 23px;
        height: 23px;
    }
}

.sol-group__list button[aria-current="true"] span,
.sol-group__list button:hover span {
    background: var(--ink);
    border-color: var(--ink);
    color: var(--white);
}

.sol-group__stage {
    min-width: 0;
    /* pin to col 2 so a group without a list (DMU, Locomotives) still lands
       the slider in the slider column rather than auto-placing into the empty
       list column (2026-10-12) */
    grid-column: 2;

    @media (max-width: 1024px) {
        grid-column: auto;
    }
}

.sol-group__stage figure {
    --par-img: 1.6vw;
    position: relative;
    height: 27.222vw;
    border-radius: 0.42vw;
    overflow: hidden;

    @media (max-width: 1024px) {
        height: 234px;
        border-radius: var(--radius);
    }
}

.sol-group__info {
    margin-left: 1.806vw;
    border-top: 2px dotted #bebebe;
    padding-top: 0.6vw;
    grid-column: 3;

    @media (max-width: 1024px) {
        order: 3;
        margin-left: 0;
        padding-top: 16px;
        grid-column: auto;
    }
}

.crrc-page .sol-group__panel h3 {
    font-size: var(--fs-h2);
    /* 2026-10-19: text-reveal wraps each line in `overflow: clip` with a 0.25em
       clip-margin tuned for a 0.9 line-height; at `--lh-h2` (1.0) and lowercase
       copy like "6-axle" Urbane's ascenders exceed that margin and get clipped
       at the top. Nudge the line-height so the glyphs sit inside the clip. */
    line-height: 1.15;
    letter-spacing: var(--ls-h2);
    text-transform: none;
    color: var(--ink);

    @media (max-width: 1024px) {
    }
}

.sol-group__panel .body-sm {
    margin: 0.7vw 0 0.9vw;

    @media (max-width: 1024px) {
        margin: 12px 0 16px;
    }
    /* 2026-10-27: narrow desktop — floored body text was running into the
       heading above and the EXPLORE button below. Floor the vertical gap. */
    @media (min-width: 1025px) and (max-width: 1439px) {
        margin: max(0.7vw, 10px) 0 max(0.9vw, 14px);
    }
}

/* parent-qualified: the theme's flow layout resets a group's first/last
   child margin at (0,2,0), which a lone class loses to (10.30) */
.sol-group > .sol-group__connector {
    margin: 2.2vw 0 2.2vw 46.42vw;
}

/* 2026-10-05 feedback (second iteration): keep the numbered list and the
   arrow nav visible under .is-scrub -- the user still wants them, so the
   base layout is left untouched here. The scroll-trigger just adds a
   second way to advance the slider on top of the existing controls, via
   the ScrollTrigger set up in sliders() pinScroll. */

/* --- SERVICES ------------------------------------------------------ */
/* Re-measured 2026-09-20 against the re-exported `pages-pdf/services.pdf`
   (1440 x 4806, was 5408). Two real changes here:
     * the intro statement is 20pt copy at x324, not the 44pt bracketed
       statement the previous export drew -- and NEITHER Services statement is
       bracketed any more (checked against the render: no top rule, no left
       leg on either);
     * a full-bleed band photo now sits between the intro and Fleet MRO
       (design y 1046-1553), which is media 182. */
.svc-intro {
    position: relative;
    display: grid;
    grid-template-columns: 24.097vw 22.639vw;
    column-gap: 4.236vw;
    padding: 5.417vw 0 4.722vw 22.5vw;

    @media (max-width: 1024px) {
        grid-template-columns: 1fr;
        column-gap: 0;
        gap: 31px;
        padding: 47px var(--gutter);
    }
}

.crrc-page .svc-intro__statement {
    font-size: var(--fs-h4);
    letter-spacing: var(--ls-h4);
    font-weight: 600;
    line-height: var(--lh-h4);
    color: var(--ink);
}

.svc-intro__notes {
    @media (max-width: 1024px) {
        justify-self: start;
    }
}

/* the full-bleed band, design y 1046-1553 */
.svc-band {
    --par-img: 2.6vw;
    position: relative;
    height: 35.208vw;
    overflow: hidden;

    @media (max-width: 1024px) {
        height: 234px;
    }
}

.svc-band img {
    object-position: center 55%;
}

/* --- services: fleet MRO ---------------------------------------------
   Rebuilt 2026-09-20. The export replaces the single centred capability card
   with a stack of four dark cards, 485 x 180 and 12pt apart, in a column at
   x732 that the section clips -- it draws the stack mid-scroll, with card 1
   cut at the top and card 4 at the bottom. Built as a scroll rail starting at
   the top of the list instead: the partly visible fourth card is the same
   affordance without opening on a half-cropped first card. */
.svc-mro {
    position: relative;
    height: 43.264vw;
    background: var(--grey-bg);
    overflow: hidden;

    @media (max-width: 1024px) {
        height: auto;
        padding: 47px var(--gutter);
    }
}

.svc-mro::before {
    content: "";
    position: absolute;
    inset: 0 0 auto;
    border-top: 1px solid #bebebe;
    pointer-events: none;
    --ln-mask: var(--ln-x);
}

.svc-mro__copy {
    padding: 4.444vw 0 0 15.417vw;

    @media (max-width: 1024px) {
        padding: 0;
    }
}

.svc-mro h2 {
    margin: 0 0 8.194vw;

    @media (max-width: 1024px) {
        margin: 0 0 23px;
    }
}

.svc-mro__copy .body-sm {
    width: 20.139vw;

    @media (max-width: 1024px) {
        width: auto;
    }
}

.crrc-page .svc-mro__copy .svc-mro__lead {
    width: 17.569vw;
    margin-bottom: 1.944vw;
    color: var(--ink);

    @media (max-width: 1024px) {
        width: auto;
    }
}

/* A vertical REEL since 2026-09-25, on request: the list is written twice
   and PAGE_JS reels() moves the track one copy's length at a constant speed,
   so the loop point is invisible. Hovering stops it and the card under the
   pointer grows to 1.1; it can be dragged either way. The rail no longer
   clips; the section's own overflow does, so a grown card can spill sideways
   over the gutter. */
.svc-mro__rail {
    position: absolute;
    left: 50.833vw;
    top: 0;
    width: 33.681vw;
    height: 100%;

    @media (max-width: 1024px) {
        position: static;
        width: auto;
        height: auto;
        padding: 39px 0 0;
    }
}

/* shared by both reels: dragging must not select the text or pick up an
   image, and the cursor says it can be dragged */
[data-reel] {
    cursor: grab;
    user-select: none;
    -webkit-user-select: none;
    touch-action: pan-y;
}

[data-reel].is-dragging {
    cursor: grabbing;
}

[data-reel] img,
[data-reel] svg {
    -webkit-user-drag: none;
    pointer-events: none;
}

.svc-mro__rail {
    @media (max-width: 1024px) {
        cursor: auto;
    }
}

.svc-mro__reel {
    will-change: transform;
}

.svc-mro__reel ol {
    list-style: none;
    margin: 0;
    padding: 0 0 0.833vw;
    display: flex;
    flex-direction: column;
    gap: 0.833vw;

    @media (max-width: 1024px) {
        padding: 0;
        gap: 16px;
    }
}

.svc-mro__reel ol[aria-hidden] {
    @media (max-width: 1024px) {
        display: none;
    }
}

.svc-cap {
    position: relative;
    flex: none;
    height: 12.5vw;
    background: var(--ink);
    color: var(--white);
    border-radius: 0.694vw;
    transition: transform 0.45s cubic-bezier(0.2, 0.7, 0.2, 1);

    @media (max-width: 1024px) {
        /* full-width stack on mobile; the 242px fixed card left a strip of
           empty page beside the column on 390vp */
        width: 100%;
        height: auto;
        min-height: 156px;
        border-radius: var(--radius);
    }
}

/* the numbered ring, design 22x22 at 12,11 inside the card */
.svc-cap:hover {
    z-index: 2;
    transform: scale(1.1);

    @media (max-width: 1024px) {
        transform: none;
    }
}

.svc-cap__num {
    position: absolute;
    left: 0.833vw;
    top: 0.764vw;
    display: grid;
    place-items: center;
    width: 1.528vw;
    height: 1.528vw;
    min-width: 20px;
    min-height: 20px;
    border-radius: 50%;
    background: var(--white);
    border: 1px solid #d5d5d5;
    color: var(--ink);
    font-size: var(--fs-p);
    letter-spacing: var(--ls-p);
    line-height: var(--lh-p);

    @media (max-width: 1024px) {
        left: 20px;
        top: 20px;
        width: 27px;
        height: 27px;
    }
}

/* dashed divider at x342 of the 485 card, 148 tall and centred */
.svc-cap::after {
    content: "";
    position: absolute;
    left: 23.75vw;
    top: 50%;
    width: 1px;
    height: 10.278vw;
    transform: translateY(-50%);
    border-left: 2px dotted #bebebe;

    @media (max-width: 1024px) {
        display: none;
    }
}

.svc-cap h3 {
    position: absolute;
    left: 0.903vw;
    bottom: 1.181vw;
    width: 20.5vw;
    font-size: var(--fs-h4);
    letter-spacing: var(--ls-h4);
    line-height: var(--lh-h4);
    color: var(--white);

    @media (max-width: 1024px) {
        position: static;
        width: auto;
        padding: 55px 23px 23px;
    }
}

.svc-cap__icon {
    position: absolute;
    left: 23.75vw;
    right: 0;
    top: 0;
    bottom: 0;
    display: grid;
    place-items: center;
    color: var(--white);

    @media (max-width: 1024px) {
        left: auto;
        right: 23px;
        top: 20px;
        bottom: auto;
    }
}

.svc-cap__icon svg {
    display: block;
    width: 2.153vw;
    height: auto;

    @media (max-width: 1024px) {
        width: 35px;
    }
}

/* --- services: maintenance-solutions tab panel --- */
.svc-layers {
    background: var(--ink);
    color: var(--white);
    padding: 4.028vw 0 4.514vw;

    @media (max-width: 1024px) {
        padding: 47px var(--gutter);
    }
}

.svc-layers__inner {
    position: relative;
    display: grid;
    grid-template-columns: 24.722vw minmax(0, 1fr);
    /* the copy ran straight into the tabs and the panel (2026-09-25) */
    column-gap: 3.472vw;
    padding: 0 4.861vw 0 4.792vw;

    @media (max-width: 1024px) {
        /* minmax(0, 1fr): the tab panel inside __main has min-content that
           blows a bare 1fr column past viewport on narrow phones.
           padding is zeroed on both sides so the panel centres inside the
           section's own gutter (otherwise desktop's 4.861vw padding-right
           leaked through and pushed the panel left — user image, 2026-10-28) */
        grid-template-columns: minmax(0, 1fr);
        gap: 31px;
        padding: 0;
    }
}

.svc-layers__side .eyebrow {
    color: var(--white);
    margin-bottom: 2vw;

    @media (max-width: 1024px) {
        margin-bottom: 23px;
    }
}

.svc-layers__side .body-sm {
    color: #8d979d;
}

/* parent-qualified: the theme's flow layout resets a group's first/last
   child margin at (0,2,0), which a lone class loses to (10.30) */
.svc-layers__main > .svc-layers__tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 1.528vw;
    margin: 0.972vw 0 0.764vw;

    @media (max-width: 1024px) {
        gap: 8px;
        margin-bottom: 20px;
    }
}

.svc-layers__tabs button {
    background: #707b82;
    color: var(--white);
    border: 0;
    border-radius: 0.417vw;
    padding: 0.833vw 1.181vw;
    font-family: inherit;
    font-size: var(--fs-p);
    font-weight: 600;
    letter-spacing: var(--ls-p);
    text-transform: uppercase;
    line-height: var(--lh-p);
    cursor: pointer;
    transition: background 0.25s ease;

    @media (max-width: 1024px) {
        padding: 10px 13px;
        border-radius: 5px;
    }
}

.svc-layers__tabs button:hover {
    background: #86929a;
}

.svc-layers__tabs button[aria-selected="true"] {
    background: var(--red);
}

/* before PAGE_JS takes over, only the first panel shows */
body:not(.editor-styles-wrapper) .svc-layers__main:not(.is-tabs) > .svc-layers__panel ~ .svc-layers__panel,
body:not(.editor-styles-wrapper) .sol-group:not(.is-mounted) .sol-group__panel ~ .sol-group__panel {
    display: none;
}

.svc-layers__panel {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 24.028vw;
    gap: 1.042vw;
    background: var(--white);
    color: var(--ink);
    border-radius: var(--radius);
    padding: 3.611vw 2.778vw 3.611vw 3.611vw;
    min-height: 42.708vw;

    @media (max-width: 1024px) {
        /* minmax(0, 1fr): the notes column has min-content ~275px (from a
           long tabular/unbreakable word in the body copy) and would blow a
           bare 1fr past the panel's own box. Per CLAUDE.md 10.8f. */
        grid-template-columns: minmax(0, 1fr);
        gap: 23px;
        min-height: 0;
        padding: 31px 27px;
    }
}

.svc-layers__panel[hidden] {
    display: none;
}

.svc-layers__panel h3 {
    align-self: start;
    font-size: var(--fs-h2);
    line-height: var(--lh-h2);
    letter-spacing: var(--ls-h2);
    position: relative;
    border-left: 1px solid transparent;
    padding-left: 1.111vw;

    @media (max-width: 1024px) {
        padding-left: 16px;
        /* The whole first sentence of each layer is this heading. At 36px
           in a ~310px-wide panel each word gets its own line. Scale down
           so the paragraph reads as copy. */
        font-size: clamp(18px, 5.1vw, 28px);
        line-height: 1.15;
        letter-spacing: -0.4px;
    }
}

.svc-layers__panel h3::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 -1px;
    border-left: 1px solid var(--line);
    pointer-events: none;
    --ln-mask: var(--ln-y);
}

.svc-layers__panel .svc-layers__notes {
    align-self: end;

    @media (max-width: 1024px) {
        align-self: start;
    }
}

/* --- services: project reference --- */
.svc-projects {
    /* 2026-10-05 feedback: equalise top/bottom padding around the heading */
    padding: 4.5vw 0;

    @media (max-width: 1024px) {
        padding: 47px var(--gutter);
    }
}

.svc-projects h2 {
    text-align: center;
    margin-bottom: 4.5vw;

    @media (max-width: 1024px) {
        margin-bottom: 31px;
    }
}

.svc-projects__grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    margin: 0 7.76vw;

    @media (max-width: 1024px) {
        /* horizontal swipe-slider on mobile (2026-10-21, on request), same
           pattern as the Solutions deck. Grid is replaced by a flex row with
           scroll-snap and the gutter padding is pulled in. */
        display: flex;
        grid-template-columns: none;
        flex-direction: row;
        gap: 16px;
        margin: 0 calc(var(--gutter) * -1);
        padding: 4px var(--gutter);
        overflow-x: auto;
        overflow-y: hidden;
        scroll-snap-type: x mandatory;
        scroll-padding: var(--gutter);
        scrollbar-width: none;
        -ms-overflow-style: none;
    }
}

@media (max-width: 1024px) {
    .svc-projects__grid::-webkit-scrollbar {
        display: none;
    }
}

.svc-project {
    position: relative;
    padding: 0 3.19vw 0 3.906vw;

    @media (max-width: 1024px) {
        padding: 0;
        /* each project is a full-width snap target in the horizontal rail */
        flex: 0 0 calc(100vw - 2 * var(--gutter));
        width: calc(100vw - 2 * var(--gutter));
        scroll-snap-align: center;
        scroll-snap-stop: always;
    }
}

/* Since 2026-09-25 the section is core groups, not one wp:html, so each card
   can carry a core Button whose url is BOUND to an ACF link field
   (project_link_1..3 on page 95, "Services — Project Reference links").
   The theme's flow layout puts 1.2rem above every child of a group; these
   are (0,1,0) like the theme's rule and load after it, and the per-element
   rules below (0,1,1) still set their own margins. */
.svc-projects > *,
.svc-projects__grid > *,
.svc-project > * {
    margin-block-start: 0;
}

/* The button is the whole card: its link is stretched over the article,
   and its label stays for screen readers only. */
.svc-project > .wp-block-buttons {
    position: static;
}

.svc-project > .wp-block-buttons {
    position: absolute;
    inset: 0;
    z-index: 2;
    display: block;
    margin: 0;
}

.svc-project .wp-block-button {
    width: 100%;
    height: 100%;
}

.svc-project .wp-block-button__link {
    display: block;
    width: 100%;
    height: 100%;
    padding: 0;
    border-radius: 0;
    background: none;
    color: transparent;
    font-size: 0;
}

/* An EMPTY field does not leave the saved fallback href in place: SCF's
   binding writes href="" over it, which would link the card to the page it
   is on. Until the field is filled the card is simply not a link. */
.svc-project > .wp-block-buttons:has(a:not([href]), a[href=""]) {
    display: none;
}

.svc-project h3,
.svc-project figure {
    transition: color 0.3s ease, filter 0.3s ease;
}

.svc-project:has(.wp-block-button__link[href]:not([href=""])):hover h3 {
    color: var(--red);
}

.svc-project:has(.wp-block-button__link[href]:not([href=""])):hover figure {
    filter: brightness(1.08);
}

.svc-project + .svc-project {
    position: relative;
    border-left: 2px solid transparent;

    @media (max-width: 1024px) {
        /* siblings now sit side-by-side in a swipe-slider, so the dotted
           divider is gone and the padding that cleared it with it. */
        border-left: 0;
        padding-top: 0;
    }
}

.svc-project + .svc-project::before {
    /* 2px dotted #BEBEBE, like the Company certs grid (2026-09-27) */
    content: "";
    position: absolute;
    inset: 0 auto 0 -2px;
    border-left: 2px dotted #bebebe;
    pointer-events: none;
    --ln-mask: var(--ln-y);

    @media (max-width: 1024px) {
        display: none;
    }
}

.svc-project h3 {
    margin: 0.833vw 0 1.25vw;
    font-size: var(--fs-h4);
    letter-spacing: var(--ls-h4);
    line-height: var(--lh-h4);

    @media (max-width: 1024px) {
        margin: 16px 0 16px;
    }
}

.svc-project figure {
    --par-img: 1.4vw;
    position: relative;
    margin-top: 3.333vw;
    height: 14.028vw;
    border-radius: 0.7vw;
    overflow: hidden;

    @media (max-width: 1024px) {
        margin-top: 23px;
        height: 203px;
        border-radius: var(--radius);
    }
}

/* --- services: parts supply ------------------------------------------
   Re-measured 2026-09-20. The capability list became a horizontal rail of
   four white 180 x 180 cards (190pt pitch), drawn mid-scroll with the first
   one half off the left edge -- the same treatment the Fleet MRO stack got.
   Built starting at the left of the list rather than mid-scroll. */
.svc-parts {
    position: relative;
    background: var(--grey-bg);
    display: grid;
    grid-template-columns: 44.722vw 45.347vw;
    /* 2026-10-05 feedback: top spacing matched to bottom (was 0 up to) */
    padding: 5vw 5.069vw 5.833vw 4.861vw;

    @media (max-width: 1024px) {
        /* minmax(0, 1fr) rather than bare 1fr: the Key Parts capability reel
           inside __caps has min-content ~917px, which a bare 1fr honours and
           blows the whole column out past viewport. Per CLAUDE.md 10.8f. */
        grid-template-columns: minmax(0, 1fr);
        gap: 31px;
        padding: 47px var(--gutter);
    }
}

.svc-parts__copy {
    padding-top: 0;
}

.svc-parts__copy .body-sm {
    width: 21.111vw;

    @media (max-width: 1024px) {
        width: auto;
    }
}

.svc-parts h2 {
    width: 38vw;
    margin-bottom: 4.028vw;

    @media (max-width: 1024px) {
        width: auto;
        margin-bottom: 23px;
    }
}

.svc-parts figure {
    --par-img: 3.2vw;
    position: relative;
    /* 2026-10-12 feedback: fill height to the title on the left, stretch up */
    margin-top: 0;
    height: 100%;
    border-radius: 0.694vw;
    overflow: hidden;

    @media (max-width: 1024px) {
        height: 304px;
        border-radius: var(--radius);
    }
}

.svc-parts__caps {
    margin-top: 4.931vw;

    @media (max-width: 1024px) {
        margin-top: 39px;
    }
}

.svc-parts__caps h3 {
    font-size: var(--fs-h4);
    letter-spacing: var(--ls-h4);
    line-height: var(--lh-h4);
    margin-bottom: 3.819vw;

    @media (max-width: 1024px) {
        margin-bottom: 23px;
    }
}

/* A horizontal reel since 2026-09-25, on request: constant speed (PAGE_JS
   reels(), the same component as the Fleet MRO cards), draggable, and faded
   out at both ends so the cards dissolve into the grey rather than being cut
   by the column edge. */
.svc-parts__rail {
    overflow: hidden;
    padding: 0.35vw 0 0;
    mask-image: linear-gradient(to right, transparent, #000 18%, #000 82%, transparent);

    @media (max-width: 1024px) {
        /* no horizontal reel on mobile; the fade mask and horizontal overflow
           are not needed when the cards stack as a column */
        overflow: visible;
        padding: 0;
        mask-image: none;
    }
}

.svc-parts__track {
    display: flex;
    width: max-content;
    will-change: transform;

    @media (max-width: 1024px) {
        /* stack the two ol copies as a column; the reels() JS stands down
           once the duplicate copy is display:none below */
        flex-direction: column;
        width: auto;
        gap: 16px;
        transform: none !important;
    }
}

.svc-parts__track ol {
    display: flex;
    gap: 0.694vw;
    margin: 0;
    padding: 0 0.694vw 0 0;
    list-style: none;

    @media (max-width: 1024px) {
        /* 2-column grid on tablet, 1-column on narrow mobile (2026-10-28,
           on request). Desktop keeps the horizontal reel. */
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 16px;
        padding: 0;
    }

    @media (max-width: 600px) {
        grid-template-columns: 1fr;
    }
}

/* The cards' desktop width is fixed vw; let them fill the 2-col grid cell
   on tablet instead. */
.svc-parts__caps li {
    @media (max-width: 1024px) {
        width: auto;
    }
}

/* mobile: hide the aria-hidden duplicate copy of the capability list
   (desktop uses it to make the reel loop). The reels() component checks
   display:none on the second copy and stands its ticker down. */
.svc-parts__track ol[aria-hidden] {
    @media (max-width: 1024px) {
        display: none;
    }
}

.svc-parts__caps li {
    position: relative;
    flex: none;
    width: 15.5vw;
    display: flex;
    align-items: flex-end;
    height: 12.5vw;
    padding: 0.833vw;
    background: var(--white);
    border-radius: 0.694vw;
    font-size: var(--fs-p);
    letter-spacing: var(--ls-p);
    font-weight: 600;
    line-height: var(--lh-p);
    color: var(--ink);
    text-transform: capitalize;

    @media (max-width: 1024px) {
        width: 100%;
        height: auto;
        min-height: 156px;
        padding: 20px;
        border-radius: var(--radius);
    }
}

.svc-parts__num {
    position: absolute;
    left: 0.833vw;
    top: 0.764vw;
    display: grid;
    place-items: center;
    width: 1.528vw;
    height: 1.528vw;
    min-width: 20px;
    min-height: 20px;
    border: 1px solid #d5d5d5;
    border-radius: 50%;
    font-size: var(--fs-p);
    letter-spacing: var(--ls-p);
    line-height: var(--lh-p);
    text-transform: none;

    @media (max-width: 1024px) {
        left: 20px;
        top: 20px;
        width: 27px;
        height: 27px;
    }
}

/* --- ABOUT (Company) ----------------------------------------------- */
.abt-overview {
    display: grid;
    grid-template-columns: 37.222vw 49.514vw;
    padding: 0.764vw 4.792vw 0 8.472vw;

    @media (max-width: 1024px) {
        grid-template-columns: 1fr;
        gap: 31px;
        padding: 47px var(--gutter);
    }
}

.abt-overview__copy {
    padding-top: 3.333vw;
    padding-bottom: 3.333vw;
    width: 22vw;

    @media (max-width: 1024px) {
        padding-top: 0;
        padding-bottom: 0;
        width: auto;
    }
}

.abt-overview__copy h2 {
    margin: 0.382vw 0 1.924vw;

    @media (max-width: 1024px) {
        margin: 16px 0 23px;
    }
}

.abt-overview__copy .rule-lead {
    width: 21vw;
    padding-bottom: 1.319vw;
    margin-bottom: 9.722vw;

    @media (max-width: 1024px) {
        width: auto;
        margin-bottom: 31px;
    }
}

.abt-overview__copy .body-sm {
    width: 20.417vw;

    @media (max-width: 1024px) {
        width: auto;
    }
}

.abt-overview__copy .body-sm strong {
    color: var(--ink);
}

/* 2026-09-24, on request: the two photos are a vertical REEL that runs
   upward at a constant speed, never pauses and takes no input. The column
   keeps the export's 52.222vw, so one photo and most of the next are always
   in view. The reel holds the pair twice and moves by exactly one pair
   (-50%), so the loop point is invisible; each <ul> carries a trailing gap
   the size of the gap between photos, which keeps the two halves equal. */
.abt-overview__pics {
    --reel-fig: 30.556vw;
    --reel-gap: 0.833vw;
    /* runs the full height of the section, however long the copy beside it
       grows (2026-09-27): the reel is absolute, so the copy alone sets the
       row and this stretches to it */
    position: relative;
    min-height: 52.222vw;
    overflow: hidden;
    border-radius: 0.7vw;
    pointer-events: none;

    @media (max-width: 1024px) {
        --reel-fig: 242px;
        --reel-gap: 16px;
        height: 460px;
        border-radius: var(--radius);
    }
}

.abt-overview__reel {
    position: absolute;
    inset: 0 0 auto;
    animation: abt-reel 18s linear infinite;
}

.abt-overview__reel ul {
    list-style: none;
    margin: 0;
    padding: 0 0 var(--reel-gap);
    display: flex;
    flex-direction: column;
    gap: var(--reel-gap);
}

.abt-overview__reel figure {
    height: var(--reel-fig);
    border-radius: 0.7vw;
    overflow: hidden;

    @media (max-width: 1024px) {
        border-radius: var(--radius);
    }
}

.abt-overview__reel img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

@keyframes abt-reel {
    /* one pair of three, so the column never runs out of photo */
    to { transform: translateY(-33.3333%); }
}

@media (prefers-reduced-motion: reduce) {
    .abt-overview__reel { animation: none; }
}

/* --- about: key milestones wheel ---------------------------------------
   Rebuilt 2026-09-20 from the re-exported `pages-pdf/About.pdf`. The section
   went 1166 -> 798pt and the whole composition changed:

     was                                   now
     623pt ring centred on the page        426pt ring at (964, 1941), right
     one year strip through the ring       active year at the LEFT, huge;
                                           the two neighbours small, right
     panel card floating at x1094          card sitting on the ring's right
     prev/next buttons under the card      no buttons -- the years are it

   Everything in the wheel is positioned absolutely against the band, so the
   band's height is stated rather than derived. Design y 1503-2301; every
   `top` below is relative to 1503. */
.abt-miles {
    position: relative;
    height: 55.417vw;
    padding: 4.792vw 0 0;
    overflow: hidden;
    background: var(--grey-bg);

    @media (max-width: 1024px) {
        height: auto;
        padding: 47px var(--gutter) 16px;
    }
}

/* the export rules the top of the band off from the white overview above it */
.abt-miles::before {
    content: "";
    position: absolute;
    inset: 0 0 auto;
    border-top: 1px solid #bebebe;
    pointer-events: none;
    --ln-mask: var(--ln-x);
}

.abt-miles__head {
    padding: 0 0 0 4.792vw;

    @media (max-width: 1024px) {
        padding: 0;
    }
}


/* design: bracket x 69-414, y 1738-1947; copy ink x 92, 8 lines at 22pt.
   The width is set so Urbane breaks those same 8 lines -- it runs ~8.7%
   wider than the export's face at a matched cap height, so a bracket at the
   drawn 345pt would spill to nine and take the box 22pt deeper. 20pt of extra
   bracket is the cheaper error, and its right edge still clears the big
   numeral, which starts at x 533. */
.crrc-page .abt-miles__lead {
    width: 25.347vw;
    margin-top: 2.528vw;
    padding-bottom: 1.389vw;

    @media (max-width: 1024px) {
        width: auto;
        margin-top: 23px;
    }
}

/* the ring: a 426pt dotted circle with a red arc riding its top-right
   quadrant, both drawn as strokes rather than the individually outlined dots
   the export carries (10.2c) -- a zero-length dash only renders as a dot with
   a round line cap. */
.abt-miles__wheel {
    position: absolute;
    inset: 0;
    pointer-events: none;

    @media (max-width: 1024px) {
        display: none;
    }
}

.abt-miles__wheel > * {
    pointer-events: auto;
}

.abt-miles__ring {
    position: absolute;
    left: 52.153vw;
    top: 15.625vw;
    width: 29.583vw;
    height: 29.583vw;
    --ln: self;
    --ln-mask: var(--ln-sweep);
}

.abt-miles__ring circle {
    fill: none;
    stroke: #bebebe;
    /* 2px dots, 2px apart: the site's one dotted line (2026-09-27) */
    stroke-width: 2;
    stroke-linecap: round;
    stroke-dasharray: 0 4;
}

.abt-miles__ring path {
    fill: none;
    stroke: var(--red);
    stroke-width: 2;
    stroke-linecap: butt;
    transition: stroke-dashoffset 0.5s ease;
}

/* The years are RIGHT-aligned. In the export "10" and "12" share a left edge
   (x 854, 855) while their right edges differ by 10pt, which is only possible
   if they are aligned on the right and the face's digits are proportional --
   so right alignment is what reproduces it, and it also keeps the active year
   growing leftward out of the ring the way the design draws it.
   x 1022 is the compromise edge: it lands "12" exactly, "10" 11pt short and
   "2011" 11pt long, which is as close as one edge gets to all three. */
.abt-miles__years {
    --year-h: 13.889vw;
    position: absolute;
    /* right edge at 70.972vw, where the years align. It starts at 30vw, not 0,
       so it encloses the "20" but not the lead copy, which it used to cover
       for clicks and inspection (2026-09-27) */
    left: 30vw;
    top: 0;
    width: 40.972vw;
    height: 100%;
    overflow: visible;
    /* 2026-10-12, on request: the two-digit suffix should sit around the
       centre of the ring. The ul is right-anchored and `--suf-w` wide, so
       its centre is at (container.right - --suf-w/2) = 70.972vw - --suf-w/2.
       Ring centre is at 66.9445vw (left 52.153 + width/2 14.7915). Shift
       the whole years container right by the difference so both "20" and
       the suffix move together -- the gap between them stays the same. */
    transform: translateX(calc(var(--suf-w, 0px) / 2 - 4.028vw));
}

/* the strip is positioned by --i, set from JS. The transition rides on
   `transform`, whose used value is a length, so --i itself never has to be
   registered with @property -- unlike the MRO accordion in 10.6, where the
   custom property IS the animated value. */
.abt-miles__years ul {
    position: absolute;
    /* fixed width = --suf-w (the widest suffix, measured in wheel()), and
       left-aligned so every year's digits sit FLUSH with "20"'s right edge
       (2026-10-12, on request). Narrower years leave space on the right
       instead of a gap next to "20". The ul's right edge still sits at the
       parent's right (right: 0), which places its left edge exactly where
       "20"'s right edge lands via its own `right: var(--suf-w)`. */
    right: 0;
    top: 29.965vw;
    width: var(--suf-w);
    list-style: none;
    padding: 0;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    transform: translateY(calc(var(--year-h) * (-0.5 - var(--i, 0))));
    transition: transform 0.55s cubic-bezier(0.4, 0, 0.2, 1);
}

/* on the scroll-driven variant --i is a continuous float set per frame by
   ScrollTrigger, so a CSS transition on top would fight it and make the
   strip chase the scroll */
.abt-miles.is-scrub .abt-miles__years ul { transition: none; }
.abt-miles.is-scrub .abt-miles__ring path { transition: none; }

.abt-miles__years li {
    flex: none;
    height: var(--year-h);
    display: flex;
    align-items: center;
    font-size: 12.069vw;  /* 173.8 (175.8 in the export, -2 sitewide); pure vw, it is a graphic (2026-10-13) */
    line-height: 0.875;
    font-weight: 600;
    letter-spacing: -0.06em;
    color: #d9d9d9;
    cursor: pointer;
    user-select: none;
    white-space: nowrap;
    transition: color 0.4s ease, font-size 0.45s cubic-bezier(0.4, 0, 0.2, 1);

    /* Below ~1200 the lead copy and the right-side card are near the
       suffix on both sides; the ghosted neighbours bulge visibly into the
       lead when the 12vw gets large relative to the shrinking columns.
       Cap at a smaller rate so the ghost column stays narrow. */
    @media (min-width: 1025px) and (max-width: 1439px) {
        font-size: 10vw;
    }
}

.abt-miles__years li:focus-visible {
    outline: 2px solid var(--red);
    outline-offset: 0.4vw;
}

/* "20" is fixed in position throughout the timeline (2026-10-12, on
   request); only the last two digits of the active year transition. Its
   right edge sits --suf-w left of the strip's right edge, where --suf-w
   is now the WIDEST suffix across all years (measured in wheel()'s
   fitCent, scaled to the century's font-size, digits being proportional).
   The old per-year measurement made the "20" shift with every year
   change -- that is the shift the client flagged. The fallback is roughly
   a two-digit width. No transition on `right`, because it does not change. */
.abt-miles__cent {
    position: absolute;
    right: var(--suf-w, 1.15em);
    top: 29.965vw;
    height: var(--year-h);
    display: flex;
    align-items: center;
    transform: translateY(-50%);
    font-size: 15.801vw;  /* 227.534 / 200.842, -13.772; pure vw, it is a graphic (2026-10-13) */
    line-height: 0.875;
    font-weight: 600;
    letter-spacing: -0.06em;
    color: var(--ink);
    pointer-events: none;

    /* 2026-10-27: cap the big "20" at narrower desktop so it stops crowding
       the lead copy on the left and the year card on the right. --suf-w is
       JS-measured against this font-size, so the panel's left edge (which
       references --suf-w) tracks the shrink automatically. */
    @media (min-width: 1025px) and (max-width: 1439px) {
        font-size: 13vw;
    }
}

.abt-miles__years li[aria-current="true"] {
    font-size: 15.801vw;  /* pure vw, it is a graphic (2026-10-13) */
    color: var(--ink);

    @media (min-width: 1025px) and (max-width: 1439px) {
        font-size: 13vw;
    }
}

.abt-miles__list {
    position: absolute;
    /* was 73.958vw -- the year suffix shifted right (centred on the ring,
       2026-10-12), so the panel's old left edge sat under "25" / "10" etc.
       Sits 1vw right of the suffix's new right edge
       (ring centre 66.9445vw + --suf-w/2). */
    left: calc(66.9445vw + var(--suf-w, 20vw) / 2 + 1vw);
    top: 21.944vw;
    width: 19.167vw;
    padding: 0.625vw 0.833vw 0.903vw;
    background: var(--white);
    border-radius: var(--radius-sm);
    box-shadow: 0 0.14vw 0.83vw rgba(27, 36, 41, 0.1);
    z-index: 3;
    /* panel scrolls internally when its content runs past the band's
       height, so the overall band keeps its design dimensions
       (2026-10-12). The wheel is Lenis's otherwise: the markup carries
       data-lenis-prevent. */
    max-height: calc(100% - 21.944vw - 2.5vw);
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: #bebebe transparent;

    @media (max-width: 1024px) {
        max-height: none;
        overflow: visible;
    }
}

.abt-miles__list ol {
    list-style: none;
    padding: 0;
    margin-top: 1.111vw;
}

.abt-miles__list li + li {
    position: relative;
    border-top: 1px solid transparent;
    margin-top: 1.875vw;
    padding-top: 1.944vw;
}

.abt-miles__list li + li::before {
    content: "";
    position: absolute;
    inset: -1px 0 auto;
    border-top: 1px solid var(--line);
    pointer-events: none;
    --ln-mask: var(--ln-x);
}

.abt-miles__list [data-year-panel][hidden] {
    display: none;
}

.abt-miles__list .tag {
    margin-bottom: 0;

    @media (max-width: 1024px) {
        margin-bottom: 16px;
    }
}

/* on mobile the wheel is dropped and the years become a plain stacked list */
.abt-miles__mobile {
    display: none;

    @media (max-width: 1024px) {
        display: block;
        margin-top: 31px;
    }
}

.abt-miles__mobile ol {
    list-style: none;
    padding: 0;
}

.abt-miles__mobile > ol > li + li {
    margin-top: 31px;
}

.abt-miles__mobile h3 {
    font-size: var(--fs-h1);
    line-height: var(--lh-h1);
    letter-spacing: var(--ls-h1);
    margin-bottom: 12px;
}

.abt-miles__mobile ul {
    list-style: none;
    padding: 0;
}

.abt-miles__mobile ul li + li {
    position: relative;
    border-top: 2px dotted transparent;
    margin-top: 16px;
    padding-top: 16px;
}

.abt-miles__mobile ul li + li::before {
    content: "";
    position: absolute;
    inset: -2px 0 auto;
    border-top: 2px dotted #bebebe;
    pointer-events: none;
}

/* --- about: Malaysia operations slider --- */
/* re-measured 2026-09-20: section 2301-3030, card 2366-2965 */
.abt-ops {
    background: var(--ink);
    color: var(--white);
    padding: 4.514vw 0;

    @media (max-width: 1024px) {
        padding: 47px var(--gutter);
    }
}

.abt-ops__inner {
    display: grid;
    grid-template-columns: 22.639vw minmax(0, 1fr);
    padding: 0 4.792vw;

    @media (max-width: 1024px) {
        /* minmax(0, 1fr): the Splide stage inside __stage has min-content
           ~760px and a bare 1fr blows the whole column -- and __side with
           it -- past viewport. Per CLAUDE.md 10.8f. */
        grid-template-columns: minmax(0, 1fr);
        gap: 31px;
        padding-left: 0;
    }
    /* 2026-10-27: narrow desktop/tablet — the side column's floored body copy
       ran into the subsidiary card. Tighten the side column and the outer
       padding so the stage has room to breathe. */
    @media (min-width: 1025px) and (max-width: 1439px) {
        grid-template-columns: 20vw minmax(0, 1fr);
        padding: 0 3.5vw;
        column-gap: 2vw;
    }
}

/* 2026-10-10 feedback #16: the side column's heading and intro were too small
   next to the subsidiary card -- eyebrow at h4 (20) and body-sm (16) made the
   section read like a note, not a header. Promoted to h2-ish (40/40) and body
   (20/22) via section-scoped overrides so the markup stays as-is. */
.abt-ops__side .eyebrow {
    color: var(--white);
    font-size: var(--fs-h1);
    line-height: var(--lh-h1);
    letter-spacing: var(--ls-h1);
    text-transform: uppercase;
    margin-bottom: 1.667vw;

    @media (max-width: 1024px) {
        margin-bottom: 23px;
    }
}

/* 2026-10-15 (client): drop the h4 size-up here -- use the plain body-sm
   token so this intro reads as body copy, not a sub-headline. */
.abt-ops__side .body-sm {
    color: #8d979d;
    width: 20vw;

    @media (max-width: 1024px) {
        width: auto;
    }
}

/* A card stack since 2026-09-25 (after wokhey.sg's milestones): the white
   card the export draws is now EACH SLIDE, not the stage, so there is a pile
   to deal from. Splide runs in fade mode, which leaves every slide in one
   place; PAGE_JS writes --r, each card's depth behind the front one, and
   .is-flip on the card leaving the front. The arrow slabs stay put over the
   pile. Phones and reduced motion: one card, the rest hidden. */
.abt-ops__stage {
    position: relative;
}

.abt-ops .splide__track {
    overflow: visible;
}

.abt-ops .splide__slide:not(.is-active) {
    visibility: hidden;
}

.abt-op {
    display: grid;
    grid-template-columns: 28.125vw minmax(0, 1fr);
    gap: 1.528vw;
    height: 100%;
    background: var(--white);
    color: var(--ink);
    border-radius: 1.389vw;
    padding: 2.208vw 10.5vw 1.875vw 1.944vw;

    @media (max-width: 1024px) {
        grid-template-columns: 1fr;
        gap: 23px;
        border-radius: var(--radius);
        padding: 27px;
    }
    /* 2026-10-27: narrow desktop — the body-copy column was being squeezed
       between a wide figure and the arrow-slab padding, and the floored
       h3/body px overflowed. Pull the figure column narrower and reduce
       the right padding; the arrow slabs (5.764vw + 2.222vw right offset)
       still clear. */
    @media (min-width: 1025px) and (max-width: 1439px) {
        grid-template-columns: 24vw minmax(0, 1fr);
        gap: 1.9vw;
        padding: 2.5vw 9vw 2.2vw 2.2vw;
    }
}

/* Malaysia operations crossfade (2026-10-05): the earlier card-stack flip
   is gone (client called it PPT-like). Only the active card is visible;
   Splide's `type: 'fade'` runs the opacity crossfade and the sliders() hook
   adds a GSAP scale/translateY to the incoming card so the swap is not a
   dead-opacity dissolve. */
.abt-op {
    box-shadow: 0 1.2vw 3vw rgba(0, 0, 0, 0.18);
}

.abt-op figure {
    --par-img: 2.2vw;
    position: relative;
    height: 100%;
    border-radius: 0.7vw;
    overflow: hidden;

    @media (max-width: 1024px) {
        height: 242px;
        border-radius: var(--radius);
    }
}

/* The export breaks this title in three -- "CRRC Rolling Stock / Center
   (Malaysia) / Sdn. Bhd. (CRM)" -- and Urbane needs 404pt for the first line
   against the 387pt column, so at the shared size it breaks in four. Sized to
   the width instead (10.8g's rule), with the line-height put back up to keep
   the export's 40pt pitch. */
.abt-op h3 {
    font-size: var(--fs-h2);
    line-height: var(--lh-h2);
    letter-spacing: var(--ls-h2);
    margin-bottom: 1.667vw;

    @media (max-width: 1024px) {
        margin-bottom: 20px;
    }
}

.abt-op .rule-lead {
    margin-bottom: 3.5vw;

    @media (max-width: 1024px) {
        margin-bottom: 23px;
    }
}

/* the two tall arrow slabs the design draws down the card's right edge */
.abt-ops__nav {
    position: absolute;
    z-index: 30;
    right: 2.222vw;
    top: 2.361vw;
    bottom: 2.361vw;
    width: 5.764vw;
    display: flex;
    flex-direction: column;
    gap: 1.042vw;

    @media (max-width: 1024px) {
        position: static;
        /* row-reverse: markup has NEXT first (red) and PREV last (dark). On
           desktop that stacks sensibly as a column. Horizontally it reads
           inverted -- the red next button lands on the left of the prev. */
        flex-direction: row-reverse;
        justify-content: flex-end;
        width: auto;
        margin-top: 23px;
        gap: 16px;
    }
}

.abt-ops__nav button {
    flex: 1;
    display: grid;
    place-items: center;
    border: 0;
    border-radius: 0.833vw;
    color: var(--white);
    cursor: pointer;
    transition: filter 0.25s ease;

    @media (max-width: 1024px) {
        flex: none;
        width: 55px;
        height: 55px;
        border-radius: 10px;
    }
}

.abt-ops__nav button:first-child {
    background: var(--red);
}

.abt-ops__nav button:last-child {
    background: #2b3a45;
}

.abt-ops__nav button:hover {
    filter: brightness(1.12);
}

.abt-ops__nav button svg {
    width: 1.528vw;
    height: auto;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;

    @media (max-width: 1024px) {
        width: 20px;
    }
}

/* --- about: standards & certifications --- */
/* Re-measured 2026-09-20: grid 122-1317 (was 85-1340), column dividers on
   x 416 / 720 / 1023 and card copy 24pt inside each. */
.abt-certs {
    padding: 3.681vw 0 4.444vw;

    @media (max-width: 1024px) {
        padding: 47px var(--gutter);
    }
}

.abt-certs h2 {
    text-align: center;
    margin-bottom: 3.083vw;

    @media (max-width: 1024px) {
        margin-bottom: 31px;
    }
}

.abt-certs__grid {
    display: grid;
    grid-template-columns: 20.417vw 21.111vw 21.042vw 20.417vw;
    position: relative;
    border-top: 2px solid transparent;
    padding-top: 1.319vw;
    margin: 0 8.472vw;
    /* 2026-10-05 feedback: all four columns start from the same top edge
       so Quality Management no longer drifts below Environmental and OH&S */
    align-items: start;

    @media (max-width: 1024px) {
        grid-template-columns: 1fr;
        gap: 31px;
        margin: 0;
        padding-top: 31px;
    }
}

.abt-certs__grid::before {
    content: "";
    position: absolute;
    inset: -2px 0 auto;
    /* 2px dotted #BEBEBE (2026-09-27) */
    border-top: 2px dotted #bebebe;
    pointer-events: none;
}

.abt-certs__grid > * {
    padding: 0 1.667vw 0 1.667vw;

    @media (max-width: 1024px) {
        padding: 0;
    }
}

.abt-certs__grid > p {
    padding-left: 0.694vw;
    width: 13.472vw;

    @media (max-width: 1024px) {
        padding-left: 0;
        width: auto;
    }
}

/* a dotted rule left of EVERY card, the first one included -- three, as the
   design draws them (2026-09-27; was between the cards only).
   The three cards share one row grid so the tag, title and body line up
   horizontally across the row (2026-10-01, client review): the title row was
   drifting because the body was anchored to the bottom with margin-top: auto. */
.abt-cert {
    position: relative;
    border-left: 2px solid transparent;
    display: grid;
    grid-template-rows: auto auto 1fr;
    row-gap: 0.9vw;
    align-content: start;
    min-height: 18.056vw;

    @media (max-width: 1024px) {
        border-left: 0;
        min-height: 0;
        row-gap: 16px;
    }
}

.abt-cert::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 -2px;
    border-left: 2px dotted #bebebe;
    pointer-events: none;
    --ln-mask: var(--ln-y);

    @media (max-width: 1024px) {
        display: none;
    }
}

.abt-cert h3 {
    margin: 0.486vw 0 0;
    font-size: var(--fs-h4);
    letter-spacing: var(--ls-h4);
    line-height: var(--lh-h4);
    /* The h3 and body copy no longer carry fixed vw widths -- they flow
       within the grid cell their .abt-cert sits in (2026-10-14). Headings
       wrap to as many lines as they need in that column; min-height
       reserves 3 lines so the body copy below top-aligns across all cards
       (the longest card -- "Occupational Health & Safety Management
       Systems" -- is a natural 3-liner at this column width). */
    min-height: calc(var(--fs-h4) * var(--lh-h4) * 3);

    @media (max-width: 1024px) {
        min-height: 0;
    }
}

.abt-cert .tag {
    align-self: flex-start;
}

/* --- OUR LOCALIZATION ----------------------------------------------
   Rebuilt 2026-09-24 from the re-exported `pages-pdf/Our Localization.pdf`
   (10.20): an intro, then the five pillars as five full-bleed photo panels.
   Each panel carries a white card on its right half and, under the card, a
   5L tab strip with that pillar current. The panels are `position: sticky`,
   so each slides up over the last like a deck being dealt; the strips jump
   between them (PAGE_JS loctabs()). */
.loc-intro {
    padding: 8.3vw 0 9.3vw;
    text-align: center;

    @media (max-width: 1024px) {
        padding: 47px var(--gutter);
    }
}

.crrc-page .loc-intro p {
    width: 66.5vw;
    margin: 0 auto;
    font-size: var(--fs-h4);
    letter-spacing: var(--ls-h4);
    line-height: var(--lh-h4);
    color: var(--ink);

    @media (max-width: 1024px) {
        width: auto;
    }
}

/* Slides since 2026-09-25 (they were sticky panels dealt by the scroll).
   Splide's fade leaves every slide in one place; the crossfade is ours: the
   incoming slide fades in ON TOP, and the outgoing one stays fully painted
   underneath until it has, so there is no dip through the dark background
   in the middle of the swap. */
.loc-panels .splide__slide {
    opacity: 0;
    transition: opacity 0s linear 0.9s;
}

.loc-panels .splide__slide.is-active {
    z-index: 1;
    opacity: 1;
    transition: opacity 0.9s ease;
}

/* mobile reduced-motion fallback: show all panels stacked, since the scrub
   needs the window to repaint. Under motion the scrub is on at every width
   (2026-10-21, on request) and the desktop sticky geometry below handles it. */
@media (max-width: 1024px) and (prefers-reduced-motion: reduce) {
    .loc-panels .splide__list {
        display: flex;
        flex-direction: column;
        gap: 31px;
        transform: none !important;
    }

    .loc-panels .splide__slide {
        position: relative !important;
        left: auto !important;
        top: auto !important;
        transform: none !important;
        width: 100% !important;
        opacity: 1;
    }

    .loc-panels .splide__track {
        overflow: visible;
    }
}

/* Scroll-driven since 2026-09-28 (on request; clicks still work). PAGE_JS
   puts .is-scrub on the section on desktop with motion: it becomes one panel
   plus 70vh of scroll per pillar, and the slider sticks in it while the
   scroll picks the slide. `top` goes negative when the panel is taller than
   the window (its 40vw floor), so its foot still gets seen. */
.loc-panels.is-scrub {
    height: calc(max(100vh, 40vw) + var(--n, 5) * 70vh);
}

.loc-panels.is-scrub .splide__slide {
    /* likova.space's location slider, copied on request (2026-09-28, which
       replaced the ETS1 strips): every slide is painted, LATER ones on top,
       and a slide's photo wipes UP from the bottom edge over the one before
       it during the last 35% of that one's segment (--sp -0.35 -> 0). The
       card does not move; only its copy changes (below). */
    --rv: clamp(0, 1 + var(--sp, 0) / 0.35, 1);
    opacity: 1;
    transition: none;
    /* no layer of its own, so the photos and the one card can stack across
       slides: Splide's inline translateX would make every slide a stacking
       context and trap its card under the next slide's photo. The slides
       share one grid cell instead of sitting side by side. */
    grid-area: 1 / 1;
    transform: none !important;
    isolation: auto;
    z-index: auto;
}

.loc-panels.is-scrub .splide__list {
    display: grid;
}

.loc-panels.is-scrub .loc-panel {
    background: none;
}

.loc-panels.is-scrub .loc-panel__bg {
    z-index: calc(1 + var(--si, 0));
    background: var(--ink);
    clip-path: inset(calc((1 - var(--rv)) * 100%) 0 0 0);
}

/* one card: the current slide's side shows, the others hide. Going forward
   the new card lands on top at once and its copy plays in; going back the
   card above fades off the one underneath */
.loc-panels.is-scrub .loc-panel > .loc-panel__side {
    position: relative;
    z-index: 20;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.35s ease, visibility 0s linear 0.35s;
}

.loc-panels.is-scrub .loc-panel.is-active > .loc-panel__side {
    opacity: 1;
    visibility: visible;
    transition: none;
}

.loc-panels.is-scrub > .splide {
    position: sticky;
    top: min(0px, calc(100vh - 40vw));
}

.loc-panel {
    position: relative;
    height: max(100vh, 40vw);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    padding-right: 0.903vw;
    isolation: isolate;

    /* mobile scrub: card centres and takes the gutter-to-gutter width */
    @media (max-width: 1024px) and (prefers-reduced-motion: no-preference) {
        justify-content: center;
        padding-right: 0;
    }
    /* opaque, so a panel whose photo is still decoding never shows the
       panel it is sliding over */
    background: var(--ink);
    /* the card centres in the part of the panel the fixed header (10.33)
       and the admin bar do not cover. --crrc-header-h is measured by the
       header part's script; without it the card centres in the whole panel */
    padding-top: calc(var(--wp-admin--admin-bar--height, 0px) + var(--crrc-header-h, 0px));

    /* only kick into auto-height block layout at mobile when the scrub
       isn't driving them -- otherwise the sticky pin needs a 100vh panel */
    @media (max-width: 1024px) and (prefers-reduced-motion: reduce) {
        height: auto;
        display: block;
        padding: 0;
    }
}

/* inner-image parallax (10.13), 2026-09-25 on request. The slides only
   move sideways (Splide's fade), so the frame's cached page position that
   img-parallax.js works from stays true on every slide. */
.loc-panel__bg {
    --par-img: 5vw;

    position: absolute;
    inset: 0;
    z-index: -1;
    overflow: hidden;

    /* mobile reduced-motion fallback only: the scrub needs the photo pinned
       in its own panel, which the base absolute position already handles. */
    @media (max-width: 1024px) and (prefers-reduced-motion: reduce) {
        position: relative;
        height: 273px;
    }

    /* the pinned panel's page position moves with the scroll, and
       img-parallax.js caches it, so under the scrub (10.13) the photo holds */
    @media (prefers-reduced-motion: no-preference) {
        --par-img: 0px;
    }
}

.loc-panel__bg img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* under the scrub the photo moves UP with the scroll so the section reads
   as scrollable (client, 2026-09-28, after likova.space): 20vh of travel
   from the moment its wipe starts to the end of its own segment, linear,
   no zoom. --sp is written per slide by PAGE_JS; clamped so a photo holds
   still outside that span. */
.loc-panels.is-scrub .loc-panel__bg img {
    height: calc(100% + 20vh);
    transform: translateY(calc(clamp(0, (var(--sp, 0) + 0.35) / 1.35, 1) * -20vh));
    will-change: transform;
}

/* parent-qualified: the theme's flow layout resets a group's first/last
   child margin at (0,2,0), which a lone class loses to (10.30) */
.loc-panel > .loc-panel__side {
    width: max(48.542vw, 580px);
    display: flex;
    flex-direction: column;
    gap: 0.972vw;

    /* mobile scrub: card takes the gutter-to-gutter width so the copy has
       room instead of a half-viewport sliver */
    @media (max-width: 1024px) and (prefers-reduced-motion: no-preference) {
        width: calc(100vw - 2 * var(--gutter));
    }

    /* reduced-motion fallback: card sits below the photo in flow */
    @media (max-width: 1024px) and (prefers-reduced-motion: reduce) {
        width: auto;
        margin: -12vw var(--gutter) 39px;
        position: relative;
    }
}

.loc-panel__card {
    position: relative;
    /* 2026-10-05 feedback: the Human Capital card used to overflow the panel
       on short viewports; tightening padding and letting min-height collapse
       lets long copy fit within 100vh while the shorter cards still read
       with their own natural height */
    min-height: 28vw;
    display: flex;
    flex-direction: column;
    padding: 1.4vw 1.6vw 1.8vw;
    background: var(--white);
    color: var(--ink);
    border-radius: 0.694vw;
    box-shadow: 0 0.6vw 2.4vw rgba(0, 0, 0, 0.22);
    /* if a very tall viewport still cannot hold the card above the fold --
       most likely to be Human Capital on a short laptop screen -- the card
       scrolls internally rather than being cropped by .loc-panel. The
       markup carries data-lenis-prevent so Lenis does not swallow the
       inner scroll. */
    max-height: 85vh;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* flex child in a flex-column parent: min-height needs to be 0 so the
       max-height actually clamps the box instead of being pushed past by
       content (2026-10-27, after image #15 showed the "Local Human Capital
       Development" card content running off the panel bottom). */
    min-height: 0;

    /* 2026-10-27: previously ALL mobile widths dropped the max-height and
       went to `overflow-y: visible`. That is only correct for the
       reduced-motion fallback below (where .loc-panel is `height: auto;
       display: block`); under the live scrub the panel is still a 100vh
       sticky and the card has to scroll internally. Split the override. */
    @media (max-width: 1024px) and (prefers-reduced-motion: no-preference) {
        padding: 27px 23px 78px;
        border-radius: var(--radius);
        /* keeps the desktop max-height + overflow-y: auto so content
           scrolls inside the sticky panel on narrow screens too */
    }
    @media (max-width: 1024px) and (prefers-reduced-motion: reduce) {
        max-height: none;
        padding: 27px 23px 78px;
        border-radius: var(--radius);
        overflow-y: visible;
    }
}

/* "Manufacturing" is the widest word: 598pt in the export's face, and Urbane
   runs ~13% wider (12.17), so 5vw would overrun the card. 4.6vw fits it with
   the export's own 65pt line pitch. */
.crrc-page .loc-panel__card h2 {
    font-size: var(--fs-h1);
    letter-spacing: var(--ls-h1);
    line-height: var(--lh-h1);
    text-transform: uppercase;
    padding-bottom: 1vw;
    border-bottom: 1px solid var(--hairline);

    @media (max-width: 1024px) {
        padding-bottom: 16px;
        /* 66px --fs-h1 cuts "MANUFACTURING" / "HUMAN CAPITAL" off against
           the panel's own box at 390 vp. Same clamp pattern as the hero. */
        font-size: clamp(28px, 9vw, 50px);
    }
}

.loc-panel__copy {
    margin-top: auto;
    padding-top: 1.6vw;
    /* widened 2026-10-05 so Human Capital's three long paragraphs wrap on
       fewer lines (was 25.6vw); 350px floor so the copy column does not
       crush below readable width at narrow desktop (2026-10-27). */
    width: max(32vw, 350px);

    @media (max-width: 1024px) {
        width: auto;
        padding-top: 27px;
    }
}

.crrc-page .loc-panel__lead {
    font-size: var(--fs-p);
    letter-spacing: var(--ls-p);
    line-height: var(--lh-p);
    color: var(--ink);
    margin-bottom: 1em;
}

.loc-panel__icon {
    position: absolute;
    right: 2.3vw;
    bottom: 2.1vw;
    width: 5vw;
    height: 5vw;
    display: grid;
    place-items: end;
    color: var(--red);

    @media (max-width: 1024px) {
        /* was anchored bottom-right at 23px, which the Human Capital
           card's long body ran into on narrow screens (user image,
           2026-10-28). Take it out of absolute positioning on tablet so
           no text can overlap it regardless of copy length; a top margin
           gives it breathing room from the body copy above. */
        position: static;
        margin-top: 24px;
        width: 44px;
        height: 44px;
        place-items: start;
    }
}

.loc-panel__icon svg {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
}

.loc-panel__tabs {
    display: grid;
    /* equal columns as the export draws them, but never narrower than a label:
       the labels break only where LOC_TABS puts a <br> (nowrap below) */
    grid-template-columns: repeat(5, minmax(max-content, 1fr));
    /* 2026-10-12: more vertical breathing room on request */
    min-height: 4.6vw;
    padding: 1vw 1vw 1vw 1.9vw;
    background: var(--white);
    border-radius: 0.694vw;
    box-shadow: 0 0.6vw 2.4vw rgba(0, 0, 0, 0.18);

    @media (max-width: 1024px) {
        display: none;
    }
}

.loc-panel__tabs button {
    position: relative;
    align-self: center;
    margin: 0 1.4vw 0 0;
    padding: 0 0 0.5vw;
    background: none;
    border: 0;
    border-bottom: 1px solid var(--hairline);
    font-family: inherit;
    font-size: clamp(10px, 0.833vw, 14px);  /* 12 at 1440; ±2px clamp (2026-10-27) */
    font-weight: 600;
    /* 2026-10-12: looser leading for the two-line labels (e.g. "LOCAL HUMAN
       CAPITAL DEVELOPMENT"); the one-line labels are unaffected. */
    line-height: 1.4;
    letter-spacing: 0;
    white-space: nowrap;
    text-align: left;
    text-transform: uppercase;
    color: #bebebe;
    cursor: pointer;
    transition: color 0.25s ease;
}

.loc-panel__tabs button:hover,
.loc-panel__tabs button[aria-current="true"] {
    color: var(--ink);
}

/* the timer: --auto is the share of the slide's time already spent, written
   onto .loc-panels by PAGE_JS. No autoplay (reduced motion), no timer, so
   the current tab's rule is simply red. */
.loc-panel__tabs button::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: -1px;
    width: 0;
    height: 2px;
    background: var(--red);
}

.loc-panel__tabs button[aria-current="true"]::after {
    width: calc(var(--auto, 0) * 100%);

    @media (prefers-reduced-motion: reduce) {
        width: 100%;
    }
}

/* --- The train pages (/ets1/, /ets2/ ... /6-axle-locomotive/) ---------
   New 2026-09-24 from `pages-pdf/pop tab intercity.pdf` (10.21), the ETS1
   page; the other nine share its layout since 2026-09-28 (10.34). Every y
   below is design pt / 14.4. */
.crrc-page-train { --page-hero-h: 43.611vw; }

/* anchored by its bottom, where ETS1's three lines end (top 24.792vw), so a
   four-line title grows upward instead of off the hero */
.page-hero.ets-hero h1 {
    left: 16.111vw;
    top: auto;
    bottom: 5.27vw;
    width: 76vw;

    @media (max-width: 1024px) {
        left: auto;
        bottom: auto;
        width: auto;
        font-size: min(var(--fs-h1), 9vw);
    }
}

/* the desktop breaks are composed for 76vw; on a phone the title wraps on
   its own, or "Bahru–Singapore" and "Automated" run off the screen */
.page-hero.ets-hero h1 br {
    @media (max-width: 1024px) {
        display: none;
    }
}

.ets-chips {
    position: absolute;
    left: 1.319vw;
    top: 27.708vw;
    display: flex;
    gap: 0.208vw;

    @media (max-width: 1024px) {
        position: static;
        gap: 6px;
    }
}

.crrc-page .ets-chips p {
    padding: 0.35vw 0.45vw;
    background: var(--white);
    border-radius: 0.208vw;
    font-size: var(--fs-p);
    letter-spacing: var(--ls-p);
    font-weight: 600;
    line-height: var(--lh-p);
    text-transform: uppercase;
    color: var(--ink);

    @media (max-width: 1024px) {
        padding: 5px 8px;
        border-radius: 4px;
    }
}

.ets-intro {
    padding: 4vw 8.472vw 5vw;
    text-align: center;

    @media (max-width: 1024px) {
        padding: 47px var(--gutter);
    }
}

.crrc-page .ets-intro .lead {
    width: 41vw;
    margin: 0 auto;
    line-height: 1.1;
    color: var(--ink);

    @media (max-width: 1024px) {
        width: auto;
    }
}

/* 2026-10-12 feedback (final): stats are a 2x2 grid now (not a 1x4 row and
   not a horizontal marquee). Text stats (Power Supply, Formation, GoA)
   wrap to two lines inside their cell; numeric stats stay on one line.
   Dotted dividers run between the two columns on each row. */
.ets-stats {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    row-gap: 2.5vw;
    margin-top: 7.1vw;
    padding-top: 1.25vw;
    border-top: 2px dotted #bebebe;
    text-align: left;

    @media (max-width: 1024px) {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        row-gap: 31px;
        margin-top: 39px;
        padding-top: 23px;
    }
}

/* Each stat lays out as a column with the number pinned to the top and the
   label pinned to the bottom, so the two rows read as consistent bands even
   when stat values take different numbers of lines. */
.ets-stat {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
}

/* Dotted vertical divider between the two columns on each row — drawn as a
   pseudo-element so the row-2 one can start BELOW the horizontal divider
   (otherwise the two meet at a connected T; the client wants a visible
   gap, matching the top-border detail on .ets-stats). */
.ets-stat:nth-child(2n) {
    position: relative;
    padding-left: 1.319vw;

    @media (max-width: 1024px) {
        padding-left: 16px;
    }
}

/* The left-column cells need matching right-side breathing room so copy does
   not run flush against the vertical divider (client, 2026-10-19). */
.ets-stat:nth-child(2n+1) {
    padding-right: 1.319vw;

    @media (max-width: 1024px) {
        padding-right: 16px;
    }
}

.ets-stat:nth-child(2n)::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    border-left: 2px dotted #bebebe;
}

/* Row-2 vertical starts below the horizontal line so they do not touch. */
.ets-stat:nth-child(4)::before {
    top: 1.25vw;

    @media (max-width: 1024px) {
        top: 23px;
    }
}

/* Horizontal dotted divider between row 1 and row 2. */
.ets-stat:nth-child(n+3) {
    border-top: 2px dotted #bebebe;
    padding-top: 1.25vw;

    @media (max-width: 1024px) {
        padding-top: 23px;
    }
}

.ets-stat--text .ets-stat__num {
    overflow-wrap: anywhere;
    word-break: break-word;
    /* 2026-10-19: no soft hyphens. On 3CS the hyphenated "Over-head" rendered
       flush against the dotted right-column rule (client's "dash is too close
       to the line"), so we let the browser break at word boundaries and only
       mid-word via overflow-wrap when a single word doesn't fit. */
    /* allow two lines; after that, truncate so we don't blow out row height */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.ets-stat--text .ets-stat__num span {
    display: inline;
    white-space: normal;
}

/* below 1440 a TEXT stat ("AC 25 kV 50 Hz Overhead Catenary") keeps the h1
   floor while its column shrinks, and ran to four lines at 891. It scales
   with the column instead, floored at the h2 size (2026-10-13). */
.crrc-page .ets-stat--text .ets-stat__num {
    @media (min-width: 1025px) and (max-width: 1439px) {
        font-size: max(4.2vw, var(--fs-h2));
    }
}

.crrc-page .ets-stat__num {
    display: flex;
    align-items: flex-start;
    font-size: var(--fs-h1);
    line-height: var(--lh-h1);
    letter-spacing: var(--ls-h1);
    color: var(--ink);

    @media (max-width: 1024px) {
        /* 66px --fs-h1 in a 179px-wide column breaks catastrophically:
           "AC 25 kV 50 Hz Overhead Catenary" wrapped character-by-character.
           Clamp down so text stats read as copy, not a chopped-up grid. */
        font-size: clamp(22px, 6.5vw, 36px);
        line-height: 1.1;
    }
}

/* 2026-10-12: in the marquee layout every stat is on a single line
   (parent .ets-stat has white-space: nowrap). Text and numeric stats
   share the same --fs-h1 size; no overflow-wrap needed. */

.ets-stat__ge {
    font-weight: 300;
    margin-right: 0.1em;
}

.ets-stat__num sup,
.ets-dial__num sup {
    margin: 0.35vw 0 0 0.3vw;
    font-size: var(--fs-p);
    font-weight: 600;
    line-height: var(--lh-p);
    letter-spacing: var(--ls-p);
    color: #bebebe;

    @media (max-width: 1024px) {
        margin-top: 4px;
    }
}

.crrc-page .ets-stat__label {
    margin-top: 3.2vw;
    font-size: var(--fs-p);
    letter-spacing: var(--ls-p);
    line-height: var(--lh-p);
    color: var(--ink);

    @media (max-width: 1024px) {
        margin-top: 8px;
    }
}

.ets-inspo,
.ets-next {
    position: relative;
    height: 55.347vw;
    overflow: hidden;
    color: var(--white);
    isolation: isolate;

    @media (max-width: 1024px) {
        height: 468px;
    }
}

/* the photo is an Image block: its figure fills the band and the img
   inside keeps the rules it had as a bare <img> */
.ets-inspo > .wp-block-image,
.ets-next > .wp-block-image {
    position: absolute;
    inset: 0;
    z-index: -2;
}

.ets-inspo > .wp-block-image > img,
.ets-next > .wp-block-image > img {
    max-width: none;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: -2;
}

.ets-inspo::after,
.ets-next::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: rgba(0, 0, 0, 0.22);
}

.ets-inspo__copy {
    position: absolute;
    left: 0;
    right: 0;
    top: 18.6vw;
    text-align: center;

    @media (max-width: 1024px) {
        top: 148px;
        padding: 0 var(--gutter);
    }
}

.crrc-page :is(.ets-inspo, .ets-next) h2 {
    font-size: var(--fs-h1);
    letter-spacing: var(--ls-h1);
    line-height: var(--lh-h1);
    text-transform: uppercase;
    color: var(--white);

    @media (max-width: 1024px) {
        /* --fs-h1 at mobile overflows ~320px viewports -- "INSPIRATION"
           alone needs ~360px. Clamp down for narrow phones, same pattern as
           the home hero title. */
        font-size: clamp(32px, 11.5vw, 66px);
    }
}

.crrc-page .ets-inspo .body-sm {
    width: 21.5vw;
    margin: 0.9vw auto 0;
    color: #f6f6f6;

    @media (max-width: 1024px) {
        width: auto;
        margin-top: 16px;
    }
}

/* 2026-10-27 (image #51): 3-column Design Inspiration (Shah Alam only).
   Single-column `.ets-inspo__copy` rules above still cover the normal
   layout. The 3-col variant adds `.ets-inspo__copy--3col` and renders
   three icon-topped cards (Window / Exterior / Interior) side by side. */
.ets-inspo__copy--3col {
    padding: 0 6vw;
}

.ets-inspo__cards {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 4vw;
    text-align: center;
}

.ets-inspo__card {
    display: flex;
    flex-direction: column;
    align-items: center;
}

.ets-inspo__icon {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 5vw;
    color: var(--red);
    margin-bottom: 1.6vw;
}

.ets-inspo__icon svg {
    width: auto;
    height: 3.6vw;
    display: block;
}

.ets-inspo__icon img {
    width: 100%;
    max-width: 4.5vw;
    aspect-ratio: 1;
    object-fit: contain;
    display: block;
}

/* seat icon is near-square so it fills the full box where the window and
   exterior icons are landscape; shrink only the seat to match them visually. */
.ets-inspo__icon img[src*="icon-sa-seat"] {
    max-width: 2.6vw;
}

.crrc-page .ets-inspo__cards h3 {
    color: var(--white);
    font-size: var(--fs-h1);
    letter-spacing: var(--ls-h1);
    line-height: var(--lh-h1);
    text-transform: uppercase;
    text-align: center;
}

.crrc-page .ets-inspo__cards .body-sm {
    width: auto;
    margin: 0.9vw 0 0;
    color: #f6f6f6;
}

@media (max-width: 1024px) {
    .ets-inspo__copy--3col {
        padding: 0 var(--gutter);
    }
    .ets-inspo__cards {
        grid-template-columns: minmax(0, 1fr);
        gap: 31px;
    }
    .ets-inspo__icon svg { height: 42px; }
    .ets-inspo__icon img { max-width: 46px; }
    .ets-inspo__icon img[src*="icon-sa-seat"] { max-width: 27px; }
    .ets-inspo__icon { height: 50px; margin-bottom: 12px; }
    .crrc-page .ets-inspo__cards .body-sm { margin-top: 12px; }
}

.ets-gallery {
    height: 55.347vw;
    padding-top: 11.944vw;
    background: var(--ink);

    @media (max-width: 1024px) {
        height: auto;
        padding: 55px 0;
    }
}

/* a constant-speed reel since 2026-09-25 (PAGE_JS reels()) */
.ets-gallery__rail {
    overflow: hidden;
}

.ets-gallery__track {
    display: flex;
    width: max-content;
    will-change: transform;
}

.ets-gallery__track ul {
    display: flex;
    gap: 2.708vw;
    margin: 0;
    padding: 0 2.708vw 0 0;
    list-style: none;

    @media (max-width: 1024px) {
        gap: 16px;
        padding-right: 16px;
    }
}

.ets-gallery__track li {
    flex: none;
    width: 42.361vw;

    @media (max-width: 1024px) {
        width: 328px;
    }
}

.ets-gallery figure {
    position: relative;
    height: 31.458vw;
    border-radius: 0.694vw;
    overflow: hidden;

    @media (max-width: 1024px) {
        height: 226px;
        border-radius: var(--radius);
    }
}

/* --- ets1: the band is pinned and masked away (2026-09-25) ------------
   After white-desert.com's .ct-antartica_section, on request. PAGE_JS
   reveal() adds .is-reveal (desktop + motion only) and writes --t, the
   pinned progress, and --p, the mask. Everything here is inert without the
   class, and the two sections stack exactly as the export draws them.

   The box is one viewport of pin plus 150vh of run. The band sits over the
   gallery in the same sticky frame; its clip closes from the bottom up and
   in from the sides, rounding as it goes, so it lifts off like a card. */
.ets-reveal.is-reveal {
    height: 250vh;
}

.is-reveal .ets-reveal__pin {
    position: sticky;
    top: 0;
    height: 100vh;
    overflow: hidden;
    background: var(--ink);
}

.is-reveal :is(.ets-inspo, .ets-gallery) {
    position: absolute;
    inset: 0;
    height: auto;
}

/* The mask is three VERTICAL bands, 1/6, 2/6 and 3/6 of the width from the
   left (2026-09-25, on request; turned from horizontal bands the same day).
   All three close from their own right edge at the same time and finish
   together, so the edges travel at 1 : 2 : 3 and the band comes away in
   steps. One polygon: the three visible rectangles joined by zero-width
   slivers along the top edge. */
.is-reveal .ets-inspo {
    --k: calc(1 - var(--p, 0));
    z-index: 2;
    clip-path: polygon(
        0 0, 0 100%,
        calc(var(--k) * 100% / 6) 100%, calc(var(--k) * 100% / 6) 0,
        calc(100% / 6) 0, calc(100% / 6) 100%,
        calc(100% / 6 + var(--k) * 200% / 6) 100%, calc(100% / 6 + var(--k) * 200% / 6) 0,
        50% 0, 50% 100%,
        calc(50% + var(--k) * 50%) 100%, calc(50% + var(--k) * 50%) 0);
}

/* the parallax: the photo runs slower than the pin across the whole passage,
   oversized so its edges never show */
.is-reveal .ets-inspo > .wp-block-image > img {
    transform: translateY(calc((var(--t, 0) - 0.5) * 18vh)) scale(1.2);
}

.is-reveal .ets-inspo__copy {
    top: 50%;
    transform: translateY(calc(-50% - var(--p, 0) * 22vh));
    opacity: calc(1 - var(--p, 0) * 1.6);
}

.is-reveal .ets-gallery {
    z-index: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding-top: 0;
}

/* the gallery does not move while the band is taken off it (2026-09-25, on
   request): it sits still in the pin and scrolls on with the page after */

.ets-speed {
    display: grid;
    grid-template-columns: 28.889vw minmax(0, 1fr);
    column-gap: 7.014vw;
    padding: 5.833vw 8vw 0 15.417vw;

    @media (max-width: 1024px) {
        grid-template-columns: minmax(0, 1fr);
        row-gap: 39px;
        padding: 55px var(--gutter) 0;
    }
}

.ets-dial {
    position: relative;
    aspect-ratio: 1;
    display: grid;
    place-items: center;

    @media (max-width: 1024px) {
        width: 304px;
        margin: 0 auto;
    }
}

.ets-dial__ring {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    fill: none;
    stroke: #bebebe;
    stroke-width: 1;
    overflow: visible;
    --ln: self;
    --ln-mask: var(--ln-sweep);
}

.ets-dial__ring * {
    vector-effect: non-scaling-stroke;
}

.crrc-page .ets-dial__num {
    position: relative;
    display: flex;
    align-items: flex-start;
    font-size: 10.855vw;  /* 156.308 / 147, -9.498; pure vw, it is a graphic (2026-10-13) */
    line-height: 0.929;
    letter-spacing: -0.06em;
    color: var(--ink);

    @media (max-width: 1024px) {
        font-size: 156px;
    }
}

.ets-speed__copy {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    padding-bottom: 2.3vw;
}

.crrc-page .ets-speed__copy h2 {
    width: 31.042vw;
    padding-bottom: 0.9vw;
    border-bottom: 2px dotted #bebebe;
    font-size: var(--fs-h2);
    line-height: var(--lh-h2);
    letter-spacing: var(--ls-h2);
    text-transform: none;

    @media (max-width: 1024px) {
        width: 100%;
    }
}

.crrc-page .ets-speed__copy .body-sm {
    width: 15vw;
    margin-top: auto;

    @media (max-width: 1024px) {
        width: auto;
        margin-top: 20px;
    }
}

/* 2026-10-12: key features injected inside .ets-speed__copy by the Insert
   PHP Code Snippet plugin (title "CRRC — train Key Features inject"). A plain
   <ul> rendered as two columns; each <li> has an <h4> and a <p>. ETS1 keeps
   the single dial_body paragraph above and renders no feats. */
.ets-speed__feats {
    width: 100%;
    margin-top: 2.5vw;
    padding: 0;
    list-style: none;
    /* 2026-10-12: switched from `columns: 2` (masonry — titles drift) to a
       2-col grid with subgrid rows, so every item's <h4> sits on the same
       row and every <p> sits on the row below, both across columns. */
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-auto-rows: auto;
    column-gap: 2.4vw;
    row-gap: 1.4vw;

    > li {
        display: grid;
        grid-template-rows: subgrid;
        grid-row: span 2;
        row-gap: 0.4em;
        margin: 0;
    }
    > li h4 {
        font-size: var(--fs-h4);
        line-height: var(--lh-h4);
        letter-spacing: var(--ls-h4);
        font-weight: 600;
        color: var(--ink);
        margin: 0;
    }
    > li p {
        font-size: var(--fs-p);
        line-height: var(--lh-p);
        letter-spacing: var(--ls-p);
        color: var(--body-grey);
        margin: 0;
    }

    @media (max-width: 1024px) {
        margin-top: 24px;
        grid-template-columns: minmax(0, 1fr);

        > li {
            grid-row: span 2;
            margin: 0 0 18px;
        }
    }
}

.ets-spec {
    padding: 6.667vw 22.5vw 5.486vw;

    @media (max-width: 1024px) {
        padding: 55px var(--gutter);
    }
}

.ets-spec table {
    width: 100%;
    margin-top: 0.5vw;
    border-collapse: collapse;
}

.ets-spec .wp-block-table :is(th, td) {
    height: 3.472vw;
    padding: 0 0 0.76vw;
    vertical-align: bottom;
    text-align: left;
    font-size: var(--fs-p);
    letter-spacing: var(--ls-p);
    font-weight: 600;
    line-height: var(--lh-p);
    /* the core Table block's own sheet boxes every cell in 1px solid
       (.wp-block-table td/th); the design has only a dotted rule under
       each row, the sitewide 2px dotted (10.29) */
    border: 0;
    border-bottom: 2px dotted #bebebe;

    @media (max-width: 1024px) {
        height: auto;
        padding: 16px 0 12px;
    }
}

.ets-spec th {
    width: 51%;
    padding-right: 2vw;
    color: var(--ink);
}

.ets-spec td {
    color: var(--body-grey);
}

.ets-next {
    height: 27.431vw;

    @media (max-width: 1024px) {
        height: 429px;
    }
}

/* the photo is the next train's own (2026-09-28), so it is a plain cover.
   The export's 148% zoom was cut for the stand-in photo it drew there. */
.ets-next > .wp-block-image > img {
    object-position: 50% 60%;
}

.ets-next__copy {
    position: absolute;
    left: 25.6vw;
    top: 3.3vw;
    transform: translateX(-50%);
    text-align: center;

    @media (max-width: 1024px) {
        left: 50%;
        top: auto;
        bottom: 47px;
        width: 335px;
    }
}

/* the next train's full name, which runs to 70 characters on RTS Link */
.crrc-page .ets-next .lead {
    width: 24vw;
    margin: 0.6vw auto 1.4vw;
    line-height: 1.1;
    color: var(--white);

    @media (max-width: 1024px) {
        width: auto;
        margin: 12px 0 20px;
    }
}

/* the button block is a flex row; centre it under the copy */
.ets-next__copy > .crrc-btn {
    justify-content: center;
}

/* Design notes and key features (2026-09-28, 10.34): the heading on the
   speed section's left column, a numbered list on its right. Each row is a
   dotted rule, a counter, a title and its copy; a single design note is one
   lead paragraph under the rule. */
.ets-feats {
    display: grid;
    grid-template-columns: 28.889vw minmax(0, 1fr);
    column-gap: 7.014vw;
    align-items: start;
    padding: 6.667vw 8vw 0 15.417vw;

    @media (max-width: 1024px) {
        grid-template-columns: minmax(0, 1fr);
        row-gap: 24px;
        padding: 55px var(--gutter) 0;
    }
}

.crrc-page .ets-feats h2 {
    font-size: var(--fs-h2);
    line-height: var(--lh-h2);
    letter-spacing: var(--ls-h2);
    text-transform: none;
}

/* Key Features heading uses the .h1 token (bigger + uppercase) --
   client, 2026-10-15. The base .ets-feats h2 override above drops h1
   sizing on every Key Features / Design Notes h2; this restores it only
   where the heading carries the .h1 class (Key Features, which is now
   always .ets-feats-key). */
.crrc-page .ets-feats h2.h1 {
    font-size: var(--fs-h1);
    line-height: var(--lh-h1);
    letter-spacing: var(--ls-h1);
    text-transform: uppercase;
}

.ets-feats__list {
    counter-reset: feat;
}

.crrc-page .ets-feat {
    counter-increment: feat;
    display: grid;
    grid-template-columns: 3.472vw minmax(0, 1fr);
    column-gap: 1.389vw;
    padding: 1.25vw 0 2.2vw;
    border-top: 2px dotted #bebebe;

    &::before {
        content: counter(feat, decimal-leading-zero);
        grid-row: 1 / span 2;
        font-size: var(--fs-p);
        line-height: var(--lh-h3);
        letter-spacing: var(--ls-p);
        font-weight: 600;
        color: #bebebe;
    }

    @media (max-width: 1024px) {
        grid-template-columns: 36px minmax(0, 1fr);
        column-gap: 12px;
        padding: 16px 0 24px;
    }
}

.is-single > .ets-feat {
    grid-template-columns: minmax(0, 1fr);

    &::before {
        content: none;
    }
}

.crrc-page .ets-feat h3 {
    width: 26vw;
    font-size: var(--fs-h3);
    line-height: var(--lh-h3);
    letter-spacing: var(--ls-h3);
    text-transform: none;
    color: var(--ink);

    @media (max-width: 1024px) {
        width: auto;
    }
}

.crrc-page .ets-feat .body-sm {
    width: 30vw;
    margin-top: 0.9vw;

    @media (max-width: 1024px) {
        width: auto;
        margin-top: 10px;
    }
}

.crrc-page .ets-feat .lead {
    width: 36vw;
    line-height: 1.1;
    color: var(--ink);

    @media (max-width: 1024px) {
        width: auto;
    }
}

/* a gallery that stands on its own (ETS2): no pin to sit in */
.ets-feats + .ets-gallery,
.ets-feats + .wp-block-html + .ets-gallery {
    margin-top: 6.667vw;
}

/* Key Features ACCORDION (2026-10-14, image #13, client). Heading on the left
   in the shared ets-feats grid; cards on the right. One card open at a time
   (PAGE_JS accordion()); the body expands via the grid-template-rows 0fr->1fr
   trick, so the transition stays smooth and the collapsed card measures from
   the closed body. Only the ets-feats-key variant becomes an accordion --
   ets-feats-design (ETS1's design notes section, Shah Alam's design notes)
   keeps the numbered-list look above. */
.ets-feats-key .ets-feats__accordion {
    display: flex;
    flex-direction: column;
    row-gap: 0.833vw;

    @media (max-width: 1024px) {
        row-gap: 10px;
    }
}

/* The card itself carries the 1px border always; the shadow lands on the
   active row only (client, 2026-10-15). */
.ets-feats-key .ets-feat {
    display: block;
    counter-increment: none;
    padding: 0;
    border: 1px solid #EBEBEB;
    border-radius: 1.111vw;
    background: #fff;
    overflow: hidden;
    transition: box-shadow 0.3s ease;

    &::before {
        content: none;
    }

    @media (max-width: 1024px) {
        border-radius: 12px;
    }
}

.ets-feats-key .ets-feat.is-open {
    box-shadow: 15px 15px 30px 0 rgba(0, 0, 0, 0.08);
}

.ets-feats-key .ets-feat__head {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    column-gap: 1.111vw;
    align-items: center;
    width: 100%;
    padding: 1.042vw 1.389vw;
    background: none;
    border: 0;
    cursor: pointer;
    text-align: left;
    color: var(--ink);
    font: inherit;

    @media (max-width: 1024px) {
        column-gap: 14px;
        padding: 14px 16px;
    }
}

.ets-feats-key .ets-feat__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.778vw;
    height: 2.778vw;
    border-radius: 0.556vw;
    /* 2026-10-19 (client): closed rows get a white tile + ink glyph with a
       hairline border; only the currently-open row keeps the red tile + white
       glyph. The .is-open rule below swaps them back for the active card. */
    background: #fff;
    color: var(--ink);
    border: 1px solid #e5e5e5;
    font-size: 1.042vw;
    line-height: 1;
    letter-spacing: -0.02em;
    font-weight: 600;
    flex-shrink: 0;
    transition: background 0.3s ease, color 0.3s ease, border-color 0.3s ease;

    /* Inline SVG icon (client-supplied). currentColor on the paths picks up
       the tile's text colour (ink when closed, white when open). */
    & > svg {
        width: 55%;
        height: 55%;
    }

    @media (max-width: 1024px) {
        width: 36px;
        height: 36px;
        border-radius: 8px;
        font-size: 13px;
    }
}

.ets-feats-key .ets-feat.is-open .ets-feat__icon {
    background: var(--red);
    color: #fff;
    border-color: transparent;
}

.crrc-page .ets-feats-key .ets-feat__title {
    display: block;
    width: auto;
    font-size: var(--fs-h4);
    line-height: var(--lh-h4);
    letter-spacing: var(--ls-h4);
    font-weight: 600;
    color: var(--ink);
    text-transform: none;
}

.ets-feats-key .ets-feat__toggle {
    position: relative;
    display: inline-block;
    width: 1.667vw;
    height: 1.667vw;
    border-radius: 50%;
    background: #f3f3f3;
    flex-shrink: 0;
    transition: background 0.3s ease;

    &::before,
    &::after {
        content: '';
        position: absolute;
        top: 50%;
        left: 50%;
        background: var(--ink);
        transition: transform 0.3s ease, opacity 0.3s ease;
    }

    /* horizontal bar: always drawn (minus state). */
    &::before {
        width: 0.694vw;
        height: 0.139vw;
        transform: translate(-50%, -50%);
    }

    /* vertical bar: drawn when closed, hidden when open (plus -> minus). */
    &::after {
        width: 0.139vw;
        height: 0.694vw;
        transform: translate(-50%, -50%);
    }

    @media (max-width: 1024px) {
        width: 24px;
        height: 24px;

        &::before { width: 10px; height: 2px; }
        &::after  { width: 2px;  height: 10px; }
    }
}

.ets-feats-key .ets-feat.is-open .ets-feat__toggle {
    background: var(--ink);

    &::before { background: #fff; }
    &::after {
        opacity: 0;
        transform: translate(-50%, -50%) scaleY(0);
    }
}

/* grid-template-rows 0fr -> 1fr, same trick as the footer dropdown
   (2026-10-14). The outer body is the grid container (the single row);
   .ets-feat__body-inner is the grid child that carries the actual padding
   so the row can collapse cleanly to 0 (client, 2026-10-15). Padding on
   the <p> did not work: padding is part of the box, and a grid row set to
   0fr can't shrink an item below its own padding -- the sliver of padding
   leaked visible body text out of every closed card. With padding on an
   inner wrapper that is INSIDE the row, the whole wrapper (padding and
   all) collapses with the row. */
.ets-feats-key .ets-feat__body {
    display: grid;
    /* minmax(0, 0fr): a bare 0fr has an implicit min of auto, which uses
       the inner wrapper's min-content (padding included), so the row
       resolves to the padding size (~22px) and the body never fully
       collapses. minmax(0, ...) pins the min to 0, so a closed card
       actually goes to 0 (client, 2026-10-15). */
    grid-template-rows: minmax(0, 0fr);
    overflow: hidden;
    transition: grid-template-rows 0.55s cubic-bezier(.22, 1, .36, 1);
}

.ets-feats-key .ets-feat.is-open .ets-feat__body {
    grid-template-rows: minmax(0, 1fr);
}

.ets-feats-key .ets-feat__body-inner {
    overflow: hidden;
    min-height: 0;
    padding: 0 1.389vw 1.389vw calc(1.389vw + 2.778vw + 1.111vw);

    @media (max-width: 1024px) {
        padding: 0 16px 16px calc(16px + 36px + 14px);
    }
}

.ets-feats-key .ets-feat__body-inner > p {
    margin: 0;
    font-size: var(--fs-p);
    line-height: var(--lh-p);
    letter-spacing: var(--ls-p);
    color: #6b7174;
}

/* Reduced motion: no toggle animation, no body transition. */
@media (prefers-reduced-motion: reduce) {
    .ets-feats-key .ets-feat,
    .ets-feats-key .ets-feat__body,
    .ets-feats-key .ets-feat__toggle,
    .ets-feats-key .ets-feat__toggle::before,
    .ets-feats-key .ets-feat__toggle::after {
        transition: none;
    }
}

/* ETS2-only Key Feature split layout: rounded photo left, big heading
   with underline + body right. Replaces the Key Features accordion for
   ETS2 (client, 2026-10-16, applied from image #7 pattern). */
.ets-feats-split {
    display: grid;
    grid-template-columns: 29.167vw minmax(0, 1fr);
    column-gap: 5vw;
    align-items: stretch;
    padding: 6.667vw 8vw 6.667vw 15.417vw;

    @media (max-width: 1024px) {
        grid-template-columns: minmax(0, 1fr);
        row-gap: 24px;
        padding: 55px var(--gutter);
    }
}

.ets-feats-split__media {
    margin: 0;
    aspect-ratio: 420 / 420;
    border-radius: 1.667vw;
    overflow: hidden;

    @media (max-width: 1024px) {
        border-radius: 16px;
    }
}

.ets-feats-split__media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.ets-feats-split__copy {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding-top: 0.5vw;

    @media (max-width: 1024px) {
        padding-top: 0;
        gap: 24px;
    }
}

.crrc-page .ets-feats-split h2 {
    font-size: var(--fs-h1);
    line-height: var(--lh-h1);
    letter-spacing: var(--ls-h1);
    text-transform: none;
    color: var(--ink);
    margin: 0;
    padding-bottom: 0.9vw;
    border-bottom: 2px dotted #bebebe;

    @media (max-width: 1024px) {
        padding-bottom: 12px;
    }
}

.ets-feats-split__copy p {
    margin: 0;
    font-size: var(--fs-p);
    line-height: var(--lh-p);
    letter-spacing: var(--ls-p);
    color: #6b7174;
    max-width: 23vw;

    @media (max-width: 1024px) {
        max-width: none;
    }
}

/* --- unique pop-tab layouts (10.37) -----------------------------------
   The two first redesigns of the train pages, from the client's second and
   third `pop tab intercity` exports. Each train gets a `layout` key in
   build-pages.py that picks one of these; the rest of the pop-tab template
   (hero, dial, feats, spec table, Discover next) stays shared.

   intercity-2 (ETS2): a dark intro+stats band and a Design Inspiration band
   that is the hero photo full-bleed with a dark overlay, the heading on the
   left and an interior shot inset to the right. All y below are design pt
   / 14.4 against the 1440 artboard, from `pages-pdf/pop tab intercity-2.pdf`
   (10.17a is the method). */
/* intercity-2 dark band: 100vh, lead paragraph centred at the top and the
   4-stat row (base .ets-stats, 5542) at the bottom (client, 2026-10-05 --
   the section is a full viewport). The lead text is placeholder lorem
   ipsum until real copy comes in. */
.ets-intro.ets-intro-dark {
    background: var(--ink);
    color: var(--white);
    height: 100vh;
    min-height: 500px;
    /* all dimensions vw against the 1440 artboard (CLAUDE.md 10.4): lead at
       ~17% from top of a 1440x900 window, stats bottom at ~82%. */
    padding: 11vw 8.47vw;
    display: flex;
    flex-direction: column;
    justify-content: space-between;

    /* 2026-10-12: if the lead paragraph is absent (SCF `intro_dark` empty),
       the plugin snippet strips it from the DOM. Collapse the band so there
       is no empty space above the stats — just enough padding around the
       stat row. */
    &:not(:has(.lead)) {
        height: auto;
        min-height: 0;
        padding: 6vw 8.47vw;
        justify-content: center;
    }

    @media (max-width: 1024px) {
        height: auto;
        min-height: 0;
        padding: 55px var(--gutter);
        gap: 55px;

        &:not(:has(.lead)) {
            padding: 55px var(--gutter);
        }
    }
}

.crrc-page .ets-intro-dark .lead {
    color: var(--white);
    width: 36vw;
    margin: 0 auto;

    @media (max-width: 1024px) {
        width: auto;
    }
}

/* dividers stand out against the dark ink; the 2px #BEBEBE sitewide dot
   (10.29) still reads */
.crrc-page .ets-intro-dark .ets-stat__num { color: var(--white); }
.crrc-page .ets-intro-dark .ets-stat__label { color: #f6f6f6; }

/* intercity-2 inspiration band: the shared .ets-inspo rules give it the
   bg photo at inset 0, the dark overlay and a 55.347vw height. The copy
   block sits on the LEFT with the text CENTRED within it (client,
   2026-10-05 -- matches ETS1's layout) and the overlay darkens, with an
   interior-shot inset on the right (inset frame 610x453 at (769, 2409)
   inside a 1440x797 section at y 2235). */
.ets-inspo-bg::after { background: rgba(0, 0, 0, 0.5); }

.ets-inspo-bg .ets-inspo__copy {
    left: 6vw;
    right: auto;
    top: 15vw;
    width: 36vw;
    text-align: center;

    @media (max-width: 1024px) {
        top: 55px;
        left: 0;
        right: 0;
        width: auto;
        padding: 0 var(--gutter);
    }
}

.crrc-page .ets-inspo-bg .body-sm {
    width: 26vw;
    margin: 1.25vw auto 0;

    @media (max-width: 1024px) {
        width: auto;
        margin-top: 16px;
    }
}

/* All train heroes are 100vh (client, 2026-10-05 -- same CSS and block
   layout across every ETS/train page; ets2 adds its own zoom + darken +
   lead cross-fade on top via heroZoom(), the rest just sit at 100vh with
   their h1 at the bottom-left). */
.crrc-page-train .ets-hero {
    height: 100vh;
    min-height: 500px;

    @media (max-width: 1024px) {
        height: auto;
        min-height: 78vh;
    }
}

.ets-hero.is-zoom > .wp-block-image {
    transform-origin: center center;
    will-change: transform, filter;
}

.ets-hero__lead-wrap {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 var(--gutter);
    z-index: 2;
    pointer-events: none;
}

.crrc-page .ets-hero__lead {
    color: var(--white);
    font-size: var(--fs-h4);
    letter-spacing: var(--ls-h4);
    line-height: var(--lh-h4);
    width: 42vw;
    text-align: center;
    margin: 0;

    @media (max-width: 1024px) {
        width: auto;
    }
}

.ets-inspo__inset {
    position: absolute;
    left: 53.4vw;
    top: 12.08vw;
    width: 42.36vw;
    height: 31.46vw;
    border-radius: 0.694vw;
    overflow: hidden;
    z-index: 1;

    @media (max-width: 1024px) {
        position: static;
        width: calc(100% - var(--gutter) * 2);
        height: auto;
        aspect-ratio: 610 / 453;
        margin: 32px var(--gutter) 0;
        border-radius: var(--radius);
    }
}

/* 2026-10-19: when there's no "Design Inspiration" copy on the left (DMU and
   6-axle Locomotive -- the snippet emits an empty .ets-inspo__copy for these),
   centre the inset frame instead of leaving it in its right-column slot. */
.ets-inspo-bg:has(.ets-inspo__copy:empty) .ets-inspo__inset {
    left: calc(50% - 42.36vw / 2);

    @media (max-width: 1024px) {
        left: auto;
    }
}

/* each pane fills the box; stacked by z-index via inspoSwap() so the first
   pane shows at rest and later panes are revealed as GSAP clip-paths each
   previous pane away toward the centre on scroll (10.37, 2026-10-05). */
.ets-inspo__pane {
    position: absolute;
    inset: 0;
}

.ets-inspo__pane img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* intercity-2 is a short, image-driven page; the inset frame extends
   below the band on short viewports, so let it come after in flow */
.ets-inspo-bg {
    @media (max-width: 1024px) {
        height: auto;
        padding-bottom: 55px;
    }
}

/* intercity-3 (ETS3): a light intro section with the lead on the left and a
   2x2 stats grid on the right (big ink numbers, 2px dotted left dividers on
   each stat, a single horizontal rule between the rows), and a dark
   Design Inspiration band whose heading and body copy are centred over
   two photos arranged below -- the larger 610x453 at (415, 2367), the
   smaller 414x307 at (1064, 2440), inside a 1440x1601 section at y 1452.
   Measurements from `pages-pdf/pop tab intercity-3.pdf`. */
/* 2026-10-27 feedback: reverted to the earlier left/right layout
   (lead on the left, 2x2 stats on the right) — the 2026-10-14 stacked
   variant was pushed back from. The right column takes more width than
   the previous version so "articulated" does not break as "articu-lated"
   on the 3-car heading; the left column is tightened to match the mock.
   Column-gap matches the stacked variant's row-gap so the spacing between
   the two blocks is unchanged. */
.ets-intro.ets-intro-grid {
    display: grid;
    grid-template-columns: 22vw minmax(0, 1fr);
    column-gap: 3vw;
    row-gap: 3vw;
    align-items: start;
    padding: 4.72vw 8.47vw 7.5vw;
    text-align: left;

    @media (max-width: 1024px) {
        grid-template-columns: minmax(0, 1fr);
        column-gap: 0;
        row-gap: 32px;
        padding: 55px var(--gutter);
    }
}

.crrc-page .ets-intro-grid .lead {
    /* lead copy sits in the narrow left column of the 2-col grid; it
       takes the column's full width, so no explicit cap is needed here. */
    width: auto;
    max-width: none;
    margin: 0;
    text-align: left;
}

/* 2x2 grid: left dotted divider on every stat (not just 2nd+), one horizontal
   dotted rule between rows 1 and 2. The sitewide 2px dotted #BEBEBE (10.29)
   still applies. */
.ets-intro-grid .ets-stats {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    margin-top: 0;
    padding-top: 0;
    border-top: none;
    column-gap: 2vw;
    row-gap: 0;

    @media (max-width: 1024px) {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        row-gap: 0;
    }
}

.ets-intro-grid .ets-stat {
    padding-left: 1.319vw;
    padding-bottom: 2vw;
    border-left: 2px dotted #bebebe;

    @media (max-width: 1024px) {
        padding-left: 16px;
        padding-bottom: 24px;
    }
}

/* The base .ets-stat:nth-child(2n)::before draws its own dotted divider on
   the right-column stats; .ets-intro-grid already gives EVERY stat a
   border-left, so suppressing the ::before stops the two from doubling up
   (2026-10-14). */
.ets-intro-grid .ets-stat:nth-child(2n)::before {
    display: none;
}

.ets-intro-grid .ets-stat:nth-child(n+3) {
    padding-top: 2vw;
    border-top: 2px dotted #bebebe;

    @media (max-width: 1024px) {
        padding-top: 24px;
    }
}

/* intercity-3 inspiration band (reworked 2026-10-05): 100vh dark panel. At
   rest the heading + body copy are centred; PAGE_JS inspoReveal() pins the
   section and GSAP cross-fades the copy out while a horizontal image slider
   fades in, then scrubs the strip through its images. */
.ets-inspo-dark {
    height: 100vh;
    min-height: 500px;
    background: var(--ink);

    @media (max-width: 1024px) {
        height: auto;
        padding-bottom: 55px;
    }
}

.ets-inspo-dark::after { display: none; }

.ets-inspo-dark .ets-inspo__copy {
    position: absolute;
    inset: 0;
    top: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 0 var(--gutter);

    @media (max-width: 1024px) {
        position: static;
        padding: 55px var(--gutter) 0;
    }
}

.crrc-page .ets-inspo-dark .body-sm {
    width: 26vw;
    margin: 1.25vw auto 0;

    @media (max-width: 1024px) {
        width: auto;
        margin-top: 16px;
    }
}

/* the slider: a horizontal strip positioned absolute over the section. One
   figure centres in the viewport, the next peeks from the right. The strip
   is translated by GSAP; the stylesheet only sets rest state. */
.ets-inspo__slider {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    overflow: hidden;
    pointer-events: none;

    @media (max-width: 1024px) {
        display: none;
    }
}

/* 2026-10-10 feedback #21: scroll-to-pan rebuild (after Hyperplexed's
   "Sliding Image Track"). The strip is a horizontal row of figures; GSAP
   pans it leftward with scroll (xPercent), and each image's own
   object-position pans the opposite way. CSS sets rest state only. */
.ets-inspo__strip {
    display: flex;
    gap: 1.5vw;
    width: max-content;         /* so pan works off scrollWidth */
    height: 100%;
    align-items: center;
    padding: 0 2vw;             /* first image is inset from viewport edge */
    will-change: transform;
}

.ets-inspo__strip > figure {
    flex: 0 0 42.36vw;
    aspect-ratio: 610 / 453;
    margin: 0;
    border-radius: 0.694vw;
    overflow: hidden;
}

.ets-inspo__strip img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
    will-change: transform;         /* scale + translateX tweened by GSAP */
}

/* the two photos: left 42.36x31.46vw starting at (415, 2367); right
   28.75x21.32vw starting at (1064, 2440). The right photo starts 5.07vw
   below the left one in the design, so the pair reads as a composition
   rather than a flat row. */
/* ==========================================================================
   TEXT REVEAL (10.14)
   ==========================================================================

   THE LINE IS THE UNIT. These exports compose every heading as stacked lines
   with explicit breaks (10.8g), so the line is what the designer drew and the
   line is what moves: each rises into place from behind its own clip edge, one
   after the next. An eyebrow lays its rule in from the left and the word arrives
   after it. Supporting copy and the button follow on a short rise. Nothing
   scrambles, types or bounces -- this is heavy geometric caps at line-height 0.9
   for a rolling-stock manufacturer, and the rest of the site's motion (the hero
   flight, the parallax, Lenis's glide) is all long, eased and single-minded.

   WHAT ANIMATES IS DECIDED HERE, not in text-reveal.js. An element opts in by
   carrying a `--tr` and out by carrying `none` or nothing; the script has no
   selector list at all. Same arrangement as `--par-img` (10.13) and for the same
   reason. Registered `inherits: false` so a value set on a heading cannot leak
   down to something inside it and enrol it too. */
@property --tr { syntax: "*"; inherits: false; }

/* The second opt-in: text that lives inside a component -- a Splide slide or an
   MRO card -- and plays again every time that component becomes current or
   comes back on screen (10.14b). `inherits: false` for the same reason as
   `--tr` above. */
@property --tr-replay { syntax: "*"; inherits: false; }

/* How long a component's replay waits before it starts, set on the component
   (only the MRO card uses it). Registered <time> so a calc() resolves to plain
   seconds the script can read; `inherits: true` so the text inside picks it up. */
@property --tr-wait { syntax: "<time>"; inherits: true; initial-value: 0s; }

/* An element's own extra start delay, added to --tr-d by the script (so the
   finish timer waits for it too). Only the home hero headline uses it. */
@property --tr-hold { syntax: "<time>"; inherits: false; initial-value: 0s; }

/* The timing lives here as well, so the numbers are in one place: the script
   reads these three back off the element. 0.9s on the site's own expo-out-ish
   curve is Lenis's glide (10.10), not a separate vocabulary. */
:is(.crrc-home, .crrc-page, .crrc-footer) {
    --ln-dur: 1.1s;
    --ln-ease: cubic-bezier(0.65, 0, 0.35, 1);
    --tr-dur: 0.9s;
    --tr-step: 0.085s;
    --tr-batch: 0.07s;
    --tr-ease: cubic-bezier(.16, 1, .3, 1);
}

:is(.crrc-home, .crrc-page) :is(h1, h2) { --tr: lines; }

/* The hero headline is a FLEX ROW of two halves set to opposite edges (9.2), so
   the row itself must not be split -- rebuilding it from text would delete the
   two spans that do the splitting. Each half is the real target.
   (0,2,0) and (0,2,1): the blanket rule above is (0,1,1) and would otherwise
   outrank a plain class here, which is 10.8b's trap. */
.crrc-home .section-hero__title { --tr: none; }
.crrc-home .section-hero__title > span { --tr: lines; --tr-hold: 1.7s; }

:is(.crrc-home, .crrc-page) .eyebrow { --tr: rule; }

/* Section-level copy only. The 65 `.body-sm` paragraphs inside cards are
   deliberately left alone: putting every one of them on a reveal reads as noise,
   and those cards already carry their own behaviour -- the MRO accordion (10.6),
   the solutions drum and the Splide carousels (10.12, 10.8c). The same rule
   keeps the button list to the two that sit in a section's own copy block; the
   other five live inside the solutions cards and the MRO panels, which are
   clipped out of view at rest, so an observer would never fire for them and they
   would still be at opacity 0 when their card was finally opened. */
:is(.crrc-home, .crrc-page) :is(.rule-lead, .lead, .svc-intro__statement),
.section-network__note,
.section-about__intro-copy > p,
.section-solutions__head > p:not(.eyebrow),
.section-mro__head > p:not(.eyebrow),
.section-localization__copy,
.section-about__intro-copy > .crrc-btn,
.section-localization > .crrc-btn { --tr: rise; }

/* --- text inside a component: same three modes, plus `--tr-replay` ---

   A Splide slide that is not current is parked off to the side of the viewport
   and NEVER INTERSECTS IT, so a one-shot reveal leaves it at opacity 0 for good
   and sliding to it shows a card with its copy missing. That was live on
   /company/ from 2026-09-10 until this block: slide 2's `.rule-lead` sat at
   x 1832 in a 1568 viewport, permanently invisible. Replay is what makes text
   inside a component correct, not just livelier.

   These rules are after the block above deliberately: `.splide__slide .rule-lead`
   is (0,2,0) against that block's (0,2,0), a tie, so source order decides. Both
   want `rise` anyway -- what the tie actually carries is `--tr-replay`. */
.splide__slide h3,
.section-mro__panel h3 { --tr: lines; --tr-replay: 1; }

.splide__slide h2 { --tr: lines; --tr-replay: 1; }

.splide__slide :is(.rule-lead, .body-sm, .loc-panel__lead),
.section-mro__panel .crrc-btn { --tr: rise; --tr-replay: 1; }

/* --- the states. Set by the script (data-tr) and never in the markup, so
       with JS blocked or under reduced motion every heading, eyebrow and
       paragraph paints exactly as it always did -- the rule that already
       governs html.crrc-anim (10.5) and data-par-img (10.13).

       ONLY THE HIDDEN STATE LIVES HERE. GSAP does all the moving, with inline
       styles (text-reveal.js, 2026-09-29): nothing below may carry a
       `transition`, or it would lag every tween by its own duration. --- */
[data-tr] { --tr-d: 0s; }

/* Phase one, and the reason it exists: splitting needs Urbane's real metrics and
   so has to wait for the font, but HIDING does not. Without this the page paints
   every heading, blanks them a moment later and then plays them in. The script
   sets this synchronously from the footer, before the first paint, and drops it
   the instant the split is done. */
[data-tr-pre] { visibility: hidden; }

/* ...and one step earlier still, for what is on screen at first paint. The
   header part sets html.tr-pending before the page body is parsed (and clears
   it after 3.5s whatever happens); text-reveal.js clears it as soon as phase one
   has marked its targets. Without it the banner headline painted, vanished
   when phase one ran from the footer, then played in (2026-09-29). */
/* The hero's two SPANS, not its h1: the home flight builds a GSAP autoAlpha
   tween on the h1, which writes `visibility: inherit` inline on it. */
html.tr-pending :is(.section-hero__title > span, .page-hero h1, .page-hero .eyebrow) { visibility: hidden; }

:is([data-tr="lines"], [data-tr="rule"]) .tr-line {
    display: block;
    /* `overflow: clip` with a clip margin, NOT padding-bottom plus a negative
       margin. The padded version leaks: the LAST line's negative bottom margin
       collapses with the parent's own margin instead of pulling the box back, so
       it never cancels its padding -- measured, it grew the eight-line About
       heading by 7px. A clip margin moves the clip edge and touches no layout at
       all: 0.00px difference in width, height, top and left on every heading on
       the site, and the document height unchanged. */
    overflow: clip;
    /* line-height is 0.9, so a lowercase descender hangs 0.11em below the line
       box -- measured off Urbane's own metrics, not guessed. Without this the
       mask would crop the descenders while the text is at rest. */
    overflow-clip-margin: 0.25em;
}

:is([data-tr="lines"], [data-tr="rule"]) .tr-line-i {
    display: block;
    /* 130%, not 100%: 100% is one line box, and the clip edge is 0.25em BELOW
       the line box, so a line parked at exactly its own height still shows a
       sliver of itself under the mask. */
    transform: translateY(130%);
}

/* The rule draws itself first and the word follows it in. Wiping the ::after
   rather than the element means the element's own opacity stays at 1 -- fading
   the eyebrow would fade the rule with it and there would be nothing to watch. */
[data-tr="rule"]::after {
    /* --tr-rule is tweened 0 -> 1 on the eyebrow by GSAP; a pseudo-element
       cannot be tweened directly, but it inherits an unregistered property */
    transform: scaleX(var(--tr-rule, 0));
    transform-origin: left center;
}

[data-tr="rise"] {
    opacity: 0;
    transform: translateY(0.9em);
}

/* ==========================================================================
   LINE DRAW (10.16)
   ==========================================================================

   EVERY LINE ON THE SITE DRAWS ITSELF IN -- the dashed rules, the dotted ones,
   the hairline dividers, the footer's bracket, the round and boxed controls.
   Same trigger, same cascade and same replay as the text above: a line is just
   one more thing text-reveal.js enrols, and the opt-in is `--ln` in this file.

   A BORDER CANNOT BE WIPED, so each line lives on a `::before` that carries the
   real border -- same width, same dash pattern, same radius -- over the host's
   own border, which stays in place but `transparent`. Layout is untouched and
   with JS blocked the ::before simply shows. What draws it is a MASK on the
   ::before, driven by `--draw` 0 -> 1:

     --ln-x        left to right                     rules, card outlines
     --ln-y        top to bottom                     vertical dividers
     --ln-sweep    clockwise round the centre        circles, square buttons
     --ln-bracket  up the left side, then along      the dashed top-left
                   the top                           brackets

   The masks are declared ON THE MASKED BOX, not on :root, because
   `var(--draw)` substitutes where the property is declared: declared higher
   up, it would freeze at that element's 1. Each host picks one by setting
   `--ln-mask` in its own ::before rule, next to its geometry; --ln-x is the
   default.

   `--ln: before` draws the host's ::before; `--ln: self` masks the element
   itself, for art that is not a border (the milestone ring, the Solutions
   S-curve). Registered `inherits: false`, like `--tr`. */
@property --ln { syntax: "*"; inherits: false; }
@property --draw { syntax: "<number>"; inherits: false; initial-value: 1; }

[data-ln="before"]::before,
[data-ln="self"] {
    /* --ln-p is tweened 0 -> 1 on the host by GSAP. Unregistered, so a
       ::before inherits it; [data-ln] resets it to 0 so a host nested in
       another host does not draw with its parent. */
    --draw: var(--ln-p, 0);
    --ln-x: linear-gradient(to right, #000 calc(var(--draw) * 100%), transparent 0);
    --ln-y: linear-gradient(to bottom, #000 calc(var(--draw) * 100%), transparent 0);
    /* 361deg, not 360: at exactly one turn the stop lands on the seam and
       leaves a hairline gap straight up */
    --ln-sweep: conic-gradient(#000 calc(var(--draw) * 361deg), transparent 0);
    /* Two legs: the first half of --draw runs a 2vw strip up the left edge, the
       second half runs a 2vw strip along the top. A single conic from the far
       corner looks the same on a square and badly wrong on the 340 x 78 footer
       bracket, where the whole left side would draw in the first 14%. */
    --ln-bracket:
        linear-gradient(to top, #000 calc(min(1, var(--draw) * 2) * 100%), transparent 0)
            left bottom / 2vw 100% no-repeat,
        linear-gradient(to right, #000 calc(max(0, var(--draw) * 2 - 1) * 100%), transparent 0)
            left top / 100% 2vw no-repeat;
    mask: var(--ln-mask, var(--ln-x));
}

[data-ln] { --ln-p: 0; }

/* --- what draws --- */
.section-solutions__tabs,
.section-solutions__card h3,
.section-mro__panel h3,
.crrc-footer .crrc-footer__tagline,
.crrc-footer__links a,
.crrc-footer .crrc-footer__copyright,
.crrc-page .rule-lead,
.sol-card,
.sol-card__tab,
.sol-card__num,
.sol-group__info,
.ets-stats,
.crrc-page .ets-feat,
.crrc-page .svc-intro__statement,
.svc-layers__panel h3,
.svc-project + .svc-project,
.svc-parts__caps li,
.abt-miles,
.abt-miles__list li + li,
.abt-miles__mobile ul li + li,
.abt-certs__grid,
.abt-cert { --ln: before; }

/* .abt-miles__ring and .ets-dial__ring carry --ln: self + --ln-mask on
   their own rules (2026-10-27 merge). */


/* Inside the Solutions drum: a card that is not current is `visibility:
   hidden`, so its rules draw again each time its tab is picked (10.14b). The
   slide and MRO headings above already replay as text. */
.section-solutions__card h3 { --tr-replay: 1; }

/* Solutions groups (10.20): the info panel beside the photo swaps with the
   slider, so its title and copy replay each time a model is picked */
.sol-group__panel h3 { --tr: lines; --tr-replay: 1; }
.sol-group__panel .body-sm,
.sol-group__panel .crrc-btn { --tr: rise; --tr-replay: 1; }

/* --- the About connectors draw with the scroll, not on arrival ---
   They are two viewports tall and start behind the hero, so a one-shot
   reveal would play out of sight at load. Instead the drawn end is held at
   70% of the viewport height all the way down: the route extends as the page
   scrolls, and is complete when the box's bottom reaches that line.

   The range is the arithmetic of that. `cover 0%` is the box's top at the
   viewport bottom; every px of scroll after it lifts the top by 1px. So the
   drawn end sits at 70vh when the range starts 30vh in, and the whole box
   (--routes-top + 93.68vw, 10.14c) has passed it 30vh + that height in.

   A CSS scroll timeline rather than GSAP: it is on Lenis's clock for free
   (Lenis scrolls the window natively, 10.10), and where it is unsupported
   the routes simply show, as they did before. */
@supports (animation-timeline: view()) {
    @media (min-width: 1025px) and (prefers-reduced-motion: no-preference) {
        .section-about__routes {
            mask: linear-gradient(to bottom, #000 calc(var(--draw) * 100%), transparent 0);
            animation: crrc-ln-draw linear both;
            animation-timeline: view();
            animation-range: cover 30vh cover calc(30vh + var(--routes-top) + 96.66vw);
        }
    }
}

/* The Solutions S-curves draw the same way, on request (2026-09-24): the
   dotted line extends as the page scrolls past it. 2026-10-05: start/end
   deferred (was `cover 10% cover 60%`) because the pinned Discover Our
   Trains groups add a tall pin-spacer below each group, which puts the
   connector into the viewport long before the user is done with the group
   above it. Now the trace does not begin until the connector is halfway
   up the viewport. */
@supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
        :is(.sol-intro__connector, .sol-group__connector) {
            mask: linear-gradient(to bottom, #000 calc(var(--draw) * 100%), transparent 0);
            animation: crrc-ln-draw linear both;
            animation-timeline: view();
            animation-range: cover 50% cover 85%;
        }
    }
}

@keyframes crrc-ln-draw {
    from { --draw: 0; }
    to { --draw: 1; }
}

/* A page printed mid-scroll must carry all of its text. */
@media print {
    [data-tr-pre] { visibility: visible; }

    :is([data-tr="lines"], [data-tr="rule"]) .tr-line-i,
    [data-tr="rule"]::after,
    [data-tr="rise"] {
        opacity: 1 !important;
        transform: none !important;
    }

    [data-ln]::before,
    [data-ln="self"],
    .section-about__routes {
        mask: none !important;
    }
}

/* --- the block editor (2026-09-27, on request) --------------------------
   This sheet also styles the editor canvas (body.editor-styles-wrapper),
   where none of the page scripts run: no slider mounts, no tab or card is
   picked. So everything a component would show one at a time -- slides,
   cards, panels -- is laid out here one after another, fully visible, so
   each can be clicked and edited. Front end untouched: nothing below can
   match outside the editor. !important is used only here, against the
   component rules' own states (visibility, opacity, transforms). */
.editor-styles-wrapper .splide,
.editor-styles-wrapper .splide__slide,
.editor-styles-wrapper .section-solutions__card,
.editor-styles-wrapper .section-mro__panel {
    visibility: visible !important;
    opacity: 1 !important;
}

.editor-styles-wrapper .splide__track {
    overflow: visible !important;
}

.editor-styles-wrapper .splide__list {
    flex-direction: column;
    gap: 2vw;
    height: auto !important;
    transform: none !important;
}

.editor-styles-wrapper .splide__slide {
    position: relative !important;
    inset: auto !important;
    width: 100% !important;
    transform: none !important;
}

.editor-styles-wrapper .splide__slide > * {
    transform: none !important;
}

/* home Solutions: the three cards in a column instead of on the wheel */
.editor-styles-wrapper .section-solutions__cards {
    display: flex;
    flex-direction: column;
    gap: 2vw;
    aspect-ratio: auto;
}

.editor-styles-wrapper .section-solutions__card {
    position: relative;
    inset: auto;
    display: grid !important;
    aspect-ratio: 859 / 553;
    transform: none !important;
}

/* home Fleet MRO: both cards open, so both panels can be edited */
.editor-styles-wrapper .section-mro__card {
    --mro-open: 1 !important;
}

/* Solutions model panels and Services layer panels: all of them, spaced */
.editor-styles-wrapper :is(.sol-group__panel, .svc-layers__panel) + :is(.sol-group__panel, .svc-layers__panel) {
    margin-top: 2vw;
}

/* The editor puts `position: relative` on every block wrapper at (0,2,0)
   (.block-editor-block-list__layout .block-editor-block-list__block), which
   outranks these blocks' own placement, so each block the design places
   absolutely would drop into the flow below its section. Restated here. */
.editor-styles-wrapper :is(
    .section-hero__title, .section-about__map > figure, .section-mro__panel,
    .section-localization__bg, .section-localization__copy,
    .section-localization > .crrc-btn, .section-network h2, .section-network__note,
    .page-hero > .wp-block-image, .page-hero__inner, .page-hero h1,
    .contact-office__lockup, .contact-office__address, .contact-office__photo,
    .ets-chips, .ets-inspo > .wp-block-image, .ets-inspo__copy,
    .ets-inspo__inset, .ets-inspo__slider,
    .ets-hero__lead-wrap,
    .ets-next > .wp-block-image, .ets-next__copy,
    .loc-panel__bg, .svc-project > .wp-block-buttons
) {
    @media (min-width: 1025px) {
        position: absolute;
    }
}

/* Contact: in the editor the offices stack vertically so the client can see
   and edit every one; on the front end they cross-fade over the photo. */
.editor-styles-wrapper .contact-band {
    height: auto;
}

.editor-styles-wrapper .contact-offices {
    position: static;
}

.editor-styles-wrapper .contact-office {
    position: relative;
    inset: auto;
    opacity: 1;
    visibility: visible;
    height: auto;

    @media (min-width: 1025px) {
        height: 46.458vw;
    }
}

/* the editor writes `height: auto` inline on every image; on the page the
   photos fill their frames (object-fit: cover), so here too */
.editor-styles-wrapper .wp-block-image:not(.is-resized) img {
    height: 100% !important;
}

/* 2026-10-19: homepage preloader is now a plain static overlay -- white bg,
   centred red CRRC wordmark, fades out on window load. Everything else
   (layout, colour, the fade-out class) is inline in the first wp:html block
   of home-blocks.html so first paint carries it. */

/* editor: the overlay would cover the editor canvas */
.editor-styles-wrapper .crrc-preload { display: none !important; }

/* editor: show every Key Features accordion row expanded (PAGE_JS doesn't run
   in the editor, so without this every closed row is a 0px body). */
.editor-styles-wrapper .ets-feats-key .ets-feat__body {
    grid-template-rows: 1fr !important;
}
.editor-styles-wrapper .ets-feats-key .ets-feat {
    border-color: #d9d9d9 !important;
}

/* reduced motion: the script fades the curtain with a 300ms transition */
