/* ==========================================================================
   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;
    /* iOS Safari "text inflation": it enlarges text in blocks it judges too
       wide to read at the viewport scale — and the header marquee track is
       deliberately thousands of pixels wide (width: max-content, one long
       nowrap line). On a real iPhone the announcement rendered at roughly
       twice its 13px, towering over the phone number beside it, while desktop
       browsers and device emulators (which do not inflate) looked fine. The
       page sets its own sizes for a phone, so the automatic adjustment is
       switched off; the user's own text-size / zoom settings still apply. */
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
    /* 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 — 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.

   These were four radial gradients painted into one fixed layer. A gradient can
   only ever be an ellipse with a soft edge, which read as four blurred circles
   rather than the reference's irregular, hand-drawn blobs. Each blob is now a
   real element with a lopsided border-radius, so it has a crisp organic edge and
   its shape can be animated — a gradient's cannot.

   .app-backdrop-art is the fixed layer holding both the wave band and the blobs.
   It is emitted by default1.php before .app-shell, so no page template has to
   know about it. .app-shell is position:relative and paints over it. */
.app-backdrop-art {
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    /* The layer is fixed and full-viewport, so a blob parked near an edge would
       otherwise widen the scroll area as it drifts. */
    overflow: hidden;
}

.app-blobs {
    position: absolute;
    inset: 0;
    display: block;
}

/* The shared blob body. Position, size and motion timing are per-blob below.
   The lopsided border-radius (four different corner pairs) is what makes the
   silhouette organic rather than an ellipse — it is also the property the morph
   animation interpolates. */
.app-blob {
    position: absolute;
    /* --app-art, not --app-theme-soft: that token is 12% of the theme mixed into
       white, which against the #f3f4f6 backdrop was very nearly the backdrop
       itself — the blobs were technically painted but invisible. */
    background: var(--app-art);
    border-radius: 58% 42% 37% 63% / 47% 53% 47% 53%;
    /* No filter:blur() here. A 6px blur on a shape this size rounded the
       lopsided border-radius straight back into the soft ellipse it was chosen
       to avoid, undoing the whole point of using an element over a gradient.
       The organic edge IS the effect, so it is left crisp. */

    /* Two animations on one element: the drift moves it, the morph reshapes it.
       Separate so they can run at different (deliberately non-multiple) periods
       — a shared period would make the blob visibly repeat the same pose. */
    animation: app-blob-drift 26s ease-in-out infinite,
               app-blob-morph 19s ease-in-out infinite;
    /* Hints the compositor to keep these on their own layer. Only four elements
       carry it, so it does not bloat memory the way a blanket rule would. */
    will-change: transform, border-radius;
}

/* Every blob is the same flat mint at full strength, as in the reference —
   there the shapes read as one set of cut-paper cutouts, not as a depth stack.
   The earlier per-blob opacities (.55/.85) made them look like faded washes at
   different distances, which is most of why the group read as weaker than the
   reference even once the colour was right. */

/* The reference's scatter is two DISTINCT kinds of shape, not four of a kind:
   a tall leaf-like form pressed against each side of the app frame, half of it
   tucked behind the column so only a crescent shows, plus a small rounded pebble
   far out in each margin. Four similar mid-size blobs floating clear of the
   frame — what this was — read as generic background bubbles instead.

   The tall pair are positioned from the frame's edge, not the window's: the
   column is a fixed --app-width centred in the viewport, so `calc(50% - half
   the frame)` pins them to the column no matter how wide the window gets. Sized
   in vmin so they scale with the viewport rather than swamping a short one.
   Negative delays start each mid-cycle, so they are never in lockstep. */

/* --- The two tall crescents hugging the frame ---------------------------- */

.app-blob-one,
.app-blob-two {
    /* Compact and only slightly taller than wide. These were 26x52vmin — at a
       950px-tall window that renders a 247x494px slab, over half the screen
       height, which is what made them read as big flat panels beside the frame
       rather than the reference's small rounded forms. Capped in px as well as
       vmin so a tall monitor cannot scale them back up into slabs. */
    width: var(--app-blob-w);
    height: min(19vmin, 190px);
    /* How far the shape is pushed outward from the frame's edge. Just over half
       its own width, so roughly that much tucks behind the opaque column and the
       rest reads as a rounded form beside it. Derived from the width rather than
       stated separately — a fixed offset against a responsive width changed the
       overlap at every screen size. */
    --app-blob-w: min(15vmin, 150px);
    --app-blob-inset: calc(var(--app-blob-w) * .55);
    /* Centred on the page's vertical midline, which is where the reference sits
       them — level with the hero, not up at the masthead. */
    top: 50%;
    /* The -50% here is the element's own height; app-blob-drift-mid restates it
       in every frame for the same reason the wave's keyframes restate
       scaleY(-1) — an animated transform replaces this one entirely. */
    transform: translateY(-50%);
    animation-name: app-blob-drift-mid, app-blob-morph;
}

/* Half-hidden behind the column: the visible part is the crescent outside it.
   The overlap is deliberate — flush to the frame's edge the shapes read as
   tabs stuck to the sides rather than forms passing behind it. */
/* max(0px, ...) keeps the shape on screen. The unclamped offset goes NEGATIVE
   once the window is near the frame's own width — at 1280-1440px, the commonest
   laptop sizes, the crescent's outer edge fell off the viewport and what
   remained beside the column was a flat-sided sliver rather than a curve. The
   clamp parks it against the window edge instead, so the whole silhouette stays
   visible; on a wide monitor the calc() wins and it tracks the frame. */
.app-blob-one {
    left: max(0px, calc(50% - (var(--app-width) / 2) - var(--app-blob-inset)));
    border-radius: 54% 46% 48% 52% / 46% 44% 56% 54%;
    animation-delay: 0s, -4s;
}

.app-blob-two {
    right: max(0px, calc(50% - (var(--app-width) / 2) - var(--app-blob-inset)));
    border-radius: 46% 54% 52% 48% / 44% 46% 54% 56%;
    animation-delay: -9s, -12s;
    animation-duration: 31s, 23s;
}

/* --- The two small pebbles out in the margins ---------------------------- */

/* Near-round and much smaller than the crescents, sitting well away from the
   frame — the size contrast between the two pairs is what gives the scatter its
   hand-placed look. */
.app-blob-three,
.app-blob-four {
    width: min(17vmin, 170px);
    height: min(16.5vmin, 165px);
}

.app-blob-three {
    bottom: 12%;
    left: 4%;
    border-radius: 54% 46% 43% 57% / 51% 44% 56% 49%;
    animation-delay: -15s, -7s;
    animation-duration: 29s, 21s;
}

.app-blob-four {
    top: 8%;
    right: 5%;
    border-radius: 47% 53% 56% 44% / 45% 57% 43% 55%;
    animation-delay: -21s, -16s;
    animation-duration: 34s, 25s;
}

/* Drift: a slow wander. The blobs sit in the empty margins beside the reading
   column, so there is room to move — the earlier ~2vmin travel was too small to
   register as motion at all. Still well short of anything that would pull the
   eye off the products.
   Used by the two small pebbles. The tall crescents need their centring offset
   folded in, so they run app-blob-drift-mid below instead. */
@keyframes app-blob-drift {
    0%   { transform: translate(0, 0) rotate(0deg) scale(1); }
    33%  { transform: translate(4.5vmin, -5.5vmin) rotate(9deg) scale(1.06); }
    66%  { transform: translate(-3.8vmin, 4.8vmin) rotate(-8deg) scale(.95); }
    100% { transform: translate(0, 0) rotate(0deg) scale(1); }
}

/* The crescents' drift. Identical motion, but every frame carries the
   translateY(-50%) that centres them on the page's midline: an animation's
   transform replaces the element's own outright, so without it restated here
   the pair would jump down by half their height the moment the animation
   started. Travel is gentler than the pebbles' — these are large shapes tight
   against the frame, and the same swing on them reads as the column wobbling. */
@keyframes app-blob-drift-mid {
    0%   { transform: translateY(-50%) translate(0, 0) rotate(0deg) scale(1); }
    33%  { transform: translateY(-50%) translate(2.4vmin, -3vmin) rotate(5deg) scale(1.04); }
    66%  { transform: translateY(-50%) translate(-2vmin, 2.6vmin) rotate(-4deg) scale(.97); }
    100% { transform: translateY(-50%) translate(0, 0) rotate(0deg) scale(1); }
}

/* Morph: the silhouette itself flexes. Every stop keeps the four corner pairs
   summing near 100% per axis, which is what stops the shape collapsing into a
   lens or a rectangle mid-cycle.

   Deliberately no 0%/100% stops. Those would declare one border-radius for
   every blob and so override the per-blob starting shapes set above, putting
   all four back on an identical silhouette — the exact thing those rules exist
   to prevent. Omitting the endpoints makes the browser use each element's own
   border-radius as the implicit first and last frame, so each blob departs from
   and returns to its own shape while passing through the shared poses between.
   That also makes the cycle seamless, since the implied ends match. */
@keyframes app-blob-morph {
    25%  { border-radius: 43% 57% 62% 38% / 56% 39% 61% 44%; }
    50%  { border-radius: 39% 61% 45% 55% / 64% 48% 52% 36%; }
    75%  { border-radius: 62% 38% 54% 46% / 41% 60% 40% 59%; }
}

/* --- Backdrop wave band --------------------------------------------------
   The green band with a wavy top edge running the full window width behind the
   top of the page, as in the reference.

   Pinned to the fixed layer rather than attached to .app-header: the band spans
   the whole window while the header is confined to the 1100px frame, and
   .app-header is sticky — anchoring the wave there would drag it down the
   screen on scroll instead of leaving it behind the masthead. (An earlier
   curved ::after on .app-header was removed for the flat bar the reference
   shows; this is a different decoration, not its return.)

   The band sits at the very top of the window, level with the masthead. It was
   offset to top:74px to clear the header, but the layer is FIXED — so that
   offset is measured from the viewport, not from the page. On a scrolled page
   (or any page whose content starts higher, like product detail) the band then
   floated across the middle of the screen, cutting a strip through the content
   instead of capping it. Anchored at the top it always reads as a masthead
   strip, which is what the reference shows.

   .app-shell is opaque white and paints over this layer, so the crests in the
   middle 1100px sit behind the frame; what reads are the wings either side of
   it. The band is wide and shallow so those wings carry the whole curve. */
.app-wave {
    position: absolute;
    /* Overhung past both window edges. The band tilts (see .app-wave-svg), and
       a rotation about its bottom centre swings each end vertically by
       half-width x sin(angle) — ~27px on a 2560px monitor. Flush to the edges
       that would lift one top corner clear of the fill and show a wedge of
       backdrop through it; the overhang keeps the corners outside the viewport
       at every width. Also why the container is NOT overflow:hidden — clipping
       here would reinstate the straight edge the tilt is meant to break up.
       The parent .app-backdrop-art is the one that clips, so the overhang
       cannot widen the page. */
    left: -4%;
    right: -4%;
    /* The band is flipped (see .app-wave-svg), so its SOLID fill is now the slab
       at the top and the scalloped edge is its underside. It therefore starts at
       the very top of the viewport and runs down, with the scallops landing at
       the foot of the header block.
       This used to be offset upward by the band's own height, back when the fill
       sat below the crest and only the crest was meant to show; keeping that
       offset after the flip would have hoisted the whole slab off the top of the
       screen.

       --app-wave-anchor is the header block's height, emitted by
       element/v1/header.php because it varies with the optional utility strip. */
    top: 0;
    /* Height IS the anchor now: the band runs from the top of the viewport down
       to the foot of the header, which is where the scalloped underside should
       sit. --app-wave-height no longer sets it — that token described the depth
       of a band whose crest rose from a baseline, which is not this shape. */
    height: var(--app-wave-anchor, 66px);
}

.app-wave-svg {
    display: block;
    width: 100%;
    height: 100%;
}

/* The paths only translate now — the tilt moved to the <svg> below — so they
   need no transform-origin of their own. */

/* The faint echo behind the main band. Low opacity and slow: as the two paths
   drift apart the crest gains a soft second edge, then closes back up. Any
   stronger and the band stops reading as one solid shape. */
.app-wave-back {
    fill: var(--app-art);
    opacity: .45;
    animation: app-wave-sway-back 21s ease-in-out infinite;
}

/* The band itself: one flat, fully-opaque mint, as in the reference. */
.app-wave-front {
    fill: var(--app-art);
    opacity: 1;
    animation: app-wave-sway-front 15s ease-in-out infinite;
}

/* The two paths slide in opposite directions with a touch of vertical travel,
   so the crests pass each other and the band's edge swells and settles.

   No rotate() in here any more. These transforms apply in the SVG's own
   coordinate space, which preserveAspectRatio="none" stretches horizontally to
   the window — roughly 1.3x at the 1100px frame, more on a wide monitor. A
   rotation through a non-uniform scale is sheared, so the tilt flattened to
   almost nothing at exactly the moment the wave got wider. The tilt now lives
   on .app-wave-svg below, outside that stretch. */
@keyframes app-wave-sway-back {
    0%   { transform: translate(0, 0); }
    50%  { transform: translate(40px, -6px); }
    100% { transform: translate(0, 0); }
}

@keyframes app-wave-sway-front {
    0%   { transform: translate(0, 0); }
    50%  { transform: translate(-32px, 4px); }
    100% { transform: translate(0, 0); }
}

/* The band's tilt. Applied to the <svg> element itself rather than to the paths
   inside it, so it happens in screen space after the viewBox stretch — which is
   what makes the angle mean the same thing at every window width.
   1.2deg across a full-width band lifts one end by a clearly visible amount
   while keeping the other end's fill below the crest; the band is drawn wider
   than the window (-200 to 1640 in a 1440 box) precisely so a tilt cannot pull
   a corner into view. */
/* scaleY(-1) flips the band vertically, so the curve hangs DOWN from the top of
   the page instead of rising up out of it — the solid fill is now the slab
   above the crest and the scalloped edge is its underside.
   A flip rather than rotate(180deg): a 180 turn would mirror horizontally too,
   which is pointless here (the wave is symmetric in intent) and would send the
   paths' -200..1640 overhang out the opposite sides.
   The flip is baked in as the animation's resting state and restated in every
   keyframe below — a transform list replaces the whole transform, so a keyframe
   carrying only rotate() would drop the flip for the duration of the animation. */
.app-wave-svg {
    transform-origin: 50% 50%;
    transform: scaleY(-1);
    animation: app-wave-tilt 19s ease-in-out infinite;
}

@keyframes app-wave-tilt {
    0%   { transform: scaleY(-1) rotate(-1.2deg); }
    50%  { transform: scaleY(-1) rotate(1.2deg); }
    100% { transform: scaleY(-1) rotate(-1.2deg); }
}

/* 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;
    /* The single gap between every section on the page. One token so the
       rhythm cannot drift section to section; raised at 768px below. */
    --app-section-gap: 24px;
    /* The breathing room a page takes at its top and bottom edges — clearing
       the header above and the fixed bottom nav below. Separate from the gap
       BETWEEN sections above: the home page stacks ~25 sections, so folding
       these two into one token would space that page out every time the page
       edges needed more room. Raised at 768px below. */
    --app-page-top: 32px;
    --app-page-bottom: 40px;
    /* Backdrop wave band. Its one measurement is --app-wave-anchor (the header
       block's height): the band hangs from the top of the viewport down to
       that line, where its scalloped underside sits. That token is NOT set
       here — it depends on whether the optional utility strip rendered, so
       element/v1/header.php emits it next to the guard that decides that. The
       fallbacks on the var() uses above cover the strip-less case if this sheet
       is ever used without that element. */
    --app-ink: #1b1b1b;
    --app-muted: #6b7280;
    /* White, not the old #f3f4f6 grey. The reference has the decorative shapes
       floating on a clean white page with no visible seam where the app frame
       ends — a grey backdrop put a distinct panel edge around the column and
       muddied the mint shapes sitting on it. */
    --app-backdrop: #fff;
    /* The hairline down each side of the app column. Neutral rather than a tint
       of the theme: it is a structural edge, not brand decoration, and a themed
       line competed with the mint shapes sitting right beside it. */
    --app-frame-edge: #e8eaed;
}

.app-shell {
    max-width: var(--app-width);
    margin-inline: auto;
    background: #fff;
    min-height: 100vh;
    /* relative (so it paints above the decorative .app-backdrop-art layer) 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;
    /* A hairline down each side, marking where the app column ends against the
       backdrop. Side borders only — a full ring would draw a lid across the top
       of the header and a line under the footer, closing the column into a box;
       the frame is meant to run off both ends of the viewport.
       Deliberately not a box-shadow: that would also need spreading to the
       sides only, and a shadow under the fixed bottom nav reads as a seam. */
    border-left: 1px solid var(--app-frame-edge);
    border-right: 1px solid var(--app-frame-edge);
    /* 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
   (v1/footer_content 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);
}

/* All the vertical rhythm lives on the top edge — bottom padding is 0. With
   padding on both edges the gap between two sections was the SUM of one's
   bottom and the next's top, so any section that tweaked either value (the
   banner row's extra top padding, the hero's trimmed bottom) opened a gap a
   different size from its neighbours'. One edge means one number: every
   section is separated by exactly --app-section-gap. */
.app-section {
    padding: var(--app-section-gap) var(--app-pad) 0;
}

/* 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;
}

/* --- Hero ----------------------------------------------------------------
   Vendor-authored headline, blurb and call-to-action buttons from the backend's
   Homepage Setting tab. Rendered OVER the banner artwork (.app-hero--overlay),
   or as a plain block in the banner's place when the vendor has uploaded none
   (.app-hero--plain). The template renders nothing at all when the vendor has
   set none of the three. */

/* Overlaid on the artwork: absolutely positioned over the slider, so the
   scroll-snap track underneath keeps scrolling and the copy stays put rather
   than travelling with whichever slide is in view. */
.app-hero--overlay {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    padding: 0 clamp(18px, 5%, 48px);
    
    pointer-events: none;
    /* Left-to-right scrim so white text holds up over a light photograph. On
       the artwork itself rather than a flat tint over the whole image, which
       would dull the right-hand half the copy never reaches. */
    background: linear-gradient(90deg, rgba(10, 22, 40, .82) 0%, rgba(10, 22, 40, .62) 45%, rgba(10, 22, 40, .12) 100%);
    /* Matches .app-banner, so the scrim's corners sit on the artwork's. */
    border-radius: 16px;
}

/* The copy is white on the scrim, and inherits down to the title and desc. */
.app-hero--overlay .app-hero-title,
.app-hero--overlay .app-hero-desc {
    color: #fff;
}

/* Bigger than the plain block — it has a full banner to fill, and the reference
   headline is the loudest thing on the page. Not capped: backend-set copy runs
   the full width of its container. */
.app-hero--overlay .app-hero-title {
    font-size: clamp(22px, 3.4vw, 38px);
    text-shadow: 0 1px 2px rgba(0, 0, 0, .28);
}

.app-hero--overlay .app-hero-desc {
    text-shadow: 0 1px 2px rgba(0, 0, 0, .28);
}

/* Centred copy — Homepage Setting "Banner Text Alignment: Center". Left is the
   default and needs no class. The overlay is a column flex box (align-items is the
   horizontal axis), so the stack is centred there and text-align centres each
   wrapped line; the button row is a wrapping flex row, so justify-content. */
.app-hero--center {
    text-align: center;
}

.app-hero--overlay.app-hero--center {
    align-items: center;
    /* The left-to-right scrim is dark where left-aligned copy sits and fades out on the
       right — centred copy would straddle the fade. An even wash keeps it readable. */
    background: linear-gradient(90deg, rgba(10, 22, 40, .6) 0%, rgba(10, 22, 40, .7) 50%, rgba(10, 22, 40, .6) 100%);
}

/* Phones: the same top-to-bottom scrim the left-aligned overlay uses there (copy is
   bottom-anchored on a phone). Needed because the rule above is more specific than
   the phone rule for .app-hero--overlay and would otherwise keep the sideways fade. */
@media (max-width: 600px) {
    .app-hero--overlay.app-hero--center {
        background: linear-gradient(180deg, rgba(10, 22, 40, .34) 0%, rgba(10, 22, 40, .72) 55%, rgba(10, 22, 40, .88) 100%);
    }
}

.app-hero--center .app-hero-btns {
    justify-content: center;
}

/* Standalone block, used only when there is no banner to sit on. Keeps the
   original light-background treatment. */
/* No padding override: sections carry their spacing on the top edge only, so
   there is no bottom padding here to trim any more. */
.app-hero--plain {
    padding-bottom: 0;
}

/* Small pill above the headline, e.g. "Same-day local delivery". */
.app-hero-badge {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    margin-bottom: 12px;
    padding: 5px 12px;
    border-radius: 999px;
    background: var(--app-theme);
    color: #fff;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .6px;
    text-transform: uppercase;
    /* Back on: the box above turned pointer events off for the whole overlay. */
    pointer-events: auto;
}

.app-hero-badge-dot {
    font-size: 6px;
    opacity: .9;
}

.app-hero-title {
    font-size: 24px;
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: -.4px;
    color: var(--app-ink);
    margin: 0;
}

.app-hero-desc {
    font-size: 14px;
    line-height: 1.5;
    color: var(--app-muted);
    margin: 8px 0 0;
}

.app-hero-btns {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 16px;
    /* Back on: the overlay above turned pointer events off for the whole box,
       so the buttons have to opt back in or they could not be clicked. Harmless
       on .app-hero--plain, which never turned them off. The button row covers
       the part of the artwork a thumb is most likely to land on, so the swipe
       script forwards a drag begun here to the track; a tap is unaffected. */
    pointer-events: auto;
}

/* On the scrim the outline variant needs a light border and white label — the
   theme-coloured text it uses on a white page disappears against the artwork. */
.app-hero--overlay .app-hero-btn--outline {
    border-color: rgba(255, 255, 255, .55);
    color: #fff;
    /* A touch of backing so the label stays legible over a busy photograph
       without becoming a second solid button beside the primary one. */
    background: rgba(255, 255, 255, .12);
}

.app-hero--overlay .app-hero-btn--outline:hover,
.app-hero--overlay .app-hero-btn--outline:focus {
    background: rgba(255, 255, 255, .95);
    color: var(--app-ink);
    border-color: transparent;
}

.app-hero-btn {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 11px 20px;
    border-radius: 8px;
    font-size: 14px;
    font-weight: 600;
    text-decoration: none;
    /* Flat fill and a plain border, matching the rest of this sheet — no
       gradients or drop shadows. */
    transition: background-color .18s ease, color .18s ease, border-color .18s ease;
}

.app-hero-btn--primary {
    background: var(--app-theme);
    border: 1px solid var(--app-theme);
    color: #fff;
}

.app-hero-btn--primary:hover,
.app-hero-btn--primary:focus {
    /* Darkened via the shared header shade so the hover tracks the vendor colour
       instead of a fixed grey. */
    background: var(--app-header-bg, var(--app-theme));
    border-color: var(--app-header-bg, var(--app-theme));
    color: #fff;
}

.app-hero-btn--outline {
    background: transparent;
    border: 1px solid var(--app-theme-border);
    color: var(--app-theme);
}

.app-hero-btn--outline:hover,
.app-hero-btn--outline:focus {
    background: var(--app-theme-soft);
    color: var(--app-theme);
}

.app-hero-btn-icon {
    font-size: 12px;
}

@media (max-width: 600px) {
    .app-hero-title {
        font-size: 20px;
    }

    /* Full-width stacked buttons: side by side at this width they were each too
       narrow to read comfortably. */
    .app-hero-btn {
        flex: 1 1 100%;
        justify-content: center;
    }

    /* Except in the overlay, where the copy sits in a fixed-height band: two
       full-width stacked buttons would need a third row the banner has not got.
       They share the line instead and shrink to fit. */
    .app-hero--overlay .app-hero-btn {
        flex: 0 1 auto;
        padding: 9px 12px;
        font-size: 12.5px;
    }

    /* The trailing arrow is the first thing to go on a phone: it costs width on
       every button and the label already says where the button leads. Dropping
       it is often what keeps a third CTA on the same line. */
    .app-hero--overlay .app-hero-btn-icon {
        display: none;
    }

    /* Tighter gap between them, for the same reason. */
    .app-hero--overlay .app-hero-btns {
        gap: 8px;
    }

    /* On a phone the scrim runs top-to-bottom rather than left-to-right: the
       band is nearly as tall as it is wide there, so a horizontal fade leaves
       the copy's own corner bright. */
    .app-hero--overlay {
        background: linear-gradient(180deg, rgba(10, 22, 40, .34) 0%, rgba(10, 22, 40, .72) 55%, rgba(10, 22, 40, .88) 100%);
        justify-content: flex-end;
        padding-bottom: 22px;
    }

    .app-hero--overlay .app-hero-desc {
        font-size: 13px;
    }
}

.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;
}

/* Optional sub-line under a section heading, set from the backend's Homepage
   Setting tab. It follows .app-section-head, which already carries the 14px gap
   to the content below — so this pulls up into that gap and re-establishes it,
   rather than adding a second one and leaving the copy stranded from its
   heading. */
.app-section-desc {
    margin: -8px 0 14px;
    font-size: 13.5px;
    line-height: 1.5;
    color: var(--app-muted);
}

.app-section-link {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    font-weight: 600;
    color: var(--app-theme);
    text-decoration: none;
    white-space: nowrap;
}

/* Trailing arrow, as in the reference. Smaller than the label and nudged on
   hover — the only motion in an otherwise flat section. */
.app-section-link-icon {
    font-size: 10px;
    transition: transform .18s ease;
}

.app-section-link:hover .app-section-link-icon,
.app-section-link:focus .app-section-link-icon {
    transform: translateX(3px);
}

/* 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);
    /* Snap positions are measured from the scrollport edge, NOT the padding
       edge. Without this, `x mandatory` snapped the first item to x=0 — flush
       against the screen edge with the padding scrolled away. A full-width item
       (the home page videos) made it obvious: the browser snapped on load and
       the card sat on the left edge. */
    scroll-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 surface 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.
       A deep shade of the vendor's colour rather than the colour itself: the
       reference bar is near-black with the brand colour used only as an accent,
       and --app-theme at full strength cannot be an accent against a bar of
       that same colour. Set in default1.php, which mixes it from the vendor's
       splash_colour. */
    background: var(--app-header-bg, 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);
}

/* --- Header utility strip -----------------------------------------------
   Announcement and contact details above the main bar. Sits inside .app-header
   so it scrolls away with it; only the bar below stays pinned. Hidden entirely
   by the template when the vendor has none of these values. */

.app-header-utility {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    /* Negative margins pull the strip out to the header's own padding so it
       spans the full bar width and sits flush on its bottom edge, then it
       re-applies that padding itself. The 8px top margin is the gap the
       header's padding would otherwise leave between the bar and the band.
       These track .app-header's 8px padding — the 768px query below restates
       both together. */
    margin: 8px calc(var(--app-pad) * -1) -8px;
    padding: 8px var(--app-pad);
    /* A light cream band below the themed bar, as in the reference — the strip
       reads as an announcement rather than as more header. Fixed for every
       vendor rather than theme-derived, 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. */
    background: #fdf6e3;
    border-top: 1px solid rgba(0, 0, 0, .06);
    font-size: 12.5px;
    line-height: 1.4;
    /* Warm dark brown — near-black on cream reads as a disabled block. */
    color: #6b4c12;
}

/* The marquee's viewport. Clips the track inside it and never wraps, so a long
   announcement can neither push the strip into a second row nor shove the bar
   down the page — it scrolls instead of being cut off with an ellipsis. */
.app-header-utility-note {
    margin: 0;
    /* Takes the slack left by the contact details, which are flex:0 0 auto.
       min-width:0 is what actually allows it to shrink below its content width
       inside a flex row — without it the track's full length would set the
       strip's width and there would be nothing to scroll. */
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
}

/* The moving track: two copies of the message side by side. Translating it by
   exactly -50% puts the second copy where the first started, so the reset is
   invisible and the text appears to loop forever. */
.app-header-marquee {
    display: flex;
    width: max-content;
    animation: app-marquee 26s linear infinite;
}

/* The gap between the two copies travels with them, so the message does not
   butt up against its own repeat as it comes round. */
.app-header-marquee-item {
    white-space: nowrap;
    padding-right: 48px;
}

/* The message is vendor-authored HTML, printed raw — it can arrive wrapped in
   <p>, <h3> or a <span style="font-size:…">. Everything inside a copy takes
   the strip's own size and line, so pasted formatting cannot blow the ticker
   up or break it onto a second line. */
.app-header-marquee-item * {
    display: inline;
    margin: 0;
    padding: 0;
    font-size: inherit !important;
    line-height: inherit !important;
    white-space: nowrap;
}

/* Between the Header Message and the notification message. Two classes deep so
   it outranks the reset above. */
.app-header-marquee-item .app-header-marquee-sep {
    margin: 0 18px;
    opacity: .55;
}

/* --- Header search suggestions --------------------------------------------
   Dropdown under the search pill, filled from Users/autoSearch by the script
   in v1/header.php. White card on the dark bar, so it reads as a surface
   above the page, not part of the header. */
.app-search-suggest {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    right: 0;
    z-index: 60;
    max-height: min(60vh, 420px);
    overflow-y: auto;
    padding: 6px;
    border: 1px solid #e6e8ec;
    border-radius: 14px;
    background: #fff;
    box-shadow: 0 12px 32px rgba(16, 24, 40, .18);
    -webkit-overflow-scrolling: touch;
}

.app-search-suggest[hidden] {
    display: none;
}

.app-search-suggest-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 10px;
    border-radius: 10px;
    color: var(--app-ink);
    text-decoration: none;
}

.app-search-suggest-item:hover,
.app-search-suggest-item.is-active {
    background: var(--app-theme-soft);
    color: var(--app-ink);
    text-decoration: none;
}

.app-search-suggest-img {
    display: flex;
    flex: 0 0 38px;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    border: 1px solid #eceef1;
    border-radius: 8px;
    background: #fafbfc;
    overflow: hidden;
}

.app-search-suggest-img img {
    width: 100%;
    height: 100%;
    object-fit: contain;
}

.app-search-suggest-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    font-size: 13.5px;
    font-weight: 600;
    line-height: 1.3;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.app-search-suggest-price {
    flex: 0 0 auto;
    color: var(--app-theme);
    font-size: 13px;
    font-weight: 700;
}

.app-search-suggest-empty {
    padding: 14px 10px;
    color: var(--app-muted);
    font-size: 13px;
    text-align: center;
}

/* Pauses on hover so the message can actually be read — it may carry a phone
   number or a date the customer wants to note. */
.app-header-utility-note:hover .app-header-marquee {
    animation-play-state: paused;
}

@keyframes app-marquee {
    from { transform: translateX(0); }
    to   { transform: translateX(-50%); }
}

/* A visitor who has asked their OS for less motion gets the message standing
   still. It is then a plain line of text, so it truncates rather than running
   under the contact details. */
@media (prefers-reduced-motion: reduce) {
    .app-header-marquee {
        animation: none;
    }

    /* The decorative backdrop holds still too. The shapes stay — they are the
       design, not the motion — they just stop drifting, morphing and swaying,
       each frozen at the pose its first keyframe declares. */
    .app-blob,
    .app-wave-svg,
    .app-wave-back,
    .app-wave-front {
        animation: none;
    }

    /* The tilt's resting pose is its -1.2deg first keyframe; with the animation
       off the band would sit dead level instead. The scaleY(-1) flip has to be
       restated alongside it — a transform list replaces the whole transform, so
       rotate() alone would un-flip the band and stand the wave back up. */
    .app-wave-svg {
        transform: scaleY(-1) rotate(-1.2deg);
    }

    /* Only the first copy is meaningful once nothing moves; the duplicate would
       just repeat the sentence inline. */
    .app-header-marquee-item + .app-header-marquee-item {
        display: none;
    }

    .app-header-marquee-item {
        padding-right: 0;
        overflow: hidden;
        text-overflow: ellipsis;
    }
}

.app-header-utility-contact {
    display: flex;
    align-items: center;
    gap: 16px;
    /* Never yields width to the announcement beside it — the contact details are
       the actionable half of the strip. */
    flex: 0 0 auto;
    padding: 0;
    list-style: none;
    /* Right-aligned even when the announcement is absent. */
    margin: 0 0 0 auto;
}

.app-header-utility-contact a {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: inherit;
    text-decoration: none;
    white-space: nowrap;
}

.app-header-utility-contact a:hover,
.app-header-utility-contact a:focus {
    /* Darkens on the cream band; the old #fff was for the dark strip this
       replaced and would have been invisible here. */
    color: #4a340a;
    text-decoration: underline;
}

/* Fixed box rather than letting the glyph size the slot: the phone and envelope
   have different intrinsic widths, so on their own the two labels started at
   different offsets and the column of text read as ragged. A common width and
   centred glyph lines both labels up. */
.app-header-utility-contact i {
    flex: 0 0 auto;
    width: 13px;
    font-size: 11px;
    line-height: 1;
    text-align: center;
    opacity: .85;
}

/* The gift/announcement mark that leads the strip in the reference. Its own
   element rather than a ::before so it can be dropped with the note on a narrow
   screen, where the strip carries only the phone. */
.app-header-utility-icon {
    flex: 0 0 auto;
    /* No negative margin: the strip is a flex row with a 16px gap, and pulling
       the note back over the icon put the first characters of a scrolling
       message underneath it — the announcement read as "…count" with its
       opening clipped. The gap alone spaces the two. */
    color: #e0a020;
    font-size: 13px;
    /* Matches the contact links opposite, so both ends of the strip sit on the
       same optical line rather than the icon riding high against the text. */
    line-height: 1;
}

/* Narrow screens: phone AND email both stay. The email used to be hidden here
   to save width, which left a phone visitor with no way to find it. Instead
   the strip tightens: smaller gaps and text, and the email is capped with an
   ellipsis so an unusually long address cannot push the phone number off the
   row. The announcement scrolls, so it gets by on whatever width is left. */
@media (max-width: 600px) {
    .app-header-utility {
        gap: 10px;
        font-size: 11.5px;
    }

    .app-header-utility-contact {
        gap: 10px;
    }

    .app-header-utility-mail a {
        max-width: 150px;
    }

    .app-header-utility-mail span {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    /* The strip keeps its space-between: the note is on the left again, so the
       flex-end that used to sit here (for when the phone stood alone) would now
       shove both items against the right edge. */

    /* Shorter loop on a narrow screen — the same text has less width to cross,
       so the desktop duration would crawl. */
    .app-header-marquee {
        animation-duration: 18s;
    }
}

/* 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;
}

/* Holds only the location picker (TMC23), and is empty otherwise. It used to
   take the slack with flex:1 1 auto, which now fights the search pill for the
   same middle space — the pill is the one that should grow, so this sizes to
   its content and may shrink but never expand. An empty box contributes
   nothing to the row. */
/* The location picker's column. Pushed to the right-hand end of the bar so it
   sits with the greeting and icons rather than floating mid-row: it has no
   width of its own, so without this it simply began wherever the search pill
   happened to stop, which on a wide frame left it stranded in the middle and
   butted against the greeting.
   margin-left:auto absorbs the slack before it; .app-header-actions carries the
   same declaration, and the first one in source order takes the space, so this
   pair ends up adjacent at the right end. */
.app-header-brand {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    flex: 0 1 auto;
    margin-left: 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;
    /* Right, not left: this block now sits at the right-hand end of the bar
       beside the greeting and the icons, and those all read right-aligned. A
       left-aligned pair here left the caption and address starting on a ragged
       edge against the cluster next to them. */
    text-align: right;
}

/* 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;
    font-size: 10px;
    opacity: .9;
}

/* Address + caret on one line; the address shrinks and ellipses first so the
   caret always stays visible right after it. */
.app-header-loc-value-row {
    display: flex;
    align-items: center;
    gap: 5px;
    min-width: 0;
    max-width: 100%;
}

/* "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;
    /* Tracks the text-align:right above — as a flex column this is what
       actually pulls the two lines to the block's right edge; text-align alone
       only aligns text inside each line, so the shorter caption still sat left
       of the address. */
    align-items: flex-end;
    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;
}

/* One line, never wrapping. The greeting used to be flex-basis:100% here, which
   forced the icons onto a second row and made the bar two-tall — in the
   reference the greeting and the icons share a single line. */
.app-header-actions {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-end;
    align-items: center;
    gap: 10px;
    flex: 0 0 auto;
    /* Pinned to the far right whatever the search pill's actual width — without
       this the cluster sits immediately after a shrunken pill.
       .app-header-brand now also carries margin-left:auto and precedes this in
       source order, so it takes the free space and this cluster follows it.
       The explicit left margin is the gap between the two: the header row's own
       `gap` still applies, but 10px alone let the location's chevron sit almost
       against the greeting. */
    margin-left: 18px;
}

.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);
}

/* --- Guest Login / Sign Up -----------------------------------------------
   A white pill on the theme bar, in place of the user icon a guest used to
   get. Same 34px height as the icons beside it so the bar does not change
   height between guest and signed-in. */
.app-auth-pill {
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
    height: 34px;
    padding: 0 14px;
    border-radius: 999px;
    background: #fff;
    color: var(--app-theme);
    font-size: 13px;
    font-weight: 700;
    line-height: 1;
    white-space: nowrap;
    box-shadow: 0 2px 6px rgba(0, 0, 0, .12);
}

.app-auth-link {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: inherit;
    text-decoration: none;
}

.app-auth-link:hover,
.app-auth-link:focus {
    color: inherit;
    text-decoration: underline;
}

.app-auth-sep {
    margin: 0 6px;
    opacity: .5;
}

/* --- Avatar --------------------------------------------------------------
   The signed-in customer's initials, standing in for the bare logout arrow that
   used to sit in this slot. It opens the account menu rather than signing out
   on the first tap — a one-tap sign-out next to the cart was easy to hit by
   mistake.

   A filled disc rather than the bare glyph the other header icons are: this one
   identifies the account, so it should read as a person, not another action. */
.app-avatar {
    background: rgba(255, 255, 255, .16);
    border: 1px solid rgba(255, 255, 255, .28);
}

.app-avatar:hover,
.app-avatar:focus,
.app-account.is-open .app-avatar {
    background: rgba(255, 255, 255, .95);
    border-color: transparent;
    /* The initials follow the disc — see .app-avatar-initials, which inherits. */
    color: var(--app-header-bg, var(--app-theme));
}

/* Smaller than the icon glyphs beside it: two letters need to fit inside the
   same 34px control without touching its edge. */
.app-avatar-initials {
    font-size: 12px;
    font-weight: 700;
    letter-spacing: .3px;
    line-height: 1;
    color: inherit;
}

/* --- 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);
}

/* The account the menu belongs to, above its entries. A separator rather than a
   link — with the header greeting hidden on a phone, this is the only place the
   signed-in customer is named. */
.app-account-name {
    margin: 0 0 4px;
    padding: 8px 12px 10px;
    border-bottom: 1px solid #eceef1;
    /* Inherited white from .app-header would be invisible on this panel. */
    color: var(--app-ink);
    font-size: 13px;
    font-weight: 700;
    /* A long name wraps rather than stretching the panel off the screen edge. */
    max-width: 200px;
    overflow-wrap: anywhere;
}

.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;
}

/* Logout is destructive, so it reads in red and sits apart from the navigation
   entries above it rather than looking like a third place to go.
   Selectors are qualified with .app-account-menu to outrank the generic
   `.app-account-menu a` rules above on specificity rather than with
   !important. */
.app-account-menu .app-account-logout {
    margin-top: 4px;
    border-top: 1px solid #eceef1;
    border-radius: 0 0 8px 8px;
    color: #d92b1f;
}

.app-account-menu .app-account-logout:hover {
    background: #fdeceb;
    color: #b3241a;
}

.app-account-menu .app-account-logout i {
    color: #d92b1f;
}

.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;
    /* Flex-centred, border included in the 16px: the number was placed by a
       hand-tuned 13px line-height, which sat it off-centre inside the ring. */
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    border-radius: 8px;
    background: #e5372e;
    /* Matches the bar behind it, so the ring reads as separation from the icon
       rather than an outline around the badge. Tracks --app-header-bg, not
       --app-theme: the bar is no longer the theme colour. */
    border: 1.5px solid var(--app-header-bg, var(--app-theme));
    color: #fff;
    font-size: 9.5px;
    font-weight: 700;
    line-height: 1;
    text-align: center;
}

/* Inline in the right-hand cluster, immediately before the icons — two stacked
   lines ("Welcome back," over the name), as in the reference. It used to be
   flex:0 0 100%, which forced .app-header-actions to wrap and pushed the icons
   onto a second row; now it is a normal item on the single header line.
   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. */
.app-greeting {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    font-size: 11px;
    font-weight: 500;
    opacity: .82;
    margin: 0;
    min-width: 0;
    flex: 0 1 auto;
    text-align: right;
    line-height: 1.25;
    white-space: nowrap;
    overflow: hidden;
}

/* The customer's name, the second and louder of the two lines. */
.app-greeting-name {
    font-size: 13px;
    font-weight: 700;
    opacity: 1;
    max-width: 130px;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* A standing search pill in the right-hand cluster. The magnifier is placed
   against this box, so it is the positioning context for it. */
.app-search {
    position: relative;
    display: flex;
    align-items: center;
    min-width: 0;
    /* Takes the middle of the bar, between the logo and the right-hand cluster,
       and yields first when the bar is tight — the icons beside it are
       flex:0 0 auto and never give up width. max-width stops it stretching the
       full frame on a wide screen, where the reference keeps it a fixed pill
       rather than a full-width field. */
    flex: 1 1 auto;
    max-width: 420px;
    /* Separates it from the logo on its left. */
    margin-left: 10px;
}

/* Always-open translucent pill, as in the reference — the magnifier sits INSIDE
   the field on the left and the box is visible at rest, rather than the field
   growing out of a collapsed icon. The expanding behaviour and its .is-open
   class are gone with it; the script that added the class is harmless if it
   still runs, since nothing keys off it any more. */
.app-search-field {
    flex: 1 1 auto;
    min-width: 0;
    /* Translucent rather than solid white: on the near-black bar a white block
       is the loudest thing in the header, and in the reference the field reads
       as a recess in the bar. */
    background: rgba(255, 255, 255, .12);
    border: 1px solid rgba(255, 255, 255, .18);
    border-radius: 999px;
    /* Room on the left for the magnifier, which is absolutely placed inside. */
    padding: 0 16px 0 40px;
    display: flex;
    align-items: center;
    /* Matches the 34px icon buttons it sits beside. */
    height: 34px;
    transition: background-color .18s ease, border-color .18s ease;
}

/* Lifts on focus so the field looks active while typing. */
.app-search:focus-within .app-search-field {
    background: rgba(255, 255, 255, .18);
    border-color: rgba(255, 255, 255, .38);
}

.app-search-field input {
    width: 100%;
    min-width: 0;
    border: 0;
    outline: 0;
    font-size: 13.5px;
    /* White on the bar, not --app-ink: the field is translucent over the dark
       header now, so the old near-black text was unreadable in it. */
    color: #fff;
    background: transparent;
}

.app-search-field input::placeholder {
    color: rgba(255, 255, 255, .6);
}

/* The magnifier, now inside the field at its left edge. Absolutely placed so it
   does not take a flex slot and push the input off-centre; the field's 40px
   left padding is what reserves the space for it. */
.app-search-toggle {
    position: absolute;
    left: 0;
    width: 40px;
    height: 34px;
    /* Overrides the round chip .app-icon-btn gives it — inside the field it is
       a bare glyph, and a hover circle here would clip against the pill. */
    border-radius: 999px;
    background: none;
    color: rgba(255, 255, 255, .75);
    font-size: 14px;
}

.app-search-toggle:hover,
.app-search-toggle:focus {
    background: none;
    color: #fff;
}


.app-banner-row .app-row {
    margin-inline: 0;
    padding-inline: 0;
    /* .app-row sets scroll-padding-inline: var(--app-pad) so a card row snaps its first card
       in from the screen edge. This row has no side padding, so that offset made every slide
       after the first snap 20px short of the left edge — the end of the previous banner
       showed as a strip down the side. */
    scroll-padding-inline: 0;
    /* .app-row keeps 4px under a card row for its focus ring and shadow. A banner has
       neither, and that 4px was added to the gap after every banner row — so the space
       below a banner was always 4px bigger than the space above the next one. All the
       spacing now comes from the next section's top padding alone. */
    padding-bottom: 0;
}

.app-banner {
    width: 100%;
    max-width: 100%;
    border-radius: 16px;
    overflow: hidden;
    background: var(--app-theme-soft);
}

.app-banner img {
    width: 100%;
    object-fit: cover;
    display: block;
}


.app-row .app-banner {
    width: 100%;
    flex-basis: 100%;
}

/* The hero band: the frame is a fixed height on a wide screen (see the 768px
   rule below), so the artwork fills it. object-fit: cover above only does
   anything once the image has a height — without one the image kept its natural
   height and the theme tint showed as a strip underneath. */
.app-banner-row:not(.app-banner-row--plain) .app-banner img {
    height: 100%;
}

/* Upper / Middle / Lower banners carry no copy, so there is no band to fill:
   each frame is exactly as tall as its own image, at every width. Slides are
   top-aligned so a shorter banner is not stretched to the tallest one's height
   (which showed the tint below it), and the tint is dropped. The extra top
   padding keeps the row off the category tiles or product heading above it. */
.app-banner-row--plain .app-row {
    align-items: flex-start;
    /* The script sizes the track to the slide on screen (a wide banner next to a square
       one); the transition keeps that from snapping, and overflow-y stays hidden so the
       taller slide cannot show a scrollbar while the height is still catching up. */
    overflow-y: hidden;
    transition: height .25s ease;
}

.app-banner-row--plain .app-row .app-banner {
    height: auto;
    background: none;
}

/* Middle + Lower banners: two equal columns when both are on, one full-width
   column when only one is. min-width: 0 on the cell is what lets a scrolling
   track inside it shrink to the column instead of widening the grid. Top-aligned
   so two banners of different heights do not stretch each other. */
.app-banner-pair {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
    gap: 12px;
}

.app-banner-pair--single {
    grid-template-columns: minmax(0, 1fr);
}

.app-banner-cell {
    min-width: 0;
}

/* Banner rows that sit BETWEEN sections (Upper, Middle + Lower) take the same gap as
   every other section — --app-section-gap, 24px, or 34px on a wide frame — instead of
   the 16px they had, which left the category strip and the featured products a different
   distance from them. The top banner keeps its own 12px: it hugs the header, it is not
   between two sections. Three classes: .app-section.app-banner-row further down sets 12px
   and is the same specificity as two, so it would win on source order. */
.app-section.app-banner-row.app-banner-row--plain {
    padding-top: var(--app-section-gap);
}

/* --- 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. z-index 2 puts
   them above the hero overlay at 1, so the arrows stay clickable where the copy
   crosses them. */
.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;
    }
}

/* On the artwork's lower edge rather than in the gap below it, as in the
   reference. Absolute for the same reason the arrows are: the hero overlay
   covers the slider, and dots in normal flow underneath would sit below the
   banner while the copy sat on it. */
.app-banner-dots {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 12px;
    z-index: 2;
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 6px;
}

.app-banner-dot {
    width: 7px;
    height: 7px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    /* Translucent white, not the old page-grey: the dots now sit on the image,
       where a light grey reads as dirt on the photograph. */
    background: rgba(255, 255, 255, .55);
    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);
}

/* The dots now sit inside the artwork, and on a phone the overlay copy is
   bottom-aligned — without this the button row would land on top of them. */
@media (max-width: 600px) {
    .app-hero--overlay {
        padding-bottom: 34px;
    }
}

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

/* Thin rule between the section heading and the tiles, as in the reference.
   An element rather than a border on .app-row, which is bled out past the
   section's padding and would draw the line wider than the heading above it. */
.app-cat-rule {
    height: 1px;
    background: #eceef1;
    margin-bottom: 18px;
}

/* A wrapping grid, replacing the horizontally-scrolling .app-row this used to
   be. Once a vendor adds more categories than fit on one line they break onto
   the next rather than disappearing off the right edge — nothing is reachable
   only by scrolling sideways.

   auto-fill with a minmax track rather than a fixed column count: the number of
   categories varies per vendor, and a fixed count would leave a short row
   stretched or a long one cramped. The tiles keep their own width (.app-cat),
   so the track only decides how many fit per line. */
.app-cat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
    gap: 18px 6px;
    /* justify-items, not justify-content: each tile centres inside its own
       track, so a last line holding two tiles still lines them up under the
       first two above rather than centring the pair in the row. */
    justify-items: center;
}

/* The tile no longer sets its own width — the grid track does. A fixed 118px
   inside a 1fr track left a ragged gap on the right of every cell. */
.app-cat-grid > .app-cat {
    width: 100%;
    max-width: 118px;
}

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

   Much wider than the 76px circle it holds, so this vendor's two-word names
   ("Water Dispensers", "Reverse Osmosis") sit on a single line the way the
   reference's do, instead of wrapping. The circle stays centred in it. */
.app-cat {
    width: 118px;
    text-align: center;
    text-decoration: none;
    color: var(--app-ink);
}

.app-cat-thumb {
    width: 76px;
    height: 76px;
    margin: 0 auto 10px;
    /* Circular, matching the reference's category row. */
    border-radius: 50%;
    /* White inside a plain light-grey ring. Not --app-theme-border: in the
       reference the rings are a neutral hairline and the colour comes from the
       icons inside them, so a themed ring put the brand colour on every tile
       and flattened that contrast. */
    background: #fff;
    border: 1px solid #eceef1;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Just enough to keep the mark off the ring. It was 16px, which shrank the
       artwork to a dot in the middle of a large white disc — in the reference
       the glyph takes up a good half of the circle. */
    padding: 7px;
    overflow: hidden;
    transition: border-color .2s ease, box-shadow .2s ease, background-color .2s ease;
}

/* Hover: the ring takes the theme colour, the disc picks up its faintest tint
   and the whole tile lifts a little. No scale on the thumb — .app-cat sits in
   an .app-row, an overflow-x scroller that clips vertically, so a growing
   circle would have its top and bottom sliced off. The lift is applied to the
   image inside instead (below), which has room to move within the disc. */
.app-cat:hover .app-cat-thumb {
    border-color: var(--app-theme);
    background: var(--app-theme-soft, #f4f7ff);
    box-shadow: 0 4px 12px rgba(16, 24, 40, .12);
}

/* The mark grows and tilts inside the fixed ring, which reads as the tile
   responding without the row's clipping getting in the way. A small angle, not a
   full turn: these are photographs and product shots, and anything more than a
   few degrees reads as the image being broken rather than animated. (The rest of
   this image's box — sizing and fit — is declared further below.) */
.app-cat:hover .app-cat-thumb img {
    transform: scale(1.1) rotate(-6deg);
}

/* The label follows the ring into the theme colour, so the whole tile responds
   as one rather than just the circle. */
.app-cat:hover .app-cat-name {
    color: var(--app-theme);
}

/* (The .app-cat--all rules that used to sit here are gone with the "All" tile
   itself — the category grid wraps now, so every category is on the page and a
   tile leading to "all" of them had nowhere new to go.) */

/* contain, not cover: these are icon-style category marks, and cover would crop
   a square glyph to fill the circle and clip its edges. The tile's padding is
   what keeps the mark clear of the ring. */
.app-cat-thumb img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    /* Circular clip so a photographic upload — some vendors have photos here,
       not flat icons — still reads as a round tile rather than a square inside
       a circle. Harmless on a transparent PNG icon. */
    border-radius: 50%;
    /* Paired with the :hover scale + tilt above. The scale is what keeps the
       tilt clean: at 1.1 the image still covers the disc when rotated, so the
       6deg turn cannot expose a bare corner inside the ring. */
    transition: transform .25s ease;
}

.app-cat-name {
    font-size: 12.5px;
    font-weight: 600;
    line-height: 1.3;
    /* Body ink, not the theme colour: in the reference only the active "All"
       label is blue and the rest are dark, so theming them all removed the
       distinction between the selected tile and its neighbours. */
    color: var(--app-ink);
    /* Up to two lines, but only as tall as the name actually needs — the box
       used to be locked to height:2.6em, which hung an empty second line under
       every short name ("All", "Spring Water") and pushed the row taller than
       the reference's. min-height still reserves one line so the tiles keep a
       common baseline, and .app-cat is wide enough that most of this vendor's
       names fit on that one line. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    min-height: 1.3em;
}

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

.app-card {
    /* Wider than the old 158px: the card now carries a category pill and a
       sub-category label side by side across its top, which had no room to sit
       on one line at the previous width. */
    width: 178px;
    border: 1px solid #eceef1;
    border-radius: 16px;
    background: #fff;
    padding: 12px;
    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);
}

/* --- Card tag row -------------------------------------------------------
   The pair of labels across the top of each card in the reference: the category
   as a filled pill on the left, the sub-category as plain muted text on the
   right. Above the image on the card's own ground, not overlaid on the artwork
   — .app-tag is the overlaid kind and still sits inside .app-card-media. */

.app-card-tags {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 8px;
    /* Holds the row's height even when only one of the two is present, so the
       images below stay aligned across a row of cards. */
    min-height: 20px;
}

.app-card-cat {
    /* Soft theme-tinted pill, as in the reference. */
    background: var(--app-theme-soft, #eef4ff);
    color: var(--app-theme);
    border-radius: 999px;
    padding: 3px 9px;
    font-size: 10.5px;
    font-weight: 600;
    line-height: 1.4;
    text-decoration: none;
    /* Never wraps to a second line — it would push the image down on one card
       and not its neighbour. A long category name is clipped instead.
       No max-width by default: the 62% cap this carried applied even when the
       pill was the row's only child, which truncated "Reverse Osmosis Water" to
       "Reverse Osmosis Wat…" on a card with the whole width free. It only has
       to yield when there is a sub-category label beside it — see below. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
}

/* Only when something follows it in the row does the pill give up width. */
.app-card-cat:not(:last-child) {
    max-width: 62%;
}

a.app-card-cat:hover,
a.app-card-cat:focus {
    background: var(--app-theme);
    color: #fff;
}

/* Plain text, not a pill — the reference's right-hand label carries no fill. */
.app-card-sub {
    font-size: 10.5px;
    font-weight: 500;
    color: var(--app-muted);
    line-height: 1.4;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    /* Yields to the pill on its left rather than pushing it out of the card. */
    min-width: 0;
}

.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;
}

/* z-index is load-bearing: .app-card:hover scales the image, and a transform
   creates a stacking context that paints the image ABOVE any positioned
   sibling without one — which is why these tags vanished on hover. */
.app-tag {
    position: absolute;
    top: 6px;
    left: 6px;
    z-index: 2;
    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;
}
.app-section.app-banner-row {
    padding-top: 12px;
}


/* The discount badge. A red pill pinned top-RIGHT, so it never collides with
   the .app-tag (both used to sit left and overlapped whenever a product carried
   each). The white ring is what separates it from artwork of any colour —
   without it the badge sank into the red and pink packaging some products use. */
.app-tag--off {
    /* 8px clears the media frame's own 10px corner radius: pinned any tighter
       the pill's top-right corner tracked outside the rounded edge and read as
       hanging off the card. */
    top: 8px;
    right: 8px;
    left: auto;
    z-index: 2;
    display: inline-flex;
    /* center, not baseline: the value and the label are set at different sizes,
       so aligning their baselines left the smaller "OFF" sitting low in the
       pill and the whole badge looked bottom-heavy rather than centred. */
    align-items: center;
    gap: 4px;
    width: auto;
    height: auto;
    /* Symmetrical padding against a pill radius — the previous 5px/10px left
       more air at the ends than above and below, which is what made the
       rounding look uneven. */
    padding: 4px 9px;
    border: 0;
    border-radius: 999px;
    background: linear-gradient(145deg, #f4452f, #cf2318);
    /* Ring drawn INSIDE the badge. As an outer 0 0 0 2px spread it painted
       beyond the pill's box, and .app-card-media's overflow:hidden then clipped
       that ring against the frame's straight edge — squaring off the corner the
       badge is pinned to. An inset ring stays within the element, so the pill
       keeps its full radius. */
    box-shadow: 0 3px 10px rgba(207, 35, 24, .35), inset 0 0 0 1.5px rgba(255, 255, 255, .9);
    color: #fff;
    text-transform: uppercase;
    white-space: nowrap;
    /* Never intercepts the tap meant for the card link behind it. */
    pointer-events: none;
}

/* The saving carries the weight; "OFF" sits quieter beside it, so the number is
   what the eye lands on rather than the two reading as one flat string. */
/* line-height 1 on both: at 1.1 each span carried a sliver of leading that the
   flex centring then had to average out, leaving the pair sitting a shade high
   in the pill. At 1 the text box is the glyph height and the padding above and
   below is what the eye actually sees. */
.app-tag--off .app-tag-off-value {
    font-size: 12px;
    font-weight: 800;
    line-height: 1;
    letter-spacing: -.2px;
}

.app-tag--off .app-tag-off-label {
    font-size: 8.5px;
    font-weight: 700;
    line-height: 1;
    letter-spacing: .5px;
    opacity: .92;
}

.app-section.app-banner-row.mt-0 {
    padding-top: 12px;
}
/* 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;
    /* Clamped at two lines, but only as tall as the name needs. The
       min-height:35px this carried reserved a second line on EVERY card, so a
       one-line name ("Paper Cups", "Bebe Agua") hung a blank line under itself
       — which is most of the gap under the name. Prices still line up across a
       row: .app-card-price carries margin-top:auto, so it sits on the card's
       bottom edge regardless of how tall the name above it turned out. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.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;
}

/* Availability line under the unit, as in the reference ("199,559 Litres In
   Stock"). Green rather than the muted grey of the unit above it: it is a
   positive signal, and the template only renders it when the figure is above
   zero, so it never has to read as a warning. */
.app-card-stock {
    font-size: 10.5px;
    font-weight: 600;
    color: #17864a;
    margin: 0 0 6px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.app-card-price {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 5px;
    /* Anchored to the bottom of the card, so prices sit on one line across a
       row whatever height the name and stock rows above came out at. This is
       what lets the name drop its two-line floor without the cards going
       ragged. Harmless where a CTA follows: that button carries its own
       margin-top:auto and simply wins the remaining space. */
    margin-top: auto;
    /* No bottom margin when the card ends here — the home screen's cards have no
       button under the price, and the old fixed 10px left a dead strip along the
       bottom of every one of them. :has is progressive: where it is unsupported
       the card simply keeps the original spacing. */
    margin-bottom: 10px;
}

.app-card:not(:has(.app-card-btn)) .app-card-price {
    margin-bottom: 0;
}

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

/* The struck-through original, beside the live price. Lighter and smaller than
   it was so the price the customer actually pays stays the loudest thing in the
   row — the two used to read as competing figures. */
.app-price--was {
    font-size: 11px;
    font-weight: 500;
    color: #9aa1ab;
    text-decoration: line-through;
    text-decoration-thickness: 1px;
}

/* A discounted price is themed rather than ink, so a card carrying an offer is
   distinguishable from a full-price one at a glance — without needing the badge
   over the image to be read first. :has is progressive: where it is
   unsupported the price simply stays ink, which is the current behaviour. */
.app-card-price:has(.app-price--was) .app-price {
    color: var(--app-theme);
}

/* --- Offer emphasis -------------------------------------------------------
   The offer was easy to miss: a 12px badge and a price the same size as a
   full-price one. The badge grows and goes flat (no gradient/glow), and the
   price row gains a "Save $x" chip so the deal reads in money too. */
.app-tag--off {
    top: 10px;
    right: 10px;
    gap: 5px;
    padding: 6px 11px;
    border-radius: 8px;
    background: #e5392b;
    box-shadow: 0 2px 6px rgba(229, 57, 43, .3);
}

.app-tag--off .app-tag-off-value {
    font-size: 16px;
    letter-spacing: -.3px;
}

.app-tag--off .app-tag-off-label {
    font-size: 10.5px;
    letter-spacing: .6px;
    opacity: 1;
}

/* The price actually paid: largest figure on a discounted card. */
.app-card-price:has(.app-price--was) .app-price {
    font-size: 19px;
    letter-spacing: -.3px;
}

.app-card-price:has(.app-price--was) .app-price--was {
    font-size: 12.5px;
}

.app-price-save {
    display: inline-flex;
    align-items: center;
    padding: 3px 8px;
    border-radius: 6px;
    background: #e7f6ec;
    color: #15803d;
    font-size: 11px;
    font-weight: 700;
    line-height: 1.2;
    white-space: nowrap;
}

.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;
}

/* --- "View All" tile -----------------------------------------------------
   Fills the last cell of the top-selling grid, completing the row the 7 product
   cards leave one short. A dashed border and a tinted fill so it reads as an
   action rather than as an eighth product with a missing image. */
.app-viewall {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    /* No height of its own: as a grid item it stretches to the row's height,
       which is set by the product cards beside it. */
    padding: 20px 14px;
    border: 1px dashed var(--app-theme-border);
    border-radius: 14px;
    background: var(--app-theme-soft);
    text-align: center;
    text-decoration: none;
    transition: border-color .15s ease, background .15s ease;
}

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

.app-viewall-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 54px;
    height: 54px;
    margin-bottom: 6px;
    border-radius: 50%;
    background: #fff;
    color: var(--app-theme);
    font-size: 17px;
    box-shadow: 0 2px 8px rgba(16, 24, 40, .08);
    transition: transform .15s ease;
}

.app-viewall:hover .app-viewall-icon {
    transform: translateX(3px);
}

.app-viewall-title {
    font-size: 15px;
    font-weight: 700;
    color: var(--app-theme);
}

.app-viewall-sub {
    font-size: 12px;
    line-height: 1.4;
    color: var(--app-muted);
}

/* 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;
    }

    /* The grid's extra inset is dropped on a phone: with only two narrow tracks
       to share, those 8px each side came straight out of the cards. */
    .app-section > .app-grid {
        padding-inline: 0;
    }

    /* Category tiles scale down on a handset — narrower tracks so three or four
       fit per line instead of two. Set on the grid, not .app-cat: the tile is
       width:100% of its track now, and a fixed width here would fight that. */
    .app-cat-grid {
        grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
        gap: 14px 4px;
    }

    .app-cat-grid > .app-cat {
        max-width: 92px;
    }

    /* Width and height must stay equal or the disc turns oval. */
    .app-cat-thumb {
        width: 64px;
        height: 64px;
        padding: 6px;
    }

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

    /* The card's own padding tightens too — at 12px on a ~160px track the
       content had barely 130px to work with. */
    .app-grid > .app-card {
        padding: 9px;
    }
}

/* --- 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 --------------------------------------------------------- */

/* Not an .app-section, but it sits in the same stack, so it takes the same gap
   token — an 18px margin here made it the one block on the page spaced
   differently from its neighbours. */
.app-next {
    margin: var(--app-section-gap) 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 {
    /* The same gap that separates two sections. Sections carry no bottom
       padding, so without this the last one would butt straight up against the
       band. */
    margin-top: var(--app-section-gap);
    /* Enough to keep the columns off the band's top edge without the footer
       opening with a stripe of empty colour. */
    padding: 28px var(--app-pad) 20px;
    /* 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;
    /* Dark band, as in the reference — the same surface the header uses, so the
       page is bookended by the vendor's colour instead of fading into grey. The
       light --app-theme-soft wash and the 2px accent rule that used to sit here
       went with it: on a dark ground the rule read as a seam, and the wash had
       nothing left to tint. */
    background: var(--app-header-bg, var(--app-theme));
    color: rgba(255, 255, 255, .72);
    /* Clears the fixed bottom nav, which overlays the end of the page — without
       this the newsletter would sit behind it. The +56px this carried was well
       past what the nav needs and left a tall band of empty colour under the
       last row; +16px is the nav's height plus a normal gap. Overrides the
       shorthand above deliberately, hence the separate declaration. */
    padding-bottom: calc(var(--app-nav-height) + 16px);
}

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

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


/* Plain uppercase white label. The little 26px accent rule that used to sit
   under it (.app-footer-heading::after) is gone — the reference's column
   headings carry no underline, and on the dark ground it read as a stray mark
   rather than an accent. */
.app-footer-heading {
    /* More air under the heading than the 14px it had — the first row of every
       column was sitting tight against its title. */
    margin: 0 0 20px;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: #fff;
}


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

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

/* Now carries the vendor's Homepage Setting tagline, not just their name as a
   stand-in for a missing logo, so it is styled as body copy rather than a bold
   label — a full sentence set in bold ink read as a heading. */
.app-footer-tagline {
    margin: 10px 0 0;
    font-size: 13px;
    line-height: 1.65;
    /* Inherits the footer's translucent white rather than --app-muted, which is
       a mid-grey meant for a light page and all but vanished on the dark band. */
    color: inherit;
}



.app-footer-stores-label {
    margin: 16px 0 8px;
    font-size: 12px;
    font-weight: 600;
    color: rgba(255, 255, 255, .6);
}

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

/* Translucent on the dark band, matching the social discs — a white chip here
   was as loud as the logo above it. */
.app-footer-store {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    border: 1px solid rgba(255, 255, 255, .18);
    border-radius: 10px;
    background: rgba(255, 255, 255, .08);
    color: #fff;
    font-size: 12.5px;
    font-weight: 600;
    text-decoration: none;
    transition: border-color .15s ease, background-color .15s ease;
}

.app-footer-store:hover {
    border-color: rgba(255, 255, 255, .4);
    background: rgba(255, 255, 255, .14);
    color: #fff;
}

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

/* Category with no image of its own, showing the vendor logo instead. A logo
   is usually a wide wordmark, so it gets more breathing room and no circular
   clip — the round mask would slice the ends off the text. */
.app-cat-thumb--logo {
    padding: 12px;
}

.app-cat-thumb--logo img {
    border-radius: 0;
}

/* apple.png is a black glyph — near-invisible on the dark band. Render it white
   like the label beside it. (The Play logo is multicolour and stays as is.) */
.app-footer-store img[src$="apple.png"] {
    filter: brightness(0) invert(1);
}

/* --- Links ---------------------------------------------------------------
   One column, as in the reference — its "Useful Links" list is a single stack
   of chevron-marked rows. (This was a two-column grid, which paired the labels
   up two-by-two and read as a block of text rather than a list.) */
.app-footer-links {
    display: flex;
    flex-direction: column;
    /* Matches the 16px rhythm of the contact rows opposite, so the two columns
       line up rather than one running denser than the other. */
    gap: 14px;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Dot marker so the list reads as navigation rather than loose text, and it
   slides on hover to confirm the row is interactive. */
.app-footer-links a {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    line-height: 1.4;
    color: rgba(255, 255, 255, .72);
    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-radius: 50%;
    /* White on the dark ground, not the theme colour — the brand colour is the
       band itself here, so a themed dot disappeared into it. */
    background: rgba(255, 255, 255, .55);
}

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

.app-footer-links a:hover::before {
    background: #fff;
}

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

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

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

/* White, NOT var(--app-theme). The footer band is itself the vendor's colour
   (--app-header-bg is that colour at 92%), so a theme-coloured glyph sat blue
   on blue and all but disappeared — which is what it was doing here. White is
   the only value that reads against this ground. */
.app-footer-contact i {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 18px;
    font-size: 14px;
    color: #fff;
}

.app-footer-contact a {
    color: #fff;
    font-weight: 600;
    text-decoration: none;
    word-break: break-word;
}

/* Underline rather than a colour change: the theme colour it used to take is
   the band's own colour, so hovering made the link fade out instead of
   highlighting. */
.app-footer-contact a:hover {
    color: #fff;
    text-decoration: underline;
}

/* --- Opening hours ------------------------------------------------------
   A contact row like phone and email: clock glyph in the same 18px column,
   text starting on the same line as theirs. It was a separate block indented
   by hand, so it drifted out of line and read as a stray caption. One line per
   line of the vendor's working_hours free text; a multi-line value keeps the
   glyph level with the first line. */
.app-footer-contact li.app-footer-hours {
    align-items: flex-start;
}

.app-footer-contact li.app-footer-hours i {
    /* Matches the first text line's height so the glyph centres on it. */
    height: 1.5em;
}

.app-footer-hours-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    line-height: 1.5;
    color: rgba(255, 255, 255, .85);
    font-weight: 600;
}

.app-footer-contact li:last-child {
    margin-bottom: 0;
}

/* --- 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: 24px;
    padding-top: 24px;
    /* Translucent white: the old #e6e9ed hairline was invisible on the dark
       band. */
    border-top: 1px solid rgba(255, 255, 255, .14);
}

/* 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: 10px;
    margin-top: 20px;
}

/* Small translucent discs on the dark band, as in the reference — the solid
   white circles this replaced were the brightest thing in the footer and pulled
   the eye away from the columns. */
.app-footer-social a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border: 1px solid rgba(255, 255, 255, .22);
    border-radius: 50%;
    background: rgba(255, 255, 255, .1);
    /* Full white, not .8 — the glyph is small and the disc behind it is only
       barely lighter than the band, so anything dimmer greyed out. */
    color: #fff;
    font-size: 14px;
    text-decoration: none;
    transition: background-color .15s ease, color .15s ease, border-color .15s ease;
}

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

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

/* Copyright on the left, the "Powered By" credit pushed to the right, as in the
   reference's bottom bar. Centred and stacked on a narrow screen, where two
   opposed ends would leave a ragged single row. */
.app-footer-legal {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 6px 10px;
    margin-top: 24px;
    padding-top: 18px;
    /* Translucent white; the old #e6e9ed was invisible on the dark band. */
    border-top: 1px solid rgba(255, 255, 255, .12);
    font-size: 11.5px;
    line-height: 1.5;
    color: rgba(255, 255, 255, .65);
    text-align: center;
}

.app-footer-powered a {
    color: rgba(255, 255, 255, .8);
    font-weight: 600;
    text-decoration: none;
}

.app-footer-powered a:hover {
    color: #fff;
}

@media (min-width: 700px) {
    .app-footer-legal {
        justify-content: space-between;
        text-align: left;
    }
}

/* 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) {
    /* Only the side padding changes here — the shorthand this replaced also
       reset padding-bottom to a flat 32px, dropping the nav clearance the base
       rule computes and letting the bottom bar overlay the last footer row. */
    .app-footer {
        padding-top: 32px;
        padding-inline: var(--app-pad);
    }

    /* 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));
        /* Wider gutters between columns — at 28px the "Useful Links" and "Get
           In Touch" stacks sat close enough to read as one block of text. */
        gap: 32px 48px;
        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) {
    /* The column IS the viewport here, so its side hairlines would sit hard
       against the screen edges as two stray lines with no backdrop beside them
       to separate from. (This cleared a box-shadow before the frame carried
       borders; the shadow is gone, so the reset moved with it.) */
    .app-shell {
        border-left: 0;
        border-right: 0;
    }

    /* The decorative backdrop (wave band + drifting blobs) goes with it: none
       of it is ever visible behind a frame that fills the screen, and dropping
       the element entirely means the phone never runs those animations. */
    .app-backdrop-art {
        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 bar is one line now, and at this width the
       greeting would eat the room the search pill needs for a label that is not
       required to use the page — the name is on the Profile tab. It returns
       beside the icons from 601px up. */
    .app-greeting {
        display: none;
    }

    /* Guest Login / Sign Up: the icon goes and the padding tightens, so the
       pill fits beside the logo and cart on a narrow phone. */
    .app-auth-pill {
        height: 30px;
        padding: 0 10px;
        font-size: 12px;
    }

    .app-auth-pill .fa-right-to-bracket {
        display: none;
    }

    .app-auth-sep {
        margin: 0 4px;
    }

    /* The pill drops onto its own full-width row beneath the bar. Sharing the
       line with the logo and four icons left it about 40px wide — enough to
       show "Se" and nothing more. order:1 puts it after the items above it in
       .app-header-top, which is set to wrap at this width. */
    .app-search {
        order: 1;
        flex: 1 0 100%;
        max-width: none;
        margin: 8px 0 0;
    }

    /* Back to a normal field now that it has the full width to itself — the
       trimmed padding here was compensating for the squeezed pill. */
    .app-search-field {
        padding-right: 14px;
    }

    /* Lets the search wrap onto a second line instead of being crushed into the
       single row. Everything before it keeps its natural size. */
    .app-header-top {
        flex-wrap: wrap;
    }
}

/* --- 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;
        /* Roomier on a wide frame, where the sections are physically wider and
           the old 24px read as crowding. */
        --app-section-gap: 34px;
        --app-page-top: 42px;
        --app-page-bottom: 52px;
    }

    /* No .app-section padding override here any more — the base rule already
       reads --app-section-gap, which the token above has just raised. Restating
       the padding was what pinned every wide-screen section to 24px regardless
       of the token. */


    .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;
    }

    /* Shorter than the phone value, not taller: the hero copy that shares this
       band stacks on a narrow screen and runs in a single column on a wide one,
       so it needs less height here, not more. 360px gives the headline, blurb
       and button row room to breathe against the artwork without the banner
       becoming a full-screen hero that pushes the category strip out of view. */
    .app-banner {
        height: 360px;
    }

    /* Wider tracks on a wide frame, so the tiles stay generously spaced rather
       than packing more per line. */
    .app-cat-grid {
        grid-template-columns: repeat(auto-fill, minmax(124px, 1fr));
        gap: 22px 10px;
    }

    .app-cat-grid > .app-cat {
        max-width: 132px;
    }

    /* 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. */
    /* Scales with the tile — the same proportion of inset as the narrow-screen
       7px on 76px, so the mark fills the disc identically at both widths. */
    .app-cat-thumb {
        width: 88px;
        height: 88px;
        padding: 8px;
    }

    .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);
    }

    /* (--app-wave-anchor's 768px value is emitted by element/v1/header.php
       alongside its base value, so the two stay together.) */

    /* Tracks the 10px padding above: the strip cancels the header's bottom
       padding and re-opens the gap above itself, so both values move together. */
    .app-header-utility {
        margin: 10px calc(var(--app-pad) * -1) -10px;
    }

    /* More room to type once the frame is wide. The base rule already grows the
       pill into the middle of the bar and caps it at 420px; this just widens the
       gap from the logo to match the roomier bar. */
    .app-search {
        margin-left: 18px;
    }

    .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;
    /* Softer than the 16px the cards use: a dialog floats over the page, so a
       rounder edge and a real shadow are what separate it from the backdrop. */
    border-radius: 20px;
    box-shadow: 0 18px 44px rgba(16, 24, 40, .22);
    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;
}

/* --- bootbox confirm / alert -------------------------------------------
   bootbox builds its own dialog rather than reusing the markup above: there is
   no .modal-title, the close button sits alone in the header, and the message
   goes in .bootbox-body. Left to the generic rules it rendered as a square
   white slab with the close button crammed into the corner and the buttons
   flush against the edge. These are the confirm dialogs behind Clear Cart, the
   guest login prompt and the quantity-limit warning. */

/* Narrower than the 460px the pickers use — a one-line question does not need
   that width, and a short dialog stretched wide reads as an empty box. */
/* border-box for the whole dialog. The reset at the top of this sheet is scoped
   to .app-shell, and bootbox appends its markup to <body> — outside that — so
   everything in here defaulted to content-box and any `width: 100%` element
   overflowed by its own padding and borders. */
.modal.bootbox,
.modal.bootbox *,
.modal.bootbox *::before,
.modal.bootbox *::after {
    box-sizing: border-box;
}

.modal.bootbox .modal-dialog {
    max-width: 380px;
}

/* position: relative so the close button below anchors to the dialog itself —
   on an untitled confirm bootbox prepends that button into .modal-body, which
   has no positioning of its own. */
.modal.bootbox .modal-content {
    position: relative;
    border: 0;
    border-radius: 20px;
}

.modal.bootbox .modal-header {
    position: relative;
    min-height: 0;
    padding: 14px 46px 14px 18px;
    border-bottom: 1px solid #f1f2f4;
}

/* Inside the dialog's padding, not over the rounded corner — it was sitting on
   the message text and half outside the radius. */
.modal.bootbox .bootbox-close-button,
.modal.bootbox .modal-header .close {
    position: absolute;
    top: 10px;
    right: 10px;
    z-index: 3;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: #f3f4f6;
    color: var(--app-muted);
    font-size: 16px;
    font-weight: 400;
    line-height: 1;
    text-shadow: none;
    opacity: 1;
    cursor: pointer;
    transition: background .15s ease, color .15s ease;
}

.modal.bootbox .bootbox-close-button:hover,
.modal.bootbox .modal-header .close:hover {
    background: #e6e8eb;
    color: var(--app-ink);
    opacity: 1;
}

/* Room at the top for the close button, and the message centred — a confirm
   dialog is a question, not a paragraph of body copy. The 44px top clears the
   button, which is prepended into this element on an untitled dialog. */
.modal.bootbox .modal-body {
    padding: 44px 22px 6px;
}

/* A titled dialog already has the button up in its header, so the body does
   not need to reserve that space twice. */
.modal.bootbox .modal-header + .modal-body {
    padding-top: 20px;
}

.modal.bootbox .bootbox-body {
    font-size: 15px;
    font-weight: 500;
    line-height: 1.5;
    color: var(--app-ink);
    text-align: center;
}

/* --- bootbox dialogs carrying a form ------------------------------------
   Some dialogs put fields in the body rather than a sentence — the subscription
   pause/resume picker on Users/v1/subscriptions.php and
   v1/subscription_details.php is the case in point. The centring above is right
   for a question and wrong for a form: it centred the field labels over their
   inputs. :has() picks out only those dialogs, so a plain confirm is untouched. */
.modal.bootbox .bootbox-body:has(.form-control) {
    text-align: left;
}

.modal.bootbox .bootbox-body:has(.form-control) label {
    display: block;
    margin-bottom: 5px;
    font-size: 11.5px;
    /* Normal weight: these are quiet captions over their fields, and at 700
       plus uppercase they read louder than the dialog's own message. */
    font-weight: 500;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--app-muted);
}

/* The label text is wrapped in <b> in the markup, which would otherwise
   reinstate the bold the rule above deliberately drops. */
.modal.bootbox .bootbox-body:has(.form-control) label b {
    font-weight: inherit;
}

/* The dialog opens with a bare <br> before the first row, which pushed the
   fields down by a blank line. */
.modal.bootbox .bootbox-body > br:first-child {
    display: none;
}

/* The rows are Bootstrap .row/.col-md-12 pairs. Their negative side margins
   pulled the fields OUT past the dialog's own padding — which is the overflow
   on the right-hand edge. !important because Bootstrap sets the margin as a
   shorthand, which beats the longhand margin-inline this used before. */
.modal.bootbox .bootbox-body .row {
    margin: 0 !important;
    width: 100%;
}

.modal.bootbox .bootbox-body .row > [class*="col-"] {
    width: 100%;
    max-width: 100%;
    flex: 0 0 100%;
    padding: 0 !important;
}

/* The second field's .mt-3 is the gap between the two groups; the label above
   each one supplies the rest. */
.modal.bootbox .bootbox-body .row > [class*="col-"].mt-3 {
    margin-top: 12px !important;
}

/* Shorter fields. These inherit the desktop sheet's input height, which on a
   dialog this size made two fields fill most of its height. */
.modal.bootbox .bootbox-body .form-control {
    height: auto;
    min-height: 36px;
    width: 100%;
    padding: 6px 12px;
    border: 1px solid #e3e5e9;
    border-radius: 9px;
    font-size: 13.5px;
    line-height: 1.4;
}

/* The trailing <br> before the hidden subscription_id input adds a blank line
   above the footer, which is the deep gap under the second field. */
.modal.bootbox .bootbox-body:has(.form-control) br {
    display: none;
}

/* With the fields shortened the body no longer needs its own bottom padding on
   top of the footer's. Set on .modal-body — that is the element carrying the
   44px/6px padding further up, not .bootbox-body inside it. */
.modal.bootbox .modal-body:has(.form-control) {
    padding-bottom: 0;
}

.modal.bootbox .modal-footer {
    justify-content: center;
    gap: 10px;
    padding: 18px 22px 22px;
    border-top: 0;
}

/* A form dialog's fields already end on their own edge, so the footer does not
   need the breathing room a bare message needs above the buttons — nor the
   deeper bottom inset.
   Matched off .modal-body, not .bootbox-body: the latter is nested INSIDE the
   former in bootbox's own template, so it has no sibling footer to select. */
/* 16px above the buttons, not 4. The body sets padding-bottom:0 for these
   dialogs, so the footer's own top padding is the ONLY gap between the last
   field and the buttons — at 4px they sat almost against it. The 18px below
   stays: that edge was the deep one being trimmed. */
.modal.bootbox .modal-body:has(.form-control) + .modal-footer {
    padding: 16px 22px 18px;
}

/* Equal-width buttons so neither action looks like the default, and a real tap
   target on a phone. .site-btn supplies the fill; these size and space them. */
.modal.bootbox .modal-footer .btn,
.modal.bootbox .modal-footer .site-btn {
    flex: 1 1 0;
    min-width: 0;
    min-height: 44px;
    margin: 0;
    padding: 11px 16px;
    border-radius: 10px;
    font-size: 14px;
    font-weight: 700;
}

/* Cancel is the quiet way out: outlined rather than a second filled block, so
   the confirming action is the only one that reads as primary. */
.modal.bootbox .modal-footer .site-btn.danger,
.modal.bootbox .modal-footer .bootbox-cancel {
    background: #fff;
    border: 1px solid #dfe3e8;
    color: var(--app-muted);
}

.modal.bootbox .modal-footer .site-btn.danger:hover,
.modal.bootbox .modal-footer .bootbox-cancel:hover {
    border-color: var(--app-muted);
    background: #f8f9fb;
    color: var(--app-ink);
}

/* The quantity-limit dialog hides its cancel button with .hidden; without a
   rule for that class it stayed visible and the two buttons shared the row. */
.modal.bootbox .modal-footer .hidden {
    display: none;
}

/* 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;
    /* Square panel: the curves belong to the head (bottom-right) and the
       "Powered by" bar (top-right). Rounding the panel itself clipped the bar's
       bottom-right corner off at the screen edge. */
    border-radius: 0;
    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);
}

/* The menu renders at body level, OUTSIDE .app-shell, so the border-box reset at
   the top of this sheet never reached it. Every link row is `width: 100%` plus
   side padding, so under content-box each row was wider than the panel — and
   since the list scrolls vertically, it then scrolled sideways too: a swipe
   dragged the whole list left and cut the icons and labels off. */
.app-side,
.app-side *,
.app-side *::before,
.app-side *::after {
    box-sizing: border-box;
}

/* Wider on a phone: 86% capped at 320px left ~265px on a 307px screen, which
   squeezed the labels. The scrim strip on the right stays wide enough to tap. */
@media (max-width: 480px) {
    .app-side {
        width: min(90%, 340px);
    }
}

/* Theme block with a rounded inner corner, holding one outlined card. */
.app-side-head {
    padding: 16px 14px 18px;
    background: var(--app-theme);
    border-radius: 0 0 24px 0;
    color: #fff;
    flex: 0 0 auto;
}

.app-side-card {
    padding: 14px;
    border: 1px solid rgba(255, 255, 255, .5);
    border-radius: 18px;
    background: rgba(255, 255, 255, .08);
}

.app-side-card-top {
    display: flex;
    align-items: center;
    gap: 12px;
}

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

/* A guest sees the vendor's logo. Same translucent disc as the signed-in
   avatar, NOT white: the logo is made to sit on the theme bar (that is where
   the header shows it), so a white or light logo vanished on a white disc. */
.app-side-avatar--logo {
    color: #fff;
}

.app-side-avatar--logo img {
    width: 100%;
    height: 100%;
    padding: 6px;
    object-fit: contain;
}

/* Fallback icon only shows when there is no logo, or it failed and removed itself. */
.app-side-avatar--logo img + i {
    display: none;
}

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

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

/* Guest prompt under "Welcome" — may wrap to two lines. */
.app-side-sub-text {
    font-size: 13px;
    line-height: 1.35;
    opacity: .9;
}

/* Full-width white Login/Sign Up button at the foot of the card. */
.app-side-cta {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    height: 44px;
    margin-top: 14px;
    border-radius: 10px;
    background: #fff;
    color: var(--app-theme);
    font-size: 15px;
    font-weight: 600;
    text-decoration: none;
}

.app-side-cta:hover,
.app-side-cta:focus {
    color: var(--app-theme);
    text-decoration: none;
    box-shadow: 0 4px 12px rgba(0, 0, 0, .15);
}

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

/* Customer ID and phone lines under the name, each led by a small glyph. */
.app-side-meta {
    line-height: 1.4;
}

.app-side-meta i {
    width: 14px;
    margin-right: 6px;
    font-size: 11px;
    text-align: center;
}

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

.app-side-close {
    flex: 0 0 auto;
    align-self: flex-start;
    width: 30px;
    height: 30px;
    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.
   The menu runs to ~16 links under four section labels; at the old 43px rows
   it was ~900px tall, so on every phone it overflowed and showed a scrollbar
   down the panel's edge. Rows and labels are tightened below so the full menu
   fits a typical phone screen. Where it still cannot (a short phone, a vendor
   with every feature on, an expanded Products list) it scrolls without a
   visible bar — touch scrolling still works, there is just no rail. */
.app-side-nav {
    flex: 1 1 auto;
    overflow-y: auto;
    /* Never sideways. overflow-y: auto alone makes the x axis scrollable too, so
       anything a pixel too wide let the list be dragged left. */
    overflow-x: hidden;
    padding: 4px 0 14px;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    scrollbar-width: none;
}

.app-side-nav::-webkit-scrollbar {
    display: none;
}

.app-side-label {
    padding: 16px 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: 14px;
    width: 100%;
    padding: 8px var(--app-pad);
    border: 0;
    background: none;
    font: inherit;
    font-size: 15px;
    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);
}

/* The row takes the tint on hover, so the tile flips to white to stay visible. */
.app-side-list a:hover > i:first-child,
.app-side-toggle:hover > i:first-child {
    background: #fff;
}

/* Each icon sits on a tinted rounded tile; the fixed size keeps every label on
   the same left edge. */
.app-side-list a > i:first-child,
.app-side-toggle > i:first-child {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: 10px;
    background: var(--app-theme-soft);
    font-size: 15px;
    color: var(--app-theme);
    flex: 0 0 auto;
}

/* Right chevron on each top-level link (Font Awesome 6 "chevron-right").
   Submenu entries and Logout go without — the toggles carry their own caret. */
.app-side-list > li > a:not(.app-side-logout)::after {
    content: "\f054";
    flex: 0 0 auto;
    margin-left: auto;
    font-family: "Font Awesome 6 Free";
    font-weight: 900;
    font-size: 11px;
    color: var(--app-muted);
}

.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;
}

/* Short screens (older / smaller phones, landscape): one notch tighter, but
   still roomy enough to tap. .app-side-nav scrolls if the list runs long. */
@media (max-height: 740px) {
    .app-side-head {
        padding-top: 12px;
        padding-bottom: 12px;
    }

    .app-side-label {
        padding-top: 13px;
        padding-bottom: 5px;
        font-size: 10.5px;
    }

    .app-side-list a,
    .app-side-toggle {
        padding-top: 6px;
        padding-bottom: 6px;
        font-size: 14px;
    }

    .app-side-list a > i:first-child,
    .app-side-toggle > i:first-child {
        width: 30px;
        height: 30px;
    }

    .app-side-card {
        padding: 12px;
    }

    .app-side-cta {
        height: 40px;
        margin-top: 12px;
    }
}

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

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

/* "Powered by trakop", pinned under the scrolling list in a theme block whose
   rounded corner mirrors the head's. */
/* A floating card, inset from the panel edges with every corner rounded — the
   same shape as the Trakop admin's flyout menu — filled with the vendor's
   theme colour so it matches the head of the panel. */
.app-side-foot {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    flex: 0 0 auto;
    margin: 0 12px calc(12px + env(safe-area-inset-bottom, 0px));
    padding: 14px 16px;
    border-radius: 14px;
    background: var(--app-theme);
    box-shadow: 0 8px 24px rgba(17, 24, 39, .18);
    color: #fff;
}

.app-side-foot-label {
    font-size: 13px;
    font-weight: 500;
    color: rgba(255, 255, 255, .85);
}

/* The white "Powered by trakop" artwork (webroot/img/powered-by-trakop.png,
   641x86). Sized by height so it keeps its proportions; capped by width so it
   never overflows a narrow panel. */
.app-side-foot-link {
    display: inline-flex;
    max-width: 100%;
}

.app-side-foot-logo {
    display: block;
    width: auto;
    max-width: 100%;
    height: 24px;
    object-fit: contain;
}

.app-side-foot-link:hover .app-side-foot-logo,
.app-side-foot-link:focus .app-side-foot-logo {
    opacity: .85;
}

.app-side-foot-brand {
    font-size: 18px;
    font-weight: 700;
    letter-spacing: .01em;
    line-height: 1;
    color: #fff;
    text-decoration: none;
}

.app-side-foot-brand:hover,
.app-side-foot-brand:focus {
    color: #fff;
    text-decoration: none;
    opacity: .85;
}

/* Shorter screens: tighter menu so it fits without scrolling.
   The full menu (head, ~15 links under five labels, the Powered-by bar) is about
   1100px tall at the sizes above, so on any phone under ~900px tall the link list
   scrolled and its last section ran under the bar. Only vertical spacing and the
   icon tiles shrink — type size and tap targets stay usable (36px rows, 30px at
   the smallest). The caps are by HEIGHT, not width: a wide but short window
   needs the same treatment. Where it still cannot fit (an expanded category
   list, a vendor with every feature on, a very short window) the list scrolls
   without a visible bar, as before. */
@media (max-height: 920px) {
    .app-side-head {
        padding: 12px 14px 14px;
    }

    .app-side-card {
        padding: 10px 12px;
    }

    .app-side-avatar {
        width: 42px;
        height: 42px;
        font-size: 16px;
    }

    .app-side-nav {
        padding: 0 0 8px;
    }

    .app-side-label {
        padding: 10px var(--app-pad) 3px;
    }

    .app-side-list a,
    .app-side-toggle {
        padding-block: 3px;
        font-size: 14.5px;
    }

    .app-side-list a > i:first-child,
    .app-side-toggle > i:first-child {
        width: 30px;
        height: 30px;
        font-size: 14px;
    }

    .app-side-list--foot {
        margin-top: 4px;
        padding-top: 4px;
    }

    .app-side-foot {
        margin-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
        padding: 10px 16px;
    }

    .app-side-foot-logo {
        height: 20px;
    }
}

@media (max-height: 760px) {
    .app-side-head {
        padding: 10px 12px 12px;
    }

    .app-side-label {
        padding: 7px var(--app-pad) 2px;
    }

    .app-side-list a,
    .app-side-toggle {
        padding-block: 2px;
        font-size: 14px;
    }

    .app-side-list a > i:first-child,
    .app-side-toggle > i:first-child {
        width: 26px;
        height: 26px;
        border-radius: 8px;
        font-size: 12.5px;
    }

    .app-side-foot {
        padding: 8px 14px;
    }
}

/* Short phones (~640–720px tall, e.g. 360x640, 375x667, most phones in landscape
   off). A vendor with containers and the help links on has ~16 links under five
   labels; at the 760px sizes above that is ~720px, so a 680px screen still
   scrolled and clipped Help & More. Rows come down to 27px, the labels and head
   tighten, and the Powered-by bar slims to one 18px line — totalling ~660px. */
@media (max-height: 720px) {
    .app-side-head {
        padding: 8px 10px 10px;
        border-radius: 0 0 18px 0;
    }

    .app-side-card {
        padding: 8px 10px;
        border-radius: 14px;
    }

    .app-side-avatar {
        width: 36px;
        height: 36px;
        font-size: 15px;
    }

    .app-side-name {
        font-size: 15.5px;
    }

    .app-side-close {
        width: 28px;
        height: 28px;
    }

    .app-side-nav {
        padding: 0 0 4px;
    }

    .app-side-label {
        padding: 5px var(--app-pad) 1px;
        font-size: 10px;
    }

    .app-side-list a,
    .app-side-toggle {
        gap: 11px;
        padding-block: 2px;
        font-size: 13.5px;
    }

    .app-side-list a > i:first-child,
    .app-side-toggle > i:first-child {
        width: 23px;
        height: 23px;
        border-radius: 7px;
        font-size: 11.5px;
    }

    .app-side-foot {
        margin: 0 10px calc(6px + env(safe-area-inset-bottom, 0px));
        padding: 6px 12px;
        border-radius: 12px;
    }

    .app-side-foot-label {
        font-size: 12px;
    }

    .app-side-foot-logo {
        height: 18px;
    }
}

/* 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) {
    /* The category tile's colour and shadow feedback survives; only the icon's
       growth and rotation are dropped. */
    .app-cat:hover .app-cat-thumb img,
    .app-grid > .app-card:hover,
    .app-card:hover .app-card-media img,
    .app-section-link:hover .app-section-link-icon,
    .app-section-link:focus .app-section-link-icon,
    .app-nav-item.is-active i,
    .app-nav-item:hover i {
        transform: none;
    }
}

/* ===========================================================================
   Datepicker. Global to layout 1, not page-scoped: the jQuery UI widget is
   appended to <body> and is opened from several pages (the subscribe panel's
   start date, the subscriptions list's pause/resume dialog, the wallet's date
   range). These rules lived in v1/product_detail.css, which only the product
   page loads — so everywhere else still rendered the raw smoothness theme.
   ========================================================================== */
/* --- Datepicker ---------------------------------------------------------
   jQuery UI's own smoothness theme renders a grey titlebar, square cells and
   text "Prev"/"Next" links — the raw widget in the screenshot. These rules
   restyle it to match layout 1. Scoped to .app-shell so the other layouts keep
   the stock theme.

   #ui-datepicker-div is appended to <body>, NOT inside .app-shell — so it is
   matched on its own id here rather than as a descendant. */

#ui-datepicker-div.ui-datepicker {
    width: 268px;
    padding: 10px;
    border: 1px solid #e9eaee;
    border-radius: 14px;
    background: #fff;
    box-shadow: 0 12px 32px rgba(16, 24, 40, .14);
    font-family: inherit;
    font-size: 13px;
    /* Above the sticky header and the fixed bottom nav, both of which the
       calendar can overlap when the field sits near either edge. */
    z-index: 1200 !important;
}

/* Month/year bar: no fill, so it reads as a heading rather than a grey slab. */
#ui-datepicker-div .ui-datepicker-header {
    position: relative;
    padding: 2px 0 10px;
    border: 0;
    background: none;
    font-weight: 700;
}

#ui-datepicker-div .ui-datepicker-title {
    margin: 0 32px;
    line-height: 28px;
    text-align: center;
    font-size: 14px;
    font-weight: 700;
    color: var(--app-ink);
}

/* Prev / next as round icon buttons. jQuery UI renders these as <a>s holding a
   sprite <span> and the words "Prev"/"Next"; the sprite is replaced with a
   chevron below and the text is hidden. */
#ui-datepicker-div .ui-datepicker-prev,
#ui-datepicker-div .ui-datepicker-next {
    top: 0;
    width: 28px;
    height: 28px;
    border: 1px solid #e9eaee !important;
    border-radius: 50%;
    background: #fff !important;
    cursor: pointer;
    transition: background .15s ease, border-color .15s ease;
}

#ui-datepicker-div .ui-datepicker-prev { left: 0; }
#ui-datepicker-div .ui-datepicker-next { right: 0; }

#ui-datepicker-div .ui-datepicker-prev:hover,
#ui-datepicker-div .ui-datepicker-next:hover {
    top: 0;
    left: 0;
    border-color: var(--app-theme) !important;
    background: var(--app-theme-soft) !important;
}

#ui-datepicker-div .ui-datepicker-next:hover { left: auto; right: 0; }

/* The stock sprite span is hidden and a Font Awesome chevron drawn instead —
   the sprite is a fixed grey image that cannot take the theme colour. */
#ui-datepicker-div .ui-datepicker-prev span,
#ui-datepicker-div .ui-datepicker-next span {
    display: none;
}

#ui-datepicker-div .ui-datepicker-prev::before,
#ui-datepicker-div .ui-datepicker-next::before {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-family: "Font Awesome 6 Free";
    font-weight: 900;
    font-size: 11px;
    color: var(--app-ink);
}

#ui-datepicker-div .ui-datepicker-prev::before { content: "\f053"; }
#ui-datepicker-div .ui-datepicker-next::before { content: "\f054"; }

/* A disabled arrow means the range stops here — it must not look clickable. */
#ui-datepicker-div .ui-state-disabled.ui-datepicker-prev,
#ui-datepicker-div .ui-state-disabled.ui-datepicker-next {
    opacity: .4;
    cursor: default;
}

#ui-datepicker-div .ui-state-disabled.ui-datepicker-prev:hover,
#ui-datepicker-div .ui-state-disabled.ui-datepicker-next:hover {
    border-color: #e9eaee !important;
    background: #fff !important;
}

/* Day-name row. */
#ui-datepicker-div .ui-datepicker-calendar th {
    padding: 6px 0;
    font-size: 11px;
    font-weight: 700;
    color: var(--app-muted);
}

#ui-datepicker-div .ui-datepicker-calendar td {
    padding: 1px;
    border: 0;
}

/* Square cells so the circle below is round rather than oval. */
#ui-datepicker-div .ui-datepicker-calendar td a,
#ui-datepicker-div .ui-datepicker-calendar td span {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    margin: 0 auto;
    padding: 0;
    border: 0 !important;
    border-radius: 50%;
    background: none !important;
    color: var(--app-ink) !important;
    font-weight: 600;
    text-align: center;
    transition: background .15s ease, color .15s ease;
}

#ui-datepicker-div .ui-datepicker-calendar td a:hover {
    background: var(--app-theme-soft) !important;
}

/* Selected day: filled with the vendor's colour. */
#ui-datepicker-div .ui-datepicker-calendar td a.ui-state-active {
    background: var(--app-theme) !important;
    color: #fff !important;
}

/* Today, when not the selection — a ring rather than a fill, so the two states
   stay distinguishable. */
#ui-datepicker-div .ui-datepicker-calendar td a.ui-state-highlight:not(.ui-state-active) {
    box-shadow: inset 0 0 0 1px var(--app-theme-border);
}

/* Unselectable dates (outside the range, or days the subscription does not
   deliver on) stay visible but clearly inert. */
/* Unselectable dates (outside the range, or days the subscription does not
   deliver on). #c7cad1 was too faint to read against white — these still need
   to be legible as dates, just clearly not choosable. A darker grey on a tinted
   cell reads as "disabled" rather than "nearly invisible". */
#ui-datepicker-div .ui-datepicker-calendar td.ui-state-disabled span,
#ui-datepicker-div .ui-datepicker-calendar td.ui-datepicker-unselectable span {
    color: #8b9099 !important;
    background: #f4f5f7 !important;
    opacity: 1;
}
/* --- Quantity stepper buttons: Safari safety net -----------------------------
   Every layout 1 stepper renders its − / + as EMPTY <span class="qtybtn">s and
   draws the signs with ::before / ::after. Safari (macOS and iOS) sizes a
   content-fitted stepper from each item's width / min-width or its content —
   not its flex-basis — so an empty span sized only by `flex: 0 0 30px`
   collapsed to 0px there and the buttons vanished, while Chrome looked fine.
   Page sheets give their steppers explicit widths; this floor catches any
   that do not (and any added later). Low specificity on purpose, so a page's
   own sizing always wins. */
.app-shell .pro-qty .qtybtn,
.app-shell .pro-qty .qtybtn_berkely {
    display: block;
    flex-shrink: 0;
    min-width: 28px;
}

/* --- iOS focus zoom ---------------------------------------------------------
   iOS Safari zooms the whole page in whenever a form field whose text is
   smaller than 16px receives focus, and does not zoom back out afterwards.
   Layout 1 sets most fields at 13–15px, so tapping into login, signup, the
   address form or a modal left the page magnified and panned sideways.
   16px is the threshold that stops it. Scoped to iOS with the iOS-only
   -webkit-touch-callout feature query, so desktop and Android keep the
   designed sizes; and to phone widths, where the zoom happens. Unscoped (not
   under .app-shell) because bootbox / Bootstrap modals sit outside it — safe,
   as this sheet is loaded by layout 1 only. Disabled / readonly fields never
   take focus (the quantity steppers' value boxes), so they are left alone. */
@supports (-webkit-touch-callout: none) {
    @media (max-width: 767px) {
        /* :not(.digit-group input) — the OTP boxes are already 20px and must
           not be pulled DOWN to 16. */
        input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="button"]):not([type="submit"]):not([type="hidden"]):not([disabled]):not([readonly]):not(.digit-group input),
        select:not([disabled]),
        textarea:not([disabled]):not([readonly]) {
            font-size: 16px !important;
        }
    }
}

/* --- Toast notifications ($.notify) ---------------------------------------
   Every error / success / warning message on layout 1 goes through notify.js
   ($.notify("…", "error")), which injects its own sheet with
   `white-space: nowrap` and pins the toast to the top-RIGHT corner of the
   window. A message wider than the screen therefore could not wrap and ran off
   the LEFT edge on phones — "kindly contact the administrator…" read with its
   start cut away.

   The corner and its toasts are appended to <body>, outside .app-shell, so
   these rules cannot be scoped under it; they are safe unscoped because this
   sheet is loaded by layout 1 only. `body` / !important are needed because the
   plugin's <style> is appended to <head> at runtime — after this file — and its
   anchor position is written inline.

   Scoped to .notifyjs-corner: that is every global $.notify() toast. A toast
   anchored to an element ($(el).notify) sits in a 0-width wrapper, where
   wrapping would collapse it to one word per line — layout 1 has none today,
   and they keep the plugin's own behaviour if one is added. */

.notifyjs-corner {
    z-index: 11000 !important;
    max-width: calc(100vw - 24px);
    margin: 12px !important;
}

.notifyjs-corner .notifyjs-wrapper,
.notifyjs-corner .notifyjs-container {
    max-width: 100%;
    margin: 0 0 8px;
}

body .notifyjs-corner .notifyjs-bootstrap-base {
    box-sizing: border-box;
    max-width: 420px;
    /* Wrap instead of overflowing; a long unbroken token (a phone number, an
       e-mail address, a URL) may break too rather than push the box wider. */
    white-space: normal;
    overflow-wrap: anywhere;
    padding: 12px 16px 12px 40px;
    border-radius: 12px;
    font-family: inherit;
    font-size: 13.5px;
    font-weight: 600;
    line-height: 1.45;
    text-shadow: none;
    box-shadow: 0 8px 24px rgba(16, 24, 40, .14);
    /* The plugin's status icon, re-seated for the larger padding. */
    background-position: 13px 13px;
}

/* Phones: one full-width toast column across the top, inside the screen's
   gutters and below any notch, whichever corner the call asked for. */
@media (max-width: 600px) {
    .notifyjs-corner {
        top: env(safe-area-inset-top, 0px) !important;
        right: 0 !important;
        bottom: auto !important;
        left: 0 !important;
        max-width: none;
        margin: 10px 12px !important;
    }

    body .notifyjs-corner .notifyjs-bootstrap-base {
        width: 100%;
        max-width: none;
        font-size: 13px;
    }
}
