/* ==========================================================================
   Layout 1 — mobile-app style storefront.
   Loaded only by templates/layout/default1.php.

   Theme colours come from the vendor's Splash Colour, emitted as custom
   properties by default1.php. Nothing here hard-codes a brand colour.

   Class names deliberately contain no digits (so: .app-shell, not .layout1-*).
   ========================================================================== */

/* --- Shell -------------------------------------------------------------- */

/* Border-box for everything this layout owns. The default content-box makes
   `width:100%` plus horizontal padding overflow its parent by the padding —
   which is exactly what pushed the card's Add button past both card edges.
   Scoped to .app-shell and its descendants rather than a bare `*` reset so it
   cannot alter anything outside the layout-1 frame. */
.app-shell,
.app-shell *,
.app-shell *::before,
.app-shell *::after {
    box-sizing: border-box;
}

html,
body {
    margin: 0;
    padding: 0;
    background: var(--app-backdrop);
    color: var(--app-ink);
    -webkit-font-smoothing: antialiased;
    /* The sidebar parks off-canvas at translateX(-100%), which otherwise widens
       the scrollable area and leaves a horizontal scrollbar on every page. */
    overflow-x: hidden;
}

/* Soft theme-tinted shapes scattered over the backdrop either side of the app
   frame, as in the reference design. Pure CSS radial gradients — no image
   assets — and fixed so they don't scroll with the content. Hidden on a phone,
   where the frame fills the screen and they would never be visible anyway. */
body::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    background-image:
        radial-gradient(38% 26% at 12% 18%, var(--app-theme-soft), transparent 70%),
        radial-gradient(30% 22% at 88% 34%, var(--app-theme-soft), transparent 70%),
        radial-gradient(26% 20% at 6% 78%, var(--app-theme-soft), transparent 70%),
        radial-gradient(34% 24% at 94% 86%, var(--app-theme-soft), transparent 70%);
}

/* The whole storefront sits in one centred column. Deliberately far wider than a
   phone: on a desktop monitor a narrow strip wastes the screen. Width lives in a
   custom property because the fixed bottom nav has to match it exactly. */
:root {
    --app-width: 1100px;
    --app-nav-height: 58px;
    /* Section side padding, doubled (16px each side). Full-bleed children inside
       a section subtract this to stay within the frame, so the two must agree —
       hence one value rather than a repeated literal. */
    --app-gutter: 32px;
    --app-pad: 16px;
    --app-ink: #1b1b1b;
    --app-muted: #6b7280;
    --app-backdrop: #f3f4f6;
}

.app-shell {
    max-width: var(--app-width);
    margin-inline: auto;
    background: #fff;
    min-height: 100vh;
    /* relative (so it paints above the decorative body::before shapes) but with
       NO z-index: a z-index here would make this a stacking context, and any
       Bootstrap modal inside it could then never rise above the .modal-backdrop
       appended to <body> — 1050 would only compete within this element. */
    position: relative;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, .04);
    /* A flex column so the footer can be pushed to the bottom on a page whose
       content is shorter than the viewport. Without this the shell's min-height
       left white space below the footer on short pages (All Categories, an
       empty cart), which read as the footer stopping halfway down.
       No padding-bottom here any more: the footer's own bottom padding clears
       the fixed nav, and on a page with no footer .app-shell-spacer does. */
    display: flex;
    flex-direction: column;
}

/* Everything the layout puts in the shell keeps its natural height; only the
   spacer grows. */
.app-shell > * {
    flex: 0 0 auto;
}

/* The one growing child. Collapses to zero on a page taller than the viewport
   and absorbs the remainder on a short one, which is what keeps the footer at
   the bottom instead of stopping partway down.
   Height stays 0 so it never adds a gap above the footer; clearing the fixed
   nav is handled by the footer's own bottom padding, and by .app-shell's
   padding below when no footer renders. */
.app-shell-spacer {
    flex: 1 1 auto;
    min-height: 0;
}

/* A vendor with nothing configured for the footer gets no .app-footer at all
   (footer_content1 returns early), and then nothing clears the fixed nav. This
   restores that clearance for exactly that case: a shell whose last child is
   the spacer rather than the footer.
   :has() is unsupported in older browsers, where this simply does not apply —
   the cost is the last section sitting under the nav on a footer-less page,
   which is the pre-existing behaviour rather than a new fault. */
.app-shell:not(:has(> .app-footer)) {
    padding-bottom: calc(var(--app-nav-height) + 16px);
}

/* Bottom padding as well as top: with none, a section's content sat directly
   against the next section's heading — the category row and the top-selling
   title were touching, which is most of what made the page read as cluttered. */
.app-section {
    padding: 20px var(--app-pad) 18px;
}

/* The product grid is inset further than the scrolling rows. A row is meant to
   run to the frame edge so the next card peeks and invites a swipe; a grid has
   no off-screen content, so the same flush edge just looks like a missing
   margin. */
.app-section > .app-grid {
    padding-inline: 8px;
}

.app-section-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    margin-bottom: 14px;
}

.app-section-title {
    font-size: 18px;
    font-weight: 700;
    letter-spacing: -.2px;
    color: var(--app-ink);
    margin: 0;
}

.app-section-link {
    font-size: 13px;
    font-weight: 600;
    color: var(--app-theme);
    text-decoration: none;
    white-space: nowrap;
}

/* Horizontal rows. Scrollbar hidden; snap keeps cards aligned after a swipe.

   The row is pulled out to the full frame width and given its own matching
   padding, so a scrolled row runs edge to edge instead of stopping at the
   section's padding. Without this the last card is sliced off flush against the
   frame edge with no gap, which reads as broken rather than scrollable. */
.app-row {
    display: flex;
    gap: 12px;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    margin-inline: calc(var(--app-pad) * -1);
    padding-inline: var(--app-pad);
    padding-bottom: 4px;
}

.app-row::-webkit-scrollbar {
    display: none;
}

.app-row > * {
    scroll-snap-align: start;
    flex: 0 0 auto;
}

/* --- Header ------------------------------------------------------------- */

.app-header {
    position: sticky;
    top: 0;
    z-index: 30;
    /* No isolation:isolate here. It forces a stacking context, which trapped the
       location-picker modal rendered inside this header at z-index 30 — far below
       the .modal-backdrop Bootstrap appends to <body> at 1040, so the backdrop
       covered the dialog and swallowed every click. position:sticky with a
       z-index already establishes the context the ::after wave needs. */
    color: #fff;
    /* Flat solid theme colour and a square bottom edge, as in the reference.
       The gradient washes and the curved ::after wave that used to sit here
       were removed with it — the reference bar is one clean block of colour. */
    background: var(--app-theme);
    /* The right-hand cluster is now two stacked lines (greeting over icons), so
       the padding is trimmed from 14px to keep the bar near its previous ~66px
       rather than letting the extra line push it past 80. */
    padding: 8px var(--app-pad);
}

/* One row at every width: burger, location (or brand), then the icon cluster.
   The search expands inside the cluster rather than taking a row of its own. */
.app-header-top {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 10px;
}


/* Sits directly after the burger, left-aligned. No white pill: vendor logos on
   this bar are light-on-dark already, and the pill only added bulk to a bar
   that needs to stay short. */
.app-header-logo {
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
    padding: 0;
    text-decoration: none;
    /* An image-less vendor falls back to text, which must not wrap. */
    min-width: 0;
}

/* Capped so a tall logo cannot stretch the bar. Raised with the bar's padding:
   the cap, not the logo's own size, is what sets the image height here. */
.app-header-logo img {
    display: block;
    max-height: 34px;
    max-width: 118px;
    width: auto;
}

/* White, not the theme colour: the logo now sits straight on the themed bar
   with no white pill behind it, so themed text would be invisible. */
.app-header-logo-text {
    font-size: 15px;
    font-weight: 700;
    color: #fff;
    white-space: nowrap;
}

/* Location picker + greeting stacked beside the logo. */
.app-header-brand {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    /* Takes the slack between logo and icons, and may shrink before they do. */
    flex: 1 1 auto;
}

/* The pin and chevron flank the location text. The text itself is allowed to
   shrink and ellipsis, so a long address cannot push the chevron off the row or
   squeeze the icon cluster. */
.app-header-loc {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
    max-width: 100%;
    background: none;
    border: 0;
    padding: 0;
    color: #fff;
    font: inherit;
    cursor: pointer;
    text-align: left;
}

/* Pin, then the two stacked lines, then the caret. The pin is sized up and
   aligned to the block rather than the first line, as in the reference. */
.app-header-loc-pin {
    flex: 0 0 auto;
    font-size: 17px;
}

.app-header-loc-caret {
    flex: 0 0 auto;
    /* Nudged onto the value line: the caret follows the address in the
       reference, not the caption above it. */
    align-self: flex-end;
    margin-bottom: 2px;
    font-size: 10px;
    opacity: .9;
}

/* "Current Location" above the address. min-width:0 so the value inside can
   ellipsis — a flex child will not shrink below its content without it. */
.app-header-loc-text {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    min-width: 0;
    line-height: 1.2;
}

.app-header-loc-label {
    display: block;
    font-size: 10.5px;
    font-weight: 500;
    opacity: .8;
    line-height: 1.3;
}

/* Shrinks to whatever the row leaves rather than a fixed 180px, so a short
   address is not truncated early on a wide frame and a long one still yields to
   the chevron instead of overflowing. min-width:0 is what lets a flex item
   ellipsis at all. */
/* max-width, not a flex basis: this is now a block inside the column, so it
   caps how much of a long address is shown before the ellipsis rather than
   competing with the caret for row space. */
.app-header-loc-value {
    display: block;
    max-width: 100%;
    font-size: 14px;
    font-weight: 700;
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Wraps so the greeting can take a line of its own above the icons (it is
   flex-basis:100%, see .app-greeting). align-items:flex-end keeps the icon row
   itself right-aligned under it. */
.app-header-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    align-items: center;
    gap: 2px 6px;
    flex: 0 0 auto;
}

.app-icon-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    /* Must not be squeezed when the search field expands beside it. */
    flex: 0 0 auto;
    border-radius: 50%;
    color: #fff;
    /* Bare icons on the bar, as in the reference — no translucent chip. The
       hover state below still gives a target for pointer users. */
    background: none;
    text-decoration: none;
    font-size: 17px;
    /* Resets for the search toggle, which is a <button> rather than an <a>. */
    border: 0;
    padding: 0;
    font-family: inherit;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
}

.app-icon-btn:hover,
.app-icon-btn:focus {
    color: #fff;
    background: rgba(255, 255, 255, .18);
}

/* Logout is destructive, so it warms to red on hover rather than taking the
   same neutral highlight as the navigation icons beside it. */
.app-icon-btn--logout:hover,
.app-icon-btn--logout:focus {
    background: rgba(255, 255, 255, .95);
    color: #d92b1f;
}

/* --- Account dropdown ---------------------------------------------------
   Login / Sign Up under the header's user icon, for a guest. Not a Bootstrap
   dropdown: its JS is loaded but its stylesheet is not, so the menu is styled
   and toggled here instead. */

.app-account {
    position: relative;
    flex: 0 0 auto;
}

.app-account-menu {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    z-index: 40;
    min-width: 168px;
    padding: 6px;
    border-radius: 12px;
    background: #fff;
    box-shadow: 0 10px 28px rgba(16, 24, 40, .16);
    /* Hidden by default; the script adds .is-open to the wrapper. visibility
       and not display:none so the panel can fade rather than snap. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(-4px);
    transition: opacity .14s ease, transform .14s ease, visibility .14s;
}

.app-account.is-open .app-account-menu {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

/* Small arrow pointing back up at the icon. */
.app-account-menu::before {
    content: "";
    position: absolute;
    top: -5px;
    right: 14px;
    width: 10px;
    height: 10px;
    background: #fff;
    transform: rotate(45deg);
}

.app-account-menu a {
    position: relative;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border-radius: 8px;
    /* Inherited white from .app-header would be invisible on this panel. */
    color: var(--app-ink);
    font-size: 14px;
    font-weight: 600;
    text-decoration: none;
    white-space: nowrap;
}

.app-account-menu a:hover {
    background: var(--app-theme-soft);
    color: var(--app-theme);
}

.app-account-menu a i {
    width: 16px;
    font-size: 13px;
    color: var(--app-theme);
    text-align: center;
}

/* Red count bubble, as in the reference. Fixed red rather than the theme
   colour: the badge sits ON the themed bar, so a themed badge would vanish
   into it. A thin ring in the header colour separates it from the icon. */
.app-badge {
    position: absolute;
    top: -3px;
    right: -3px;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    border-radius: 8px;
    background: #e5372e;
    border: 1.5px solid var(--app-theme);
    color: #fff;
    font-size: 9.5px;
    font-weight: 700;
    line-height: 13px;
    text-align: center;
}

/* First item in the right-hand cluster, ahead of the icons. A secondary caption
   rather than a heading: the vendor logo on the left is what the bar leads with.
   It may shrink and ellipsis so a long customer name yields to the icons — those
   are flex:0 0 auto and never give up width — instead of pushing them off. */
/* Sits on its own line ABOVE the icon row, right-aligned over it, rather than
   inline beside the icons. flex-basis:100% is what forces the wrap in
   .app-header-actions; the icons then flow onto the line beneath. */
.app-greeting {
    font-size: 12.5px;
    font-weight: 500;
    opacity: .92;
    margin: 0;
    min-width: 0;
    flex: 0 0 100%;
    text-align: right;
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The greeting no longer has to step aside for the expanding search: it sits on
   its own line above the icons, so the two never compete for the same row. (A
   rule hiding it on .app-search.is-open used to live here, from when both were
   inline in one cluster.) */

/* Collapsed search: just the magnifier button in the right-hand icon cluster.
   The field grows out of it on click, so the header spends no width on search
   until it is actually used. */
.app-search {
    display: flex;
    align-items: center;
    min-width: 0;
}

.app-search-field {
    /* Zero width until .is-open; overflow:hidden keeps the input from spilling
       out of the collapsed pill. */
    width: 0;
    overflow: hidden;
    background: #fff;
    border-radius: 17px 0 0 17px;
    transition: width .22s ease, padding .22s ease;
    padding: 0;
    /* Matches the 34px icon buttons it sits beside. Vertical padding used to
       set this height, which made the open field taller than the header's own
       controls so its white pill hung below the bar. */
    display: flex;
    align-items: center;
    height: 34px;
}

.app-search.is-open .app-search-field {
    width: 190px;
    padding: 0 4px 0 14px;
}

.app-search-field input {
    width: 100%;
    min-width: 0;
    border: 0;
    outline: 0;
    font-size: 14px;
    color: var(--app-ink);
    background: transparent;
}

/* While collapsed the field has no width, so keep it out of the tab order —
   otherwise keyboard users land in an invisible input. The toggle is a real
   button and stays focusable, which is the intended entry point. */
.app-search:not(.is-open) .app-search-field input {
    visibility: hidden;
}

/* Base state inherits .app-icon-btn from the markup; only the transition is
   declared here so opening animates instead of snapping. */
.app-search-toggle {
    transition: border-radius .22s ease, background-color .22s ease, color .22s ease;
}

/* Open: squares off the left edge so button and field read as one pill, and
   goes solid white to match the field it is now attached to. 17px is half the
   34px control height — the old 19px was half the previous 38px button and
   left the pill's right end a different curve from its left. */
.app-search.is-open .app-search-toggle {
    border-radius: 0 17px 17px 0;
    background: #fff;
    color: var(--app-theme);
}

/* --- Announcement bar --------------------------------------------------- */

/* Deliberately NOT the vendor theme colour. This is an announcement, so it has
   to read as a highlight against the page rather than as more of the same brand
   colour — on a blue-themed vendor the old theme-tinted version was almost
   invisible under a blue header. Amber is fixed for every vendor, the same way
   the veg/non-veg marks and the red account-lock notice are: its job is to mean
   "notice", not to match the brand.
   A left accent bar rather than a uniform border, so the eye catches the edge
   first. */
.app-notice {
    display: flex;
    align-items: center;
    gap: 11px;
    margin: 14px var(--app-pad) 0;
    padding: 12px 14px;
    border-radius: 10px;
    background: linear-gradient(180deg, #fff9e8, #fff4d6);
    border: 1px solid #f2d999;
    border-left: 4px solid #e0a020;
    font-size: 13px;
    font-weight: 500;
    line-height: 1.45;
    /* Warm dark brown rather than --app-ink: near-black on amber reads as a
       disabled block, and the vendor's ink could itself be a brand colour. */
    color: #6b4c12;
    box-shadow: 0 1px 3px rgba(146, 104, 20, .1);
}

.app-notice i {
    /* Matches the accent bar, not the theme. */
    color: #e0a020;
    flex: 0 0 auto;
    font-size: 14px;
}

/* --- Banners ------------------------------------------------------------ */

.app-banner-row {
    padding-top: 14px;
}

/* The banner track does NOT full-bleed like the card rows.
   .app-row pulls itself out by --app-pad and re-pads, which lets the
   neighbouring card peek past the section's edge — right for a row of product
   cards, wrong here: padding does not clip, so the previous/next slide showed
   through the 16px strip and the banner looked flush against the frame edge.
   Overflow is clipped instead, so one slide is framed at a time with the
   section's padding visible either side. */
.app-banner-row .app-row {
    margin-inline: 0;
    padding-inline: 0;
}

.app-banner {
    width: 100%;
    max-width: 100%;
    border-radius: 16px;
    overflow: hidden;
    display: block;
    /* Fixed banner height rather than an aspect ratio. Without this the slide
       took the image's own proportions, so a portrait upload rendered as a
       full-column-height wall — the image, not the design, set the section
       height. A ratio alone is not enough either: at 1056px wide even 16:9
       computes to ~590px, which is no one's idea of a small banner. */
    height: 170px;
    background: var(--app-theme-soft);
}

.app-banner img {
    width: 100%;
    height: 100%;
    /* cover, so a portrait or square upload is cropped to the band instead of
       letterboxed or stretched. */
    object-fit: cover;
    display: block;
}

/* One slide fills the track exactly. Now that .app-banner-row's track is no
   longer bled out, its content box is already the section's inner width, so a
   plain 100% is both correct and self-maintaining — the previous
   min(100vw…, --app-width…) had to restate the frame's geometry in viewport
   units and drifted from it whenever the padding changed.
   flex-basis too: .app-row children are flex:0 0 auto, which sizes from
   content and would otherwise ignore the width. */
.app-row .app-banner {
    width: 100%;
    flex-basis: 100%;
}

/* --- Banner slider ------------------------------------------------------
   Arrows and dots layered over the existing scroll-snap track. The track keeps
   doing the scrolling; nothing here moves it, so native swipe and momentum are
   untouched and the banner still works with the script disabled. */

.app-banner-slider {
    position: relative;
}

/* Circular glass chips over the artwork, as in the reference. */
.app-banner-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, .82);
    color: var(--app-ink);
    font-size: 13px;
    cursor: pointer;
    /* The dots sit at the bottom of the same box; without this the arrows would
       be centred against the dots' height too and ride low. */
    margin-top: -9px;
    transition: background .15s ease;
}

.app-banner-nav:hover {
    background: #fff;
}

/* A small inset from the artwork's own edge. No --app-pad term: the slider box
   already begins inside the section's padding, so adding it again pushed the
   chips a full gutter too far in. */
.app-banner-nav--prev {
    left: 10px;
}

.app-banner-nav--next {
    right: 10px;
}

/* Hidden on touch screens: a swipe is the natural gesture there and the chips
   only cover the image. Shown wherever a pointer can hover. */
@media (hover: none) {
    .app-banner-nav {
        display: none;
    }
}

.app-banner-dots {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 6px;
    margin-top: 10px;
}

.app-banner-dot {
    width: 7px;
    height: 7px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: #d3d7dd;
    cursor: pointer;
    transition: width .2s ease, background .2s ease;
}

/* The active dot stretches into a pill rather than just recolouring, which is
   what the reference shows and reads at a glance on a small screen. */
.app-banner-dot.is-active {
    width: 18px;
    border-radius: 4px;
    background: var(--app-theme);
}

/* --- Category strip ----------------------------------------------------- */

/* Circular tile with the name beneath, as in the reference.

   Slightly wider than the 82px circle it holds, so this vendor's long names
   ("Reverse Osmosis Water") wrap to two lines rather than three. The circle
   stays centred in it. */
.app-cat {
    width: 92px;
    text-align: center;
    text-decoration: none;
    color: var(--app-ink);
    /* Adds to .app-row's 12px gap for ~22px between tiles. Applied here rather
       than raising the row's gap, which is shared with the product-card rows
       where 12px is right. */
    margin-right: 10px;
}

.app-cat:last-child {
    margin-right: 0;
}

.app-cat-thumb {
    width: 82px;
    height: 82px;
    margin: 0 auto 9px;
    /* Circular, matching the reference's category row. */
    border-radius: 50%;
    /* White inside a thin theme ring, as in the reference — the previous filled
       tint read as a coloured block sitting behind the photo rather than a ring
       around it. */
    background: #fff;
    border: 1px solid var(--app-theme-border, #e6e8eb);
    display: flex;
    align-items: center;
    justify-content: center;
    /* Thin inset only: in the reference the artwork nearly fills the ring, and
       a larger pad shrank the photo to a dot in the middle of a white disc. */
    padding: 4px;
    overflow: hidden;
    transition: border-color .2s ease, box-shadow .2s ease, transform .2s ease;
}

/* No translateY — .app-cat sits in an .app-row, which clips vertically because
   it is an overflow-x scroller, so a lift would have its top sliced off. scale
   grows from the centre instead, which stays inside the row's box. */
.app-cat:hover .app-cat-thumb {
    border-color: var(--app-theme);
    transform: scale(1.06);
    box-shadow: 0 4px 14px rgba(16, 24, 40, .14);
}

/* The photo pushes in slightly further than the ring, so the image itself
   reads as zooming inside a fixed circle. */
.app-cat-thumb img {
    transition: transform .35s ease;
}

.app-cat:hover .app-cat-thumb img {
    transform: scale(1.12);
}


/* The leading "All" tile: a solid theme disc with a white glyph, as in the
   reference, rather than a photo in a ring like the categories after it. */
.app-cat--all .app-cat-thumb {
    background: var(--app-theme);
    border-color: var(--app-theme);
    color: #fff;
    font-size: 26px;
}

/* Same scale + shadow as the photo tiles, plus a brightness shift since this
   one is a solid disc with no image to zoom. */
.app-cat--all:hover .app-cat-thumb {
    filter: brightness(.94);
    transform: scale(1.06);
    box-shadow: 0 4px 14px rgba(16, 24, 40, .18);
}

/* The grid glyph turns with the tile — a small flourish the photo tiles get
   from their image zoom. */
.app-cat--all .app-cat-thumb i {
    transition: transform .35s ease;
}

.app-cat--all:hover .app-cat-thumb i {
    transform: rotate(90deg);
}


/* Cover, not contain: these are photographic category images, and contain would
   letterbox them inside the circle with tinted bars down the sides. The tile is
   padded, so the image is inset from the ring rather than bleeding to its edge. */
.app-cat-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 50%;
}

.app-cat-name {
    font-size: 12.5px;
    font-weight: 600;
    line-height: 1.3;
    /* Theme-coloured, as in the reference, where the labels pick up the brand
       green rather than sitting in body ink. */
    color: var(--app-theme);
    /* The reference labels are single-line, but this vendor's are long enough
       ("Reverse Osmosis Water") that one line would ellipsis them down to
       "Reverse Osmo…". Two lines are clamped instead, with the box held at
       exactly two lines' height so a one-word and a three-word name still
       occupy the same space and the circles above stay in a straight row. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    height: 2.6em;
}

/* --- Product card ------------------------------------------------------- */

.app-card {
    width: 158px;
    border: 1px solid #eceef1;
    border-radius: 16px;
    background: #fff;
    padding: 10px;
    display: flex;
    flex-direction: column;
    position: relative;
    text-decoration: none;
    color: var(--app-ink);
    /* Soft lift so the cards read as tiles against the grey backdrop rather
       than as a flat grid of outlines. */
    box-shadow: 0 1px 3px rgba(16, 24, 40, .05);
    transition: box-shadow .15s ease, transform .15s ease;
}

/* Shadow only in a scrolling .app-row: it is an overflow-x scroller, which
   clips vertically too, so a translateY would slice the top off the card. */
.app-card:hover {
    box-shadow: 0 8px 22px rgba(16, 24, 40, .13);
    border-color: var(--app-theme-border);
}

/* .app-grid wraps rather than scrolling, so it does not clip — cards there can
   lift properly. */
.app-grid > .app-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 12px 26px rgba(16, 24, 40, .15);
}

/* Image zoom inside the fixed media frame, which already has overflow:hidden. */
.app-card-media img {
    transition: transform .4s ease;
}

.app-card:hover .app-card-media img {
    transform: scale(1.07);
}

/* The CTA darkens on hover of the whole card, not just the button, so the card
   reads as one target. The transition itself lives on .app-card-btn further
   down, alongside the rest of that button's styling. */
.app-card:hover .app-card-btn {
    filter: brightness(.93);
}

.app-card-media {
    position: relative;
    border-radius: 10px;
    overflow: hidden;
    background: #fafafa;
    aspect-ratio: 1 / 1;
    margin-bottom: 8px;
}

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

.app-tag {
    position: absolute;
    top: 6px;
    left: 6px;
    padding: 2px 7px;
    border-radius: 6px;
    font-size: 9.5px;
    font-weight: 700;
    letter-spacing: .3px;
    text-transform: uppercase;
    background: var(--app-theme);
    color: #fff;
}

/* Discount flash. A filled red disc with a glow, not the pale outlined pill it
   used to be — a discount has to catch the eye, and a theme-tinted outline on a
   white card barely registered.
   Red is fixed for every vendor, like the veg/non-veg marks below: it signals a
   saving, so it must not drift with the brand colour (a red-themed vendor would
   otherwise lose the distinction entirely).
   A circle rather than a pill: the label is short ("22% OFF") and wraps to two
   lines inside the disc, which is what the reference shows. */
.app-tag--off {
    left: auto;
    right: 6px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    padding: 0;
    border-radius: 50%;
    background: radial-gradient(circle at 35% 30%, #f4574d, #d92b1f);
    border: 0;
    color: #fff;
    font-size: 9px;
    line-height: 1.15;
    letter-spacing: 0;
    text-align: center;
    /* Soft red halo, so the badge lifts off the artwork behind it. */
    box-shadow: 0 0 0 2px rgba(255, 255, 255, .9), 0 2px 8px rgba(217, 43, 31, .45);
}

/* Veg / non-veg mark — fixed red & green, these are regulated colours and
   must not follow the vendor theme. */
.app-diet {
    position: absolute;
    bottom: 6px;
    left: 6px;
    display: inline-flex;
    align-items: center;
    gap: 3px;
    padding: 1px 5px;
    border-radius: 4px;
    background: #fff;
    font-size: 9px;
    font-weight: 600;
}

.app-diet::before {
    content: "";
    width: 7px;
    height: 7px;
    border: 1.5px solid currentColor;
    border-radius: 2px;
    display: inline-block;
}

.app-diet--veg {
    color: #1a7f37;
}

.app-diet--nonveg {
    color: #c62828;
}

/* The name is an <a>, so the colour and underline are set explicitly — left to
   the UA default it renders as blue underlined link text, which is what made
   the rows look like a list of links rather than product cards. */
.app-card-name {
    font-size: 13px;
    font-weight: 600;
    line-height: 1.35;
    margin: 0 0 4px;
    color: var(--app-ink);
    text-decoration: none;
    /* Two-line clamp keeps every card in a row the same height. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    /* The clamp needs a floor, or a one-line name shortens the card and leaves
       the price and button misaligned against its neighbours. */
    min-height: 35px;
}

.app-card-name:hover {
    color: var(--app-theme);
    text-decoration: none;
}

.app-card-unit {
    font-size: 11px;
    color: var(--app-muted);
    margin: 0 0 5px;
}

.app-card-price {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 5px;
    margin-bottom: 10px;
}

.app-price {
    font-size: 15px;
    font-weight: 800;
    color: var(--app-ink);
}

.app-price--was {
    font-size: 11px;
    font-weight: 500;
    color: var(--app-muted);
    text-decoration: line-through;
}

.app-rating {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    font-size: 10.5px;
    color: var(--app-muted);
    margin-bottom: 6px;
}

.app-rating i {
    color: #f5a623;
    font-size: 9.5px;
}

.app-card-btn {
    margin-top: auto;
    display: block;
    width: 100%;
    /* 36px tall — a comfortable thumb target without crowding the card. */
    padding: 9px 8px;
    border: 0;
    border-radius: 10px;
    background: var(--app-theme);
    color: #fff;
    font-size: 12.5px;
    font-weight: 700;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    transition: filter .18s ease;
}

.app-card-btn:hover,
.app-card-btn:focus {
    color: #fff;
    filter: brightness(.94);
}

.app-card-btn--ghost {
    background: var(--app-theme-soft);
    color: var(--app-theme);
    border: 1px solid var(--app-theme-border);
}

.app-card-btn--ghost:hover,
.app-card-btn--ghost:focus {
    color: var(--app-theme);
}

/* --- Product grid -------------------------------------------------------
   Wrapping grid for sections that show everything at once rather than
   scrolling sideways (top selling). Four cards per line, breaking to the next
   line after the fourth.

   The card sets a fixed width for the scrolling rows, so it is overridden to
   auto here — otherwise the columns would be four fixed-width cards with the
   remaining track left empty. */
.app-grid {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 14px;
}

.app-grid > .app-card {
    width: auto;
}

/* Two per line on a phone, where four would leave each card too narrow to read
   a name and price in. */
@media (max-width: 620px) {
    .app-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 10px;
    }
}

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

.app-video {
    /* Two videos side by side once there is room, one on a phone. The 16px
       subtrahend is the .app-row gap between the pair. */
    width: min(
        calc(100vw - var(--app-gutter)),
        calc((var(--app-width) - var(--app-gutter) - 16px) / 2)
    );
    aspect-ratio: 16 / 9;
    border-radius: 14px;
    overflow: hidden;
    background: #000;
}

.app-video iframe {
    width: 100%;
    height: 100%;
    border: 0;
    display: block;
}

/* --- Next order --------------------------------------------------------- */

.app-next {
    margin: 18px var(--app-pad) 0;
    border: 1px solid var(--app-theme-border);
    border-radius: 14px;
    overflow: hidden;
}

.app-next-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 10px 13px;
    background: var(--app-theme-soft);
}

.app-next-label {
    font-size: 12px;
    font-weight: 700;
    color: var(--app-ink);
}

.app-next-date {
    font-size: 11.5px;
    color: var(--app-muted);
}

.app-next-body {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 11px 13px;
}

.app-next-thumb {
    width: 42px;
    height: 42px;
    border-radius: 8px;
    object-fit: cover;
    background: #fafafa;
    flex: 0 0 auto;
}

.app-next-info {
    flex: 1;
    min-width: 0;
}

.app-next-name {
    font-size: 12.5px;
    font-weight: 600;
    margin: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.app-next-qty {
    font-size: 11px;
    color: var(--app-muted);
    margin: 2px 0 0;
}



.app-footer {
    margin-top: 34px;
    padding: 28px var(--app-pad) 26px;
    /* Pinned to the frame rather than left to inherit its width from .app-shell.
       Belt-and-braces: a stray closing tag in a page template can end the shell
       early, and the footer then lands at body level and paints edge to edge
       (this happened on the cart — see the note at the end of Users/v1/cart.php).
       Capping here keeps the band lined up with the bottom nav, which clamps
       itself the same way, even when the markup above it is malformed. */
    box-sizing: border-box;
    width: 100%;
    max-width: var(--app-width);
    margin-inline: auto;
    border-top: 2px solid var(--app-theme);
    background:
        linear-gradient(180deg, var(--app-theme-soft), transparent 220px),
        #f7f8fa;
    padding-bottom: calc(var(--app-nav-height) + 56px);
}

.app-footer-block {
    margin-bottom: 22px;
}

.app-footer-block:last-of-type {
    margin-bottom: 16px;
}


.app-footer-heading {
    position: relative;
    margin: 0 0 14px;
    padding-bottom: 8px;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--app-ink);
}

.app-footer-heading::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: 0;
    width: 26px;
    height: 2px;
    border-radius: 2px;
    background: var(--app-theme);
}


.app-footer-logo {
    display: inline-block;
}

.app-footer-logo img {
    display: block;
    max-height: 56px;
    max-width: 170px;
    width: auto;
    object-fit: contain;
}

.app-footer-tagline {
    margin: 10px 0 0;
    font-size: 14px;
    font-weight: 700;
    color: var(--app-ink);
}



.app-footer-stores-label {
    margin: 16px 0 8px;
    font-size: 12px;
    font-weight: 600;
    color: var(--app-muted);
}

.app-footer-stores {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.app-footer-store {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    border: 1px solid #dfe3e8;
    border-radius: 10px;
    background: #fff;
    color: var(--app-ink);
    font-size: 12.5px;
    font-weight: 600;
    text-decoration: none;
    transition: border-color .15s ease, box-shadow .15s ease;
}

.app-footer-store:hover {
    border-color: var(--app-theme);
    box-shadow: 0 2px 8px rgba(16, 24, 40, .08);
    color: var(--app-ink);
}

/* Native 24px, never scaled up. */
.app-footer-store img {
    display: block;
    width: 18px;
    height: 18px;
    object-fit: contain;
}

/* --- Links ---------------------------------------------------------------
   Two columns: the labels are short ("Contact Us", "Feedback"), so a single
   column would leave most of the width empty and make the footer needlessly
   tall on a phone. */
.app-footer-links {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px 14px;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Chevron marker so the list reads as navigation rather than loose grey text,
   and it slides on hover to confirm the row is interactive. */
.app-footer-links a {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    font-size: 13.5px;
    line-height: 1.4;
    color: #5b6472;
    text-decoration: none;
    transition: color .15s ease, transform .15s ease;
}

.app-footer-links a::before {
    content: "";
    flex: 0 0 auto;
    width: 5px;
    height: 5px;
    border-top: 1.5px solid var(--app-theme);
    border-right: 1.5px solid var(--app-theme);
    transform: rotate(45deg);
    opacity: .6;
}

.app-footer-links a:hover {
    color: var(--app-theme);
    transform: translateX(2px);
}

.app-footer-links a:hover::before {
    opacity: 1;
}

/* --- Contact ------------------------------------------------------------ */

.app-footer-contact {
    margin: 0;
    padding: 0;
    list-style: none;
}

.app-footer-contact li {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 10px;
    font-size: 13.5px;
}

/* Tinted round chip rather than a bare glyph, matching the social icons below
   so the two blocks read as one family. Fixed size keeps the labels aligned
   whatever the glyph's own width. */
.app-footer-contact i {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 28px;
    height: 28px;
    border-radius: 50%;
    background: var(--app-theme-soft);
    font-size: 11px;
    color: var(--app-theme);
}

.app-footer-contact a {
    color: #5b6472;
    font-weight: 500;
    text-decoration: none;
    word-break: break-word;
}

.app-footer-contact a:hover {
    color: var(--app-theme);
}

/* --- Newsletter --------------------------------------------------------- */

/* Inside the Get In Touch column, under the phone and email. A rule above it
   separates it from the contact rows without making it a separate block. */
.app-footer-newsletter {
    margin-top: 16px;
    padding-top: 16px;
    border-top: 1px solid #e6e9ed;
}

/* One rounded pill holding the field and a small send button, as in the
   reference. The pill is the bordered element; the input inside it is
   transparent and borderless, so the two read as a single control — which also
   means the pair fits the ~250px Get In Touch column that a side-by-side input
   and full "Subscribe" button did not. */
.app-footer-newsletter form {
    display: flex;
    align-items: center;
    gap: 6px;
    width: 100%;
    max-width: 300px;
    height: 44px;
    padding: 4px 4px 4px 6px;
    border: 1px solid #dfe3e8;
    border-radius: 12px;
    background: #fff;
    box-sizing: border-box;
    transition: border-color .15s ease, box-shadow .15s ease;
}

/* Focus is shown on the pill rather than the input, since the input has no
   visible edge of its own. :focus-within is what makes that possible. */
.app-footer-newsletter form:focus-within {
    border-color: var(--app-theme);
    box-shadow: 0 0 0 3px var(--app-theme-soft);
}

.app-footer-newsletter input {
    flex: 1 1 auto;
    min-width: 0;
    height: 100%;
    padding: 0 6px;
    border: 0;
    outline: 0;
    font-size: 13px;
    font-family: inherit;
    color: var(--app-ink);
    background: transparent;
}

.app-footer-newsletter input::placeholder {
    color: #9aa1ab;
}

/* Square icon button tucked inside the pill. */
.app-footer-subscribe {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 34px;
    height: 34px;
    padding: 0;
    border: 0;
    border-radius: 9px;
    background: var(--app-theme);
    color: #fff;
    font-size: 13px;
    font-family: inherit;
    cursor: pointer;
    transition: filter .15s ease;
}

.app-footer-subscribe:hover {
    filter: brightness(.94);
    color: #fff;
}

/* --- Social ------------------------------------------------------------- */

/* Inside the brand block, under the store links. */
.app-footer-social {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 16px;
}

.app-footer-social a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    border: 1px solid var(--app-theme-border);
    border-radius: 50%;
    background: #fff;
    color: var(--app-theme);
    font-size: 15px;
    text-decoration: none;
}

.app-footer-social a:hover {
    background: var(--app-theme);
    color: #fff;
}

/* --- Legal -------------------------------------------------------------- */

.app-footer-legal {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 4px 10px;
    margin-top: 22px;
    padding-top: 18px;
    border-top: 1px solid #e6e9ed;
    font-size: 12px;
    line-height: 1.5;
    color: var(--app-muted);
    text-align: center;
}

.app-footer-powered a {
    color: var(--app-theme);
    font-weight: 600;
    text-decoration: none;
}

/* Wide frame: the blocks sit side by side instead of stacking down the left
   with the rest of the width empty. Newsletter takes the wider column since it
   holds a form; links and contact share the remainder. */
@media (min-width: 700px) {
    .app-footer {
        padding: 28px var(--app-pad) 32px;
    }

    /* auto-fit rather than a fixed column count: the number of blocks varies
       with what the vendor has configured (brand, links, contact), and a fixed
       3-column grid would leave a gap or wrap awkwardly at 2 or 4. The
       newsletter is NOT one of these — it is its own band below the grid. */
    .app-footer-cols {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
        gap: 26px 28px;
        align-items: start;
    }

    .app-footer-block {
        margin-bottom: 0;
    }

    /* Two short columns of links is right in a narrow stack, but inside its own
       footer column one list reads better. */
    .app-footer-links {
        grid-template-columns: 1fr;
        gap: 8px;
    }

}

/* --- Bottom nav --------------------------------------------------------- */

/* Fixed, but clamped and centred so it lines up with .app-shell instead of
   spanning the full monitor width. */
.app-nav {
    position: fixed;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 100%;
    max-width: var(--app-width);
    height: var(--app-nav-height);
    z-index: 40;
    display: flex;
    align-items: stretch;
    background: #fff;
    border-top: 1px solid #ececec;
    /* Keeps the row clear of the iOS home-bar area. */
    padding-bottom: env(safe-area-inset-bottom, 0);
}

.app-nav-item {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    text-decoration: none;
    color: var(--app-muted);
    font-size: 10.5px;
    font-weight: 600;
    position: relative;
}

.app-nav-item i {
    font-size: 17px;
    line-height: 1;
    transition: transform .2s ease;
}

.app-nav-item.is-active,
.app-nav-item:hover,
.app-nav-item:focus {
    color: var(--app-theme);
}

/* The icon lifts slightly on the current and hovered tab. The nav is a fixed
   bar with room above the icon, so this does not clip. */
.app-nav-item.is-active i,
.app-nav-item:hover i {
    transform: translateY(-2px);
}

.app-nav-badge {
    position: absolute;
    top: 6px;
    left: 50%;
    margin-left: 4px;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    border-radius: 8px;
    background: var(--app-theme);
    color: #fff;
    font-size: 9.5px;
    line-height: 16px;
    text-align: center;
}

/* --- Misc --------------------------------------------------------------- */

.app-empty {
    margin: 24px var(--app-pad);
    padding: 22px 16px;
    border: 1px solid #ececec;
    border-radius: 14px;
    text-align: center;
    font-size: 13px;
    color: var(--app-muted);
}

.app-flash {
    margin: 12px var(--app-pad) 0;
}

/* On a phone the column IS the viewport, so drop the frame's own inset — and the
   backdrop shapes with it, since nothing of the backdrop is ever visible. */
@media (max-width: 600px) {
    .app-shell {
        box-shadow: none;
    }

    body::before {
        display: none;
    }

    /* The header is tight on a phone: logo plus three icons leave ~78px, so cap
       the logo and let the location label ellipsis rather than shove the icons
       off the edge. */
    .app-header-logo {
        padding: 4px 7px;
    }

    .app-header-logo img {
        max-width: 84px;
        max-height: 28px;
    }

    .app-header-loc-value {
        max-width: 116px;
    }

    /* Hidden on a phone. The greeting takes a second line in the right-hand
       cluster, which at this width would push the bar to two tall rows for a
       label that is not needed to use the page — the name is on the Profile
       tab. It returns above the icons from 601px up. */
    .app-greeting {
        display: none;
    }
}

/* --- Wide screens ------------------------------------------------------- */

/* Past the phone breakpoint the container is much wider than a handset, so the
   content scales with it — at 1100px the phone-sized cards and tiles would leave
   the rows looking sparse. Section padding grows to match the frame. */
@media (min-width: 768px) {
    /* Wider frame gets a wider gutter; the full-bleed banner/video widths above
       read --app-gutter, so they follow automatically. */
    :root {
        --app-gutter: 44px;
        --app-pad: 22px;
    }

    .app-section {
        padding: 24px var(--app-pad) 0;
    }

    .app-notice {
        margin: 18px var(--app-pad) 0;
        padding: 14px 16px;
        font-size: 14px;
    }

    .app-next {
        margin: 24px var(--app-pad) 0;
    }

    .app-section-title {
        font-size: 21px;
    }

    .app-section-link {
        font-size: 14px;
    }

    .app-row {
        gap: 16px;
    }

    .app-card {
        width: 200px;
        padding: 10px;
    }

    /* Grid cards are sized by the track, not this fixed width. Restated inside
       the media query so the intent is visible next to the rule it counteracts;
       .app-grid > .app-card already wins on specificity. */
    .app-grid > .app-card {
        width: auto;
    }

    .app-card-name {
        font-size: 14px;
    }

    .app-card-unit {
        font-size: 12px;
    }

    .app-price {
        font-size: 16px;
    }

    .app-price--was {
        font-size: 12px;
    }

    .app-card-btn {
        padding: 9px 10px;
        font-size: 13px;
    }

    /* Proportionally taller on a wide frame — 170px reads as a thin strip once
       the slide is ~1056px across. 320px keeps it near 3.3:1, wide enough to
       feel like a banner without cropping a portrait upload to a sliver. */
    .app-banner {
        height: 320px;
    }

    .app-cat {
        width: 104px;
        margin-right: 14px;
    }

    /* No border-radius override here: the tile is a circle at every width in
       the reference, and a rounded-square value at this breakpoint was what
       turned the wide-screen row back into squares. Width and height must stay
       equal or the circle becomes an ellipse — the old rule widened .app-cat
       and .app-cat-thumb together, which stretched the disc. */
    .app-cat-thumb {
        width: 94px;
        height: 94px;
        padding: 4px;
    }

    .app-cat--all .app-cat-thumb {
        font-size: 30px;
    }

    .app-cat-name {
        font-size: 13px;
    }

    /* Only slightly roomier than the compact bar — a wide frame does not need
       a taller header, just a little more breathing space. */
    .app-header {
        padding: 10px var(--app-pad);
    }

    /* More room to type once the frame is wide. Width only — the height stays
       tied to the 34px controls, so no vertical padding here either. */
    .app-search.is-open .app-search-field {
        width: 280px;
        padding: 0 4px 0 16px;
    }

    .app-nav-item {
        font-size: 12px;
    }

    .app-nav-item i {
        font-size: 19px;
    }

    /* Icons and labels grew, so the bar needs the extra room. Set on :root, not
       .app-nav — .app-shell's bottom padding reads the same variable and would
       otherwise reserve too little space. */
    :root {
        --app-nav-height: 66px;
    }
}

/* --- Modals ------------------------------------------------------------- */

/* The shared dialogs (location pickers, legal-age gate, account-lock gate) are
   Bootstrap modals. Bootstrap's JS is loaded by default1.php, but its full
   stylesheet is NOT — bootstrap.min.css resets would fight everything above.
   These are the few rules the modal markup actually needs, restyled to match
   this layout. Without them the dialog renders as unstyled inline content at
   the top of the page. */
.modal {
    position: fixed;
    inset: 0;
    z-index: 1050;
    display: none;
    overflow-y: auto;
    outline: 0;
    /* No background here. Bootstrap appends its own .modal-backdrop element, so
       dimming this too stacked two scrims and washed the whole dialog out. */
}

/* !important because v1/base.css reimplements Bootstrap's `.show` display
   helper as `.app-shell .show { display: block !important }` for the signup
   markup. That selector is more specific than the `.modal` above and would win,
   so a closed dialog nested inside the shell could be forced visible. Modals are
   opened by Bootstrap's JS adding .show/.in, never by that helper, so the state
   is pinned here rather than letting specificity decide. */
.modal:not(.show):not(.in) {
    display: none !important;
}

.modal.show,
.modal.in {
    display: block;
}

/* Matches Bootstrap 4's own pair: .fade slides the dialog in via a transform on
   .modal-dialog (NOT opacity on .modal — an opacity rule here would leave a
   dialog invisible whenever the transition does not complete). display:none
   above is what actually keeps it shut; this is only the animation. */
.modal.fade .modal-dialog {
    transition: transform .3s ease-out;
    transform: translate(0, -50px);
}

.modal.show .modal-dialog,
.modal.in .modal-dialog {
    transform: none;
}

@media (prefers-reduced-motion: reduce) {
    .modal.fade .modal-dialog {
        transition: none;
    }
}

.modal-dialog {
    position: relative;
    width: auto;
    max-width: 460px;
    margin: 6vh auto;
    padding: 0 16px;
    /* Bootstrap's own sheet sets pointer-events:none here (so a click on the
       gap around the dialog reaches the backdrop and dismisses it) and re-enables
       it on .modal-content. That sheet is not loaded, but the pin element's CSS
       assumes the pairing, so declare both explicitly — omitting the auto below
       leaves the whole dialog unclickable. */
    pointer-events: none;
}

/* Vertically centred variant, used by user_address_pin. */
.modal-dialog-centered {
    display: flex;
    align-items: center;
    min-height: calc(100% - 12vh);
}

.modal-content {
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    background: #fff;
    border-radius: 16px;
    overflow: hidden;
    /* Re-enable clicks for the dialog itself. */
    pointer-events: auto;
}

.modal-header,
.modal-footer {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 14px 18px;
}

.modal-header {
    justify-content: space-between;
    border-bottom: 1px solid #efefef;
}

.modal-footer {
    justify-content: flex-end;
    border-top: 1px solid #efefef;
}

.modal-title {
    margin: 0;
    font-size: 16px;
    font-weight: 700;
    color: var(--app-ink);
}

.modal-body {
    padding: 18px;
}

.modal-header .close {
    background: none;
    border: 0;
    font-size: 24px;
    line-height: 1;
    color: var(--app-muted);
    cursor: pointer;
    padding: 0;
}

/* Bootstrap adds this class to <body> while a modal is open; without the rule
   the page keeps scrolling behind the open dialog. */
.modal-open {
    overflow: hidden;
}

/* Bootstrap appends this to <body> while a modal is open. */
.modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1040;
    background: #111827;
    opacity: .5;
}

/* The pickers use Bootstrap form classes, which are not loaded either. */
.modal .form-group {
    margin-bottom: 14px;
}

.modal .form-group label {
    display: block;
    margin-bottom: 6px;
    font-size: 13px;
    font-weight: 600;
    color: var(--app-ink);
}

.modal .form-control {
    display: block;
    width: 100%;
    padding: 10px 12px;
    font: inherit;
    font-size: 14px;
    color: var(--app-ink);
    background: #fff;
    border: 1px solid #dcdcdc;
    border-radius: 10px;
    outline: 0;
    -webkit-appearance: none;
    appearance: none;
    /* Native select arrow is removed by appearance:none, so draw one back. */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%236b7280' d='M4.5 6.5 8 10l3.5-3.5z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 10px center;
    background-size: 16px;
    padding-right: 32px;
}

.modal .form-control:focus {
    border-color: var(--app-theme);
}

.modal .btn {
    padding: 9px 18px;
    font: inherit;
    font-size: 14px;
    font-weight: 600;
    border: 0;
    border-radius: 10px;
    cursor: pointer;
}

.modal .site-btn {
    background: var(--app-theme);
    color: #fff;
}

.modal .site-cancel-btn {
    background: #f1f2f4;
    color: var(--app-ink);
}

.modal span.form-error {
    display: block;
    margin-top: 4px;
    font-size: 12px;
}

/* user_address_pin / user_edit_region emit their trigger markup alongside their
   modal, and default1.php renders those elements at body level (a modal inside
   the sticky header or .app-shell cannot clear the backdrop Bootstrap appends to
   <body>). The header supplies its own trigger button, so hide the element's
   built-in one rather than leaving it loose at the foot of the page. */
body > .user-address-pin,
body > .user-area-info {
    display: none;
}

/* --- Sidebar ------------------------------------------------------------ */

/* Hamburger, left of the logo. Matches .app-icon-btn exactly — in the reference
   every header control is the same round translucent chip, and a rounded square
   here beside circular icons made the bar look assembled from two designs. */
.app-burger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 34px;
    height: 34px;
    border: 0;
    padding: 0;
    border-radius: 50%;
    /* Bare, matching .app-icon-btn — the reference bar has no chips behind any
       of its controls. */
    background: none;
    color: #fff;
    font-size: 18px;
    font-family: inherit;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
}

.app-burger:hover,
.app-burger:focus {
    background: rgba(255, 255, 255, .28);
    color: #fff;
}

/* Scrim. z-index below the panel but above everything else; the location
   dialog is a Bootstrap modal at 1040/1050 and deliberately outranks both, so
   opening a dialog over the menu still works. */
/* Clipping frame: exactly the app container's box, centred the same way
   .app-nav is (left:50% + translateX(-50%), percentages not vw, so the
   scrollbar cannot shift it). overflow:hidden is what keeps the closed panel
   from showing in the page backdrop, and makes the menu read as opening
   *inside* the container. [hidden] while closed so it never eats a click. */
.app-side-frame {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 100%;
    max-width: var(--app-width);
    z-index: 900;
    overflow: hidden;
    pointer-events: none;
}

.app-side-frame[hidden] {
    display: none;
}

/* Scrim fills the frame, so the dim covers the storefront and not the monitor. */
.app-side-overlay {
    position: absolute;
    inset: 0;
    background: rgba(17, 24, 39, .45);
    opacity: 0;
    transition: opacity .2s ease;
    pointer-events: auto;
}

.app-side-overlay.is-open {
    opacity: 1;
}

/* Panel: simply the left edge of the clipping frame. No viewport maths needed —
   left:0 is the container's left edge because .app-side-frame IS the container's
   box, and translateX(-100%) parks it behind that frame's overflow:hidden. */
.app-side {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 910;
    width: min(86%, 320px);
    background: #fff;
    display: flex;
    flex-direction: column;
    /* Off-canvas until .is-open. transform rather than left, so the slide is
       composited and does not reflow the page. */
    transform: translateX(-100%);
    transition: transform .22s ease;
    overflow: hidden;
    pointer-events: auto;
}

.app-side.is-open {
    transform: translateX(0);
}

.app-side-head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 16px var(--app-pad);
    background: var(--app-theme);
    color: #fff;
    flex: 0 0 auto;
}

.app-side-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    flex: 0 0 auto;
    border-radius: 50%;
    background: rgba(255, 255, 255, .22);
    font-size: 16px;
    font-weight: 700;
}

.app-side-who {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    flex: 1 1 auto;
}

.app-side-name {
    font-size: 14px;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.app-side-mail {
    font-size: 12px;
    opacity: .9;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.app-side-login {
    color: #fff;
    text-decoration: underline;
    opacity: 1;
}

.app-side-close {
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    border: 0;
    padding: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, .18);
    color: #fff;
    font-size: 14px;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
}

/* Only the nav scrolls, so the header stays pinned. */
.app-side-nav {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: 8px 0 24px;
    -webkit-overflow-scrolling: touch;
}

.app-side-label {
    padding: 14px var(--app-pad) 6px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--app-muted);
}

.app-side-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.app-side-list--foot {
    margin-top: 8px;
    border-top: 1px solid #efefef;
    padding-top: 8px;
}

.app-side-list a,
.app-side-toggle {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 11px var(--app-pad);
    border: 0;
    background: none;
    font: inherit;
    font-size: 14px;
    font-weight: 500;
    color: var(--app-ink);
    text-decoration: none;
    text-align: left;
    cursor: pointer;
}

.app-side-list a:hover,
.app-side-toggle:hover {
    background: var(--app-theme-soft);
    color: var(--app-ink);
}

/* Fixed-width icon column keeps every label on the same left edge. */
.app-side-list a > i:first-child,
.app-side-toggle > i:first-child {
    width: 18px;
    text-align: center;
    font-size: 15px;
    color: var(--app-theme);
    flex: 0 0 auto;
}

.app-side-list a > span,
.app-side-toggle > span {
    flex: 1 1 auto;
    min-width: 0;
}

.app-side-caret {
    flex: 0 0 auto;
    font-size: 11px;
    color: var(--app-muted);
    transition: transform .18s ease;
}

.app-side-group.is-open .app-side-caret {
    transform: rotate(180deg);
}

/* Collapsed submenu. max-height rather than display:none so it animates. */
.app-side-sub {
    list-style: none;
    margin: 0;
    padding: 0;
    max-height: 0;
    overflow: hidden;
    background: #fafafa;
    transition: max-height .22s ease;
}

.app-side-group.is-open .app-side-sub {
    /* Generous cap: a vendor may have many categories or policy pages, and a
       value too small would silently clip the tail of the list. */
    max-height: 60vh;
    overflow-y: auto;
}

.app-side-sub a {
    padding-left: calc(var(--app-pad) + 30px);
    font-size: 13.5px;
    font-weight: 400;
}

.app-side-logout {
    color: #c62828;
}

.app-side-logout > i:first-child {
    color: #c62828;
}

/* Roomier on a wide frame, matching the rest of the layout. */
@media (min-width: 768px) {
    .app-burger {
        width: 38px;
        height: 38px;
        font-size: 17px;
    }

    .app-side {
        max-width: 340px;
    }
}

/* --- Reduced motion ------------------------------------------------------
   The hover effects above are decorative: image zooms, card lifts, tile
   scaling. A visitor who has asked their OS for less animation should still
   get the colour and shadow feedback, so only the movement is dropped rather
   than disabling transitions wholesale. */
@media (prefers-reduced-motion: reduce) {
    .app-cat:hover .app-cat-thumb,
    .app-cat:hover .app-cat-thumb img,
    .app-cat--all:hover .app-cat-thumb,
    .app-cat--all:hover .app-cat-thumb i,
    .app-grid > .app-card:hover,
    .app-card:hover .app-card-media img,
    .app-nav-item.is-active i,
    .app-nav-item:hover i {
        transform: none;
    }
}
