/* =========================================================================
   MAGICX — authored styles
   =========================================================================
   Hand-written CSS only. Tailwind utilities come from assets/css/style.css,
   which is generated by `npm run build:css` and must not be edited by hand.

   Load order in every page:
       assets/css/style.css   (generated utilities)
       assets/css/main.css    (this file — overrides win)

   Contents
       1. Brand tokens
       2. Navigation
       3. Exit-intent popup
       4. Sliders (Swiper overrides)
       5. "Your journey" stepper
       6. Reveal on scroll
       7. Interaction & feedback
       8. Gallery
       9. Home — industries marquee
   ========================================================================= */

/* -------------------------------------------------------------------------
   1. BRAND TOKENS
------------------------------------------------------------------------- */

:root {
    --magicx-green: #2cb34a;
    --magicx-navy: #0b1f5b;
    --magicx-ink: #374151;

    /* The brand green is a fill colour, not a text colour. #2CB34A measures
       2.74:1 on white and 2.50:1 on the green-100 pills, where WCAG AA wants
       4.5:1 for body text and 3:1 for large text — it failed everywhere it was
       used as type. This darker green reads as the same brand colour and
       measures 5.28:1 on white, 4.81:1 on the pills, and 5.28:1 behind white
       button text. It is used ONLY for text and for green buttons carrying
       white labels; every fill, border, icon and decoration keeps #2CB34A. */
    --magicx-green-text: #1b7c33;
}

/* -------------------------------------------------------------------------
   1a. ACCESSIBLE GREEN TEXT
   These override the Tailwind arbitrary-value utilities by name, so no markup
   changes and nothing that uses green as a background is affected.
------------------------------------------------------------------------- */

.text-\[\#2CB34A\] {
    color: var(--magicx-green-text);
}

/* Contrast is directional. Darkening helps against white, but against the navy
   panels the ORIGINAL green is the accessible one — 5.62:1 on #0B1F5B and
   6.36:1 on the footer's #07173F, where the darker green falls to 2.92 and
   3.31. So on dark surfaces the brand green is restored. */
[class*="bg-[#0B1F5B]"] .text-\[\#2CB34A\],
[class*="bg-[#07173F]"] .text-\[\#2CB34A\],
footer .text-\[\#2CB34A\] {
    color: var(--magicx-green);
}

/* White text on a green fill is the same 2.74:1 the other way round, so those
   buttons take the darker green as their background. The second selector
   catches green cards that inherit their white text from an ancestor rather
   than carrying text-white themselves. */
[class*="bg-[#2CB34A]"][class*="text-white"],
[class*="text-white"] [class*="bg-[#2CB34A]"] {
    background-color: var(--magicx-green-text);
}

/* -------------------------------------------------------------------------
   1b. VISUALLY HIDDEN
   contact.html already uses .sr-only for its field labels, but the class is
   not in the generated bundle — it was only ever coming from the Tailwind CDN
   at runtime. Defining it here means the labels stay hidden even if that CDN
   is blocked, instead of four stray words appearing above the form.
------------------------------------------------------------------------- */

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
}

/* -------------------------------------------------------------------------
   2. NAVIGATION
------------------------------------------------------------------------- */

/* Desktop links — underline grows from the centre on hover / when active. */
.nav-link {
    position: relative;
    padding-bottom: 8px;
    font-size: 16px;
    font-weight: 600;
    color: var(--magicx-ink);
    transition: 0.3s ease;
}

.nav-link:hover {
    color: var(--magicx-navy);
}

.nav-link::after {
    content: "";
    position: absolute;
    left: 50%;
    bottom: 0;
    width: 0;
    height: 3px;
    transform: translateX(-50%);
    background: var(--magicx-green);
    border-radius: 20px;
    transition: 0.3s;
}

.nav-link:hover::after,
.nav-link.active::after {
    width: 100%;
}

/* Mobile drawer links — indent on hover. */
.mobile-link {
    padding: 18px 25px;
    border-bottom: 1px solid #eee;
    font-size: 16px;
    font-weight: 600;
    color: var(--magicx-ink);
}

.mobile-link:hover {
    padding-left: 35px;
    color: var(--magicx-green);
}

/* -------------------------------------------------------------------------
   3. EXIT-INTENT POPUP
------------------------------------------------------------------------- */

#exit-popup > div {
    animation: exitPopup 0.35s ease;
}

@keyframes exitPopup {
    from {
        opacity: 0;
        transform: scale(0.8);
    }

    to {
        opacity: 1;
        transform: scale(1);
    }
}

/* -------------------------------------------------------------------------
   4. SLIDERS (Swiper overrides)
------------------------------------------------------------------------- */

/* Testimonials: let every slide size to its own content instead of being
   stretched to the tallest one, and strip the gap Swiper leaves below. */
.testimonialSwiper {
    height: auto !important;
    overflow: hidden;
}

.testimonialSwiper .swiper-wrapper {
    height: auto !important;
    align-items: flex-start !important;
}

.testimonialSwiper .swiper-slide {
    height: auto !important;
    display: flex;
    align-items: flex-start;
    padding-bottom: 0 !important;
}

.testimonialSwiper .swiper-slide > div {
    height: auto !important;
    min-height: 0 !important;
    margin-bottom: 0 !important;
}

/* Founder strip runs as a continuous marquee (autoplay delay 0 in main.js),
   so the easing has to be linear or it visibly stutters between slides. */
.founderSwiper .swiper-wrapper {
    transition-timing-function: linear !important;
    align-items: stretch;
}

/* Swiper's own CSS leaves slide height at "normal", which does not actually
   stretch here, so cards with a one-line title render shorter than cards
   whose title wraps to two or three lines. Force every slide to match the
   tallest one so all award cards render at the same size. */
.founderSwiper .swiper-slide {
    height: auto;
}

/* -------------------------------------------------------------------------
   5. "YOUR JOURNEY" STEPPER
------------------------------------------------------------------------- */

#your-journey .journey-scroll {
    scrollbar-width: none;
}

#your-journey .journey-scroll::-webkit-scrollbar {
    display: none;
}

/* Step states — driven by the data-state attribute set in main.js. */
#your-journey .journey-check {
    display: none;
}

#your-journey [data-state="done"] .journey-ico {
    display: none;
}

#your-journey [data-state="done"] .journey-check {
    display: inline-block;
}

#your-journey [data-state="active"] .journey-icon {
    background: var(--magicx-green);
    border-color: transparent;
    color: #fff;
    box-shadow: 0 10px 24px rgba(44, 179, 74, 0.35);
    transform: scale(1.06);
}

#your-journey [data-state="active"] .journey-label {
    color: var(--magicx-navy);
    font-weight: 600;
}

#your-journey [data-line="done"] .journey-line {
    background: var(--magicx-green);
}

/* Motion character: glides along the track when the active stage changes,
   with a constant little running bob. */
#journey-runner {
    transition: left 0.7s cubic-bezier(0.22, 0.61, 0.36, 1);
}

#journey-runner i {
    animation: journey-runner-bob 0.45s ease-in-out infinite alternate;
}

@keyframes journey-runner-bob {
    from {
        transform: translateY(-1px) rotate(-4deg);
    }

    to {
        transform: translateY(1px) rotate(4deg);
    }
}

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

    #journey-runner i {
        animation: none;
    }
}

#your-journey .journey-step:focus-visible .journey-icon,
#your-journey .journey-dot:focus-visible {
    outline: 2px solid var(--magicx-navy);
    outline-offset: 3px;
}

#your-journey .journey-panel:not([hidden]) {
    animation: journeyFade 0.35s ease both;
}

@keyframes journeyFade {
    from {
        opacity: 0;
        transform: translateY(10px);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    #your-journey .journey-panel:not([hidden]) {
        animation: none;
    }

    #your-journey .journey-icon,
    #your-journey .journey-label,
    #your-journey .journey-line,
    #your-journey .journey-dot {
        transition: none !important;
    }

    #your-journey [data-state="active"] .journey-icon {
        transform: none;
    }
}

/* Completion popup — small notification (not a modal) shown every time
   the visitor reaches the final "Get Placed" stage above, positioned by
   main.js right next to that stage's icon. */
#journey-complete-popup {
    animation: journeyToastIn 0.35s ease;
}

@keyframes journeyToastIn {
    from {
        opacity: 0;
        transform: translateY(16px);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

/* -------------------------------------------------------------------------
   6. REVEAL ON SCROLL
   main.js adds .is-visible as each element scrolls into view. Stagger a group
   by setting --d on the individual elements, e.g. style="--d:120ms".
   Add .js-reveal to any element to opt it in.
------------------------------------------------------------------------- */

.js-reveal,
#life-at-magicx .life-tile {
    opacity: 0;
    transform: translateY(26px);
    transition:
        opacity 0.7s ease,
        transform 0.7s ease;
    transition-delay: var(--d, 0ms);
    will-change: opacity, transform;
}

.js-reveal.is-visible,
#life-at-magicx .life-tile.is-visible {
    opacity: 1;
    transform: none;
}

@media (prefers-reduced-motion: reduce) {
    .js-reveal,
    #life-at-magicx .life-tile,
    #life-at-magicx .life-tile img,
    #life-at-magicx .life-tile span,
    #life-at-magicx .life-tile div {
        opacity: 1 !important;
        transform: none !important;
        transition: none !important;
    }
}

/* -------------------------------------------------------------------------
   7. INTERACTION & FEEDBACK

   Polish only. Nothing here changes a colour, font, size, spacing value or
   position — it makes the elements that were already there respond more
   clearly. Every motion rule is switched off under prefers-reduced-motion at
   the end of the section.
------------------------------------------------------------------------- */

/* --- 7.1 Page level ------------------------------------------------------
   The navbar is fixed and about 102px tall, so jumping to an in-page anchor
   used to leave the target hidden underneath it. scroll-padding-top reserves
   that space for every anchor jump, and smooth scrolling makes the jump
   readable instead of instant. */
html {
    scroll-behavior: smooth;
    scroll-padding-top: 7.5rem;
}

/* The fixed mobile navbar must not cover the first About-page content. */
@media (max-width: 639px) {
    .about-page {
        padding-top: 5.5rem;
    }

    .about-vision {
        border-radius: 28px;
    }

    .about-vision > div > div:first-child {
        padding: 6.5rem 1.5rem 1rem;
    }

    .about-vision > div > div:last-child {
        padding-inline: 1.5rem;
    }

    .about-vision > div > div:last-child img {
        transform: translateX(-4rem);
    }
}

/* iOS paints a grey box over anything tapped. The :active states below are a
   better cue, so drop the default flash. */
a,
button,
[role="tab"],
label {
    -webkit-tap-highlight-color: transparent;
}

/* Browsers give <button> a text cursor by default, which made the FAQ rows,
   journey tabs and menu toggles feel inert. */
button:not(:disabled),
[role="tab"],
.journey-dot,
.faq-question {
    cursor: pointer;
}

/* --- 7.2 Focus visibility -----------------------------------------------
   The Tailwind reset clears focus outlines and the form fields add
   `outline-none` on top, which left keyboard users with almost nothing to
   follow. :focus-visible shows a ring for keyboard and assistive navigation
   while leaving mouse clicks completely unchanged. Green reads against both
   the white sections and the navy ones. */
:where(a, button, input, textarea, select, summary, [tabindex]):focus-visible {
    outline: 2px solid var(--magicx-green);
    outline-offset: 3px;
}

/* --- 7.3 Buttons and button-styled links --------------------------------
   A quick, shallow response: lift on hover, press in on click. The transform
   is deliberately tiny so no neighbouring element shifts. */
button,
a[class*="bg-[#0B1F5B]"],
a[class*="bg-[#2CB34A]"] {
    transition:
        transform 0.18s ease,
        box-shadow 0.25s ease,
        background-color 0.25s ease,
        border-color 0.25s ease,
        color 0.25s ease,
        opacity 0.25s ease;
}

a[class*="bg-[#0B1F5B]"]:hover,
a[class*="bg-[#2CB34A]"]:hover {
    transform: translateY(-1px);
}

/* Icons inside a link or button grow very slightly with it. Kept in CSS rather
   than driven from the motion layer so it cannot fight a GSAP transform, and
   so it still works when GSAP is unavailable.

   .faq-icon is excluded deliberately: main.js toggles Tailwind's `rotate-45`
   on it, and a `transform: scale()` here would replace that rotation
   outright — the plus sign would stop turning into a cross. */
a i[class*="fa-"]:not(.faq-icon),
button i[class*="fa-"]:not(.faq-icon) {
    transition: transform 0.25s ease;
}

a:hover i[class*="fa-"]:not(.faq-icon),
button:not(:disabled):hover i[class*="fa-"]:not(.faq-icon) {
    transform: scale(1.08);
}

button:not(:disabled):active,
a[class*="bg-[#0B1F5B]"]:active,
a[class*="bg-[#2CB34A]"]:active {
    transform: scale(0.985);
}

/* A disabled button previously looked identical to an active one — the only
   signal was that clicking did nothing. */
button:disabled {
    opacity: 0.65;
    cursor: not-allowed;
    transform: none;
}

/* --- 7.4 Submit / loading state -----------------------------------------
   main.js adds .is-sending to the submit button while EmailJS is in flight,
   next to the existing "Sending..." label. */
.is-sending::after {
    content: "";
    display: inline-block;
    width: 0.9em;
    height: 0.9em;
    margin-left: 0.6em;
    vertical-align: -0.1em;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: uxSpin 0.6s linear infinite;
}

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

/* --- 7.5 Interactive cards ----------------------------------------------
   Only links lift. A static panel that moves under the cursor suggests it can
   be clicked, so the non-interactive cards are deliberately left alone. */
/* Elements that already declare their own hover transform are excluded, or
   this higher-specificity rule would silently replace it — the floating
   WhatsApp button (rounded-full + shadow-2xl + hover:scale-110) was lifting
   instead of scaling. */
a[class*="rounded-"][class*="border"]:not([class*="hover:scale-"]):hover,
a[class*="rounded-"][class*="shadow"]:not([class*="hover:scale-"]):hover {
    transform: translateY(-2px);
    box-shadow: 0 18px 40px rgba(11, 31, 91, 0.12);
}

a[class*="rounded-"][class*="border"],
a[class*="rounded-"][class*="shadow"] {
    transition:
        transform 0.25s ease,
        box-shadow 0.25s ease,
        border-color 0.25s ease;
}

/* --- 7.6 Form fields -----------------------------------------------------
   The existing focus border stays exactly as it was; this adds a soft ring in
   the same green so the active field is obvious at a glance. box-shadow does
   not affect layout, so nothing reflows. */
input:not([type="hidden"]),
textarea,
select {
    transition:
        border-color 0.2s ease,
        box-shadow 0.2s ease;
}

input:not([type="hidden"]):focus,
textarea:focus,
select:focus {
    box-shadow: 0 0 0 4px rgba(44, 179, 74, 0.14);
}

/* :user-invalid only matches after the browser has actually validated the
   field — so a required input does not turn red while it is still being
   filled in for the first time. */
input:not([type="hidden"]):user-invalid,
textarea:user-invalid {
    border-color: #dc2626;
    box-shadow: 0 0 0 4px rgba(220, 38, 38, 0.12);
}

/* --- 7.7 Touch targets ---------------------------------------------------
   These controls stay exactly the size they look; the invisible pseudo-element
   widens only the tappable area to the 44px minimum. Each expansion is smaller
   than the gap to its neighbour, so no control steals another's taps. */
#menu-btn,
#close-menu,
footer .w-9 {
    position: relative;
}

#menu-btn::after,
#close-menu::after,
#exit-close::after,
footer .w-9::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: max(100%, 44px);
    height: max(100%, 44px);
    transform: translate(-50%, -50%);
}

/* --- 7.8 Overlay and popup entrances -------------------------------------
   Both are toggled through Tailwind's `hidden`, which cannot be transitioned.
   A one-shot animation on appearance gives the same softening without
   touching the show/hide logic in main.js. */
#mobile-overlay:not(.hidden) {
    animation: uxFadeIn 0.3s ease;
}

#exit-popup:not(.hidden) {
    animation: uxFadeIn 0.25s ease;
}

@keyframes uxFadeIn {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

/* The drawer's own 500ms slide felt slow to close. Same distance, same
   direction — a shorter curve that settles instead of coasting. */
#mobile-menu {
    /* visibility is deliberately NOT transitioned here. It is a discrete
       property, so transitioning it flips the value at the halfway point — the
       drawer was therefore still hidden when main.js called closeBtn.focus() on
       open, and the focus move was silently dropped. Opening now flips it in
       the same style recalc; only the closing direction below delays it. */
    transition-property: transform;
    transition-duration: 380ms;
    transition-timing-function: cubic-bezier(0.32, 0.72, 0, 1);

    /* h-screen is 100vh, which on mobile browsers excludes the collapsing URL
       bar. The drawer's own content is ~587px, so on a short viewport the last
       links and the Enroll Now button sat below the fold with no way to reach
       them. dvh tracks the visible viewport, and the scroll path is the
       fallback when the content is taller still. */
    height: 100vh;
    height: 100dvh;
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* Closed, the drawer is only moved off-screen — it stays rendered, so its 8
   controls remained in the tab order on every page, including desktop where
   the drawer can never be opened.

   The delay matters in both directions. Closing: transform runs immediately and
   visibility flips only after the slide-out finishes, so the drawer does not
   vanish mid-animation. Opening: the class is removed, this rule stops applying
   and visibility returns to visible with no delay — without that, main.js calls
   closeBtn.focus() while the button is still hidden and the focus move is
   silently dropped. */
#mobile-menu.translate-x-full {
    visibility: hidden;
    transition-property: transform, visibility;
    transition-duration: 380ms, 0s;
    transition-delay: 0s, 380ms;
}

/* --- 7.9 Reduced motion --------------------------------------------------
   Everything above degrades to an instant state change. */
@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }

    #mobile-overlay:not(.hidden),
    #exit-popup:not(.hidden),
    /* the popup card's own scale-in, from section 3 */
    #exit-popup > div,
    .is-sending::after {
        animation: none;
    }

    button,
    a[class*="bg-[#0B1F5B]"],
    a[class*="bg-[#2CB34A]"],
    a[class*="rounded-"][class*="border"],
    a[class*="rounded-"][class*="shadow"],
    input:not([type="hidden"]),
    textarea,
    select,
    #mobile-menu,
    a i[class*="fa-"],
    button i[class*="fa-"] {
        transition: none !important;
    }

    button:hover,
    button:active,
    a[class*="bg-[#0B1F5B]"]:hover,
    a[class*="bg-[#2CB34A]"]:hover,
    a[class*="bg-[#0B1F5B]"]:active,
    a[class*="bg-[#2CB34A]"]:active,
    a[class*="rounded-"][class*="border"]:hover,
    a[class*="rounded-"][class*="shadow"]:hover,
    a:hover i[class*="fa-"]:not(.faq-icon),
    button:not(:disabled):hover i[class*="fa-"]:not(.faq-icon) {
        transform: none;
    }
}

/* --- 7.10 Narrow-viewport shrinking -------------------------------------
   Grid and flex items default to min-width:auto, which refuses to shrink an
   item below its content's intrinsic width. The contact form's padded inputs
   made that intrinsic width larger than the column at 320-375px, so the card
   pushed ~29px past its container and ate its own padding. Allowing the items
   to shrink fixes it; above those widths the column is already wide enough, so
   nothing changes. */
#contact-form .grid > *,
#contact-form form {
    min-width: 0;
}

/* An email address has no natural break opportunity, so at 320px it is wider
   than its own column and pushes the card out. Let it wrap instead. The footer
   already handles this with Tailwind's break-all. */
#contact-form a[href^="mailto:"] p {
    overflow-wrap: anywhere;
}

@media (max-width: 639px) {
    #contact-form .space-y-4 > a {
        width: 100%;
    }

    #contact-form .space-y-4 + .flex.flex-wrap {
        display: grid;
        grid-template-columns: 1fr;
    }

    #contact-form .space-y-4 + .flex.flex-wrap > div {
        width: 100%;
    }
}

/* --- 7.11 Touch comfort --------------------------------------------------
   Scoped to coarse pointers, so mouse/desktop rendering is untouched. The
   footer's link lists sit on a 28px pitch (16px text + 12px gap), which is
   tight for a fingertip; this lifts each row to a 44px pitch without changing
   the visual gap between them. */
@media (pointer: coarse) {
    footer li > a,
    footer a[href^="tel:"],
    footer a[href^="mailto:"],
    /* Privacy / Terms / Sitemap sit in a flex row, not a list */
    footer .flex > a:not([class*="w-9"]) {
        display: inline-block;
        padding-block: 8px;
    }

    /* Standalone links in the page body — phone numbers and "Learn more" style
       calls to action — that carry no padding utility of their own. Links that
       sit inside a sentence are exempt from the target-size rule and are left
       alone; these are discrete controls, so they get a comfortable height. */
    main a[href^="tel:"],
    main a[class*="inline-block"] {
        display: inline-block;
        padding-block: 4px;
    }

    /* inline-flex links are already flex containers, so a floor on the height
       is enough — links that already clear 24px are unaffected, which is why
       this is a min-height rather than padding. */
    main a[class*="inline-flex"] {
        align-items: center;
        min-height: 24px;
    }

    /* The journey pagination dots are 6px tall by design. Rather than resize
       them, give each one a taller invisible hit area. Vertical only, so a dot
       can never steal the tap intended for its neighbour. */
    .journey-dot {
        position: relative;
    }

    .journey-dot::after {
        content: "";
        position: absolute;
        left: 0;
        right: 0;
        top: 50%;
        height: 24px;
        transform: translateY(-50%);
    }
}

/* -------------------------------------------------------------------------
   8. GALLERY

   The photo set runs from a 1.78 landscape to a 0.45 portrait, so a fixed
   aspect grid with object-cover would have cropped the tall ones down to a
   sliver. A masonry column layout lets every photo keep its own shape.

   These rules live here rather than as Tailwind utilities because the column
   and break-inside properties are structural — the gallery must not depend on
   whether a given utility happens to be in the generated bundle.
------------------------------------------------------------------------- */

/* --- 8.1 Filter pills ---------------------------------------------------- */

/* Five pills wrapped onto five lines on a phone and pushed the first photo
   most of a screen down. One swipeable row instead. From sm there is room to
   wrap and centre them, which is what the desktop layout wants. */
.gallery-filters {
    display: flex;
    gap: 10px;
    overflow-x: auto;
    /* Bleeds to the screen edges, so a half-visible pill shows there is more
       to the right rather than the row just looking clipped. */
    margin-inline: -20px;
    padding-inline: 20px;
    padding-bottom: 2px;
    /* Deliberately no scroll-snap: snapping aligned the first pill to the
       scrollport edge, which cancelled the padding above and left it flush
       against the screen. Free scrolling is what a chip strip wants anyway. */
    /* The strip is short and obviously swipeable; a scrollbar over it only
       adds clutter on the platforms that draw one. */
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}

.gallery-filters::-webkit-scrollbar {
    display: none;
}

.gallery-filters > .gallery-filter {
    flex: 0 0 auto;
}

@media (min-width: 640px) {
    .gallery-filters {
        flex-wrap: wrap;
        justify-content: center;
        gap: 12px;
        overflow-x: visible;
        margin-inline: 0;
        padding-inline: 0;
    }
}

.gallery-filter {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 10px 20px;
    border: 1px solid #e5e7eb;
    border-radius: 9999px;
    background: #fff;
    font-size: 15px;
    font-weight: 600;
    color: var(--magicx-ink);
    transition:
        background 0.3s ease,
        border-color 0.3s ease,
        color 0.3s ease;
}

.gallery-filter:hover {
    border-color: var(--magicx-green);
    color: var(--magicx-navy);
}

.gallery-filter.is-active {
    border-color: var(--magicx-navy);
    background: var(--magicx-navy);
    color: #fff;
}

/* The count sits on a tinted chip so it reads as metadata, not as part of the
   label. On the active pill the navy fill needs the lighter brand green to
   stay legible, for the same directional-contrast reason as section 1a. */
.gallery-filter__count {
    padding: 1px 9px;
    border-radius: 9999px;
    background: #f1f5f9;
    font-size: 12px;
    font-weight: 700;
    color: var(--magicx-green-text);
}

.gallery-filter.is-active .gallery-filter__count {
    background: rgb(255 255 255 / 0.15);
    color: #fff;
}

/* --- 8.2 Masonry grid ---------------------------------------------------- */

#gallery-grid {
    /* Two columns even on the narrowest phone. One photo per screen turned 15
       of them into a long scroll with only one thing to look at; paired up
       they read as a mosaic and halve the distance. */
    columns: 2;
    column-gap: 12px;
}

@media (min-width: 640px) {
    #gallery-grid {
        column-gap: 24px;
    }
}

@media (min-width: 1024px) {
    #gallery-grid {
        columns: 3;
    }
}

/* --- 8.3 Cards ----------------------------------------------------------- */

/* Each card is a <button>: activating it opens a dialog, so it has to be
   reachable and operable from the keyboard, which a <div> with a click
   handler is not. */
.gallery-item {
    position: relative;
    display: block;
    width: 100%;
    margin: 0 0 12px;
    padding: 0;
    border: 0;
    border-radius: 16px;
    overflow: hidden;
    background: #f8fafc;
    text-align: left;
    cursor: zoom-in;
    /* Without this a card can be split across two columns mid-photo. */
    break-inside: avoid;
    -webkit-column-break-inside: avoid;
}

/* The filter hides cards with the hidden attribute. `display: block` above
   would otherwise beat the user-agent rule that makes [hidden] disappear. */
.gallery-item[hidden] {
    display: none;
}

@media (min-width: 640px) {
    .gallery-item {
        margin-bottom: 24px;
        border-radius: 24px;
    }
}

.gallery-item img {
    display: block;
    width: 100%;
    height: auto;
    transition: transform 0.6s ease;
}

.gallery-item:hover img,
.gallery-item:focus-visible img {
    transform: scale(1.06);
}

.gallery-item__veil {
    position: absolute;
    inset: 0;
    background: linear-gradient(to top, rgb(11 31 91 / 0.92) 0%, rgb(11 31 91 / 0.55) 38%, rgb(11 31 91 / 0) 72%);
    opacity: 0;
    transition: opacity 0.4s ease;
}

.gallery-item__body {
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    padding: 20px;
    opacity: 0;
    transform: translateY(12px);
    transition:
        opacity 0.4s ease,
        transform 0.4s ease;
}

.gallery-item:hover .gallery-item__veil,
.gallery-item:focus-visible .gallery-item__veil,
.gallery-item:hover .gallery-item__body,
.gallery-item:focus-visible .gallery-item__body {
    opacity: 1;
    transform: none;
}

.gallery-item__tag {
    display: block;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    /* On the navy veil the original brand green is the accessible one. */
    color: var(--magicx-green);
}

.gallery-item__title {
    display: block;
    margin-top: 6px;
    font-size: 17px;
    font-weight: 700;
    line-height: 1.35;
    color: #fff;
}

.gallery-item__zoom {
    position: absolute;
    top: 16px;
    right: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    border-radius: 12px;
    background: rgb(255 255 255 / 0.9);
    font-size: 13px;
    color: var(--magicx-navy);
    opacity: 0;
    transform: scale(0.8);
    transition:
        opacity 0.3s ease,
        transform 0.3s ease;
}

.gallery-item:hover .gallery-item__zoom,
.gallery-item:focus-visible .gallery-item__zoom {
    opacity: 1;
    transform: none;
}

/* At two columns on a phone a tile is roughly 165px wide — too narrow to
   carry a title without burying the photo it describes. The mosaic stays
   clean and the caption lives in the lightbox, one tap away. The zoom badge
   stays on as the affordance that a tap does something. */
@media (max-width: 639px) {
    .gallery-item__veil,
    .gallery-item__body {
        display: none;
    }

    /* The zoom badge goes too. On a tile this size it covers the photo it is
       meant to point at, and tapping a photo grid to enlarge needs no hint. */
    .gallery-item__zoom {
        display: none;
    }
}

/* A caption that only appears on hover is a caption a touch device never
   sees, so on a tablet — wide enough for the tile to carry one — it is
   simply always on. */
@media (hover: none) and (min-width: 640px) {
    .gallery-item__veil,
    .gallery-item__body {
        opacity: 1;
        transform: none;
    }

    .gallery-item__zoom {
        opacity: 1;
        transform: none;
    }
}

/* --- 8.4 Lightbox --------------------------------------------------------
   A modal that loses its backdrop is not merely unstyled, it is unreadable —
   the caption ends up sitting on top of the grid behind it. So unlike the rest
   of the page, the dialog carries its own layout and colour here rather than
   leaning on utilities, in the same way sections 2 and 5 own the navigation and
   the stepper. Typography stays in the markup.
------------------------------------------------------------------------- */

#gallery-lightbox {
    position: fixed;
    inset: 0;
    z-index: 99999;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    background: rgb(5 11 31 / 0.95);
}

/* main.js swaps `hidden` for `flex`, the same pair it uses for the exit popup.
   The ID above would outrank a bare `.hidden`, so the hidden state is spelled
   out at matching weight. */
#gallery-lightbox.hidden {
    display: none;
}

/* Toggled through a class, which cannot be transitioned — same one-shot
   entrance as the overlays in section 7.8. */
#gallery-lightbox:not(.hidden) {
    animation: uxFadeIn 0.25s ease;
}

.lightbox-figure {
    width: 100%;
    max-width: 64rem;
    margin: 0;
    text-align: center;
}

#lightbox-image {
    width: auto;
    height: auto;
    max-width: 100%;
    margin: 0 auto;
    border-radius: 16px;
    /* Leaves room for the caption underneath. The tallest photo in the set is a
       0.45 portrait, which without a cap would push its own caption off the
       bottom of the viewport. */
    max-height: 58vh;
    object-fit: contain;
}

.lightbox-figcaption {
    max-width: 42rem;
    margin: 20px auto 0;
}

.lightbox-tag {
    display: block;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    /* On the near-black backdrop the original brand green is the accessible
       one, for the reason set out in section 1a. */
    color: var(--magicx-green);
}

.lightbox-title {
    margin-top: 8px;
    font-size: 20px;
    font-weight: 700;
    line-height: 1.35;
    color: #fff;
}

.lightbox-text {
    margin-top: 8px;
    font-size: 14px;
    line-height: 1.75;
    color: rgb(255 255 255 / 0.72);
}

.lightbox-counter {
    position: absolute;
    top: 20px;
    left: 24px;
    font-size: 14px;
    font-weight: 600;
    color: rgb(255 255 255 / 0.6);
}

.lightbox-close {
    position: absolute;
    top: 14px;
    right: 18px;
    width: 44px;
    height: 44px;
    border-radius: 9999px;
    font-size: 30px;
    line-height: 1;
    color: rgb(255 255 255 / 0.7);
    transition: color 0.3s ease;
}

.lightbox-close:hover {
    color: #fff;
}

.lightbox-nav {
    position: absolute;
    top: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 48px;
    height: 48px;
    border-radius: 9999px;
    background: rgb(255 255 255 / 0.1);
    font-size: 18px;
    color: #fff;
    transform: translateY(-50%);
    transition: background 0.3s ease;
}

.lightbox-nav:hover {
    background: var(--magicx-green);
}

.lightbox-nav--prev {
    left: 12px;
}

.lightbox-nav--next {
    right: 12px;
}

/* One photo left after filtering — main.js hides both arrows. */
.lightbox-nav[hidden] {
    display: none;
}

@media (min-width: 1024px) {
    #gallery-lightbox {
        padding: 32px;
    }

    #lightbox-image {
        max-height: 66vh;
    }

    .lightbox-title {
        font-size: 24px;
    }

    .lightbox-text {
        font-size: 16px;
    }

    .lightbox-nav--prev {
        left: 32px;
    }

    .lightbox-nav--next {
        right: 32px;
    }
}

/* --- 8.5 Reduced motion -------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
    .gallery-item img,
    .gallery-item__veil,
    .gallery-item__body,
    .gallery-item__zoom,
    .gallery-filter,
    .lightbox-nav {
        transition: none;
    }

    .gallery-item:hover img,
    .gallery-item:focus-visible img {
        transform: none;
    }

    #gallery-lightbox:not(.hidden) {
        animation: none;
    }
}

/* -------------------------------------------------------------------------
   9. HOME — INDUSTRIES MARQUEE

   The strip's content is duplicated 4x in the markup so the loop can wrap
   seamlessly: sliding the whole track exactly -25% (one copy's width) moves
   the first copy fully out of view just as the second reaches the start,
   with two more queued up behind it. That queue matters because this strip
   is full-bleed — on a wide viewport a single copy of the six industries is
   narrower than the screen, so with only one backup copy the track would
   run out of content before the loop point and flash a gap of bare
   background; three trailing copies keep it covered up to ~2850px wide.
   The extra copies are aria-hidden — a screen reader only needs to hear the
   list once.
------------------------------------------------------------------------- */

.magicx-marquee {
    overflow: hidden;
    -webkit-mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
    mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
}

.magicx-marquee__track {
    display: flex;
    width: max-content;
    animation: magicx-marquee 32s linear infinite;
}

.magicx-marquee:hover .magicx-marquee__track {
    animation-play-state: paused;
}

.magicx-marquee__group {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    gap: 14px;
    padding-right: 14px;
}

.magicx-marquee__item {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    flex: 0 0 auto;
    padding: 10px 20px;
    border: 1px solid rgb(255 255 255 / 0.12);
    border-radius: 9999px;
    background: rgb(255 255 255 / 0.06);
    font-size: 14px;
    font-weight: 600;
    color: rgb(255 255 255 / 0.85);
    white-space: nowrap;
}

.magicx-marquee__item i {
    color: var(--magicx-green);
    font-size: 13px;
}

@keyframes magicx-marquee {
    from {
        transform: translateX(0);
    }

    to {
        transform: translateX(-25%);
    }
}

/* The duplicate copy exists only to make the animated version loop — with no
   animation running there is nothing for it to paper over, so it collapses
   to a single readable row instead of two. */
@media (prefers-reduced-motion: reduce) {
    .magicx-marquee__track {
        animation: none;
        flex-wrap: wrap;
        width: auto;
    }

    .magicx-marquee__group[aria-hidden="true"] {
        display: none;
    }
}
