/* ============================================
   NATANS OFFSET LANDING PAGE STYLES
   ============================================ */

:root {
    --primary-color: #ea580c;
    --primary-dark: #c2410c;
    --secondary-color: #64748b;
    --accent-color: #f59e0b;
    --bg-primary: #ffffff;
    --bg-secondary: #f8fafc;
    --text-primary: #1e293b;
    --text-secondary: #64748b;
}

[data-bs-theme="dark"] {
    --primary-color: #f97316;
    --primary-dark: #ea580c;
    --secondary-color: #94a3b8;
    --accent-color: #fbbf24;
    --bg-primary: #0f172a;
    --bg-secondary: #1e293b;
    --text-primary: #f8fafc;
    --text-secondary: #94a3b8;
}

/* Global Styles */
html {
    /*
     * Bootstrap's `.row` offsets its container with a negative margin equal to
     * half the gutter. With `g-5` (3rem gutters) that exceeds the container's
     * padding in the 992–1200px band, so the rows stick out past the viewport
     * and the page scrolls sideways. Clipping the axis fixes it.
     *
     * It must go on <html>, not <body>: <body> is a flex child here, so its
     * own overflow does not clip, and the horizontal scrollbar reappears. Note
     * that overflow on <html> does NOT create a scroll container, so
     * position:sticky on the header keeps working — verified by the probe.
     */
    overflow-x: hidden;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
    color: var(--text-primary);
    line-height: 1.7;
    /* Must be min-height, never height: a fixed-height flex body clips the
       sticky header's containing block to the viewport. */
    min-height: 100vh;
    margin: 0;
}

/* ==========================================================
   Site Header / Primary Navigation
   ========================================================== */

/*
 * Sticky behaviour is opt-in via a class on <html> (Dashboard →
 * Tampilan & Header). Keeping the default here as `relative` means a
 * cached stylesheet without this rule degrades to a normal header
 * instead of a permanently stuck one.
 */
.site-header {
    position: relative;
    z-index: 1030;
    /* flex: none stops the flex parent from squashing or stretching the
       header, which would change the height sticky measures against. */
    flex: 0 0 auto;
    background: #ffffff;
    box-shadow: 0 1px 3px rgba(15, 23, 42, .08);
    transition: box-shadow .25s ease, background-color .25s ease;
}

.sticky-header .site-header {
    position: sticky;
    top: 0;
}

.site-header.is-scrolled {
    box-shadow: 0 4px 18px rgba(15, 23, 42, .12);
}

.site-navbar {
    min-height: 70px;
    padding: .5rem 0;
}

/*
 * Centre the menu in the header bar.
 *
 * The nav used to be a single <ul> pushed right with `ms-auto`, which left the
 * brand alone on the left and the menu hugging the right edge. On a wide screen
 * that reads as lopsided: the logo and the links are the two things a visitor
 * looks at, and they end up at opposite ends.
 *
 * `flex: 1` + `justify-content: center` centres the links within the space
 * BETWEEN the brand and the CTA. That is centring within the leftover room, so
 * if the brand and the CTA are different widths the menu is off-centre by half
 * the difference. `position: absolute` on the nav, with equal auto margins
 * between three flex items, is what actually pins it to the container's
 * midpoint regardless of the two neighbours' widths.
 */
.site-navbar > .container {
    position: relative;
}

.site-brand {
    flex: 0 0 auto;
}

.site-header .navbar-collapse {
    display: flex;
    align-items: center;
    /* The CTA is the last flex item, so it is pushed to the right edge while the
     * absolutely-positioned nav centres itself. */
    flex: 1;
    justify-content: flex-end;
}

.site-nav-cta-item {
    margin-left: 1rem;
    flex: 0 0 auto;
}

.site-header .site-nav {
    /*
     * Anchored to the container's centre line and centred on it. The left/right
     * auto margins are what do the centring — the width is set by the content,
     * so a long menu stays centred instead of drifting.
     */
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    margin-left: auto;
    margin-right: auto;
    /* The brand and CTA must never sit underneath the links. */
    max-width: calc(100% - 340px);
}

/* Below the lg breakpoint the nav is a collapsed panel, not a centred row, so
 * it returns to the normal flow and the absolute positioning is dropped. */
@media (max-width: 991.98px) {
    .site-header .site-nav {
        position: static;
        transform: none;
        max-width: none;
        margin-left: 0 !important;
    }
}

/* Reserves the header height so in-page anchors don't land under it. */
.site-nav-spacer {
    height: 0;
}

.site-brand {
    display: flex;
    align-items: center;
    gap: .6rem;
    font-weight: 600;
    text-decoration: none;
    color: var(--text-primary);
    margin-right: 1.5rem;
}

.site-brand-mark {
    display: grid;
    place-items: center;
    width: 38px;
    height: 38px;
    flex: 0 0 38px;
    border-radius: 10px;
    background: var(--primary-color);
    color: #fff;
    font-size: 1.15rem;
    font-weight: 700;
}

/*
 * Uploaded logo.
 *
 * `height` is fixed and `width` is left to auto so an arbitrary upload scales
 * to the header's line height instead of being distorted or overflowing the
 * bar. max-width caps a very wide logo, since the brand must not push the nav
 * links off a 992px screen.
 */
.site-logo-img {
    display: block;
    height: 44px;
    width: auto;
    max-width: 170px;
    object-fit: contain;
}

/*
 * Footer logo.
 *
 * This rule used to be `filter: brightness(0) invert(1)`, on the assumption that
 * the uploaded logo had an alpha channel so only the artwork would turn white.
 * It does not: the logo is a JPEG, which has no alpha, so the filter repainted
 * the file's own white background as well. The result was a featureless white
 * rectangle — present in the DOM, loaded fine, and invisible on screen.
 *
 * So the filter is gone, and a white plate is provided instead.
 *
 * The plate is necessary, not decorative. Sampling the asset: its background is
 * luminance 254 (61% of the file) and its artwork is dark, mean luminance 149.
 * A JPEG cannot be transparent, so on a dark footer it WILL show as a white
 * rectangle. Making that rectangle deliberate — white, rounded, with a soft
 * shadow — turns an artefact into a legible logo plate; the logo itself then
 * reads at roughly 3:1, which is the asset's own contrast and cannot be improved
 * from CSS.
 *
 * The proper fix is a PNG with a transparent background uploaded in its place,
 * after which this plate can be dropped.
 */
.site-logo-footer {
    display: inline-block;
    height: 52px;
    max-width: 200px;
    width: auto;
    object-fit: contain;
    padding: .5rem .9rem;
    border-radius: .5rem;
    background-color: #fff;
    box-shadow: 0 2px 10px rgba(0, 0, 0, .18);
}

@media (max-width: 575.98px) {
    .site-logo-img {
        height: 36px;
        max-width: 130px;
    }

    .site-logo-footer {
        /*
         * NOT 44px. This element is now a padded plate, and `height` measures the
         * whole box, so 44px would squeeze the artwork into 44-16=28px while the
         * padding stayed at its full size. Add back the 16px of vertical padding
         * (.5rem top + .5rem bottom) so the logo itself is 44px tall.
         */
        height: 60px;
        padding: .5rem .75rem;
    }
}

.site-brand-text strong {
    display: block;
    font-size: .98rem;
    line-height: 1.25;
    color: var(--text-primary);
}

.site-brand-text small {
    display: block;
    font-size: .7rem;
    font-weight: 400;
    color: #64748b;
    line-height: 1.3;
}

.site-nav-link {
    position: relative;
    color: #334155;
    font-size: .9rem;
    font-weight: 500;
    padding: .5rem .8rem !important;
    white-space: nowrap;
    transition: color .18s ease;
}

.site-nav-link::after {
    content: '';
    position: absolute;
    left: .8rem;
    right: .8rem;
    bottom: .28rem;
    height: 2px;
    border-radius: 2px;
    background: var(--primary-color);
    transform: scaleX(0);
    transform-origin: center;
    transition: transform .22s ease;
}

.site-nav-link:hover,
.site-nav-link.is-active {
    color: var(--primary-color);
}

.site-nav-link:hover::after,
.site-nav-link.is-active::after {
    transform: scaleX(1);
}

.site-nav-cta {
    border-radius: 999px;
    padding: .45rem 1.15rem;
    font-size: .88rem;
    font-weight: 500;
    white-space: nowrap;
}

/*
 * Dashboard nav item — mobile-only. The <li> and <a> carry the
 * .site-nav-dashboard class. On desktop (>= 992px, where the menu expands),
 * the item is hidden so the public menu stays clean; on mobile it appears
 * as a normal link in the collapsed hamburger panel.
 */
@media (min-width: 992px) {
    .site-nav-dashboard {
        display: none;
    }
}


/*
 * Eight nav items plus the CTA stop fitting between 992px (where Bootstrap
 * expands the menu) and roughly 1200px — the CTA is pushed out of the header
 * and the page starts scrolling sideways. Rather than raise the collapse
 * breakpoint, which would change the mobile menu's behaviour, tighten the
 * type and padding across that band only. Below 992px the collapsed hamburger
 * menu has room, so nothing here applies.
 */
@media (min-width: 992px) and (max-width: 1200px) {
    .site-nav-link {
        font-size: .82rem;
        padding: .5rem .5rem !important;
    }

    .site-nav-link::after {
        left: .5rem;
        right: .5rem;
    }

    .site-nav-cta {
        padding: .4rem .8rem;
        font-size: .8rem;
    }

    .site-nav-cta-item {
        margin-left: .5rem !important;
    }
}

/* The header sits above the hero, so the mobile collapse drops a shadow. */
.navbar-toggler {
    border-color: #cbd5e1;
    padding: .4rem .6rem;
}

.navbar-toggler:focus {
    box-shadow: 0 0 0 .2rem rgba(234, 88, 12, .2);
}

/*
 * Across the lg range the expanded menu is a horizontal row whose natural
 * width can exceed the viewport by a few pixels, pushing the page into a
 * horizontal scroll. Clipping the axis is safe because the menu only stacks
 * into a column below 992px, where this rule stops applying.
 *
 * The selector targets .navbar-collapse rather than .site-navbar because the
 * collapse element is what actually overflows — it is the wide child, and
 * hiding overflow on the parent does not clip a descendant's own box.
 */
@media (min-width: 768px) {
    .site-navbar .navbar-collapse {
        overflow-x: hidden;
    }
}

@media (max-width: 991.98px) {
    /* The nav starts closed. Bootstrap only adds .show when toggled, so
     * hide everything that isn't explicitly shown. */
    #mainNav:not(.show),
    #mainNav.collapse:not(.show) {
        display: none !important;
    }
    #mainNav.show,
    #mainNav.collapse.show {
        display: flex !important;
        /*
         * Column is what makes the panel a stack. `display:flex` alone lays the
         * <ul> and the CTA out on ONE row, which is why the button used to end up
         * level with the links instead of underneath them.
         */
        flex-direction: column;
        align-items: flex-start;
    }

    .site-navbar .navbar-collapse {
        background: #fff;
        border-top: 1px solid #e2e8f0;
        margin-top: .5rem;
        padding-bottom: .75rem;
        flex: none;
        justify-content: flex-start;
        align-items: flex-start;
        flex-direction: column;
    }

    .site-nav-link {
        padding: .55rem .25rem !important;
    }

    .site-nav-link::after {
        left: .25rem;
        right: auto;
        width: 22px;
        bottom: .3rem;
    }

    /*
     * The CTA is the last flex item of the collapse panel, and the panel is
     * `display:flex` (set above for `.show`), so on mobile it would sit on the
     * same row as the nav list, stretched to fill it. Push it to a new row so
     * it stacks below the links instead of sharing their line.
     */
    .site-nav-cta-item {
        /* `flex-basis:100%` forces the wrap even at desktop-ish widths, where
         * the panel is still flexed. Combined with `margin-left:0` it aligns
         * flush with the links rather than inheriting the desktop 1rem offset. */
        flex: 0 0 100%;
        margin-top: .75rem;
        margin-left: 0;
    }

    /*
     * The button itself is NOT full width on mobile.
     *
     * `width:100%` stretched it edge to edge, which read as a stray bar rather
     * than a button aligned with the menu items — the left edge sat ~24px inside
     * the panel because the nav list keeps its own padding, so the two never
     * lined up. Sizing to content and left-aligning puts it on the same
     * baseline as the links.
     */
    .site-nav-cta {
        display: inline-block;
        width: auto;
        text-align: center;
    }

    .site-brand-text small {
        display: none;
    }
}

@media (max-width: 575.98px) {
    .site-brand-text strong {
        font-size: .88rem;
    }

    .site-brand-mark {
        width: 34px;
        height: 34px;
        flex-basis: 34px;
        font-size: 1rem;
    }
}

/* Hero Carousel Section */
.hero-carousel {
    position: relative;
    /*
     * 80% of the original full-viewport height (a 20% reduction).
     *
     * The hero used to be `calc(100vh - 70px)`, which made the first screen
     * almost entirely hero and pushed everything else below the fold. The
     * reduction is applied as a factor on the same expression rather than as a
     * fixed pixel height, so it still tracks the viewport on any screen: on a
     * tall monitor the hero is still proportionally shorter, and the header
     * allowance stays subtracted.
     */
    min-height: calc((100vh - 70px) * 0.8);
    display: flex;
    /* stretch, not center: the carousel must fill the section, otherwise the
     * photo (which lives inside a .carousel-item) only covers the height of the
     * text and the rest of the hero shows the bare gradient. */
    align-items: stretch;
    overflow: hidden;
    background: linear-gradient(135deg, #9a3412, #f97316);
}

/*
 * The carousel must take the full height of the flex row, and each slide must
 * fill the carousel, so an absolutely-positioned .hero-bg with inset:0 covers the
 * whole section rather than a 149px strip in the middle.
 *
 * height:100% is unreliable here: Bootstrap's .carousel-item is float:left inside
 * .carousel-inner, so it is a float rather than a flex item, and a float
 * ignores height on its own. The section is the flex container, so the reliable
 * way to fill it is to let the single carousel child stretch (align-items:stretch)
 * and give .carousel-inner height:100% against that stretched box.
 */
.hero-carousel > .carousel,
.hero-carousel .carousel-inner {
    width: 100%;
}

.hero-carousel > .carousel {
    /* The one child of the flex row: let it take the row's full height. */
    align-self: stretch;
    height: auto;
}

.hero-carousel .carousel-inner {
    height: 100%;
}

.hero-carousel .carousel-item {
    width: 100%;
    /*
     * A float ignores height, so the slide's own box cannot establish a definite
     * height — and a percentage height on .container needs one. min-height:100%
     * therefore resolves to "auto" here and the inner column collapses to its
     * content height, which is why justify-content:center had nothing to
     * distribute and the copy sat at the top (measured: 302px off centre).
     *
     * The float is cleared with overflow:hidden — it establishes a block
     * formatting context, so the slide becomes self-contained and the percentage
     * heights below resolve. Bootstrap's own display rules are untouched, so
     * `.carousel-item:not(.active) { display: none }` still switches slides.
     */
    overflow: hidden;
    height: 100%;
    position: relative;
}

/*
 * On a phone the hero used to fill the whole viewport (min-height:100vh), so the
 * visitor saw nothing but a gradient and had to scroll past a full screen of
 * hero to reach anything. It now takes 60% of that height, which keeps the photo
 * and the headline readable while letting the next section appear on the first
 * screen.
 *
 * The breakpoint is 991.98px, not 767.98px: measured at 768px wide the hero was
 * still 93% of the viewport, because that is exactly Bootstrap's `md` edge, and a
 * `max-width: 767.98px` query misses it. Tablets in portrait hit the same
 * "one full screen of hero" problem as phones.
 *
 * The 70px header allowance is scaled down to 42px for the narrower range, where
 * the real header is shorter. Both declarations are kept so the second wins,
 * matching the pattern used elsewhere in this file.
 */
@media (max-width: 991.98px) {
    .hero-carousel {
        min-height: 60vh;
        min-height: calc(60vh - 42px);
    }
}

.hero-carousel .container {
    margin-inline: auto;
}

/* Bootstrap gives .container min-height:100vh in its own stylesheet when used as
 * a full-height wrapper, which would push the text off-centre inside a
 * fixed-height hero. Keep it to auto so the text is centred in the slide. */
.hero-carousel .container {
    min-height: 0;
}

/*
 * The photo is a sibling of the text column inside .carousel-item, so it is
 * layered with ordinary positive z-index values rather than a negative one.
 *
 * z-index: -1 pushed the element behind the section's own background: a
 * negative-z-index child paints before positioned descendants, and .hero-carousel
 * is position:relative, so the orange gradient was drawn *over* the photo. The
 * hero showed a flat gradient no matter which image was uploaded — the markup,
 * the setting, and the file were all correct. z-index:0 with the content at 1
 * puts the photo in front of the gradient and behind the text.
 */
.hero-bg {
    position: absolute;
    inset: 0;
    /*
     * The photo auto-resizes to whatever the hero's box happens to be, at every
     * viewport, without distortion: `cover` scales the image until it fills the
     * box and crops the overflow. `contain` was rejected — it would letterbox a
     * landscape photo inside a portrait hero and expose the section's gradient
     * along two edges, which is the "background looks wrong" symptom again.
     *
     * Only background-image is set inline in the markup. background-size and
     * background-position live here on purpose: an inline declaration outranks
     * any media query, so leaving them in the style attribute made this element
     * impossible to adapt below the desktop width.
     */
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    z-index: 0;
    /*
     * The darkening exists so white text stays readable over a photo, but at
     * brightness(0.7) an already-dark photo became indistinguishable from a
     * flat panel. A lighter 0.85 plus a gradient scrim that only darkens the
     * lower half keeps the text legible while leaving the image visible.
     */
    filter: brightness(0.85) contrast(1.05);
}

/* Scrim: keeps the headline readable without flattening the whole photo. */
.hero-bg::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, rgba(15, 23, 42, 0.45) 0%, rgba(15, 23, 42, 0.15) 45%, rgba(15, 23, 42, 0.55) 100%);
    pointer-events: none;
}

/* The text column must sit above the photo. */
.hero-carousel .container,
.hero-content {
    position: relative;
    z-index: 1;
}

/*
 * Auto-resize tuning for the shorter mobile hero. These live *after* the base
 * .hero-bg rule on purpose: same-specificity rules resolve in source order, so a
 * media query placed earlier is silently overridden by the later base rule and
 * every phone keeps the desktop crop. Measured at 390px after that mistake —
 * background-position still read "50% 50%".
 *
 * The photo keeps `cover` so it fills the box, but the crop point moves up: a
 * landscape photo centred inside a short box loses its subject at the bottom
 * half, which reads as "the background is wrong" rather than as a crop.
 */
@media (max-width: 991.98px) {
    .hero-bg {
        background-position: center 35%;
    }
}

/*
 * Short landscape phones: the hero is now roughly half the viewport, and a
 * headline sized for a 60vh portrait box would overflow it. Scale the type down
 * rather than letting it spill over the photo.
 */
@media (max-width: 991.98px) and (orientation: landscape) {
    .hero-bg {
        background-position: center 40%;
    }

    .hero-content .display-4 {
        font-size: clamp(1.5rem, 5.5vw, 2.1rem);
        margin-bottom: 1rem !important;
    }

    .hero-content .lead {
        font-size: 1rem;
        margin-bottom: 1rem !important;
    }
}

/*
 * Vertical centring of the hero copy.
 *
 * Two earlier attempts failed, and the second one is why the hero text
 * "stacked on itself":
 *
 *  1. `padding-top: 8vh` — a guess, not centring, which drifts as the title wraps.
 *  2. `display: flex` on .carousel-item — this overrode Bootstrap's own
 *     `.carousel-item:not(.active) { display: none }`. Every slide became
 *     visible at once, so all three sets of copy were painted on top of each
 *     other. Centring was never the tradeoff; it broke slide switching.
 *
 * So the slide itself must keep Bootstrap's display rules untouched. The
 * container *inside* the slide is free to be a flex column, and it is that
 * column which centres the copy. It gets a definite height from
 * `.hero-carousel .carousel { height: 100% }` plus the full-height chain, so
 * justify-content has real space to distribute.
 */
.hero-carousel .carousel-item > .container {
    min-width: 0;
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

/*
 * The text block fills the centred column. align-items:center on the column
 * already handles the horizontal case, so margin:auto would be a second,
 * conflicting instruction — keep the block's own alignment instead.
 */
.hero-carousel .hero-content {
    width: 100%;
}

/*
 * Text alignment inside the hero, driven by the admin switch
 * "Posisi Teks Hero" (Settings -> hero_text_align).
 *
 * The buttons live in their own row so they can be centred independently of the
 * heading: centring the whole block would also centre the CTA, which is what
 * looks wrong when only the text was asked to be centred.
 */
.hero-content.hero-text-center {
    text-align: center;
}

.hero-content.hero-text-start {
    text-align: left;
}

.hero-content.hero-text-end {
    text-align: right;
}

/* The CTA row follows the same alignment as the text. */
.hero-content.hero-text-center .hero-cta-row {
    justify-content: center;
}

.hero-content.hero-text-start .hero-cta-row {
    justify-content: flex-start;
}

.hero-content.hero-text-end .hero-cta-row {
    justify-content: flex-end;
}

/*
 * Centring a single wide line with margin:auto leaves it hugging the left when
 * the copy is narrower than the column, so constrain the measure: centred text
 * should not run the full 1140px container width.
 */
.hero-content.hero-text-center h1,
.hero-content.hero-text-center .lead {
    margin-left: auto;
    margin-right: auto;
    max-width: 22ch;
}

.hero-content.hero-text-center .lead {
    max-width: 46ch;
}

.hero-content {
    color: white;
    text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}

.hero-content h1 {
    font-size: clamp(2.5rem, 5vw, 4rem);
    text-shadow: 0 3px 20px rgba(0, 0, 0, 0.4);
}

.hero-content .lead {
    font-size: 1.25rem;
    max-width: 600px;
}

.carousel-control-prev-icon,
.carousel-control-next-icon {
    filter: white;
}

.logo-small {
    filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.3));
}

/* Section Base Styles */
.section {
    padding: 80px 0;
    position: relative;
    z-index: 1;
}

.section:last-of-type {
    padding-bottom: 60px;
}

.bg-light {
    background: var(--bg-secondary);
}

/*
 * Per-section background colours.
 *
 * The admin can paint each section from "Background Section". The fallback in
 * every rule repeats that section's original template colour, so a site that
 * sets nothing looks exactly as before.
 *
 * Why ID selectors and why `bg-light` was removed from the markup: the utility
 * `.bg-light { background-color: ... !important }` — which Bootstrap defines and
 * this file also overrode — carries `!important`, so no same-specificity rule can
 * beat it. An ID selector can, because `!important` from a class loses to a plain
 * declaration on an ID. Rather than fight it with another `!important`, the four
 * sections that used `bg-light` now take their colour from these rules and the
 * utility is no longer applied to them.
 *
 * The colours are always `#rrggbb` — ContentForm validates the hex before
 * storing — so no untrusted CSS reaches the page.
 */
#hero-section { background-color: var(--bg-hero, transparent); }
#about-section { background-color: var(--bg-about, var(--bg-secondary)); }
#vision-section { background-color: var(--bg-vision, transparent); }
#products-section { background-color: var(--bg-products, var(--bg-secondary)); }
#services-section { background-color: var(--bg-services, transparent); }
#portfolio-section { background-color: var(--bg-gallery, var(--bg-secondary)); }
#testimonials-section { background-color: var(--bg-testimonials, transparent); }
#contact-section { background-color: var(--bg-contact, var(--bg-secondary)); }
/* The footer paints its own background image and keeps white text, so its
 * colour sits behind that image — see the note on the admin field. */
#footer { background-color: var(--bg-footer, #0f172a); }

/*
 * Background images per section.
 *
 * `background-image` is set SEPARATELY from the `background-color` above rather
 * than via a `background` shorthand. The admin picks a colour and an image
 * independently and both must show; a single shorthand would let the later
 * declaration silently reset the earlier one, which is how a chosen colour
 * disappears the moment a picture is added.
 *
 * Each var falls back to `none`, so a section with no image paints no image at
 * all and is otherwise untouched — no per-section override needed to switch it
 * off.
 *
 * `cover` fills a section of any height without letterboxing; `no-repeat` stops
 * a small image tiling into a visible grid.
 *
 * No scrim here. A wash over the image would have to be dark to keep white text
 * readable, which turns these sections' dark body copy grey-on-grey. Instead the
 * image is painted at reduced strength, so the section keeps its own colour and
 * text keeps its own contrast, and the admin still sees the picture they picked.
 */
#hero-section {
    background-image: var(--bg-hero-image, none);
}
#about-section {
    background-image: var(--bg-about-image, none);
}
#vision-section {
    background-image: var(--bg-vision-image, none);
}
#products-section {
    background-image: var(--bg-products-image, none);
}
#services-section {
    background-image: var(--bg-services-image, none);
}
#portfolio-section {
    background-image: var(--bg-gallery-image, none);
}
#testimonials-section {
    background-image: var(--bg-testimonials-image, none);
}
#contact-section {
    background-image: var(--bg-contact-image, none);
}

#hero-section,
#about-section,
#vision-section,
#products-section,
#services-section,
#portfolio-section,
#testimonials-section,
#contact-section {
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
}

section.id-section {
    scroll-margin-top: 80px;
}

/* Section Title */
.section-title {
    font-size: clamp(2rem, 4vw, 2.5rem);
    font-weight: 700;
    margin-bottom: 10px;
}

.section-title + p {
    max-width: 600px;
    margin: 0 auto 0;
}

/*
 * On mobile view, paragraph text must be justified (left-right alignment).
 */
@media (max-width: 991.98px) {
    #about-section .section-title + p,
    #about-section .about-content {
        text-align: justify;
        max-width: none;
        margin: 0 auto 0;
    }
    
    /* Commitment section paragraph justification on mobile */
    #commitment-section .commitment-content p.lead {
        text-align: justify;
        max-width: none;
        margin: 0 auto 0;
    }
}

/*
 * Section content width.
 *
 * The site was pinned to Bootstrap's stock 1140px cap at every desktop size,
 * so on a 1920px monitor the content occupied 59% of the width and 390px of
 * dead space sat on each side. The cap is now raised per breakpoint and the
 * widest screens get their own tier.
 *
 * Values are not arbitrary: each step matches a Bootstrap column breakpoint, so
 * the existing col-* grid still divides evenly. Going past ~1520px stops looking
 * like "more content" and starts looking like a stretched line length, which is
 * why it stops there instead of following the viewport indefinitely.
 */
.container {
    max-width: 1200px;
    /*
     * Bootstrap gives `.container` 12px side padding, but `g-5` rows pull the
     * content out by half a 3rem gutter (24px). The row therefore stuck out
     * past the viewport and the page scrolled sideways. Widening the container
     * made this visible at more breakpoints, not fewer.
     *
     * Padding must be >= half the gutter for any `g-5` row inside it to stay
     * within the box. 24px is exactly that, so the row now starts at the
     * container edge instead of outside it.
     */
    padding-left: 24px;
    padding-right: 24px;
}

/*
 * Bootstrap sets container padding via its own `.container` rules in a
 * stylesheet loaded after this one, so equal-specificity declarations here
 * would lose. These selectors are a notch more specific on purpose.
 */
.landing-body .container,
body .container {
    padding-left: 24px;
    padding-right: 24px;
}

@media (min-width: 1200px) {
    .container {
        max-width: 1280px;
    }
}

@media (min-width: 1400px) {
    .container {
        max-width: 1440px;
    }
}

@media (min-width: 1700px) {
    .container {
        max-width: 1520px;
    }
}

/* Fade-up Animation */
@keyframes fadeUp {
    from {
        opacity: 0;
        transform: translateY(30px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Scroll-reveal animation.
 *
 * Design rule: content is visible by default. There is no CSS state that
 * hides anything. The animation is purely additive — JS adds .show to fade
 * elements in, and if JS never runs, the page is simply static. */
.fade-up {
    opacity: 1;
    transform: none;
}

.fade-up.show {
    animation: fadeUp 0.6s ease both;
}

@keyframes fadeUp {
    from {
        opacity: 0;
        transform: translateY(24px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.fade-up.show.delay-100 { animation-delay: 0.1s; }
.fade-up.show.delay-200 { animation-delay: 0.2s; }
.fade-up.show.delay-300 { animation-delay: 0.3s; }
.fade-up.show.delay-400 { animation-delay: 0.4s; }
.fade-up.show.delay-500 { animation-delay: 0.5s; }
.fade-up.show.delay-600 { animation-delay: 0.6s; }
.fade-up.show.delay-700 { animation-delay: 0.7s; }
.fade-up.show.delay-800 { animation-delay: 0.8s; }
.fade-up.show.delay-900 { animation-delay: 0.9s; }
.fade-up.show.delay-1000 { animation-delay: 1s; }

/* Respect the OS setting — and never leave anything invisible if the
   animation never runs. */
@media (prefers-reduced-motion: reduce) {
    .fade-up.show {
        animation: none;
        opacity: 1;
        transform: none;
    }
}

.no-anim {
    opacity: 1 !important;
    transform: none !important;
}

/*
 * In-page anchors must clear the sticky header — but only when there is one.
 * With the header switched off this padding would push every anchor target
 * 82px below where the visitor expects it.
 */
html {
    scroll-behavior: smooth;
    scroll-padding-top: 0;
}

.sticky-header {
    scroll-padding-top: 82px;
}

/* Button Styles */
.btn-primary {
    background: var(--primary-color);
    border: none;
}

.btn-primary:hover {
    background: var(--primary-dark);
    transform: translateY(-2px);
    box-shadow: 0 8px 25px rgba(234, 88, 12, 0.35);
}

.btn-outline-primary {
    border: 2px solid var(--primary-color);
    color: var(--primary-color);
}

.btn-outline-primary:hover {
    background: var(--primary-color);
    color: white;
}

/* Card Styles */
.card {
    border: 1px solid #e2e8f0;
    transition: all 0.3s ease;
}

.card:hover {
    transform: translateY(-5px);
    box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
}

.card-header {
    border-bottom: none;
}

.card-title {
    font-weight: 600;
}

/* Product Grid */
.product-card {
    height: 100%;
    display: flex;
    flex-direction: column;
}

.product-card .card-body {
    flex: 1;
}

/*
 * Centre the card text.
 *
 * A card is symmetrical around its image, so left-aligned text under a centred
 * photo reads as misaligned rather than deliberate. The price and category are
 * centred with it for the same reason.
 */
.product-card .card-body,
.product-card .card-title,
.product-card .card-text {
    text-align: center;
}

.product-card .card-title {
    /* Two lines is the most a product name ever needs; more wraps into a tall
     * ragged block that pushes the price off the card. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.product-card .price {
    font-size: 1.25rem;
    text-align: center;
}

/* Service Cards */
.service-card {
    height: 100%;
}

.service-icon {
    width: 60px;
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 12px;
}

/*
 * Service category filter.
 *
 * Pill buttons rather than a <select>: on a landing page the categories are
 * usually three or four, and showing them all at once is faster than opening a
 * dropdown on a phone. They wrap, so the row grows instead of scrolling
 * sideways.
 */
.service-filter {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: .5rem;
    margin-bottom: 2rem;
}

.service-filter-btn {
    padding: .45rem 1.1rem;
    border: 1px solid #dee2e6;
    border-radius: 999px;
    background: #fff;
    color: #495057;
    font-size: .875rem;
    font-weight: 500;
    line-height: 1.4;
    cursor: pointer;
    transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}

.service-filter-btn:hover {
    border-color: var(--primary-color);
    color: var(--primary-color);
}

.service-filter-btn.is-active {
    background: var(--primary-color);
    border-color: var(--primary-color);
    color: #fff;
}

.service-filter-btn:focus-visible {
    outline: 2px solid var(--primary-color);
    outline-offset: 2px;
}

/*
 * Hiding a filtered-out card.
 *
 * `display:none` on the Bootstrap column itself, not on an inner wrapper: the
 * card is a direct child of .row, and Bootstrap's negative margins on .row
 * would otherwise leave a gap where a hidden column's width used to be.
 *
 * Applies to both the Layanan and Produk columns. The rule used to name only
 * .service-col, so a filtered-out product card stayed visible.
 */
.service-col.is-filtered,
[data-product-category].is-filtered {
    display: none;
}

/* Gallery Grid
 *
 * The markup is a Bootstrap row: <div class="row g-3" id="gallery-grid"> whose
 * children are col-6 col-sm-4 col-md-3 columns. This block used to declare
 * `display: grid` with `repeat(auto-fill, minmax(120px, 1fr))` on the same id.
 *
 * Those two systems fought: each column is a grid *item*, so its width came from
 * the grid track (as small as 96px) rather than from its Bootstrap column class,
 * and the row's own gutters/width were ignored. A 600×450 photo rendered at
 * 32×24 px. The row/column classes already size the layout correctly, so this
 * block now only handles the image treatment and the filter hooks.
 */
#gallery-grid {
    /* Intentionally not display:grid — the Bootstrap row/columns own the layout.
     * Nothing else is set here on purpose: the id sits on a .row, so any margin
     * declared here would override the row's own spacing. */
}

.gallery-item {
    overflow: hidden;
    border-radius: 0.5rem;
    height: 100%;
    background: #e2e8f0;
    cursor: pointer;
}

.gallery-item img {
    width: 100%;
    /* A fixed 4:3 box so the grid keeps even rows regardless of the mix of
     * portrait and landscape uploads; object-fit then crops to fill it. */
    aspect-ratio: 4 / 3;
    height: auto;
    object-fit: cover;
    display: block;
    transition: transform 0.3s ease, transform-origin: center;
}

/*
 * Product card image box.
 *
 * The markup asked for `class="card-image ratio"`, but this site ships no
 * Bootstrap ratio CSS, and a bare `.ratio` has no dimensions of its own — the
 * ratio helpers are `.ratio-4x3` and friends. So the wrapper collapsed to zero
 * height, the <img> inside it was measured 330x0, and every product picture was
 * invisible on the page even though the tag was present and the file loaded.
 *
 * The box is a fixed 4:3 so the card grid stays on a tidy grid whatever the
 * uploads look like — that part is deliberate.
 *
 * `object-fit: contain`, not `cover`: cover crops to fill, which silently cut
 * the top and bottom off every portrait photo and left people wondering why
 * their uploaded picture did not match what they saw. Contain scales the whole
 * photo to fit inside the box, so nothing is ever cut off; a portrait upload
 * simply sits centred with the letterbox showing the background colour.
 */
.product-card .card-image {
    width: 100%;
    aspect-ratio: 4 / 3;
    overflow: hidden;
    /* Set from the admin's "Warna Latar Foto Produk". The fallback keeps the
     * box opaque if the variable is ever undefined. */
    background-color: var(--product-image-bg, #f1f5f9);
}

.product-card .card-image img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

.gallery-item:hover img {
    transform: scale(1.1);
}

/* Gallery Modal */
.gallery-modal {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.9);
    z-index: 1000;
    opacity: 0;
    transition: opacity 0.3s ease;
}

.gallery-modal.active {
    display: block;
    opacity: 1;
}

.gallery-modal-content {
    position: relative;
    max-width: 90%;
    max-height: 90%;
    margin: 5% auto;
}

.gallery-modal-img {
    width: 100%;
    height: auto;
    border-radius: 8px;
}

/* Footer Styles */
/*
 * The gradient lives in `background-image`, NOT in a `background` shorthand.
 *
 * A shorthand here resets every other background property, including the
 * `background-color` set by the admin (`--bg-footer`). That is what made the
 * footer logo disappear: the logo is filtered to solid white so it reads on a
 * dark surface, and with the colour reset the footer fell back to transparent,
 * so white sat on white and the image vanished.
 *
 * The gradient is semi-transparent on purpose. Fully opaque `#0f172a` → `#1e293b`
 * would hide the admin's colour completely, making the Background Footer setting
 * look broken. At this alpha it tints whatever colour was chosen underneath,
 * which keeps the setting honest and keeps the footer dark enough for white
 * text and the white-filtered logo.
 */
#footer {
    background-image: linear-gradient(135deg,
        rgba(15, 23, 42, .92),
        rgba(30, 41, 59, .92));
}

#footer a {
    transition: opacity 0.3s ease;
}

#footer a:hover {
    opacity: 0.8;
}

/* Contact Info */
.contact-info i {
    width: 24px;
}

/*
 * Social links in the contact section. These are labelled chips rather than
 * bare icons: a grid of unlabelled glyphs is a guessing game for anyone who
 * does not recognise the logo, and the platform names are needed on hover
 * anyway.
 */
.social-links {
    margin: 0;
    padding: 0;
}

.social-link {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    padding: .5rem .9rem;
    border: 1px solid var(--bs-border-color, #e2e8f0);
    border-radius: 999px;
    background: #fff;
    color: var(--text-primary);
    font-size: .9rem;
    font-weight: 500;
    text-decoration: none;
    transition: border-color .2s ease, color .2s ease,
                background-color .2s ease, transform .2s ease;
}

.social-link i {
    font-size: 1.05rem;
    line-height: 1;
    color: var(--primary-color);
    transition: color .2s ease;
}

.social-link:hover,
.social-link:focus-visible {
    border-color: var(--primary-color);
    background: var(--primary-color);
    color: #fff;
    transform: translateY(-2px);
}

.social-link:hover i,
.social-link:focus-visible i {
    color: #fff;
}

.social-link:focus-visible {
    outline: 3px solid rgba(234, 88, 12, .4);
    outline-offset: 2px;
}

/*
 * Social links live in the footer, which is dark, so the chip needs its own
 * colours — the light-surface defaults above would render dark text on dark.
 */
.social-link-footer {
    border-color: rgba(255, 255, 255, .25);
    background: rgba(255, 255, 255, .06);
    color: rgba(255, 255, 255, .9);
}

.social-link-footer i {
    color: var(--accent-color, #fbbf24);
}

.social-link-footer:hover,
.social-link-footer:focus-visible {
    border-color: #fff;
    background: #fff;
    color: var(--text-primary);
}

.social-link-footer:hover i,
.social-link-footer:focus-visible i {
    color: var(--primary-color);
}

/*
 * Icon-only variant: a square touch target with the glyph centred. The
 * accessible name stays on the anchor's aria-label, so hiding the text does
 * not hide the link from screen readers.
 */
.social-link-icon-only {
    width: 42px;
    height: 42px;
    padding: 0;
    justify-content: center;
    border-radius: 50%;
}

.social-link-icon-only i {
    font-size: 1.15rem;
}

@media (max-width: 575.98px) {
    .social-link-icon-only {
        width: 40px;
        height: 40px;
    }
}

/* On narrow screens the label can be dropped, but the accessible name stays. */
@media (max-width: 575.98px) {
    .social-link {
        padding: .5rem .7rem;
    }

    .social-link-label {
        font-size: .85rem;
    }
}

@media (prefers-reduced-motion: reduce) {
    .social-link {
        transition: background-color .2s ease, color .2s ease;
    }

    .social-link:hover,
    .social-link:focus-visible {
        transform: none;
    }
}

/* Commitment Section */
#commitment-section {
    /* Optional per-section colour; transparent falls through to the page. An
     * ID selector so the admin's colour cannot be beaten by another rule. */
    background-color: var(--commitment-bg, transparent);
    /*
     * The image is a SEPARATE property, not a `background` shorthand: a
     * shorthand would reset the background-color set just above and the
     * admin's colour would vanish the moment a picture was chosen. Same rule as
     * the other sections.
     */
    background-image: var(--commitment-bg-image, none);
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
}

/*
 * The eyebrow is a small label above the heading, styled to read as a badge
 * rather than a heading. It uses the brand colour, which is what ties the
 * section to the rest of the page.
 */
.commitment-eyebrow {
    font-size: .875rem;
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--primary-color);
}

/*
 * The points list gets a tinted panel rather than plain rows: it needs to hold
 * its own against the text column, and an unboxed list reads as leftover
 * content. The panel is subtle on purpose — it sits below the statement, not
 * in front of it.
 */

/*
 * The points sit in a right-hand panel beside the statement: title and
 * description on the left, the C-R-E-A-T-I-V-E list on the right.
 *
 * They used to be three long value/label pairs — "15+ / Tahun Pengalaman" — in
 * this same `col-lg-5` panel, stacked as plain rows. The list is now the
 * acronym: eight short single-letter values against short English words, so the
 * letter became a badge (a tick in front of every letter was decoration that
 * competed with the one thing the reader should take away) and the panel needs
 * tighter spacing to stay shorter than the text column it sits beside.
 *
 * A single column, NOT a grid.
 *
 * Two columns were tried here and it is what the layout should not do: at ~552px
 * the panel would only give each cell about 240px, and the rows would fill
 * left-to-right, so the list read as C, E, T, V, R, A, I, E — the acronym
 * destroyed. One column keeps the sequence exactly as authored and fits
 * "Responsibility" on one line.
 */
.commitment-points {
    background: var(--bg-secondary);
    border-radius: 1rem;
    padding: 1.5rem 1.75rem;
    display: flex;
    flex-direction: column;
    gap: .75rem;
}

.commitment-point {
    display: flex;
    align-items: center;
    gap: .75rem;
    /* Zero min-width: a long word like "Responsibility" must be allowed to use
     * the whole cell, but it must never be the reason the grid widens. */
    min-width: 0;
}

/*
 * The letter is the value, so it is drawn as a badge instead of the old
 * check-circle icon. A tick in front of every letter was decoration that
 * competed with the one thing the reader is meant to take away.
 */
.commitment-point-value {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: .5rem;
    background: var(--primary-color);
    color: #fff;
    font-weight: 700;
    font-size: 1rem;
    line-height: 1;
}

.commitment-point-label {
    color: var(--text-primary);
    font-weight: 500;
    /* `min-width: 0` on the flex parent lets this shrink; without `overflow-wrap`
     * a single long word would push the cell wider than its track. */
    min-width: 0;
    overflow-wrap: break-word;
}

/* Testimonials */
.testimonial-card {
    background: white;
}

.testimonial-text {
    font-style: italic;
    position: relative;
    padding: 0 20px;
}

.testimonial-text::before {
    content: """;
    position: absolute;
    left: -20px;
    top: 0;
    font-size: 4rem;
    color: rgba(234, 88, 12, 0.1);
    font-family: Georgia, serif;
}

.testimonial-text::after {
    content: "";
    position: absolute;
    right: -20px;
    bottom: -20px;
    font-size: 4rem;
    color: rgba(234, 88, 12, 0.1);
    font-family: Georgia, serif;
}

/* Responsive Adjustments */
@media (max-width: 991.98px) {
    .section {
        padding: 60px 0;
    }
    
    .hero-content h1 {
        font-size: 2rem;
    }
    
    .hero-content .lead {
        font-size: 1rem;
    }
    
    .about-content {
        text-align: center;
    }
    
    .about-visual {
        margin-top: 30px;
    }
}

.about-visual {
    /* Fill the column rather than the image's natural size. `height: auto` keeps
     * the aspect ratio, so a tall photo gets taller instead of squashed. */
    width: 100%;
    /* A CSS grid with an auto row per photo. This is what makes the second
     * image work: the photos share the column's height instead of each taking
     * its own natural height, so two stacked photos stay as tall as one. */
    display: grid;
    gap: 1rem;
}

.about-visual img {
    width: 100%;
    height: 100%;
    max-width: 100%;
    /* Without this, a grid item's automatic minimum size is its content size, so
     * an image refuses to shrink below its own pixel height and overflows the
     * capped row. `min-height: 0` lets the `1fr` row actually shrink it. */
    min-height: 0;
    display: block;
    /*
     * `cover` crops rather than distorts. Combined with the row height set below
     * this is what stops a portrait upload from growing to 900px tall beside a
     * 500px paragraph.
     */
    object-fit: cover;
    object-position: center;
}

/*
 * Match the photo column to the copy.
 *
 * The ask was "tinggi gambar disesuaikan dengan panjang deskripsi saja, supaya
 * tidak terlalu panjang", plus an optional second photo stacked below the first.
 *
 * Three CSS facts make this awkward, and each one defeated the obvious fix:
 *
 *  1. `max-height: 100%` on the image resolves against the PARENT's height.
 *     `.about-visual` is `height: auto`, so that percentage is indefinite, the
 *     declaration is dropped, and the image keeps its full aspect-ratio height.
 *     This is why an earlier `max-height` attempt changed nothing at all.
 *  2. `align-items: stretch` on the row does not cap anything. The row's height
 *     is the tallest column, so a tall photo still sets the row height and the
 *     text column merely stretches to match — the photo stays long.
 *  3. `height: 0` + `grid-auto-rows: 1fr` collapses to nothing, because an
 *     `fr` row in a zero-height container resolves to zero. An earlier version of
 *     this rule used exactly that and rendered two invisible images.
 *
 * What actually works: the photo column takes the row height, but the row
 * itself is capped. `min-height: 100%` on a grid makes it fill the row the text
 * column defines, and `max-height` then stops it exceeding the cap. Because the
 * grid has a definite height from the row, `1fr` rows resolve properly and one
 * photo or two split that height evenly.
 *
 * The cap is what actually delivers "not too tall": the photo can never exceed
 * it regardless of the description length or the uploaded aspect ratio.
 */
@media (min-width: 992px) {
    .about-section-row {
        /*
         * `flex-end`, not `stretch` and not `center`.
         *
         * The row's height must come from the copy column alone. With `stretch`
         * the photo column is stretched TO the row, and the row is sized by its
         * tallest child — so the two chase each other and the photo settles on
         * its own cap instead of the description's height. With `flex-end` the
         * photo column is only as tall as it needs to be and is aligned to the
         * bottom edge, so the row is driven by the text.
         */
        align-items: flex-end;
    }

    .about-visual {
        height: auto;
        /* No height here on purpose: the wrapper must collapse to its content,
         * otherwise it re-enters the same circular sizing that made it settle
         * on a fixed height regardless of the description.
         *
         * There is deliberately NO max-height on the wrapper. The slot below
         * takes --about-match-h (the measured copy height), and a cap here would
         * silently override it — the photo would stop matching the description
         * again the moment the copy grew past the cap, which is exactly the bug
         * this fixes. The cap that matters is the one on the MOBILE side, where
         * there is no height to match.
         */
        max-height: none;
        display: grid;
        grid-auto-rows: 1fr;
        gap: 1rem;
    }

    .about-visual-slot {
        /*
         * The slot takes its height from the copy column, measured and handed
         * down as --about-match-h by web/js/landing.js.
         *
         * CSS cannot derive this on its own: the row's height is the tallest of
         * its columns, and the photo column was a percentage of the row, so each
         * waited on the other and the photo settled on a fixed number instead.
         *
         * Two stacked photos split that height evenly (`grid-auto-rows: 1fr`), so
         * adding a second image never makes the section taller.
         *
         * `align-self: flex-end` on the wrapper is what keeps the row driven by
         * the text: with `stretch` the photo would set the row height and the
         * copy would be the thing matching it.
         */
        height: var(--about-match-h, auto);
        min-height: 0;
        overflow: hidden;
        display: flex;
        /* Never shorter than the photo's own aspect ratio would need, so a short
         * column cannot crop the photo down to a sliver. */
        min-height: min(180px, 100%);
    }

    .about-visual-slot img {
        width: 100%;
        height: 100%;
        min-height: 0;
        object-fit: cover;
        object-position: center;
        display: block;
    }
}

/*
 * Below 992px the columns stack, so the copy's height is irrelevant — there is
 * nothing to match against. The photos go back to their natural aspect ratio,
 * still bounded by the cap so a very tall upload cannot dominate the page.
 */
@media (max-width: 991.98px) {
    .about-visual {
        /*
         * A plain block, not a grid. With auto rows the grid's height is the sum
         * of the photos' natural heights, so a `max-height` on the wrapper would
         * clip the container while the images kept their full height and spilled
         * out of it.
         */
        display: block;
        height: auto;
        max-height: none;
        max-block-size: none;
    }

    /*
     * The slot is a plain wrapper on mobile: it must not inherit the desktop
     * height rule, or every photo collapses to a fixed box instead of keeping
     * its own aspect ratio in the stacked layout.
     */
    .about-visual-slot {
        height: auto;
        min-height: 0;
        overflow: visible;
        display: block;
    }

    .about-visual-slot img {
        width: 100%;
        height: auto;
        min-height: 0;
        display: block;
        /*
         * Bound each photo on its own. Stacked, there is no height to match, so
         * the photo keeps its own aspect ratio; this ceiling only exists so a
         * very tall upload cannot dominate a phone screen. `object-fit: contain`
         * keeps the whole photo inside the bound instead of cropping it.
         *
         * Defined here rather than relying on a global custom property, because
         * the desktop rule must NOT be capped — matching the description is the
         * whole point there.
         */
        max-height: 640px;
        object-fit: contain;
        object-position: center;
    }

    /* The two photos stack with a gap even though the wrapper is a plain block,
     * so the spacing does not depend on grid being active. */
    .about-visual-slot + .about-visual-slot {
        margin-top: 1rem;
    }
}

/*
 * Justify the About copy.
 *
 * A ragged right edge on body text is the classic "not finished" look, so the
 * title and the paragraph are both justified. `text-align: justify` leaves the
 * last line of every paragraph ragged, which is correct — only full lines
 * stretch. The heading is centred rather than justified because a justified
 * single-line heading is stretched across the full width and looks broken.
 *
 * Capped so the measure stays readable: a 1900px-wide screen would otherwise
 * set one enormous line per row.
 */
.about-content .lead {
    text-align: justify;
    text-justify: inter-word;
    hyphens: auto;
    max-width: 68ch;
}

.about-content .section-title {
    text-align: center;
    max-width: none;
}

/*
 * On a phone there is no room to justify: a 340px measure stretched to both
 * margins opens rivers between words that are worse than a ragged edge.
 */
@media (max-width: 767.98px) {
    .section {
        padding: 50px 15px;
    }

    .about-content {
        text-align: center;
    }

    .about-content .lead {
        text-align: left;
        max-width: none;
    }

    .about-content .section-title {
        text-align: center;
    }

    .about-visual {
        margin-top: 30px;
    }
}

@media (max-width: 575.98px) {
    /* The gallery no longer uses CSS grid — the Bootstrap column classes
     * (col-6 col-sm-4 col-md-3) handle the responsive column count, so there is
     * nothing to override here. */

    .row {
        margin-left: 0;
        margin-right: 0;
    }
    
    .col {
        padding-left: 0;
        padding-right: 0;
    }
    
    .gallery-modal-content {
        margin: 2% auto;
    }
}

@media (max-width: 575.98px) {
    /* The gallery is laid out by the Bootstrap column classes
     * (col-6 col-sm-4 col-md-3), not by a CSS grid, so the column count is
     * already responsive. This rule used to re-impose grid-template-columns on
     * #gallery-grid, which reintroduced the tiny 96px tracks the fix removed. */

    .btn-lg {
        padding: 0.5rem 1rem;
        font-size: 0.875rem;
    }
}

/* Custom Scrollbar */
::-webkit-scrollbar {
    width: 8px;
}

::-webkit-scrollbar-track {
    background: var(--bg-secondary);
}

::-webkit-scrollbar-thumb {
    background: var(--primary-color);
    border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
    background: var(--primary-dark);
}

/* Animation for Scroll Reveal */
[data-aos="fade-up"] {
    opacity: 0;
    transform: translateY(30px);
    transition: all 0.6s ease;
}

[data-aos="fade-up".active] {
    opacity: 1;
    transform: translateY(0);
}

/* Image Optimization */
img {
    max-width: 100%;
    height: auto;
    display: block;
}

/* Print Styles */
@media print {
    .hero-carousel,
    .navbar,
    .footer {
        display: none;
    }
    
    body {
        padding-top: 20px;
    }
}
/* ================================================================
   WhatsApp CTA
   ================================================================ */

.btn-whatsapp {
    --wa-green: #25d366;
    --wa-dark: #1da851;
    background: var(--wa-green);
    border-color: var(--wa-green);
    color: #fff;
    font-weight: 600;
}

.btn-whatsapp:hover,
.btn-whatsapp:focus,
.btn-whatsapp:active {
    background: var(--wa-dark);
    border-color: var(--wa-dark);
    color: #fff;
}

.btn-whatsapp .bi {
    font-size: 1.1em;
    line-height: 1;
}

/* The header CTA sits on a white bar, so it needs the solid green. */
.site-nav-cta.btn-whatsapp {
    box-shadow: 0 2px 8px rgba(37, 211, 102, .35);
}

.site-nav-cta.btn-whatsapp:hover {
    box-shadow: 0 4px 14px rgba(37, 211, 102, .5);
}

/* On the hero the CTA sits on a coloured background. */
#hero-section .btn-whatsapp {
    background: var(--wa-green);
    border-color: #fff;
}

@media (prefers-reduced-motion: no-preference) {
    .btn-whatsapp .bi-whatsapp {
        animation: waPulse 2.4s ease-in-out infinite;
    }

    @keyframes waPulse {
        0%, 100% { transform: scale(1); }
        50%      { transform: scale(1.12); }
    }
}

/* ================================================================
   Header compaction (only while sticky is enabled)
   ================================================================ */

.sticky-compact .site-header.is-compact .site-navbar {
    min-height: 56px;
    padding: .25rem 0;
    transition: min-height .2s ease, padding .2s ease;
}

.sticky-compact .site-header.is-compact .site-brand-text small {
    opacity: 0;
    max-height: 0;
    transition: opacity .2s ease, max-height .2s ease;
}

.site-brand-text small {
    transition: opacity .2s ease, max-height .2s ease;
}

@media (prefers-reduced-motion: reduce) {
    .sticky-compact .site-header.is-compact .site-navbar,
    .sticky-compact .site-header.is-compact .site-brand-text small,
    .site-brand-text small {
        transition: none;
    }
}

/* Admin toggle rows need a little more breathing room than a plain checkbox. */
.admin-toggle {
    padding-left: 2.75em;
    margin-bottom: .25rem;
}

.admin-toggle .form-check-input {
    width: 2.4em;
    height: 1.3em;
    margin-top: .2em;
    cursor: pointer;
}

/* ================================================================
   Back to top
   ================================================================ */

.back-to-top {
    position: fixed;
    right: 1.25rem;
    /* Sits above the sticky header's shadow but below modals. */
    bottom: 1.25rem;
    z-index: 1020;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 46px;
    height: 46px;
    border-radius: 50%;
    background: var(--primary-color);
    color: #fff;
    font-size: 1.25rem;
    text-decoration: none;
    box-shadow: 0 6px 18px rgba(234, 88, 12, .4);
    /* Start hidden and below the fold; .is-visible is added by JS. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(12px);
    transition: opacity .25s ease, transform .25s ease,
                visibility .25s ease, background-color .2s ease;
}

.back-to-top:hover,
.back-to-top:focus-visible {
    background: var(--primary-dark);
    color: #fff;
    transform: translateY(-2px);
}

.back-to-top:focus-visible {
    outline: 3px solid rgba(234, 88, 12, .45);
    outline-offset: 3px;
}

.back-to-top.is-visible {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

/*
 * Nudge the glyph itself up. The icon font's optical centre sits slightly
 * below the middle of the em box, which reads as "a bit low" in a circle.
 * Moving the glyph rather than the button keeps the hit area where it is.
 */
.back-to-top .bi-arrow-up {
    position: relative;
    top: -2px;
}

/* Lift it clear of the mobile browser chrome. */
@media (max-width: 575.98px) {
    .back-to-top {
        right: .9rem;
        bottom: .9rem;
        width: 42px;
        height: 42px;
        font-size: 1.1rem;
    }

    .back-to-top .bi-arrow-up {
        top: -1px;
    }
}

@media (prefers-reduced-motion: reduce) {
    .back-to-top,
    .back-to-top:hover {
        transition: background-color .2s ease;
        transform: none;
    }
}

/* ================================================================
   Contact map
   ================================================================ */

.contact-map {
    margin-top: .5rem;
}

/*
 * Desktop contact layout: map on the left, details on the right, both columns
 * the same height. `align-items:stretch` (the default) makes the row's height
 * the tallest column's, and the map card then fills it via flex, so the map is
 * not left floating above a short list of phone numbers.
 *
 * On mobile the row is a single column, so nothing needs to match.
 */
@media (min-width: 992px) {
    #contact-section .contact-layout > [class*='col-lg-'] {
        display: flex;
    }

    #contact-section .contact-map {
        display: flex;
        flex-direction: column;
        width: 100%;
    }

    /*
     * `flex:1` on the frame plus a min-height is what actually makes the map
     * grow: the frame keeps its padding-top aspect ratio as a floor, and any
     * extra column height is absorbed here.
     */
    #contact-section .contact-map-frame,
    #contact-section .contact-map-placeholder {
        flex: 1 1 auto;
    }

    #contact-section .contact-map-frame {
        min-height: 340px;
    }
}

/*
 * A fixed aspect ratio via padding-top, not a height: the iframe has no
 * intrinsic height of its own, and height:400px breaks on narrow phones.
 */
.contact-map-frame {
    position: relative;
    width: 100%;
    padding-top: 56.25%; /* 16:9 */
    border-radius: 12px;
    overflow: hidden;
    background: #e2e8f0;
}

.contact-map-frame iframe {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
}

/* Shallower on tall/narrow screens so the map does not eat the fold. */
@media (max-width: 767.98px) {
    .contact-map-frame {
        padding-top: 75%; /* 4:3 */
    }
}

.contact-map-placeholder {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .5rem;
    padding: 2.5rem 1.5rem;
    border: 1px dashed var(--bs-border-color, #cbd5e1);
    border-radius: 12px;
    background: #fff;
    text-align: center;
    color: var(--text-secondary);
}

.contact-map-placeholder i {
    font-size: 2rem;
    color: var(--primary-color);
    opacity: .6;
}

.contact-map-placeholder p {
    max-width: 46ch;
    margin: 0;
    font-size: .95rem;
}
