/*
 * Bizterra CMS — shared MECHANICS.
 * ---------------------------------------------------------------------------
 *
 * Loaded before every theme. It holds only things that are structural rather
 * than aesthetic: scroll-snap behaviour, list resets, the honeypot's position,
 * reveal transitions, aspect ratios.
 *
 * ## The line, and why it is drawn exactly here
 *
 * There is **no colour, no font, no spacing value in this file**. Those are what
 * a theme IS, and a base that set them would be a theme every other theme had to
 * fight — the reason 4c gave for each theme being wholly self-contained.
 *
 * What changed by 4d is the arithmetic: six themes duplicating the same
 * scroll-snap and list-reset rules is six chances for them to drift, and no
 * theme would ever want `scroll-snap-type` different. So mechanics moved here
 * and skins stayed separate. If you find yourself wanting to add a colour to
 * this file, that is the signal it belongs in the themes instead.
 */

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

body { margin: 0; }

img, video { max-width: 100%; height: auto; }

/* Lists that are structure rather than bullets. */
.cms-plain-list { list-style: none; margin: 0; padding: 0; }

/*
 * The opening-hours <dl>. Browsers indent <dd> by 40px and space the <dl>, so
 * every theme would have to undo the same two rules before it could lay a row
 * out. Mechanics, not skin: no colour, no type, no spacing of its own.
 */
.cms-block-opening-hours__list { margin: 0; }
.cms-block-opening-hours__list dd { margin: 0; }

/*
 * Off-screen, but still in the accessibility tree — which is WRONG for a
 * honeypot on its own. It pairs with `aria-hidden` + `tabindex="-1"` in the
 * markup so a screen-reader user is never offered the trap.
 */
.cms-honeypot { position: absolute; left: -9999px; }

/*
 * The reCAPTCHA widget (ADR 023).
 *
 * Structure only, no colour — the same rule every mechanic in this file
 * follows. It exists because the widget is an opaque iframe Google sizes
 * itself, so without a margin it sits flush against the submit button and
 * reads as part of it.
 *
 * `max-width: 100%` because the v2 checkbox is a fixed 304px and would push a
 * narrow phone layout sideways otherwise — and the public site must never
 * scroll horizontally.
 */
.cms-captcha { margin-block: 1rem; max-width: 100%; overflow-x: auto; }

/*
 * Built forms (stage 1a).
 *
 * Structure only, no colour — the inputs already inherit `.cms-input`,
 * `.cms-field` and `.cms-btn` from whichever theme is loaded, which is the
 * whole point of the `contact_form` style alias.
 *
 * `[hidden]` is forced because `.cms-form__page` is a fieldset and several
 * themes set `display` on one, which beats the attribute's default `none` and
 * would show every step of a wizard at once.
 */
.cms-form__page { border: 0; margin: 0; padding: 0; min-inline-size: 0; }
.cms-form [hidden] { display: none !important; }
.cms-form__nav { display: flex; gap: .5rem; flex-wrap: wrap; margin-block: 1rem; }
.cms-form__step-desc { margin-block-start: .25rem; }
.cms-field__required { margin-inline-start: .15rem; }
.cms-choices { display: flex; flex-direction: column; gap: .35rem; }
.cms-choice { display: flex; align-items: baseline; gap: .5rem; }

/* Visible only on focus. Colours belong to the theme. */
.cms-skip { position: absolute; left: -9999px; z-index: 100; }
.cms-skip:focus { left: 1rem; top: 1rem; }

.cms-icon { display: block; flex: none; }

/* ---------------------------------------------------------------------------
 * Slider mechanics
 *
 * The track is a scroll-snap carousel with NO JavaScript involved: swipeable on
 * a phone, scrollable with a trackpad, reachable with a keyboard. cms.js only
 * adds arrows, dots and autoplay on top of this.
 * ------------------------------------------------------------------------ */

.cms-block-slider__viewport {
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
.cms-block-slider__viewport::-webkit-scrollbar { display: none; }

.cms-block-slider__track { display: flex; }

.cms-block-slider__slide {
    position: relative;
    flex: 0 0 100%;
    scroll-snap-align: start;
    display: flex;
    align-items: flex-end;
    overflow: hidden;
}

.cms-block-slider__img,
.cms-block-slider__art {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.cms-block-slider__caption { position: relative; z-index: 1; }

.cms-block-slider__controls { display: flex; align-items: center; }
.cms-block-slider__dots { display: flex; }
.cms-block-slider__arrow { display: grid; place-items: center; cursor: pointer; }
.cms-block-slider__arrow svg { width: 1.25rem; height: 1.25rem; }
.cms-block-slider__dot { cursor: pointer; border: 0; padding: 0; }

/* ---------------------------------------------------------------------------
 * Video mechanics
 * ------------------------------------------------------------------------ */

.cms-block-video__frame { position: relative; overflow: hidden; }
.cms-block-video__player,
.cms-block-video__poster { display: block; width: 100%; }

.cms-block-video__embed {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    border: 0;
}

/* A background video fills its band and sits behind whatever is over it. */
.cms-block-video--background .cms-block-video__frame { position: relative; }
.cms-block-video--background .cms-block-video__player {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* ---------------------------------------------------------------------------
 * Reveal on scroll
 *
 * `cms-reveal` is added by JavaScript, never by the markup — so a page whose
 * script never arrives shows everything rather than nothing. See cms.js, which
 * also carries a failsafe for the OTHER way this hides content: an observer
 * that is installed and then never fires.
 * ------------------------------------------------------------------------ */

.cms-reveal {
    opacity: 0;
    transform: translateY(12px);
    transition: opacity .5s ease, transform .5s ease;
}
.cms-reveal.is-revealed { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
    .cms-reveal { opacity: 1; transform: none; transition: none; }

    /* Nothing on a customer's website may animate against this setting. */
    *, *::before, *::after {
        animation-duration: .001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .001ms !important;
        scroll-behavior: auto !important;
    }
}

/*
 * HOW WIDE A BLOCK RUNS.
 *
 * Every block on a page is wrapped in `.cms-w .cms-w--{width}` by
 * public/page.blade.php, from a setting the owner picks per block.
 *
 * MECHANICS ONLY, per this file's rule: not one measurement is written here.
 * How wide "the container" is belongs to the THEME, so each supplies
 * `--cms-shell`, `--cms-wide`, `--cms-narrow` and `--cms-gutter`. The fallbacks
 * exist only so a theme that has not declared them still renders something sane
 * rather than collapsing.
 *
 * ## The full-bleed trick, and why it takes TWO elements
 *
 * A block sits inside a centred, max-width shell. `margin-inline: calc(50% -
 * 50vw)` pulls each edge out to the viewport; padding puts the CONTENT back
 * where it was. No markup change beyond the wrapper, no JavaScript, and it works
 * inside any container without knowing what that container is.
 *
 * The WRAPPER takes the negative margin and the BLOCK takes the padding — never
 * both on one element. Put the padding on the wrapper and the block only ever
 * spans the padded width, so a band with a background colour paints a stripe
 * with gaps at both ends instead of running edge to edge. That is the whole
 * reason a full-width dark CTA looks broken, and it looks *nearly* right, which
 * is worse.
 *
 * `max-width: 100vw` is load-bearing: without it a flex or grid ancestor lets
 * the negative margins push the document wider than the viewport, which shows
 * up as a horizontal scrollbar on mobile and nowhere else.
 */
.cms-w { max-width: 100%; }

/*
 * THE FULL-BLEED BREAKOUT, AND THE SCROLLBAR IT USED TO EXPOSE.
 *
 * `100vw` includes the vertical scrollbar; the content area does not. So on any
 * desktop browser with a classic scrollbar, a full-width block is ~15px wider
 * than the space it sits in and the PAGE gains a horizontal scrollbar — every
 * section shifted a few pixels, on every site using `full` or `wide`.
 *
 * The breakout itself is still `vw`-based, because it has to be: the block is
 * inside a centred container and nothing else knows how wide the window is.
 * What changed is that the scrollbar is subtracted from the arithmetic — see
 * the rule below.
 */
.cms-w--full,
.cms-w--wide {
    /*
     * `50vw` MINUS HALF THE SCROLLBAR, and `100vw` minus all of it.
     *
     * `vw` counts the vertical scrollbar; the content area does not. Plain
     * `100vw` therefore makes every full-bleed block about 15px wider than the
     * space it sits in, and the PAGE gains a horizontal scrollbar — a few
     * pixels of sideways drift on every section, on every site using `full` or
     * `wide`.
     *
     * `--cms-scrollbar` is measured once by cms.js. The `0px` fallback is
     * exactly today's behaviour, so a visitor with JavaScript off gets what
     * they got before rather than a broken layout.
     *
     * `overflow: clip` and `overflow: hidden` on the root were both tried and
     * BOTH LEFT THE 8px SCROLL IN PLACE — measured, not assumed. Correcting the
     * arithmetic is what actually works, and it fixes the cause rather than
     * hiding a genuine overflow somebody may need to see later.
     */
    margin-inline: calc(50% - 50vw + var(--cms-scrollbar, 0px) / 2);
    max-width: calc(100vw - var(--cms-scrollbar, 0px));
}

/*
 * The block pads itself back in, and any bleed geometry a theme applied before
 * this setting existed is neutralised — the wrapper is now the single place
 * width is decided, and two negative margins would overflow by exactly one
 * gutter without ever looking obviously wrong.
 */
.cms-w--full > .cms-block,
.cms-w--wide > .cms-block {
    margin-inline: 0;
    max-width: none;
}

.cms-w--full > .cms-block {
    --cms-inset: max(
        var(--cms-gutter, 1.5rem),
        calc(50vw - (var(--cms-shell, 68rem) / 2))
    );

    padding-inline: var(--cms-inset);
}

/*
 * `wide` breaks the shell but stops short of the edge — the treatment most
 * commercial templates use for a gallery or a stats band, where full bleed is
 * too much and the reading column is too little.
 */
.cms-w--wide > .cms-block {
    --cms-inset: max(
        var(--cms-gutter, 1.5rem),
        calc(50vw - (var(--cms-wide, 84rem) / 2))
    );

    padding-inline: var(--cms-inset);
}

/*
 * A SLIDESHOW IS THE EXCEPTION, and it is worth understanding why.
 *
 * The rule above pads the BLOCK so its own background still reaches the
 * viewport while its words come back to the shell. That is right for a hero: the
 * dark band IS the section's background.
 *
 * A slideshow's visual surface is not the section — it is the viewport INSIDE
 * it, holding the pictures. Padding the section therefore insets the
 * photographs, which is exactly what "full width" was asked to stop. The block
 * looked full-bleed in every check that measured the SECTION and was plainly
 * boxed on screen.
 *
 * So: the section pads nothing, the pictures reach the edge, and the parts made
 * of words take the inset individually.
 */
.cms-w--full > .cms-block-slider,
.cms-w--wide > .cms-block-slider { padding-inline: 0; }

.cms-w--full > .cms-block-slider > .cms-block-slider__heading,
.cms-w--wide > .cms-block-slider > .cms-block-slider__heading,
.cms-w--full > .cms-block-slider > .cms-block-slider__controls,
.cms-w--wide > .cms-block-slider > .cms-block-slider__controls,
.cms-w--full > .cms-block-slider .cms-block-slider__caption,
.cms-w--wide > .cms-block-slider .cms-block-slider__caption {
    padding-inline: var(--cms-inset);
}

/*
 * Nothing that touches the viewport edge keeps a rounded corner. Geometry, not
 * taste: a radius only reads as a radius when there is something beside it.
 */
.cms-w--full > .cms-block-slider .cms-block-slider__viewport { border-radius: 0; }

.cms-w--narrow { max-width: var(--cms-narrow, 40rem); }


/* A PHOTOGRAPH BEHIND THE WORDS -------------------------------------------
 *
 * Shared by the hero, the call to action and every slide of a slideshow.
 * Geometry only: where the picture sits, what covers it, and what stacks above.
 *
 * Written against GENERIC class names rather than per block. The contrast rule
 * below is the kind that has to hold everywhere or it is worthless, and three
 * near-copies of it is three places for one of them to drift.
 *
 * ## The scrim's OPACITY is here; its COLOUR is the theme's
 *
 * How dark "medium" is, is a contrast decision and identical everywhere. What
 * colour it tints toward is a design decision and belongs to the theme, which
 * sets `--cms-scrim`.
 *
 * The fallback in `var(--cms-scrim, #0b1220)` exists so a theme that forgets to
 * declare it still produces a DARK scrim and readable light text. Failing toward
 * legible is the only acceptable direction: the alternative is a transparent
 * scrim, white text, and somebody's bright photograph — an unreadable headline
 * that nobody sees until it is live on a customer's domain.
 * ------------------------------------------------------------------------ */

.cms-has-bg { position: relative; }

.cms-bg {
    position: absolute;
    inset: 0;
    overflow: hidden;
    border-radius: inherit;
    z-index: 0;
}

.cms-bg__img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* A MOVING BACKGROUND (14d) -------------------------------------------------
 *
 * The same box as `.cms-bg__img`, and that is the point: the scrim, the
 * stacking and the height rules above are already correct, so a video
 * background is the existing mechanic with different bytes inside it.
 *
 * `object-fit: cover` on the video and the poster together means the two are
 * cropped identically, so the moment the video has a frame there is no visible
 * jump from one to the other.
 */
.cms-bg__video {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/*
 * A THIRD-PARTY PLAYER CANNOT BE `object-fit`, so it is covered by hand.
 *
 * An iframe has no intrinsic ratio to fit, so the trick is to make it far
 * larger than the box in both directions and centre it: 100vw wide at 16:9 is
 * 56.25vw tall, and 100vh tall at 16:9 is 177.78vh wide. Taking the larger of
 * each guarantees the frame always overflows the banner rather than letterboxing
 * inside it, whatever shape the banner happens to be.
 *
 * `pointer-events: none` — this is scenery. Without it the player swallows every
 * click that lands on the banner, including the call-to-action button sitting on
 * top of it, and the button simply stops working with nothing to show why.
 *
 * ## THE OVERSCAN IS WHAT REMOVES YOUTUBE'S CHROME, NOT A PARAMETER
 *
 * `controls=0` takes away the play bar and nothing else. The title, the channel
 * avatar, the share and watch-later buttons and "Watch on YouTube" are drawn by
 * the player at the TOP of its own viewport, and YouTube offers no parameter
 * that turns them off — `showinfo=0` was removed in 2018 and is ignored. The
 * only thing that works is to make the frame BIGGER than the banner and let the
 * banner crop those bands off, which is what every video background on the web
 * does.
 *
 * `max(100%, 177.78vh)` alone is exactly cover, so whichever dimension is not
 * overflowing has the player's own edges sitting precisely on the banner's —
 * which is where the chrome lives. The multiplier pushes them outside it in BOTH
 * directions. 1.25 crops about an eighth off each edge: enough for the title bar
 * at any banner height offered here, and a zoom nobody reads as wrong on
 * scenery.
 *
 * A variable, so a theme can say otherwise without restating the geometry.
 */
.cms-bg__frame {
    --cms-bg-overscan: 1.25;

    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: calc(max(100%, 177.78vh) * var(--cms-bg-overscan));
    height: calc(max(100%, 56.25vw) * var(--cms-bg-overscan));
    min-width: 100%;
    min-height: 100%;
    border: 0;
    pointer-events: none;
    transition: opacity .6s ease;
}

/*
 * Waiting for YouTube to report PLAYING (cms.js adds and removes this). The
 * poster or the scrim shows through meanwhile, instead of the player's title
 * bar and play button. Only ever set by the script, so no script means a
 * visible video.
 */
.cms-bg__frame.is-waiting { opacity: 0; }

/*
 * Everything that is not the picture sits above it. Stated over the children
 * rather than per element, so a block that gains a new part later is covered
 * without anybody remembering this file exists.
 */
.cms-has-bg > *:not(.cms-bg) {
    position: relative;
    z-index: 1;
}

.cms-bg::after,
.cms-block-slider__scrim {
    content: "";
    position: absolute;
    inset: 0;
    background: var(--cms-scrim, #0b1220);
    pointer-events: none;
}

.cms-block-slider__scrim { z-index: 0; }

/* The strengths. Opacity is the mechanic; the colour above is the theme's. */
.cms-bg--light .cms-bg::after,
.cms-block-slider--overlay-light .cms-block-slider__scrim { opacity: .35; }

.cms-bg--medium .cms-bg::after,
.cms-block-slider--overlay-medium .cms-block-slider__scrim { opacity: .55; }

.cms-bg--strong .cms-bg::after,
.cms-block-slider--overlay-strong .cms-block-slider__scrim { opacity: .75; }


/* HOW TALL A BANNER STANDS --------------------------------------------------
 *
 * EVERY ONE IS A MIN-HEIGHT. The reference pack sets `height: 505px` with
 * `overflow: hidden`, which crops real copy the moment somebody writes two more
 * words. Growing past the setting is always allowed; being shorter than the
 * content never is.
 *
 * Two classes deep (`.cms-block-hero.cms-block-hero--h-tall`) ON PURPOSE. A
 * theme's own `.cms-block-hero { min-height: … }` is one class and loads after
 * this file, so a single-class rule here would lose — and the owner's choice
 * would silently do nothing. `auto` emits no class at all, which is how a theme
 * keeps its designed height until somebody overrides it.
 *
 * `svh` rather than `vh`: on a phone `vh` is measured against the tallest
 * possible viewport, so a "full height" banner sits under the address bar.
 * ------------------------------------------------------------------------ */

.cms-block-hero.cms-block-hero--h-short,
.cms-block-cta.cms-block-cta--h-short,
.cms-block-slider--h-short .cms-block-slider__slide { min-height: min(45svh, 26rem); }

.cms-block-hero.cms-block-hero--h-medium,
.cms-block-cta.cms-block-cta--h-medium,
.cms-block-slider--h-medium .cms-block-slider__slide { min-height: min(62svh, 34rem); }

.cms-block-hero.cms-block-hero--h-tall,
.cms-block-cta.cms-block-cta--h-tall,
.cms-block-slider--h-tall .cms-block-slider__slide { min-height: min(78svh, 44rem); }

.cms-block-hero.cms-block-hero--h-full,
.cms-block-cta.cms-block-cta--h-full,
.cms-block-slider--h-full .cms-block-slider__slide { min-height: 100svh; }

/*
 * A hero or CTA with a height set has to hold its content somewhere. Centred,
 * because a tall band with everything jammed against the top edge reads as a
 * layout that failed rather than one that was chosen.
 */
.cms-block-hero--h-short, .cms-block-hero--h-medium,
.cms-block-hero--h-tall, .cms-block-hero--h-full,
.cms-block-cta--h-short, .cms-block-cta--h-medium,
.cms-block-cta--h-tall, .cms-block-cta--h-full {
    display: flex;
    flex-direction: column;
    justify-content: center;
}


/* WHERE A SLIDE'S WORDS SIT -------------------------------------------------
 *
 * `bottom` is a caption band along the foot of the picture. `center` gives the
 * slide the HERO's composition — words held in the middle of the frame — which
 * is what a slideshow standing in as the page's banner should look like.
 * ------------------------------------------------------------------------ */

.cms-block-slider--caption-center .cms-block-slider__slide { align-items: center; }

.cms-block-slider--caption-center .cms-block-slider__caption {
    /* No gradient band: the scrim already carries the contrast. */
    background: none;
}


/* ===========================================================================
 * THE MAIN MENU — MECHANICS ONLY (phase 6)
 * ===========================================================================
 *
 * Positioning, disclosure, the drawer and focus behaviour. No colour, no font,
 * no spacing that a theme would want to own — a theme styles `.cms-nav__link`,
 * `.cms-nav__submenu` and `.cms-nav-toggle` and inherits all of this for free.
 *
 * ## The desktop submenu opens on :hover AND :focus-within
 *
 * Hover alone makes every child page unreachable from a keyboard. That is the
 * exact fault 5a fixed in Bexon's service grid, and it is worth stating again
 * because it is invisible to anybody testing with a mouse: the page looks
 * finished, and a whole tier of the site cannot be reached.
 *
 * `:focus-within` is also why the disclosure button carries `tabindex="-1"` —
 * by the time a keyboard reaches the parent link the submenu is already open,
 * so a focusable toggle would be a stop that does nothing.
 *
 * ## Everything JS adds is subtractive from THIS state, never additive to it
 *
 * With no script, `.cms-nav` is a plain open list and the toggle button is a
 * visible no-op. cms.js sets `hidden` on the button only after it has bound its
 * handler, and only then does the drawer collapse. A menu that started hidden
 * and waited for a script would be a site with no navigation for anybody the
 * script failed for, and would look perfect to everybody else — the failure
 * shape 5a found twice in the reveal code.
 * ------------------------------------------------------------------------ */

.cms-nav__list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
}

.cms-nav__item { position: relative; }

.cms-nav__link { display: inline-block; }

/* A parent's link and its disclosure sit on one line. */
.cms-nav__item--has-children > .cms-nav__link { display: inline-block; }

.cms-nav__disclosure {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    cursor: pointer;
}

/*
 * The chevron is drawn, not a glyph.
 *
 * An icon font is a network request that can fail and a character that can go
 * missing, and the reference packs' answer — Font Awesome Pro — cannot ship in
 * a product sold to tenants (5a). Two borders rotated 45° cannot 404.
 */
.cms-nav__chevron {
    display: block;
    inline-size: .45em;
    block-size: .45em;
    border-inline-end: 2px solid currentColor;
    border-block-end: 2px solid currentColor;
    transform: rotate(45deg) translate(-15%, -15%);
    transition: transform .18s ease;
}

/* ---------------------------------------------------------------------------
 * DESKTOP: submenus float, and the menu is always open.
 * ------------------------------------------------------------------------ */

@media (min-width: 48em) {
    .cms-nav__list--d1 > .cms-nav__item > .cms-nav__submenu,
    .cms-nav__submenu {
        position: absolute;
        inset-block-start: 100%;
        inset-inline-start: 0;
        z-index: 40;
        min-inline-size: 12rem;

        /*
         * `visibility: hidden`, and the keyboard path is worth stating exactly,
         * because the obvious description of it is wrong.
         *
         * A `visibility: hidden` subtree is NOT focusable — no more than a
         * `display: none` one is. So focus does not "reach inside" a closed
         * submenu, and `:focus-within` is not what lets it. What actually
         * happens is one step earlier: Tab lands on the PARENT LINK, which is
         * always visible, that makes the parent `li` match `:focus-within`, the
         * rule below reveals this panel, and only THEN are the links in it
         * focusable — so the next Tab goes into the submenu. Style recalc
         * happens between tab stops, which is what makes the sequence work.
         *
         * The alternative — hiding with `opacity: 0` alone — leaves every child
         * link focusable while invisible, so a sighted keyboard user's focus
         * ring disappears into nothing for four stops. That is worse than the
         * problem it solves.
         *
         * Opacity and transform are here for the transition only; visibility is
         * what does the hiding, and it is in the transition list so the panel
         * does not vanish before it has faded.
         */
        visibility: hidden;
        opacity: 0;
        transform: translateY(.25rem);
        transition: opacity .16s ease, transform .16s ease, visibility .16s;
    }

    /* Levels below the first fly out to the SIDE, not further down. */
    .cms-nav__submenu .cms-nav__submenu {
        inset-block-start: 0;
        inset-inline-start: 100%;
    }

    .cms-nav__submenu .cms-nav__list { flex-direction: column; align-items: stretch; }

    /*
     * A THIRD-LEVEL disclosure sits at the end of its own row.
     *
     * A theme makes submenu links `display: block` so the whole row is a
     * target, which pushes an inline-flex button onto the next line — the
     * chevron ends up under the word instead of beside it, looking like a
     * stray mark. Taking it out of flow is what the mobile drawer already
     * does, so both do the same thing for the same reason.
     */
    .cms-nav__submenu .cms-nav__disclosure {
        position: absolute;
        inset-block-start: 0;
        inset-inline-end: .25rem;
        block-size: 100%;
    }

    /* Room for it, so a long label never runs underneath. */
    .cms-nav__submenu .cms-nav__item--has-children > .cms-nav__link {
        padding-inline-end: 1.5rem;
    }

    .cms-nav__item--has-children:hover > .cms-nav__submenu,
    .cms-nav__item--has-children:focus-within > .cms-nav__submenu,
    .cms-nav__item--has-children > .cms-nav__disclosure[aria-expanded="true"] + .cms-nav__submenu {
        visibility: visible;
        opacity: 1;
        transform: none;
    }

    .cms-nav__item--has-children:hover > .cms-nav__disclosure .cms-nav__chevron,
    .cms-nav__item--has-children:focus-within > .cms-nav__disclosure .cms-nav__chevron {
        transform: rotate(225deg) translate(-15%, -15%);
    }

    /*
     * A submenu near the right edge would otherwise run off the viewport. The
     * component cannot know where the header sits, so cms.js measures and adds
     * this class — and if it never runs, the menu is merely clipped rather than
     * gone.
     */
    .cms-nav__submenu.cms-nav__submenu--flip {
        inset-inline-start: auto;
        inset-inline-end: 0;
    }

    .cms-nav__submenu .cms-nav__submenu.cms-nav__submenu--flip {
        inset-inline-end: 100%;
    }

}

/* ---------------------------------------------------------------------------
 * MOBILE: one drawer, and the submenus are stacked in it.
 *
 * The same <ul>. Two navigations would be two copies of every link for a screen
 * reader to walk, and the copy nobody looks at is the one that goes stale.
 * ------------------------------------------------------------------------ */

@media (max-width: 47.999em) {
    .cms-nav__list { flex-direction: column; align-items: stretch; }

    .cms-nav__item--has-children > .cms-nav__disclosure {
        /*
         * On a touch screen this IS the control — there is no hover and no
         * focus-within to open a submenu with, so it returns to the tab order.
         */
        position: absolute;
        inset-block-start: 0;
        inset-inline-end: 0;
        block-size: 2.75rem;
        inline-size: 2.75rem;
    }

    /*
     * Collapsed only once cms.js has said so. Without the attribute every
     * submenu is simply open, which is a long menu rather than a broken one.
     */
    [data-cms-nav-ready] .cms-nav__submenu[hidden] { display: none; }

    [data-cms-nav-ready] .cms-nav {
        display: none;
    }

    [data-cms-nav-ready] .cms-nav[data-cms-nav-open] {
        display: block;
    }

    .cms-nav__disclosure[aria-expanded="true"] .cms-nav__chevron {
        transform: rotate(225deg) translate(-15%, -15%);
    }
}

/*
 * `[hidden]` has to win, and it does not by default: any `display` declaration
 * beats the user agent's `[hidden] { display: none }`. The button ships hidden
 * and cms.js unhides it, so without this rule a dead control would be visible
 * on exactly the pages where the script failed.
 *
 * Two classes deep, so it wins in EVERY media query regardless of source order
 * — the same reasoning the banner-height selectors use.
 */
.cms-nav-toggle[hidden] { display: none; }

/*
 * HIDDEN BY DEFAULT, shown only on a narrow screen.
 *
 * Written this way round on purpose. It was `display: inline-flex` here with a
 * `display: none` inside the desktop query — and that does not work: both
 * selectors are one class, the media query sits EARLIER in the file, and later
 * wins at equal specificity. The result was a "Menu" button sitting next to the
 * logo on every desktop page, which is exactly the sort of thing that reads as
 * a design choice rather than a bug. Caught by looking at it in a browser.
 */
.cms-nav-toggle {
    display: none;
    align-items: center;
    gap: .5rem;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    cursor: pointer;
}

@media (max-width: 47.999em) {
    .cms-nav-toggle { display: inline-flex; }
}

/* Three bars, drawn. Same argument as the chevron above. */
.cms-nav-toggle__bars,
.cms-nav-toggle__bars::before,
.cms-nav-toggle__bars::after {
    display: block;
    inline-size: 1.25rem;
    block-size: 2px;
    background: currentColor;
}

.cms-nav-toggle__bars {
    position: relative;
}

.cms-nav-toggle__bars::before,
.cms-nav-toggle__bars::after {
    content: '';
    position: absolute;
    inset-inline-start: 0;
}

.cms-nav-toggle__bars::before { inset-block-start: -6px; }
.cms-nav-toggle__bars::after { inset-block-start: 6px; }

/*
 * Somebody who has asked their system for less movement gets none of it. The
 * menu still opens and closes — only the transition goes.
 */
@media (prefers-reduced-motion: reduce) {
    .cms-nav__submenu,
    .cms-nav__chevron {
        transition: none;
    }
}


/* ===========================================================================
 * LIGHTBOX, GALLERY COLUMNS AND VIDEO FACADES — MECHANICS (phase 6)
 *
 * Positioning, stacking and aspect ratios only. Colour, radius and type belong
 * to the theme, as everywhere else in this file — except the lightbox
 * BACKDROP, which is deliberately not a theme decision: a picture viewer is a
 * dark room whatever the site looks like, and a theme that made it pale would
 * be showing photographs against their own background.
 * ------------------------------------------------------------------------ */

.cms-lightbox {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    padding: clamp(1rem, 4vw, 3rem);
    background: rgb(8 10 14 / .92);
}

.cms-lightbox[hidden] { display: none; }

/* The page behind must not scroll while the viewer is open. */
.cms-lightbox-open { overflow: hidden; }

.cms-lightbox__figure {
    margin: 0;
    max-inline-size: 100%;
    max-block-size: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .75rem;
}

.cms-lightbox__img {
    max-inline-size: 100%;
    /* Room for the caption; the picture is never cropped to make it fit. */
    max-block-size: calc(100svh - 9rem);
    object-fit: contain;
}

.cms-lightbox__caption {
    color: rgb(255 255 255 / .82);
    font-size: .9rem;
    text-align: center;
    max-inline-size: 46rem;
}

.cms-lightbox__caption[hidden] { display: none; }

.cms-lightbox__close,
.cms-lightbox__nav {
    flex: none;
    inline-size: 2.75rem;
    block-size: 2.75rem;
    display: grid;
    place-items: center;
    border: 1px solid rgb(255 255 255 / .35);
    border-radius: 50%;
    background: rgb(255 255 255 / .08);
    color: #fff;
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
}

.cms-lightbox__close {
    position: absolute;
    inset-block-start: 1rem;
    inset-inline-end: 1rem;
}

.cms-lightbox__nav[hidden] { display: none; }

/* Chevrons drawn, not glyphs — an icon font is a request that can fail. */
.cms-lightbox__nav::before {
    content: '';
    inline-size: .6rem;
    block-size: .6rem;
    border-block-start: 2px solid currentColor;
    border-inline-end: 2px solid currentColor;
}

.cms-lightbox__nav--prev::before { transform: rotate(-135deg) translate(-20%, -20%); }
.cms-lightbox__nav--next::before { transform: rotate(45deg) translate(-20%, -20%); }

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

/* --- gallery columns ------------------------------------------------------
 *
 * The owner's `columns` setting is a MAXIMUM, not a fixed count: the track
 * still collapses on a narrow screen. A fixed `repeat(4, 1fr)` would put four
 * columns on a phone, which is four unreadable thumbnails.
 * ------------------------------------------------------------------------ */

.cms-block-gallery__grid {
    grid-template-columns: repeat(
        auto-fit,
        minmax(min(calc((100% - (var(--cms-gallery-columns, 3) - 1) * 1rem) / var(--cms-gallery-columns, 3)), 100%), 1fr)
    );
}

.cms-block-gallery__item { display: flex; flex-direction: column; gap: .5rem; }
.cms-block-gallery__link { display: block; }

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

.cms-block-video-gallery__grid {
    display: grid;
    grid-template-columns: repeat(
        auto-fit,
        minmax(min(calc((100% - (var(--cms-video-columns, 3) - 1) * 1rem) / var(--cms-video-columns, 3)), 100%), 1fr)
    );
    gap: 1rem;
}

.cms-block-video-gallery__tile {
    position: relative;
    display: block;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    text-decoration: none;
}

.cms-block-video-gallery__poster {
    display: block;
    inline-size: 100%;
    block-size: 100%;
    object-fit: cover;
}

/* The play mark is a triangle drawn with borders — no image, nothing to 404. */
.cms-block-video-gallery__play {
    position: absolute;
    inset-block-start: 50%;
    inset-inline-start: 50%;
    transform: translate(-50%, -50%);
    inline-size: 3.5rem;
    block-size: 3.5rem;
    border-radius: 50%;
    background: rgb(8 10 14 / .58);
    display: grid;
    place-items: center;
}

.cms-block-video-gallery__play::before {
    content: '';
    inline-size: 0;
    block-size: 0;
    border-block: .55rem solid transparent;
    border-inline-start: .9rem solid #fff;
    border-inline-end: 0;
    margin-inline-start: .2rem;
}

.cms-block-video-gallery__title {
    position: absolute;
    inset-inline: 0;
    inset-block-end: 0;
    padding: 2.5rem .85rem .7rem;
    color: #fff;
    background: linear-gradient(to top, rgb(8 10 14 / .85), transparent);
}

/* The real player, once somebody has pressed play. */
.cms-block-video-gallery__frame {
    inline-size: 100%;
    aspect-ratio: 16 / 9;
    border: 0;
    display: block;
}

/* --- map ------------------------------------------------------------------ */

.cms-block-map__frame {
    inline-size: 100%;
    block-size: 100%;
    min-block-size: 20rem;
    border: 0;
    display: block;
}

/* MIN-heights, as everywhere: growing past the setting is allowed, being
 * shorter than the content never is. `svh`, or a full-height band sits under a
 * phone's address bar. */
.cms-block-map.cms-block-map--h-short .cms-block-map__frame { min-block-size: min(42svh, 22rem); }
.cms-block-map.cms-block-map--h-medium .cms-block-map__frame { min-block-size: min(56svh, 30rem); }
.cms-block-map.cms-block-map--h-tall .cms-block-map__frame { min-block-size: min(72svh, 40rem); }
.cms-block-map.cms-block-map--h-full .cms-block-map__frame { min-block-size: 90svh; }


/* ===========================================================================
 * BLOG — MECHANICS (phase 6)
 *
 * Grid arithmetic and aspect ratios. Everything visual belongs to the theme.
 * ------------------------------------------------------------------------ */

.cms-blog__list {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
    gap: 1.5rem;
}

.cms-blog__card {
    display: flex;
    flex-direction: column;
    gap: .5rem;
    min-inline-size: 0;
}

.cms-blog__thumb {
    inline-size: 100%;
    aspect-ratio: 16 / 10;
    object-fit: cover;
    display: block;
}

.cms-block-blog-grid__text { display: flex; flex-direction: column; gap: .5rem; min-inline-size: 0; }
.cms-block-blog-grid__figure { display: block; min-inline-size: 0; }
.cms-block-blog-grid__thumb { display: block; inline-size: 100%; object-fit: cover; }

/* --- layout: CARDS — picture on top, several across ---------------------- */

.cms-block-blog-grid--cards .cms-block-blog-grid__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
    gap: 1.5rem;
}

.cms-block-blog-grid--cards .cms-block-blog-grid__item {
    display: flex;
    flex-direction: column;
    gap: .75rem;
    min-inline-size: 0;
}

.cms-block-blog-grid--cards .cms-block-blog-grid__thumb { aspect-ratio: 16 / 10; }

/* --- layout: LIST — small thumbnail beside the text ---------------------- *
 *
 * One post per row. The thumbnail is a FIXED track rather than a fraction, so
 * a row with a picture and a row without line up down the page — a percentage
 * would make every headline start at a different place.
 * ------------------------------------------------------------------------ */

.cms-block-blog-grid--list .cms-block-blog-grid__grid {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
}

.cms-block-blog-grid--list .cms-block-blog-grid__item {
    display: grid;
    grid-template-columns: 10rem minmax(0, 1fr);
    gap: 1.25rem;
    align-items: start;
}

/* A post with no picture spans the whole row rather than leaving a hole. */
.cms-block-blog-grid--list .cms-block-blog-grid__item:not(:has(.cms-block-blog-grid__figure)) {
    grid-template-columns: minmax(0, 1fr);
}

.cms-block-blog-grid--list .cms-block-blog-grid__thumb { aspect-ratio: 4 / 3; }

/* --- layout: SPLIT — half picture, half text ----------------------------- */

.cms-block-blog-grid--split .cms-block-blog-grid__grid {
    display: flex;
    flex-direction: column;
    gap: 2.5rem;
}

.cms-block-blog-grid--split .cms-block-blog-grid__item {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 2rem;
    align-items: center;
}

.cms-block-blog-grid--split .cms-block-blog-grid__item:not(:has(.cms-block-blog-grid__figure)) {
    grid-template-columns: minmax(0, 1fr);
}

.cms-block-blog-grid--split .cms-block-blog-grid__thumb { aspect-ratio: 3 / 2; }

/* --- which side the picture sits on -------------------------------------- *
 *
 * `order`, not `direction` — reversing the writing direction would also
 * reverse the text inside it. The DOM keeps picture-then-text so a screen
 * reader and a keyboard meet them in the reading order either way.
 * ------------------------------------------------------------------------ */

.cms-block-blog-grid--image-end .cms-block-blog-grid__figure { order: 2; }
.cms-block-blog-grid--image-end .cms-block-blog-grid__text { order: 1; }

/* --- stacking on a narrow screen ----------------------------------------- *
 *
 * Below this, a 10rem thumbnail beside text is two unreadable columns. Both
 * side-by-side layouts become a single column and the picture goes back on top.
 * ------------------------------------------------------------------------ */

@media (max-width: 40em) {
    .cms-block-blog-grid--list .cms-block-blog-grid__item,
    .cms-block-blog-grid--split .cms-block-blog-grid__item {
        grid-template-columns: minmax(0, 1fr);
    }

    .cms-block-blog-grid--image-end .cms-block-blog-grid__figure,
    .cms-block-blog-grid--image-end .cms-block-blog-grid__text { order: 0; }

    .cms-block-blog-grid--list .cms-block-blog-grid__thumb,
    .cms-block-blog-grid--split .cms-block-blog-grid__thumb { aspect-ratio: 16 / 9; }
}

/* --- the archive's own controls ------------------------------------------ */

.cms-block-blog-grid__search {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
    align-items: center;
    margin-block-end: 2rem;
}

.cms-block-blog-grid__search .cms-input { inline-size: auto; flex: 1 1 16rem; }

/* --- pagination (phase 6) ------------------------------------------------
 *
 * Structure only. The module renders its OWN pagination view — Laravel's
 * default is Tailwind markup and this surface loads no Tailwind (ADR 021),
 * which is why the numbers came out in stray boxes. See cms::pagination.
 * ------------------------------------------------------------------------ */

.cms-pagination { margin-block-start: 2rem; }

.cms-pagination__list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .35rem;
}

.cms-pagination__link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-inline-size: 2.25rem;
    min-block-size: 2.25rem;
    padding-inline: .6rem;
    text-decoration: none;
    font-variant-numeric: tabular-nums;
}

/* A control that goes nowhere should not look pressable. */
.cms-pagination__link--disabled { opacity: .4; cursor: default; }

.cms-pagination__gap { padding-inline: .35rem; }

/* A single post is PROSE and wants one column, not a grid. */
.cms-blog__list--compact { grid-template-columns: 1fr; gap: .75rem; }

.cms-blog__filters { display: flex; flex-wrap: wrap; gap: .5rem; margin-block-end: 2rem; }

.cms-blog__pagination { margin-block-start: 2.5rem; }
.cms-blog__pagination ul,
.cms-blog__pagination nav { display: flex; flex-wrap: wrap; gap: .35rem; list-style: none; padding: 0; margin: 0; }

.cms-post__media { inline-size: 100%; display: block; }
.cms-post__more { margin-block-start: var(--cms-space, 4rem); }


/* ===========================================================================
 * VISUALLY HIDDEN (phase 6)
 *
 * Present for a screen reader and a crawler, absent on screen.
 *
 * Used by the page heading when an owner has turned the visible title off: the
 * document keeps exactly one <h1> either way. `display: none` and
 * `visibility: hidden` would BOTH remove it from the accessibility tree, which
 * is the opposite of what is wanted — this is the clip-rect technique precisely
 * because it hides the box without hiding the content.
 * ------------------------------------------------------------------------ */

.cms-visually-hidden {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}


/* ===========================================================================
 * WHERE A BANNER'S WORDS SIT ACROSS IT (phase 6)
 *
 * Every banner in this module held its copy against the inline start. That is
 * right for a text-led hero and wrong the moment a photograph goes behind it:
 * the subject of the picture is usually on one side and the words have to go on
 * the other.
 *
 * ## `start` / `center` / `end`, never `left` / `right`
 *
 * This module uses logical properties throughout, so in an RTL locale "left" is
 * the wrong end of the banner. The admin says "Left" and "Right" because that is
 * what an owner means; the CSS resolves it per writing direction.
 *
 * ## Two classes deep, deliberately
 *
 * A theme's own `.cms-block-hero__inner { align-items: … }` is one class and
 * loads after base.css, so a single-class rule here would lose and the owner's
 * choice would silently do nothing — the same trap the banner heights document.
 *
 * The default emits NO class at all, so a theme keeps its own composition until
 * somebody actually chooses otherwise.
 * ------------------------------------------------------------------------ */

/* --- hero ---------------------------------------------------------------- */

.cms-block-hero.cms-block-hero--text-center .cms-block-hero__inner {
    align-items: center;
    text-align: center;
    margin-inline: auto;
}

.cms-block-hero.cms-block-hero--text-end .cms-block-hero__inner {
    align-items: flex-end;
    text-align: end;
    margin-inline-start: auto;
}

/* ANY CHILD WITH ITS OWN WIDTH CAP NEEDS THE MARGIN TOO.
 *
 * `text-align` centres the text INSIDE a box; it does not move the box. A theme
 * that caps its headline — bexon sets `max-width: 18ch` on it, and capping a
 * headline is completely normal typography — leaves that box hard against the
 * start edge, so "Middle" appeared to do nothing at all.
 *
 * The first version of this rule only did the body copy, which is why the
 * setting looked broken on the one theme that caps its headline. Every part of
 * the hero that can carry a width is listed, rather than assuming which ones a
 * theme happens to constrain — that assumption is what was wrong. */
.cms-block-hero--text-center .cms-block-hero__headline,
.cms-block-hero--text-center .cms-block-hero__body,
.cms-block-hero--text-center .cms-block-hero__eyebrow,
.cms-block-hero--text-center .cms-block-hero__inner > * { margin-inline: auto; }

.cms-block-hero--text-end .cms-block-hero__headline,
.cms-block-hero--text-end .cms-block-hero__body,
.cms-block-hero--text-end .cms-block-hero__eyebrow,
.cms-block-hero--text-end .cms-block-hero__inner > * { margin-inline-start: auto; }

.cms-block-hero--text-center .cms-block-hero__actions { justify-content: center; }
.cms-block-hero--text-end .cms-block-hero__actions { justify-content: flex-end; }

/* --- call to action ------------------------------------------------------- */

.cms-block-cta.cms-block-cta--text-start {
    text-align: start;
}

.cms-block-cta.cms-block-cta--text-end {
    text-align: end;
}

.cms-block-cta--text-start .cms-block-cta__headline,
.cms-block-cta--text-start .cms-block-cta__body { margin-inline: 0 auto; }

.cms-block-cta--text-end .cms-block-cta__headline,
.cms-block-cta--text-end .cms-block-cta__body { margin-inline: auto 0; }

.cms-block-cta--text-start .cms-block-cta__actions { justify-content: flex-start; }
.cms-block-cta--text-end .cms-block-cta__actions { justify-content: flex-end; }

/* --- slideshow ------------------------------------------------------------ */

.cms-block-slider.cms-block-slider--text-center .cms-block-slider__caption {
    text-align: center;
    margin-inline: auto;
}

.cms-block-slider.cms-block-slider--text-end .cms-block-slider__caption {
    text-align: end;
    margin-inline-start: auto;
}

/* Same reasoning as the hero: a capped headline is a box that has to be moved,
 * not text to be aligned. */
.cms-block-slider--text-center .cms-block-slider__headline,
.cms-block-slider--text-center .cms-block-slider__body,
.cms-block-slider--text-center .cms-block-slider__eyebrow { margin-inline: auto; }

.cms-block-slider--text-end .cms-block-slider__headline,
.cms-block-slider--text-end .cms-block-slider__body,
.cms-block-slider--text-end .cms-block-slider__eyebrow { margin-inline-start: auto; }

.cms-block-slider--text-center .cms-block-slider__actions { justify-content: center; }
.cms-block-slider--text-end .cms-block-slider__actions { justify-content: flex-end; }

/*
 * A caption pinned to one side still needs a width limit, or a short headline
 * sits alone in the middle of a very wide band with the picture showing through
 * on both sides.
 */
.cms-block-slider--text-center .cms-block-slider__caption,
.cms-block-slider--text-end .cms-block-slider__caption {
    max-inline-size: min(48rem, 100%);
}

/* THE SCROLL-DRIVEN FRAME SEQUENCE (14j) --------------------------------------
 *
 * MECHANICS ONLY, like everything else in this file. Colour, type and spacing
 * come from the theme, which already styles this block: `hero_frames` emits
 * `.cms-block-hero` alongside its own class (CmsBlock::STYLE_ALIASES), so all
 * seventeen themes dress it with no CSS written anywhere.
 *
 * ## Sticky inside a tall spacer, and no scroll-jacking anywhere
 *
 * The spacer is the scroll distance; the stage sticks to the viewport while it
 * passes. When the spacer ends the stage releases and the next section arrives
 * on its own — so the page scrolls normally, the scrollbar tells the truth, and
 * nothing listens to `wheel` or calls `preventDefault`.
 *
 * `svh` rather than `vh`: on a phone `vh` is measured against the tallest
 * possible viewport, so a full-height stage sits under the address bar.
 * -------------------------------------------------------------------------- */

/*
 * FULL BLEED, and it has to fight the theme for it.
 *
 * Themes give `.cms-block-hero` their own padding and background, which is
 * right for a hero made of words on a colour and wrong for one that IS a
 * picture — the padding became a visible frame of theme colour around the
 * animation. Two classes deep so a theme's single-class rule loses, the same
 * technique the banner heights above use.
 */
.cms-block-hero.cms-block-hero-frames {
    padding: 0;
    background: none;
}

.cms-frames__spacer { min-block-size: var(--cms-frames-scroll, 300vh); }

.cms-frames__stage {
    position: sticky;
    inset-block-start: 0;
    block-size: 100svh;
    overflow: hidden;
    display: grid;

    /*
     * `grid-template` explicitly, NOT `place-items: center`.
     *
     * `place-items: center` was the bug that letterboxed the whole banner: it
     * sets `justify-items`/`align-items` to `center`, which sizes every grid
     * item to its CONTENT instead of stretching it to the cell. The canvas then
     * drew at its intrinsic 1600x900, centred, with the theme's background
     * showing on all four sides.
     *
     * One cell that fills the stage, items stretching into it by default, and
     * the captions doing their own centring below.
     */
    grid-template: 1fr / 1fr;
}

/*
 * The still and the canvas occupy the same cell, the canvas on top. Grid rather
 * than absolute positioning so the stage keeps a real size even before the
 * canvas has been given one.
 */
/*
 * COVER THE STAGE, cropping rather than letterboxing.
 *
 * `inline-size`/`block-size` at 100% plus `object-fit: cover` — both are needed
 * on the canvas. It is a REPLACED element with an intrinsic size (whatever
 * `canvas.width` was set to), so without an explicit size it draws at 1600x900
 * regardless of the cell, and without `object-fit` it would stretch out of
 * proportion instead of cropping.
 *
 * `min-*: 100%` guards the case where a theme sets a max on descendants.
 */
.cms-frames__still,
.cms-frames__canvas {
    grid-area: 1 / 1;
    inline-size: 100%;
    block-size: 100%;
    min-inline-size: 100%;
    min-block-size: 100%;
    object-fit: cover;
    object-position: center;
    display: block;
}

.cms-frames__canvas { z-index: 1; }

.cms-frames__spacer .cms-bg__scrim { grid-area: 1 / 1; z-index: 2; }

.cms-frames__captions {
    grid-area: 1 / 1;
    z-index: 3;
    display: grid;

    /*
     * The captions centre THEMSELVES. The stage used to do it for everything,
     * which is what shrank the canvas — so the centring lives here, on the one
     * layer that actually wants it.
     */
    place-items: center;
    inline-size: 100%;
    block-size: 100%;
    padding: 2rem;
}

/*
 * EVERY CAPTION IN THE SAME CELL, stacked. They are alternatives in time rather
 * than a list in space, and laying them out in flow would make the section as
 * tall as all of them at once before the scrubber ever hid one.
 */
.cms-frames__caption {
    grid-area: 1 / 1;
    max-inline-size: min(48rem, 100%);
    align-self: center;

    /*
     * Visible by DEFAULT. With no JavaScript, reduced motion, or a frame set
     * that has been deleted, this section is a picture and its words — which is
     * the markup the component actually ships. `cms.js` takes over and fades
     * them only once it has something to scrub.
     */
    opacity: 1;
    transition: opacity 120ms linear;
}

/*
 * Stacked captions overlap, so only the live one may be readable. Until the
 * scrubber is running they are laid out in flow instead, where they read as the
 * paragraphs they are.
 */
.cms-block-hero-frames:not(.cms-frames--live) .cms-frames__captions {
    display: block;
    overflow-y: auto;
}

.cms-block-hero-frames:not(.cms-frames--live) .cms-frames__caption {
    margin-block-end: 1.5rem;
}

/*
 * THE OWNER'S TYPEFACE AND COLOUR (14j).
 *
 * Read from custom properties the component sets, with the theme's own display
 * face and plain white as the fallbacks — so a block saved before these
 * settings existed renders exactly as it did.
 *
 * `!important` is NOT used. The properties are set on the section, so they beat
 * a theme's descendant rules by cascade order rather than by force, and a theme
 * that deliberately styles `.cms-frames__headline` can still win — which is the
 * behaviour a theme author would expect.
 */
.cms-frames__eyebrow,
.cms-frames__headline,
.cms-frames__body {
    font-family: var(--cms-frames-font, var(--cms-display));
    color: var(--cms-frames-colour, #fff);
}

.cms-frames__eyebrow {
    text-transform: uppercase;
    letter-spacing: .12em;
    font-size: .85rem;
    margin-block-end: .5rem;
    opacity: .85;
}

.cms-frames__headline {
    font-size: clamp(2rem, 6vw, 4.5rem);
    line-height: 1.05;
    margin: 0;
}

.cms-frames__body {
    font-size: clamp(1rem, 1.8vw, 1.35rem);
    margin-block-start: .75rem;
    max-inline-size: 42rem;
}

/*
 * The button keeps the THEME's colours, deliberately. It is an action rather
 * than part of the picture, and a button tinted to match the headline over a
 * photograph is how a call to action becomes invisible.
 */
.cms-frames__actions { margin-block-start: 1.5rem; }

.cms-frames__captions--start { justify-items: start; text-align: start; }
.cms-frames__captions--center { justify-items: center; text-align: center; }
.cms-frames__captions--end { justify-items: end; text-align: end; }

/*
 * The visitor asked for less movement. The stage stops sticking, the spacer
 * collapses to one screen, and the section becomes an ordinary hero — which is
 * also why `initFrameScroller` returns early rather than drawing at all.
 */
@media (prefers-reduced-motion: reduce) {
    .cms-frames__spacer { min-block-size: auto; }
    .cms-frames__stage { position: static; block-size: auto; min-block-size: min(62svh, 34rem); }
    .cms-frames__captions { display: block; }
    .cms-frames__caption { margin-block-end: 1.5rem; }
}

/* ===========================================================================
 * FEATURE GRID — lining up a short last row (`last_row: center | end`)
 * ======================================================================== */

/*
 * CSS Grid cannot move an incomplete row: `auto-fit` leaves six cards four
 * across as four, then two stranded on the left. Flexbox can centre, but its
 * cards grow to fill whatever row they land in, so the last two come out wider
 * than the four above them.
 *
 * So the grid is laid on HALF-columns — twice as many tracks, every card
 * spanning two — and the first card of a short row starts part-way along. Every
 * card stays exactly as wide as every other, and the theme's own `gap` is kept
 * because nothing here sets one: the gap inside a two-track span is the same on
 * every card, so it cancels out.
 *
 * The first card of the last row is found without knowing how many cards there
 * are: `:nth-child(4n+1)` starts a row, `:nth-last-child(2)` means two remain.
 * With C columns and R cards remaining, it starts on line
 *     centre:  C - R + 1
 *     right:   2 × (C - R) + 1
 * and the cards after it follow by ordinary auto-placement.
 *
 * Measured against the SECTION with a container query, not the viewport: the
 * same grid sits in a full-width band on one theme and a narrow column on
 * another. The breakpoints are the width at which a card would fall below
 * ~15rem, which is where every theme's own `minmax()` already wraps. The
 * `--upto-N` class caps the columns at the number of cards, so three cards stay
 * three across instead of sitting in a four-column track.
 *
 * Two class names deep on purpose: a theme's `.cms-block-feature-grid__grid`
 * loads AFTER this file, and with equal weight it would win and the owner's
 * choice would silently do nothing — the banner-height lesson from 5a.
 */
.cms-block-feature-grid--aligned { container-type: inline-size; }

.cms-block-feature-grid--aligned .cms-block-feature-grid__grid { grid-template-columns: minmax(0, 1fr); }

/* Two across. Every aligned grid of two or more cards. */
@container (32rem <= width < 48rem) {
    .cms-block-feature-grid--aligned .cms-block-feature-grid__grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .cms-block-feature-grid--aligned .cms-block-feature-grid__item { grid-column: span 2; }
    .cms-block-feature-grid--center .cms-block-feature-grid__item:nth-child(2n+1):nth-last-child(1) { grid-column: 2 / span 2; }
    .cms-block-feature-grid--end .cms-block-feature-grid__item:nth-child(2n+1):nth-last-child(1) { grid-column: 3 / span 2; }
}

@container (width >= 48rem) {
    .cms-block-feature-grid--aligned .cms-block-feature-grid__item { grid-column: span 2; }
    .cms-block-feature-grid--upto-2 .cms-block-feature-grid__grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .cms-block-feature-grid--upto-3 .cms-block-feature-grid__grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}

/*
 * Three across. Offsets are scoped to --upto-4: a two- or three-card grid has
 * no short row here, and a two-card grid's first card is `3n+1` with two
 * remaining — an unscoped rule would push it off its line.
 */
@container (48rem <= width < 64rem) {
    .cms-block-feature-grid--upto-4 .cms-block-feature-grid__grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
    .cms-block-feature-grid--upto-4.cms-block-feature-grid--center .cms-block-feature-grid__item:nth-child(3n+1):nth-last-child(1) { grid-column: 3 / span 2; }
    .cms-block-feature-grid--upto-4.cms-block-feature-grid--center .cms-block-feature-grid__item:nth-child(3n+1):nth-last-child(2) { grid-column: 2 / span 2; }
    .cms-block-feature-grid--upto-4.cms-block-feature-grid--end .cms-block-feature-grid__item:nth-child(3n+1):nth-last-child(1) { grid-column: 5 / span 2; }
    .cms-block-feature-grid--upto-4.cms-block-feature-grid--end .cms-block-feature-grid__item:nth-child(3n+1):nth-last-child(2) { grid-column: 3 / span 2; }
}

/* Four across. */
@container (width >= 64rem) {
    .cms-block-feature-grid--upto-4 .cms-block-feature-grid__grid { grid-template-columns: repeat(8, minmax(0, 1fr)); }
    .cms-block-feature-grid--upto-4.cms-block-feature-grid--center .cms-block-feature-grid__item:nth-child(4n+1):nth-last-child(1) { grid-column: 4 / span 2; }
    .cms-block-feature-grid--upto-4.cms-block-feature-grid--center .cms-block-feature-grid__item:nth-child(4n+1):nth-last-child(2) { grid-column: 3 / span 2; }
    .cms-block-feature-grid--upto-4.cms-block-feature-grid--center .cms-block-feature-grid__item:nth-child(4n+1):nth-last-child(3) { grid-column: 2 / span 2; }
    .cms-block-feature-grid--upto-4.cms-block-feature-grid--end .cms-block-feature-grid__item:nth-child(4n+1):nth-last-child(1) { grid-column: 7 / span 2; }
    .cms-block-feature-grid--upto-4.cms-block-feature-grid--end .cms-block-feature-grid__item:nth-child(4n+1):nth-last-child(2) { grid-column: 5 / span 2; }
    .cms-block-feature-grid--upto-4.cms-block-feature-grid--end .cms-block-feature-grid__item:nth-child(4n+1):nth-last-child(3) { grid-column: 3 / span 2; }
}

/*
 * A LOGO IN PLACE OF A FEATURE CARD'S ICON (2026-09-16).
 *
 * Its own box, never the icon's: every theme draws `__icon` as a small tinted
 * disc sized for a 24px mark, and a wide wordmark inside it is unreadable. The
 * box has a FIXED HEIGHT so a row of logos with different proportions keeps
 * its titles on one line, and `contain` so nothing is cropped — a brand mark
 * with its edge cut off is worse than none. Structure only, no colour.
 */
.cms-block-feature-grid__logo {
    /* A line box, not flex, so the logo follows the theme's own text-align —
       left on most themes, centred on the ones that centre their cards. */
    display: block;
    block-size: 3.5rem;
    line-height: 3.5rem;
    margin-block-end: 1rem;
}
.cms-block-feature-grid__logo img {
    display: inline-block;
    vertical-align: middle;
    max-block-size: 100%;
    max-inline-size: min(100%, 11rem);
    inline-size: auto;
    block-size: auto;
    object-fit: contain;
}
