/* Happy Pet Connect — site.css
 *
 * THE single CSS file for this platform (3.12.0 — rebuilt from scratch after
 * the old tokens.css/site.css/admin.css/timegrid.css/theme.css split proved
 * too easy to get out of sync with itself). Loaded on EVERY site page —
 * this component's own views AND everywhere else (Cassiopeia chrome,
 * Joomla's own profile page, articles, other extensions) — by
 * plg_system_hpctheme, a system plugin that fires regardless of which
 * extension is active. There is no template-level copy of this file: it's
 * one physical file, in this component's own media/ folder, linked directly
 * by path from the system plugin. Also registered normally (WebAssetManager)
 * by this component's own site views — same file either way, never a second
 * copy to drift out of sync.
 *
 * SCOPE, DELIBERATELY, FOR NOW: brand-colour hook (so BrandingHelper's
 * per-instance colour keeps working) + a real, elegant dark mode covering
 * Bootstrap/Cassiopeia site-wide. Component-specific look (cards, badges,
 * the booking calendar, rewards widgets...) is NOT rebuilt yet — those
 * render with stock Bootstrap styling until asked for.
 *
 * DARK MODE ACTIVATION: driven by `data-bs-theme="dark"` on <html>, set by
 * plg_system_hpctheme — either an explicit pet-parent choice, or (the
 * default, for guests and anyone who's never touched the toggle) resolved
 * client-side from the device's own prefers-color-scheme signal, so the
 * WHOLE site follows the visitor's browser/OS automatically with no manual
 * step (see HpcTheme::themeScript(); restored in 3.15.1 after a brief
 * "light always" detour in 3.14.5). Every dark rule below is scoped under
 * that one attribute — nothing here reacts to anything else.
 */

:root {
    /* "light dark" tells the browser this page supports both and to use the
       user's own preference for anything not explicitly covered by the
       data-bs-theme rules below (native scrollbars, form-control chrome,
       etc.) — consistent with plg_system_hpctheme resolving the SAME
       device signal for data-bs-theme itself, so there's no brief mismatch
       between UA-styled bits and this file's own dark rules. */
    color-scheme: light dark;

    /* Brand colour — BrandingHelper (Options → Branding) overrides this per
       instance via an inline :root block it emits on every page load. Falls
       back to the template's own --bs-primary, then a fixed default, so an
       unbranded instance still looks intentional. */
    --hpc-primary: var(--bs-primary, #1f3c88);
    --hpc-primary-contrast: #fff;
    /* Hover/selection wash, derived from --hpc-primary via color-mix() against
       --hpc-surface/transparent rather than a precomputed literal — so the
       same declaration already reads correctly once --hpc-surface flips dark
       below, with no separate override needed. BrandingHelper mirrors this
       exact formula for a per-instance brand_primary override. */
    --hpc-primary-tint: color-mix(in srgb, var(--hpc-primary) 8%, var(--hpc-surface));
    --hpc-primary-ring: color-mix(in srgb, var(--hpc-primary) 16%, transparent);

    /* Selected-slot pill (booking calendar) — deliberately distinct from
       --hpc-primary so a chosen time reads unmistakably "picked" against
       the primary-coloured chrome around it (nav buttons, Add to cart),
       rather than blending into "just another primary-coloured thing".
       Same value in both themes — chosen for contrast against both. */
    --hpc-slot-selected: #bd4c9b;

    /* Status/semantic — overridable via brand_danger (Options → Branding). */
    --hpc-danger: var(--bs-danger, #dc3545);
    --hpc-muted: var(--bs-secondary-color, #64748b);

    /* Surfaces & lines the booking calendar (Month grid + timegrid.css's
       Week view) is built on — overridable via brand_surface/brand_border
       (Options → Branding). */
    /* 3.35: softened off pure white — glare complaint on the live site.
       --hpc-surface now a neutral light grey instead of #fff (earlier warm/
       cream attempts read as "yellow" once tiled across the Month grid's
       30+ day cells — .hcc-book-day in booking.css — even though the same
       tint looked fine on a single card), sunken/border darkened to match
       so the tiered-elevation contrast relationships (surface floats above
       sunken, border still reads against both) stay intact. Chosen
       interactively against the actual booking UI, not picked blind. */
    --hpc-surface: #f2f2f2;
    --hpc-surface-sunken: #d6d6d6;
    --hpc-border: #dbdbdb;
    --hpc-border-light: #e8e8e8;
    /* Phantom token fix (THEME_AUDIT.md §0): var(--hpc-text) is used at 15+
       call sites across this file but was never actually defined here — it
       only exists under this name in the admin-only tokens.css, which site
       pages don't load. Every one of those call sites was silently resolving
       to whatever colour happened to be inherited from its ancestor instead
       of a real token, which is why "make it theme-aware" fixes routed
       through var(--hpc-text) kept visibly failing (confirmed live: header
       business-name text stayed white in light mode after being pointed at
       this token). Defining it for real here backs every existing call site
       at once — none of them need to change. */
    --hpc-text: #1e293b;
    --hpc-text-faint: #94a3b8;
    --hpc-text-soft: #475569;
    --hpc-text-placeholder: #cbd5e1;

    /* Component-level vars several Bootstrap parts read directly — the
       light-mode counterpart to the same block dark mode retunes below.
       Without this, .card/.modal/.dropdown/.list-group (e.g. .hpc-modal-
       dialog's .card, which the slot-picker/rewards/cart modals all use)
       fall back to Bootstrap's own hardcoded white/grey defaults and never
       pick up the --hpc-surface softening above — confirmed live via
       devtools on the slot-picker modal (.hpc-modal-dialog.card showed
       Background: #FFFFFF while the rest of the page correctly softened). */
    --bs-card-bg: var(--hpc-surface);
    --bs-card-border-color: var(--hpc-border);
    --bs-modal-bg: var(--hpc-surface);
    --bs-modal-content-bg: var(--hpc-surface);
    --bs-modal-content-border-color: var(--hpc-border);
    --bs-modal-header-border-color: var(--hpc-border);
    --bs-modal-footer-border-color: var(--hpc-border);
    --bs-dropdown-bg: var(--hpc-surface);
    --bs-dropdown-border-color: var(--hpc-border);
    --bs-list-group-bg: var(--hpc-surface);
    --bs-list-group-border-color: var(--hpc-border);

    /* Elevation — a tight contact shadow + a soft diffuse one, so a lifted
       calendar event/card reads as an actual object rising off the page. */
    --hpc-shadow-resting: 0 1px 2px rgba(15, 23, 42, .06), 0 1px 1px rgba(15, 23, 42, .04);
    --hpc-shadow-lifted: 0 20px 30px -14px rgba(15, 23, 42, .35), 0 8px 12px -6px rgba(15, 23, 42, .14);

    --hpc-radius: 10px;
    --hpc-radius-sm: 6px;
    --hpc-radius-lg: 18px;

    /* Accent — second gradient stop alongside --hpc-primary, overridable per
       instance via brand_accent (Options → Branding; BrandingHelper already
       read this token before it had a default here). Used for the dark-mode
       gradient/glow identity (see --hpc-gradient-primary/--hpc-glow-shadow
       below) — light mode never applies a gradient, so this only surfaces
       once data-bs-theme="dark" overrides it further down. */
    --hpc-accent: #14b8a6;
    --hpc-gradient-primary: linear-gradient(135deg, var(--hpc-primary), var(--hpc-accent));

    /* Soft-shadow surface pair (3.26 "neomorphic-hybrid" redesign) — a light
       shadow + a dark shadow on opposite corners, both colour-mixed FROM
       --hpc-surface itself (never a flat literal) so a per-instance
       brand_surface override still drives the right tone automatically.
       Deliberately used ONLY on passive/structural surfaces (cards, panels,
       inputs at rest) — never on .btn-primary/.btn-danger, which keep a
       solid fill + real contrast so actionable buttons stay unambiguous.
       Dark mode does not get an --hpc-neo-shadow-* pair at all: on a near-
       black surface a light/dark dual shadow reads as noise, not depth — the
       dark side's equivalent "soft" identity is --hpc-glow-shadow instead. */
    --hpc-neo-shadow-out: 6px 6px 14px color-mix(in srgb, var(--hpc-surface) 80%, #000 20%),
                         -6px -6px 14px color-mix(in srgb, var(--hpc-surface) 96%, #fff 4%);
    --hpc-neo-shadow-in: inset 3px 3px 8px color-mix(in srgb, var(--hpc-surface) 82%, #000 18%),
                        inset -3px -3px 8px color-mix(in srgb, var(--hpc-surface) 97%, #fff 3%);

    /* The card system's base elevation, indirected through one token so a
       SPECIFIC card (e.g. a star-tier pet card, see .hpc-star-glow) can add
       its own extra shadow layer on top without fighting the generic card
       rule's specificity for control of the box-shadow property — both
       reference this same variable instead of each hard-coding the raw
       shadow value. */
    --hpc-card-shadow: var(--hpc-neo-shadow-out);

    /* Rewards medal colours (referenced by templates since v3.3.0, never
       actually defined after the v3.12.0 CSS rebuild — confirmed missing by
       grep, 3.15.1). Literal, deliberately NOT brand-overridable — a medal
       colour means something specific regardless of a Business's own brand
       hue, same precedent as the booking-status cues elsewhere. Same values
       in both themes on purpose: a gold star should read as gold whether the
       page around it is light or dark. */
    --hpc-star-bronze: #b0764a;
    --hpc-star-silver: #9aa4b2;
    --hpc-star-gold: #d4a72c;

    /* Notification badge (cart count, PWA bottom-bar indicators) — fixed
       high-contrast red, deliberately NOT tied to --hpc-primary. The old
       badge used bg-primary, which matched the button it sat on (and could
       match --hpc-primary-derived surfaces like the mobile bar's centre
       icon) and lost all affordance as an "action needed" signal. #dc2626
       on white passes WCAG AA (4.5:1+) and is left un-themed like the medal
       colours above — a notification badge should read the same regardless
       of light/dark. */
    --hpc-badge-notify-bg: #dc2626;
    --hpc-badge-notify-fg: #fff;
}

/* ======================================================================
   Scrollbar visually hidden, scrolling still works (3.25.8x) — third attempt
   at this exact problem. History: negative-margin overlay trick (3.25.71) ->
   scrollbar-gutter: stable (3.25.80, reintroduced a permanent blank gutter
   on every non-scrolling page, the same bug that got `stable` dropped the
   FIRST time round at 3.23.28) -> this. Scrolling itself is untouched (wheel/
   touch/keyboard/trackpad all still work); only the visible track/thumb and
   any reserved gutter space are removed, on both the root scroller and any
   internal `overflow:auto` panel. Deliberate tradeoff, explicitly requested:
   desktop mouse users lose the scroll affordance and can't drag a thumb.
   Do not silently swap this for scrollbar-gutter or the negative-margin
   trick again without a fresh discussion — this exact spot has flip-flopped
   three times already. */
* {
    scrollbar-width: none; /* Firefox */
    -ms-overflow-style: none; /* legacy Edge/IE */
}
*::-webkit-scrollbar {
    display: none; /* Chrome/Safari/Edge — covers html's own scrollbar too,
    unlike the old dark-mode rule this file already got bitten by (see the
    ::-webkit-scrollbar block further down): a bare `*` universal selector
    has no descendant-combinator gap, so it matches <html> itself. */
    width: 0;
    height: 0;
}

/* ======================================================================
   Light-mode elevation (3.15.1) — the missing half of the "tiered surfaces"
   idea dark mode already has (body recedes, cards visibly float above it,
   see the dark palette block below). Light mode never got its own version
   of that: body sat at pure white, identical to every card on it, so
   nothing ever visually lifted off the page — one root cause of the "dog's
   breakfast" read on My Rewards/My Pets, on top of the missing component
   CSS fixed further down. --hpc-surface-sunken already existed as a token
   (used by the booking calendar) but nothing ever applied it to body
   itself. Dark mode's own body rule (below) uses !important and wins
   regardless of source order, so this is safe to state unconditionally.
   ====================================================================== */
body {
    background-color: var(--hpc-surface-sunken);
}

/* ======================================================================
   Paw-print background (3.16.2, broadened to every page in 3.19.1 — was
   inconsistently scoped to `body.hpc-home` only, so most of the site still
   sat on the plain --hpc-surface-sunken fill while the Home page alone got
   the pattern; now every page gets it, standardising the look site-wide).
   `body.hpc-home` (see HpcTheme::isHomePage()/applyHomeBackgroundClass() —
   "Home" means Joomla's own default Main Menu item) still exists and still
   drives the SEPARATE Home-page article polish further down this file
   (the hero-photo/card treatment) — only the background itself moved off
   that class onto plain `body`. No image asset exists for this, so the pattern is a small
   inline SVG tile (three paws at varied size/rotation so the repeat doesn't
   read as a rigid grid) — no request, no file to ship. Each paw is drawn
   THREE times at the same spot, offset by 1px: a dark copy, a light copy,
   then a faint base fill — that's what gives the "embossed/pressed into the
   surface" look rather than a flat printed paw, done inside the SVG itself
   so no CSS filter (which would also blur real content) is needed. Two full
   tiles, not one recoloured via currentColor — a background-image data URI
   is loaded as an external image context, so `fill="currentColor"` inside
   it does NOT reliably inherit the page's `color` in every engine; explicit
   colours per theme is the safe choice already used elsewhere in this file.
   Never verified in a real browser (see this file's own header + CLAUDE.md's
   FullCalendar history for why that's an honest, standing limitation of this
   dev sandbox) — low-opacity and additive-only (background-image on top of
   the existing background-color), so a rendering surprise here is cosmetic,
   never a layout break. */
body {
    /* Light mode never overrode the template's own --body-bg (#fff,
       big_goose/media/css/template.css:80) — pure white plus a ~5%-opacity
       paw print reads as harsh, especially next to dark UI chrome (header,
       banners). Dark mode already repaints this to --bs-body-bg; give light
       mode the same "sunken page, raised card" treatment every card on the
       platform already assumes (cards sit on --hpc-surface #fff) instead of
       leaving the page itself flat white. */
    background-color: var(--hpc-surface-sunken);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='170' height='170' viewBox='0 0 170 170'%3E%3Cdefs%3E%3Cg id='p'%3E%3Cellipse cx='0' cy='14' rx='13' ry='10'/%3E%3Ccircle cx='-12' cy='-4' r='5.2'/%3E%3Ccircle cx='-4' cy='-11' r='5.6'/%3E%3Ccircle cx='5' cy='-11' r='5.6'/%3E%3Ccircle cx='13' cy='-4' r='5.2'/%3E%3C/g%3E%3C/defs%3E%3Cg transform='translate(36,40) rotate(-18) scale(0.9)'%3E%3Cuse href='%23p' x='1' y='1' fill='%23000000' fill-opacity='0.05'/%3E%3Cuse href='%23p' x='-1' y='-1' fill='%23ffffff' fill-opacity='0.6'/%3E%3Cuse href='%23p' fill='%23000000' fill-opacity='0.025'/%3E%3C/g%3E%3Cg transform='translate(126,100) rotate(15) scale(1.05)'%3E%3Cuse href='%23p' x='1' y='1' fill='%23000000' fill-opacity='0.05'/%3E%3Cuse href='%23p' x='-1' y='-1' fill='%23ffffff' fill-opacity='0.6'/%3E%3Cuse href='%23p' fill='%23000000' fill-opacity='0.025'/%3E%3C/g%3E%3Cg transform='translate(74,140) rotate(-6) scale(0.78)'%3E%3Cuse href='%23p' x='1' y='1' fill='%23000000' fill-opacity='0.045'/%3E%3Cuse href='%23p' x='-1' y='-1' fill='%23ffffff' fill-opacity='0.55'/%3E%3Cuse href='%23p' fill='%23000000' fill-opacity='0.02'/%3E%3C/g%3E%3C/svg%3E");
    background-repeat: repeat;
    background-size: 170px 170px;
    background-attachment: fixed;
}

/* ======================================================================
   Home page article polish (3.16.3) — reported directly from a live
   screenshot: the Home page's own content (a raw, full-column-width
   uploaded photo + plain WYSIWYG paragraphs) renders as bare, unstyled
   Joomla core article markup — no different from any other article on the
   site, and now visibly at odds with the paw-print treatment just given the
   page around it. Scoped under `body.hpc-home` (see above) plus Joomla
   core's OWN guaranteed article wrapper class, `.item-page` — present in
   com_content's article view across every recent Joomla version, so this
   doesn't depend on knowing this Business's actual article HTML in advance.
   Same "best-effort, inert if the live markup doesn't match" caveat as
   every other Cassiopeia-chrome selector in this file (see this file's own
   header — no real browser here to verify against) — only the Home page is
   touched, nothing else on the site.

   Four moves, all reversible/inert independently if one doesn't match:
    1. The whole article sits on its own card surface (--hpc-surface, same
       language as every card elsewhere on this platform), so it visibly
       lifts off the paw-print background instead of floating bare on it —
       "tiered surfaces" carried through to this page too.
    2. Cassiopeia's own page-header title ("Home") is shrunk to a small
       uppercase kicker rather than hidden outright — the article's OWN
       heading ("Welcome to ...") already says more, so a second full-size
       title directly above it was pure repetition, but removing the
       heading entirely would lose the admin's inline "Edit" affordance
       Cassiopeia attaches to it.
    3. The hero photo — the actual ask ("make the image smaller") — goes
       from its raw uploaded size to a fixed, cropped banner height,
       rounded and lifted with a shadow, so it reads as a designed hero
       strip instead of a raw photo dropped into the page.
    4. Body copy gets a capped line length (readability) and real vertical
       rhythm between headings/paragraphs instead of the WYSIWYG default's
       cramped spacing. The "What We Offer" list specifically — each
       service is a bold emoji-led label followed by its own paragraph —
       gets a quiet left-rail treatment via `:has(> strong:first-child)`,
       styling paragraphs by their actual existing shape rather than
       requiring the article content itself to be restructured with new
       classes (which a Business's own content editor doesn't control).
   ====================================================================== */
body.hpc-home .item-page {
    max-width: 900px;
    margin-inline: auto;
    background: var(--hpc-surface);
    border: 1px solid var(--hpc-border);
    border-radius: 16px;
    padding: 1.75rem clamp(1rem, 4vw, 2.5rem) 2.25rem;
    box-shadow: var(--hpc-shadow-resting);
    /* Contains the hero image's float below (see that rule's own comment) so
       the card's own background/padding always wraps the full article
       height, never collapsing around a floated child the way a plain block
       formatting context would — a modern, non-clipping alternative to the
       classic "overflow: hidden" clearfix (which would risk cutting off the
       image's own box-shadow). Live-screenshot-confirmed bug (3.16.3 first
       pass): without this, the card visibly failed to wrap the article at
       all — its box shrank to just the text beside the still-floated image. */
    display: flow-root;
}
body.hpc-home .item-page .page-header {
    border: 0;
    margin-bottom: .25rem;
    padding-bottom: 0;
}
body.hpc-home .item-page .page-header h1 {
    font-size: .78rem;
    text-transform: uppercase;
    letter-spacing: .08em;
    font-weight: 600;
    color: var(--hpc-muted);
    margin: 0;
}
/* Two-layer float fix (3.16.5 — the 3.16.4 attempt targeted only the <img>
   itself and a second live screenshot, taken AFTER that version genuinely
   reached the site this time, showed the exact same wrap persisting: the
   image's own rounded corners/shadow (only ever declared on the <img> tag)
   rendered correctly, proving this selector DOES match — but the image
   stayed narrow and text still wrapped beside it, which only makes sense if
   `float`/an explicit width live on the element WRAPPING the image (Joomla's
   WYSIWYG typically inserts a bare image inside its own `<p>`), not the
   `<img>` tag itself — so overriding those two properties on the img alone
   was correctly fixing the wrong element. Layer 1 below neutralises
   whichever direct child of `.item-page` actually wraps the image, via
   `:has()`, without needing to know its tag/class up front. Layer 2 is a
   second, independent net: `clear: both` on every other direct child means
   nothing can ever wrap beside a float again regardless of which element
   turns out to carry it — even one this codebase hasn't anticipated. Same
   "force it directly, don't just retune a value" precedent this file has
   already reached for more than once (v3.9.8/v3.12.6's own dark-mode
   specificity hotfixes). */
body.hpc-home .item-page > :has(> img) {
    float: none !important;
    width: auto !important;
    max-width: none !important;
    margin: 0 0 1.5rem !important;
}
body.hpc-home .item-page > :not(.page-header) {
    clear: both;
}
body.hpc-home .item-page img {
    display: block !important;
    float: none !important;
    width: 100% !important;
    height: auto !important;
    max-height: 300px;
    object-fit: cover;
    border-radius: 12px;
    box-shadow: var(--hpc-shadow-lifted);
    margin: 0 0 1.5rem !important;
}
body.hpc-home .item-page :is(h2, h3) {
    margin-top: 1.75rem;
    margin-bottom: .5rem;
}
body.hpc-home .item-page > :is(h2, h3, p):first-child {
    margin-top: 0;
}
body.hpc-home .item-page p {
    max-width: 68ch;
    line-height: 1.65;
}
body.hpc-home .item-page p:has(> strong:first-child) {
    max-width: none;
    background: var(--hpc-surface-sunken);
    border-left: 3px solid var(--hpc-primary);
    border-radius: 0 8px 8px 0;
    padding: .65rem .9rem;
    margin: .6rem 0;
}
body.hpc-home .item-page p:has(> strong:first-child) > strong {
    display: block;
    margin-bottom: .15rem;
    color: var(--hpc-primary);
}

/* ======================================================================
   Branding — makes Bootstrap's OWN ".btn-primary"/".btn-outline-primary"/
   ".bg-primary"/plain links actually follow --hpc-primary. Needed because
   Bootstrap pre-computes a button's hover/active shade at ITS OWN build
   time from ITS OWN Sass $primary, not live from a CSS custom property —
   so without this, every "primary" action silently stays the template's
   stock blue regardless of what an instance is branded with.
   ====================================================================== */

/* Primary CTA gradient/glow identity — same in light and dark (3.37: was
   dark-mode-only via [data-bs-theme="dark"], but the gradient is decoration
   on top of an already-unambiguous solid-fill button, not a contrast fix, so
   there's no reason light mode should look flatter/duller than dark for the
   same action (Add to cart etc). --hpc-gradient-primary/--hpc-glow-shadow
   are defined in both theme blocks, so this needs no theme scoping at all. */
.btn-primary {
    background: var(--hpc-gradient-primary);
    border-color: transparent;
    color: var(--hpc-primary-contrast);
}
.btn-primary:hover,
.btn-primary:focus-visible,
.btn-primary.is-pressed {
    background: var(--hpc-gradient-primary);
    border-color: transparent;
    box-shadow: var(--hpc-glow-shadow);
    color: var(--hpc-primary-contrast);
}
.btn-outline-primary {
    color: var(--hpc-primary);
    border-color: var(--hpc-primary);
}
.btn-outline-primary:hover,
.btn-outline-primary:focus-visible,
.btn-outline-primary.is-pressed {
    background-color: var(--hpc-primary);
    border-color: var(--hpc-primary);
    color: var(--hpc-primary-contrast);
}
.badge.bg-primary {
    background-color: var(--hpc-primary) !important;
}
/* 3.23.20: same "solid, not just outlined" visibility fix as
   .btn-outline-secondary below (dark mode only — light mode's white page
   already gives a bare outline plenty of contrast). The Cart button
   (#hcc-cart-btn) is .btn-outline-primary specifically, and was one of the
   buttons reported as unreadable against the dark paw-print background. */
[data-bs-theme="dark"] .btn-outline-primary {
    background-color: var(--hpc-primary) !important;
    border-color: var(--hpc-primary) !important;
    color: var(--hpc-primary-contrast) !important;
}
[data-bs-theme="dark"] .btn-outline-primary:hover,
[data-bs-theme="dark"] .btn-outline-primary:focus-visible,
[data-bs-theme="dark"] .btn-outline-primary.is-pressed {
    background-color: color-mix(in srgb, var(--hpc-primary) 85%, black) !important;
    border-color: color-mix(in srgb, var(--hpc-primary) 85%, black) !important;
    color: var(--hpc-primary-contrast) !important;
}
/* Plain links go through Bootstrap's own --bs-link-color, which it DOES
   read live — remapping the variable is enough here, no element selector
   needed. */
:root {
    --bs-link-color: var(--hpc-primary);
    --bs-link-hover-color: color-mix(in srgb, var(--hpc-primary) 80%, black);
}

/* ======================================================================
   JS-driven press effect + ripple (3.15.1) — fixes a real bug: the old
   `:active`/`:focus`-only "wave" darkened a tapped `<a class="btn">` and
   never reliably cleared, because a link that navigates away stays
   logically focused, and a mobile back-gesture restoring the page from the
   browser's bfcache brings that stuck `:focus` state back with it — no real
   interaction happening, no `:active` involved (that pseudo-class can't
   persist without a pointer actually down). Fixed by owning the "pressed"
   look with an explicit class instead of the pseudo-classes it can't fully
   control — see media/com_happypetconnect/js/buttonfx.js, which adds/removes
   `.is-pressed` on pointer events and explicitly clears it (plus any
   leftover ripple) on `pageshow` when the page was restored from bfcache.
   `:focus-visible` is untouched above — a real keyboard focus ring is a
   different, legitimate concern from the tap "pressed" look and isn't what
   was buggy. Ripple is a plain radial `<span>` buttonfx.js injects at the
   tap point; `@keyframes` scales/fades it, `animationend` removes it.

   3.23.20 fix: `overflow: hidden` used to live directly on `.btn` to clip
   the ripple to the button's shape — but that also clipped anything ELSE
   positioned to stick outside the button's box, including the Cart badge
   (`#hcc-cart-count`, `position-absolute` + `translate-middle` so half of
   it sits outside the button on purpose) — only its bottom-right sliver
   survived the clip. The ripple now gets its own absolutely-positioned,
   full-size `.hpc-btn-ripple-mask` wrapper (buttonfx.js creates/reuses one
   per button) that owns `overflow: hidden` instead of `.btn` itself — the
   ripple still clips to the button's shape, but a sibling like the cart
   badge is never inside that wrapper, so it's never clipped by it. */
.btn {
    position: relative;
}

/* Shared high-contrast notification badge (3.27) — cart count in the header
   button and the mobile PWA bottom-bar icons both use this for colour, so a
   badge always reads as "action needed" instead of blending into whatever
   colour its parent button/icon happens to be. Geometry/position stay
   per-context (Bootstrap's position-absolute/translate-middle on the header
   badge, .hpc-mobilebar-badge's own sizing on the bottom bar) since the two
   live on very differently sized parents; only colour + shape are shared. */
.hpc-badge-notify {
    background: var(--hpc-badge-notify-bg);
    color: var(--hpc-badge-notify-fg);
    border-radius: 9999px;
    font-weight: 700;
    line-height: 1;
    text-align: center;
}
.hpc-badge-notify:empty {
    display: none;
}
.hpc-btn-ripple-mask {
    position: absolute;
    inset: 0;
    overflow: hidden;
    border-radius: inherit;
    pointer-events: none;
}
/* buttonfx.js appends this mask as the button's LAST child (needed so the
   ripple always paints above earlier content) — but Joomla's own core
   password-toggle script updates its label by grabbing the button's
   `:last-child`, assuming that's its own `.visually-hidden` span. Once the
   ripple mask became the actual last child, Joomla started writing
   "Show/Hide Password" into THIS empty span instead, and because it's
   absolutely positioned over the whole button, that stray text rendered
   visibly on top of the icon (confirmed live via outerHTML — the real
   `.visually-hidden` span was untouched and still correctly hidden).
   font-size: 0 makes any text landing in here invisible regardless of DOM
   order, without touching buttonfx.js itself (a site-wide effect on every
   .btn) or its ripple children, which are sized via inline width/height and
   filled with `background: currentColor`, neither of which depends on
   font-size. */
.input-password-toggle .hpc-btn-ripple-mask {
    font-size: 0;
}
.hpc-btn-ripple {
    position: absolute;
    border-radius: 50%;
    background: currentColor;
    opacity: .35;
    transform: scale(0);
    pointer-events: none;
    animation: hpc-btn-ripple .55s ease-out forwards;
}
@keyframes hpc-btn-ripple {
    to {
        transform: scale(2.6);
        opacity: 0;
    }
}
@media (prefers-reduced-motion: reduce) {
    .hpc-btn-ripple {
        animation: none;
        display: none;
    }
}

/* Submit-in-flight state (3.29.29) — see formstate.js. Bootstrap's own
   `:disabled`/`[disabled]` styling already dims the button (default ~.65
   opacity); this only adds the "please wait" cursor and a touch more dimming
   so a busy button reads as distinctly different from a plain disabled one
   (e.g. an already-unavailable cart line), not just "greyed out". */
.btn.is-busy {
    cursor: wait;
    opacity: .75;
}

/* Cart button "just added" pulse (3.23.20) — see cartpulse.js. A plain
   grow/shrink scale, twice over ~3s, so it reads as a deliberate pulse
   rather than a single blip; scoped to transform only so it composes fine
   with the ripple/press effects above (both animate different properties). */
#hcc-cart-btn.hpc-cart-pulse {
    animation: hpc-cart-pulse 3s ease-in-out;
}
@keyframes hpc-cart-pulse {
    0%, 30%, 60%, 100% { transform: scale(1); }
    15%, 45%           { transform: scale(1.08); }
}
@media (prefers-reduced-motion: reduce) {
    #hcc-cart-btn.hpc-cart-pulse {
        animation: none;
    }
}

/* ======================================================================
   Dark mode — a real, elegant palette: soft charcoal-slate (Replit/VS
   Code/Office 365 style), never near-black, never pure-white text. Retunes
   Bootstrap 5.3's OWN dark custom-property map, so anything not explicitly
   listed below still gets Bootstrap's own sensible dark value rather than
   nothing.
   ====================================================================== */

[data-bs-theme="dark"] {
    color-scheme: dark;

    /* Body */
    --bs-body-bg: #0e131d;
    --bs-body-bg-rgb: 14, 19, 29;
    --bs-body-color: #dce2ec;
    --bs-body-color-rgb: 220, 226, 236;
    --bs-emphasis-color: #f5f6f8;
    --bs-emphasis-color-rgb: 245, 246, 248;
    --bs-heading-color: #eef0f3;

    /* Tiered surfaces — page recedes, cards visibly float above it */
    --bs-secondary-bg: #161d2c;
    --bs-secondary-bg-rgb: 22, 29, 44;
    --bs-tertiary-bg: #1f2a3f;
    --bs-tertiary-bg-rgb: 31, 42, 63;
    --bs-secondary-color: #a7b0c4;
    --bs-secondary-color-rgb: 167, 176, 196;
    --bs-tertiary-color: #77839b;
    --bs-tertiary-color-rgb: 119, 131, 155;

    /* Borders */
    --bs-border-color: #2c3650;
    --bs-border-color-translucent: rgba(190, 205, 235, .12);

    /* Links — same brand colour, lifted slightly for AA contrast on a dark
       body (a saturated indigo that reads fine on white can go muddy on a
       dark surface without a touch more lightness). */
    --bs-link-color: color-mix(in srgb, var(--hpc-primary, #1f3c88) 70%, white);
    --bs-link-hover-color: color-mix(in srgb, var(--hpc-primary, #1f3c88) 55%, white);

    /* Component-level vars several Bootstrap parts read directly */
    --bs-card-bg: var(--bs-secondary-bg);
    --bs-card-cap-bg: var(--bs-tertiary-bg);
    --bs-card-border-color: var(--bs-border-color);
    --bs-dropdown-bg: var(--bs-secondary-bg);
    --bs-dropdown-border-color: var(--bs-border-color);
    --bs-dropdown-link-color: var(--bs-body-color);
    --bs-dropdown-link-hover-color: var(--bs-emphasis-color);
    --bs-dropdown-link-hover-bg: var(--bs-tertiary-bg);
    --bs-modal-bg: var(--bs-secondary-bg);
    --bs-modal-content-bg: var(--bs-secondary-bg);
    --bs-modal-content-border-color: var(--bs-border-color);
    --bs-modal-header-border-color: var(--bs-border-color);
    --bs-modal-footer-border-color: var(--bs-border-color);
    --bs-table-color: var(--bs-body-color);
    --bs-table-bg: transparent;
    --bs-table-border-color: var(--bs-border-color);
    --bs-table-striped-bg: rgba(190, 205, 235, .04);
    --bs-table-hover-bg: rgba(190, 205, 235, .05);
    --bs-list-group-bg: var(--bs-secondary-bg);
    --bs-list-group-border-color: var(--bs-border-color);
    --bs-list-group-action-hover-bg: var(--bs-tertiary-bg);
    --bs-nav-tabs-link-active-bg: var(--bs-secondary-bg);
    --bs-nav-tabs-link-active-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-secondary-bg);
    --bs-nav-link-color: var(--bs-body-color);
    --bs-nav-link-hover-color: var(--bs-emphasis-color);
    --bs-code-color: #f0a032;

    /* --hpc-* surfaces/lines the booking calendar (Month grid above +
       timegrid.css's Week view) is built on — read from the SAME --bs-*
       custom properties this block just retuned, not a second independent
       copy, so the calendar can never drift out of sync with the rest of
       the dark palette. --hpc-primary-tint/-ring need no entry here: they're
       color-mix() expressions against var(--hpc-surface) in the :root block
       above, so they already resolve correctly the moment --hpc-surface
       flips dark on the next line. */
    --hpc-danger: var(--bs-danger, #ea868f);
    --hpc-muted: var(--bs-secondary-color, #aeb3bc);
    --hpc-surface: var(--bs-secondary-bg, #161d2c);
    --hpc-surface-sunken: var(--bs-body-bg, #0e131d);
    --hpc-border: var(--bs-border-color, #2c3650);
    --hpc-border-light: rgba(190, 205, 235, .10);
    --hpc-text: var(--bs-body-color, #e6e9f0);
    --hpc-text-faint: var(--bs-tertiary-color, #77839b);
    --hpc-text-soft: var(--bs-secondary-color, #a7b0c4);
    /* 3.35+: was #566078 on --hpc-surface-sunken (#0e131d) — ~1.9:1, fails
       WCAG AA. Lifted for ~4.6:1 (disabled/past calendar dates, see
       .hcc-day-card.hcc-day-past / .hcc-book-past below). */
    --hpc-text-placeholder: #8b95ad;
    --hpc-shadow-resting: 0 1px 2px rgba(0, 0, 0, .3), 0 1px 1px rgba(0, 0, 0, .2);
    --hpc-shadow-lifted: 0 20px 30px -14px rgba(0, 0, 0, .6), 0 8px 12px -6px rgba(0, 0, 0, .4);

    /* Dark-mode identity — brighter accent (dark surfaces need more
       lightness to read the same hue as they do on white, same reasoning as
       the --bs-link-color lift above) + a glow shadow standing in for light
       mode's neomorphic shadow pair. Applied to hero/CTA elements only (see
       .hpc-glow / .btn-primary below) — most surfaces stay flat-and-dark. */
    --hpc-accent: #2dd4bf;
    --hpc-gradient-primary: linear-gradient(135deg, var(--hpc-primary), var(--hpc-accent));
    --hpc-glow-shadow: 0 0 1px color-mix(in srgb, var(--hpc-accent) 60%, transparent),
                      0 0 40px color-mix(in srgb, var(--hpc-primary) 35%, transparent);
    --hpc-card-shadow: var(--hpc-shadow-lifted);
}

/* Bootstrap's own compiled `body` rule is supposed to read --bs-body-bg/
   -color live, which would make the retuning above enough on its own — but
   a live screenshot showed body staying light while every compound-
   selector rule below (.card etc) correctly went dark, meaning Cassiopeia's
   own compiled CSS is winning that fight for body specifically, the same
   class of problem the component-hotfix section further down exists to
   solve. Same fix: set the rendered colour directly, with !important. */
[data-bs-theme="dark"] body {
    background-color: var(--bs-body-bg, #1a1d24) !important;
    color: var(--bs-body-color, #d7dae0) !important;
}

/* Dark-mode paw print swap (3.16.2) — a separate tile, not a recolour of the
   light one: on a dark surface the "shadow" copy needs to all but disappear
   (there's no darker tone left to carve into) while the "highlight" copy
   needs to read clearly as the raised edge, so the two opacities invert
   rather than just flip in sign. Same three-paw layout/positions as the
   light tile, so the pattern doesn't visibly jump size/scale when the theme
   switches — only its colouring does. background-color above is untouched
   (only background-color carries !important there, background-image is a
   separate property), so this layers on top of it exactly like the light
   rule layers on top of --hpc-surface-sunken. */
[data-bs-theme="dark"] body {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='170' height='170' viewBox='0 0 170 170'%3E%3Cdefs%3E%3Cg id='p'%3E%3Cellipse cx='0' cy='14' rx='13' ry='10'/%3E%3Ccircle cx='-12' cy='-4' r='5.2'/%3E%3Ccircle cx='-4' cy='-11' r='5.6'/%3E%3Ccircle cx='5' cy='-11' r='5.6'/%3E%3Ccircle cx='13' cy='-4' r='5.2'/%3E%3C/g%3E%3C/defs%3E%3Cg transform='translate(36,40) rotate(-18) scale(0.9)'%3E%3Cuse href='%23p' x='1' y='1' fill='%23000000' fill-opacity='0.3'/%3E%3Cuse href='%23p' x='-1' y='-1' fill='%23ffffff' fill-opacity='0.07'/%3E%3Cuse href='%23p' fill='%23ffffff' fill-opacity='0.02'/%3E%3C/g%3E%3Cg transform='translate(126,100) rotate(15) scale(1.05)'%3E%3Cuse href='%23p' x='1' y='1' fill='%23000000' fill-opacity='0.3'/%3E%3Cuse href='%23p' x='-1' y='-1' fill='%23ffffff' fill-opacity='0.07'/%3E%3Cuse href='%23p' fill='%23ffffff' fill-opacity='0.02'/%3E%3C/g%3E%3Cg transform='translate(74,140) rotate(-6) scale(0.78)'%3E%3Cuse href='%23p' x='1' y='1' fill='%23000000' fill-opacity='0.25'/%3E%3Cuse href='%23p' x='-1' y='-1' fill='%23ffffff' fill-opacity='0.06'/%3E%3Cuse href='%23p' fill='%23ffffff' fill-opacity='0.015'/%3E%3C/g%3E%3C/svg%3E") !important;
}

[data-bs-theme="light"] {
    color-scheme: light;
}

/* Base elements Bootstrap doesn't fully hand off to variables */
[data-bs-theme="dark"] hr {
    border-color: var(--bs-border-color);
    opacity: .5;
}
[data-bs-theme="dark"] ::selection {
    background-color: rgba(240, 160, 50, .35);
    color: var(--bs-emphasis-color);
}

/* Shadows — Bootstrap's utility shadow classes are pure black at a fixed
   alpha regardless of theme, far too heavy against an already-dark body */
[data-bs-theme="dark"] .shadow,
[data-bs-theme="dark"] .shadow-lg {
    box-shadow: 0 20px 30px -14px rgba(0, 0, 0, .6), 0 8px 12px -6px rgba(0, 0, 0, .4) !important;
}
[data-bs-theme="dark"] .shadow-sm {
    box-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 1px 1px rgba(0, 0, 0, .2) !important;
}

/* Cassiopeia chrome — best-effort selector names (no local copy of the
   template to read class names back from); an unmatched selector here is
   simply inert, never destructive. */
/* Not scoped to [data-bs-theme="dark"] — --hpc-surface/--hpc-border already
   resolve to the right value per theme on their own (light default in
   :root, branded/derived value under [data-bs-theme="dark"] further down
   this file), so one unscoped rule covers both instead of needing a light
   copy and a dark copy that could drift apart. Before this, LIGHT mode
   header/footer were never touched at all and stayed on big_goose's own
   stock --big_goose-color-primary navy (#112855) — completely unrelated to
   branding — while only dark mode got the --hpc-surface fix.
   `header.header`/`footer.footer` (element+class, specificity 0-1-1)
   deliberately beat big_goose's own `.container-header`/`.footer`
   (0-1-0) regardless of stylesheet load order — same reasoning as the
   sticky-header rule above.
   background-image: none is required alongside background-color —
   big_goose's _header.scss/_footer.scss also paint an opaque
   linear-gradient keyed to --big_goose-color-primary/-hover, a completely
   separate, un-branded variable pair. background-image and background-color
   are different properties, so overriding only the colour never contests
   that gradient; it keeps painting over the branded fill regardless of
   selector specificity. */
.header,
header.header,
#header {
    background-color: var(--hpc-surface);
    background-image: none;
    border-bottom: 1px solid var(--hpc-border);
}
[data-bs-theme="dark"] .site-title,
[data-bs-theme="dark"] .site-title a {
    color: var(--bs-heading-color);
}
.footer,
footer.footer,
#footer {
    background-color: var(--hpc-surface);
    background-image: none;
    border-top: 1px solid var(--hpc-border);
    color: var(--hpc-text-soft);
}
[data-bs-theme="dark"] .offcanvas,
[data-bs-theme="dark"] .offcanvas-body {
    background-color: var(--bs-secondary-bg) !important;
    color: var(--bs-body-color) !important;
}
[data-bs-theme="dark"] .search input.form-control,
[data-bs-theme="dark"] .finder-selected-tag {
    background-color: var(--bs-tertiary-bg);
    border-color: var(--bs-border-color);
    color: var(--bs-body-color);
}
[data-bs-theme="dark"] .metismenu .nav-link,
[data-bs-theme="dark"] .metismenu a {
    color: var(--bs-body-color);
}
[data-bs-theme="dark"] .metismenu .nav-link:hover,
[data-bs-theme="dark"] .metismenu a:hover {
    background-color: var(--bs-tertiary-bg);
}

/* Form controls — light mode (3.26). Recessed/"pressed-in" neomorphic
   treatment (same --hpc-neo-shadow-in token .hpc-surface-inset uses) so a
   field reads as part of the soft-shadow surface system, not stock
   Bootstrap chrome. Includes .hpc-select-trigger (the visible closed box
   customselect.js swaps in for every real <select> — see that section
   below) explicitly rather than relying on it merely inheriting the
   .form-select class it also carries, so this can't silently stop applying
   if that inheritance ever changes. Scoped like the Button/Card sections
   above so core Joomla forms elsewhere on the site are untouched. */
[class^="com-happypetconnect-"] .form-control,
[class^="com-happypetconnect-"] .form-select,
[class^="com-happypetconnect-"] .hpc-select-trigger,
.hpc-profile-wrap .form-control,
.hpc-profile-wrap .form-select,
.hpc-profile-wrap .hpc-select-trigger,
.hpc-profile-page .form-control,
.hpc-profile-page .form-select,
.hpc-profile-page .hpc-select-trigger {
    background-color: var(--hpc-surface);
    box-shadow: var(--hpc-neo-shadow-in);
    border-color: transparent;
    /* Replaces big_goose's stock chevron sprite (media/images/select-bg.svg) —
       see the dark-mode block below for why: that sprite is ~97% an opaque
       white rect, and at its own background-size:116rem it paints solid white
       over almost the whole control face in EITHER theme, not just dark. This
       small inline chevron is the only image actually rendered, correctly
       sized, so background-color is what shows through everywhere else. */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23495057' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M2 5l6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right .9rem center;
    background-size: 14px 10px;
}
[class^="com-happypetconnect-"] .form-control:focus,
[class^="com-happypetconnect-"] .form-select:focus,
[class^="com-happypetconnect-"] .hpc-select-trigger:focus-visible,
.hpc-profile-wrap .form-control:focus,
.hpc-profile-wrap .form-select:focus,
.hpc-profile-wrap .hpc-select-trigger:focus-visible,
.hpc-profile-page .form-control:focus,
.hpc-profile-page .form-select:focus,
.hpc-profile-page .hpc-select-trigger:focus-visible {
    /* Inset shadow stays (still "pressed in") + a real outer ring on top —
       focus visibility must never depend on the soft shadow alone. */
    box-shadow: var(--hpc-neo-shadow-in), 0 0 0 .2rem var(--hpc-primary-ring);
    border-color: var(--hpc-primary);
}

/* customselect.js's popup (.hpc-select-menu, built from Bootstrap's own
   .dropdown-menu/.dropdown-item) never had a LIGHT-mode rule at all — only
   the dark-mode hotfix further down (~L1114) exists, so in light mode it was
   always big_goose's stock white (template.css ~L15053, `.dropdown-menu {
   background-color: #fff }`), stock white read wrong against every other
   neomorphic surface on the page even though it's "correct" Bootstrap
   default. Matches the trigger's own light-mode treatment immediately above
   instead of leaving the open list the only unstyled piece of the control. */
[class^="com-happypetconnect-"] .dropdown-menu,
.hpc-profile-wrap .dropdown-menu,
.hpc-profile-page .dropdown-menu {
    background-color: var(--hpc-surface);
    border-color: var(--hpc-border-light);
    box-shadow: var(--hpc-card-shadow);
}
[class^="com-happypetconnect-"] .dropdown-item:hover,
[class^="com-happypetconnect-"] .dropdown-item:focus,
.hpc-profile-wrap .dropdown-item:hover,
.hpc-profile-wrap .dropdown-item:focus,
.hpc-profile-page .dropdown-item:hover,
.hpc-profile-page .dropdown-item:focus {
    background-color: var(--hpc-surface-sunken);
}

/* Form controls — dark mode. !important-forced same as the .card/.table/
   .dropdown-menu hotfixes above: confirmed (3.17.0) this block was the one
   component-local dark-mode consumer that never got that treatment, so a
   select's own compiled Bootstrap background could still beat the bare
   attribute rule exactly the same way .card's did before its own fix. */
[data-bs-theme="dark"] .form-control,
[data-bs-theme="dark"] .form-select,
[data-bs-theme="dark"] .hpc-select-trigger,
[data-bs-theme="dark"] input.form-control,
[data-bs-theme="dark"] textarea.form-control {
    background-color: var(--bs-tertiary-bg) !important;
    border-color: var(--bs-border-color) !important;
    color: var(--bs-body-color) !important;
    /* <select>'s own native OPTIONS POPUP doesn't inherit CSS colour at
       all — some browsers key its light/dark rendering off color-scheme
       on the control itself, not only an ancestor. */
    color-scheme: dark;
}
[data-bs-theme="dark"] .form-control::placeholder {
    color: var(--bs-tertiary-color) !important;
}
/* Recolours the chevron for dark backgrounds — !important needed to beat the
   light-mode chevron set above (same selector, no theme guard on that one).
   The root cause this whole control replaces (big_goose's stock chevron
   sprite painting solid white over background-color in EITHER theme) is
   explained where the light-mode version of this image is set, above. */
[data-bs-theme="dark"] .form-select,
[data-bs-theme="dark"] .hpc-select-trigger {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23d7dae0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M2 5l6 6 6-6'/%3E%3C/svg%3E") !important;
    background-repeat: no-repeat !important;
    background-position: right .9rem center !important;
    background-size: 14px 10px !important;
}
[data-bs-theme="dark"] .form-control:focus,
[data-bs-theme="dark"] .form-select:focus,
[data-bs-theme="dark"] .hpc-select-trigger:focus-visible {
    background-color: var(--bs-tertiary-bg) !important;
    color: var(--bs-body-color) !important;
    border-color: var(--bs-primary) !important;
    box-shadow: 0 0 0 .25rem rgba(240, 160, 50, .15);
}
/* Custom dropdown (media/com_happypetconnect/js/customselect.js) — replaces
   every native <select class="form-select"> site-wide. A plain <select>'s
   closed box CAN be dark-styled (see the .form-select rule above), but its
   OPEN OPTIONS POPUP on mobile is rendered by the OS/browser itself and
   cannot be themed by any CSS — confirmed from a live device screenshot
   still showing a stark white popup despite the closed box being correctly
   dark. This builds a real HTML popup instead (Bootstrap's own
   .dropdown-menu/.dropdown-item, already dark-styled above), so both the
   closed control and the open list are actually this platform's CSS, in
   both themes, on every device. */
.hpc-select {
    position: relative;
}
.hpc-select-trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    text-align: left;
}
.hpc-select-trigger:disabled {
    opacity: .65;
    cursor: not-allowed;
}
.hpc-select-native {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}
.hpc-select-menu {
    display: none;
    position: absolute;
    inset-inline: 0;
    top: 100%;
    z-index: 1055;
    margin-top: .25rem;
    max-height: 15rem;
    overflow-y: auto;
    min-width: 100%;
}
.hpc-select-menu.show {
    display: block;
}
.hpc-select-option {
    display: block;
    width: 100%;
    text-align: left;
    white-space: normal;
}

/* Breed picker (breedpicker.js) — a filtered suggestion dropdown under the
   pet edit form's free-text Breed field, same positioned-menu shape as
   .hpc-select-menu above, reusing the same already-dark-styled
   .dropdown-menu/.dropdown-item. Unlike .hpc-select this never replaces the
   underlying control — the input stays a normal, focusable text field. */
.hpc-breed-picker {
    position: relative;
}
.hpc-breed-picker-menu {
    display: none;
    position: absolute;
    inset-inline: 0;
    top: 100%;
    z-index: 1055;
    margin-top: .25rem;
    max-height: 15rem;
    overflow-y: auto;
    min-width: 100%;
}
.hpc-breed-picker-menu.show {
    display: block;
}
.hpc-breed-picker-option {
    display: block;
    width: 100%;
    text-align: left;
    white-space: normal;
}

[data-bs-theme="dark"] .form-check-input {
    background-color: var(--bs-tertiary-bg) !important;
    border-color: var(--bs-border-color) !important;
}

/* Breadcrumbs / pagination */
[data-bs-theme="dark"] .breadcrumb {
    background-color: transparent;
}
[data-bs-theme="dark"] .page-link {
    background-color: var(--bs-secondary-bg);
    border-color: var(--bs-border-color);
    color: var(--bs-body-color);
}
[data-bs-theme="dark"] .page-item.active .page-link {
    background-color: var(--bs-primary);
    border-color: var(--bs-primary);
    color: var(--bs-primary-contrast, #fff);
}

/* ----------------------------------------------------------------------
   Card surface treatment (3.26) — every component card picks up the same
   .hpc-surface-raised look (soft-shadow in light, flat+lifted in dark;
   see the Surface system section further down for the full rationale).
   Scoped to component views + the two profile pages, same convention the
   Button system section below uses, so core Joomla/article cards elsewhere
   on the site are untouched. Radius/shadow only — background/border colour
   in dark mode is still owned by the hotfix immediately below (it needs
   !important to win a Bootstrap specificity fight this rule doesn't need
   to enter, since border-radius/box-shadow aren't in that fight).

   --card-border-radius (3.26 follow-up, corrected 3.26.x): a card's OWN
   border-radius was set directly above, but the compiled template's own
   .card-header:first-child / .card-footer:last-child rules compute THEIR
   corner radius from a separate variable, not from the .card element's own
   rendered border-radius — still its stock small default until set here, so
   any card with a header/footer (every modal, the pet-card footer button,
   the profile page's card headers) had a small round-off next to the card's
   now much bigger 18px corners: the exact "layers overlapping, corners
   sticking out" bug reported live, one level up from the thumbnail-image
   version of the same mismatch fixed just above this section.

   REAL BUG fixed here: this originally set `--bs-card-border-radius` — the
   Bootstrap-5.3-with-bs-prefix name — but big_goose (media/css/template.css)
   is a standalone Cassiopeia fork compiled WITHOUT the `bs-` prefix (its own
   `.card` rule defines `--card-border-radius`/`--card-inner-border-radius`,
   never a `--bs-` variant), so that variable was never read by anything and
   the footer/header corner mismatch silently never actually got fixed —
   confirmed by reading the compiled template.css directly, not guessed.
   Every OTHER `--bs-*` custom property in this file is harmless despite the
   same wrong prefix, because each of those pairs a `var(--bs-x, fallback)`
   read with a hardcoded fallback colour AND a direct-property `!important`
   override elsewhere (see the "Component background hotfix" block below) —
   this border-radius property had no such fallback, so it was the one place
   the wrong prefix actually mattered. Named correctly now, the template's
   own calc() keeps every nested corner in sync automatically — no need to
   chase each child individually.

   STILL not enough on its own, though (confirmed live — footer corners kept
   sticking out even after the rename above): .card-footer:last-child's
   radius is drawn from `--card-inner-border-radius`, and the template's own
   .card rule computes THAT from the global `--border-radius` token
   (`calc(var(--border-radius) - var(--card-border-width))`), never from
   `--card-border-radius` at all — the two are independent variables that
   only coincidentally share a name prefix. Overriding just the outer one
   fixed the card's own corner but left the footer's calc untouched, still
   resolving to the template's small default. Both must be set here. */
[class^="com-happypetconnect-"] .card,
.hpc-profile-wrap .card,
.hpc-profile-page .card {
    --card-border-radius: var(--hpc-radius-lg);
    --card-inner-border-radius: calc(var(--hpc-radius-lg) - var(--card-border-width, 1px));
    border-radius: var(--hpc-radius-lg);
    box-shadow: var(--hpc-card-shadow);
    border-color: var(--hpc-border-light);
}

/* Secondary/outline buttons — light mode ONLY (3.26). Per the plan: keep
   .btn-primary/.btn-danger fully solid so an actionable CTA never loses
   contrast, but a non-critical action (Store, My Pets, the calendar's
   Month/Week toggle) can take the same soft-shadow surface language as a
   card. Explicitly reset to flat inside a .btn-group — adjacent buttons in
   a group share touching edges/collapsed border-radius, and each carrying
   its own outer shadow would visually fight its neighbours instead of
   reading as one control. */
[class^="com-happypetconnect-"] .btn-outline-secondary,
[class^="com-happypetconnect-"] .btn-secondary,
.hpc-profile-wrap .btn-outline-secondary,
.hpc-profile-page .btn-outline-secondary {
    box-shadow: var(--hpc-neo-shadow-out);
    border-color: transparent;
}
[class^="com-happypetconnect-"] .btn-group .btn,
.hpc-profile-wrap .btn-group .btn,
.hpc-profile-page .btn-group .btn {
    box-shadow: none;
}

/* Booking calendar's OUTER panel (3.26) — the box the Month/Week/Day grid
   sits inside gets the soft-shadow card treatment; the grid's own cells
   (timegrid.css / availability.css) are untouched — functional, high-risk
   to restyle, explicitly out of scope. */
.hcc-book-cal {
    background: var(--hpc-surface);
    border-radius: var(--hpc-radius-lg);
    box-shadow: var(--hpc-card-shadow);
    padding: .75rem;
    max-width: 640px;
    margin-inline: auto;
}

/* Shared frame for the Service/Option/Pet row + Month/Week/nav row, above
   the calendar. Wider than .hcc-book-cal's own 640px cap on purpose — the
   Week/Day timegrid (timegrid.css) flex-shrinks cleanly and reads best with
   more room, while Month's narrower calendar just centers inside this. */

/* Standardised content column (3.35) — every component page gets the same
   reading-focused 900px column, keeping the user's eye centered instead of
   sprawling across a wide desktop viewport (same fix as the booking page
   above, generalised). Sole exception: .com-happypetconnect-home (the site
   root) keeps its own full-width hero layout — not a form/list page,
   narrowing it would fight its design. Pet profile (.hpc-profile-wrap) is
   NOT exempted — its own max-width rule further down is set to the same
   900px instead of a separate 1100px, so it matches every other page. */
.com-happypetconnect-booking,
.com-happypetconnect-bookings,
.com-happypetconnect-store,
.com-happypetconnect-activity,
.com-happypetconnect-pet-edit,
.com-happypetconnect-product,
.com-happypetconnect-dashboard,
.com-happypetconnect-pets,
.com-happypetconnect-orders,
.com-happypetconnect-order,
.com-happypetconnect-cart,
.hpc-profile-page {
    max-width: 900px;
    margin-inline: auto;
}
/* Suppresses every transition below while booking.js's initial single-pet/
   single-choice auto-fill cascade is resolving (see #hcc-booking's own
   hcc-no-anim comment in booking.js) — without this, a page load that lands
   straight on a completed Pet+Service+Option selection could visibly
   animate through the intermediate narrow/locked states instead of just
   landing on the final one. Removed via double rAF once the cascade
   settles, so real, user-driven changes still animate normally. */
#hcc-booking.hcc-no-anim #hcc-book-fields-row,
#hcc-booking.hcc-no-anim #hcc-book-fields-row .hcc-field {
    transition: none !important;
}
.hcc-book-controls {
    background: var(--hpc-surface);
    border-radius: var(--hpc-radius-lg);
    box-shadow: var(--hpc-card-shadow);
    padding: 1rem 1.1rem .85rem;
    margin-bottom: .4rem;
}
/* Mobile (<1200px, before the desktop flex layout below takes over): the
   Pet/Service/Option fields stack as plain col-md-4s, so Bootstrap's g-3
   row-gutter is what puts space ABOVE each stacked field via --bs-gutter-y
   margin-top. Trimmed from the 1rem default to match the rest of this box's
   tightened spacing. */
#hcc-book-fields-row {
    --bs-gutter-y: .5rem;
}
/* Deliberately NOT sticky (was, briefly — see git history). A sticky
   .hcc-book-controls stacked against .hcc-booking-header's own sticky top:0
   (below) needed a live-measured offset between them, and broke silently
   whenever either bar's real height changed (e.g. header.header collapsing
   to 0 when mobile chrome moved to the nav bar) — the two bars landed on
   the same offset and overlapped instead of stacking. Plain flow avoids the
   whole offset-tracking problem: the Pet/Service/Option selectors and
   Month/Week toggle scroll away with the rest of the page like everything
   else on the site, at the cost of needing a scroll back up to change them
   while deep in a tall Week/Day view. */

/* Desktop width reveal — same progressive idea as the mobile sticky bar
   above, but growing WIDTH instead of scroll position: the Pet/Service/
   Option ROW ONLY (#hcc-book-fields-row) starts just wide enough for the Pet
   field, centered, then widens as Service then Option come into play
   (booking.js sets [data-fields] via updateChainVisibility()). Deliberately
   scoped to that one row, not the whole .hcc-book-controls box — the
   Month/Week toggle + calendar-nav row below it (#hcc-book-nav-row) needs
   its full width regardless of how many dropdowns are showing; shrinking
   the whole container squeezed that row too (live-screenshot regression,
   fixed). Gated at 1200px — this project's own actual mobile/desktop line
   (.hpc-mobilebar / desktop chrome switch at the same breakpoint, see
   plg_system_hpctheme) — NOT Bootstrap's generic 768px "md". A "PC" browser
   narrower than 1200px is still mobile chrome here, so it keeps the plain
   stacked layout (nothing to animate — reveals by height instead, the
   sticky rule above); ≥1200px is the only range where side-by-side is
   promised.

   Field layout is a plain, ungapped-math flex row (display:flex;
   flex-wrap:nowrap) with every visible .hcc-field sharing space equally
   (flex:1 1 0) — NOT percentage widths cloned from Bootstrap's .col-md-4.
   A percentage-based attempt here previously still wrapped the 3rd column
   onto its own line (live-screenshot regression): this template's compiled
   .col-md-4 (big_goose/media/css/template.css) is `flex: none; width:
   33.3333%`, and even after overriding both `flex`/`width` explicitly,
   Bootstrap's own .row gutter math (negative margins + column padding from
   .g-3) left just enough cumulative overflow to trigger wrap. Sidestepping
   that entirely — flex:1 1 0 with flex-wrap:nowrap can't wrap and needs no
   gutter-percentage arithmetic at all, regardless of 1, 2, or 3 fields
   being shown.

   --hpc-book-nav-w (measured live by booking.js's syncNavRowWidth(), since
   CSS can't reference a sibling's natural width) is used as a floor via
   max() so the narrow steps never look thinner than #hcc-book-nav-row's own
   unwrapped content width — falls back to the fixed rem value if JS hasn't
   run yet or is unavailable. */
@media (min-width: 1200px) {
    #hcc-book-fields-row {
        display: flex;
        flex-wrap: nowrap;
        gap: 1rem;
        margin-inline: auto;
        transition: max-width .35s ease;
    }
    #hcc-book-fields-row[data-fields="1"] { max-width: max(22rem, var(--hpc-book-nav-w, 22rem)); }
    #hcc-book-fields-row[data-fields="2"] { max-width: max(42rem, var(--hpc-book-nav-w, 42rem)); }
    #hcc-book-fields-row[data-fields="3"] { max-width: 100%; }

    #hcc-book-fields-row .hcc-field {
        flex: 1 1 0;
        width: auto;
        max-width: none;
        transition: flex-grow .35s ease;
    }
}
[data-bs-theme="dark"] .hcc-book-cal {
    border: 1px solid var(--hpc-border);
}
/* Greys out Month/Week and blocks clicks until Service + Option are both
   chosen (booking.js toggles [hidden] via updateChainVisibility()) — avoids
   ever reaching the slot modal / timegrid with no priced, scheduled Option
   to attach a booking to. #hcc-calendar-area is the shared positioning
   context for both #hcc-month-wrap and #hcc-timegrid (only one of the two
   is ever unhidden at once — see renderCurrentView()), so one overlay
   covers whichever is showing. Scrim uses the same rgba(0,0,0,.5) as
   .hpc-modal-backdrop (theme-neutral, already proven to read fine in both
   modes); the message card itself draws from --hpc-surface/--hpc-border so
   it stays legible in both themes without a separate dark-mode override. */
.hcc-calendar-area {
    position: relative;
}
.hcc-calendar-lock {
    position: absolute;
    inset: 0;
    z-index: 5;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 1.5rem;
    background: rgba(0, 0, 0, .55);
    border-radius: var(--hpc-radius-lg);
}
.hcc-calendar-lock[hidden] { display: none; }
.hcc-calendar-lock p {
    background: var(--hpc-surface);
    border: 1px solid var(--hpc-border);
    border-radius: var(--hpc-radius-lg);
    padding: .85rem 1.25rem;
    max-width: 26rem;
    box-shadow: var(--hpc-card-shadow);
}

/* ----------------------------------------------------------------------
   Component background hotfix — Bootstrap's OWN compiled CSS declares
   --bs-card-bg (and siblings) LOCALLY on component selectors like .card,
   .list-group-item, .dropdown-menu, .modal-content — a selector that can
   beat a bare [data-bs-theme="dark"] attribute rule on specificity/cascade
   order. Confirmed live: without this, the body goes dark correctly but
   every card/table/dropdown stays stuck light. Fixed by setting the actual
   rendered colours directly, with !important — the only place in this file
   that reaches for it, deliberately, because there's no real browser here
   to verify a subtler specificity-only fix actually wins.
   ---------------------------------------------------------------------- */
[data-bs-theme="dark"] .card {
    /* --hpc-surface/--hpc-border, not --bs-secondary-bg/--bs-border-color —
       the latter never pick up brand_dark_bg/brand_dark_surface, which left
       plain .card components on a different dark tone than .hpc-list-card/
       .hcc-book-cal (both already read --hpc-surface directly). */
    background-color: var(--hpc-surface) !important;
    border-color: var(--hpc-border) !important;
    color: var(--bs-body-color, #d7dae0) !important;
}
[data-bs-theme="dark"] .card-header,
[data-bs-theme="dark"] .card-footer {
    background-color: var(--bs-tertiary-bg, #2c313b) !important;
    border-color: var(--bs-border-color, #3a4049) !important;
    color: var(--bs-body-color, #d7dae0) !important;
}
[data-bs-theme="dark"] .table-responsive {
    background-color: var(--bs-secondary-bg, #23272f) !important;
    border-color: var(--bs-border-color, #3a4049) !important;
}
[data-bs-theme="dark"] .list-group-item {
    background-color: var(--bs-secondary-bg, #23272f) !important;
    border-color: var(--bs-border-color, #3a4049) !important;
    color: var(--bs-body-color, #d7dae0) !important;
}
[data-bs-theme="dark"] .dropdown-menu {
    background-color: var(--bs-secondary-bg, #23272f) !important;
    border-color: var(--bs-border-color, #3a4049) !important;
}
[data-bs-theme="dark"] .dropdown-item {
    color: var(--bs-body-color, #d7dae0) !important;
}
[data-bs-theme="dark"] .dropdown-item:hover,
[data-bs-theme="dark"] .dropdown-item:focus {
    background-color: var(--bs-tertiary-bg, #2c313b) !important;
    color: var(--bs-emphasis-color, #f5f6f8) !important;
}
[data-bs-theme="dark"] .modal-content {
    background-color: var(--bs-secondary-bg, #23272f) !important;
    border-color: var(--bs-border-color, #3a4049) !important;
    color: var(--bs-body-color, #d7dae0) !important;
}
[data-bs-theme="dark"] .accordion-item,
[data-bs-theme="dark"] .accordion-button {
    background-color: var(--bs-secondary-bg, #23272f) !important;
    border-color: var(--bs-border-color, #3a4049) !important;
    color: var(--bs-body-color, #d7dae0) !important;
}
[data-bs-theme="dark"] .accordion-button:not(.collapsed) {
    background-color: var(--bs-tertiary-bg, #2c313b) !important;
}
[data-bs-theme="dark"] .nav-tabs .nav-link.active {
    background-color: var(--bs-secondary-bg, #23272f) !important;
    border-color: var(--bs-border-color, #3a4049) var(--bs-border-color, #3a4049) var(--bs-secondary-bg, #23272f) !important;
    color: var(--bs-body-color, #d7dae0) !important;
}
[data-bs-theme="dark"] .table {
    --bs-table-bg: transparent !important;
    --bs-table-striped-color: var(--bs-body-color, #d7dae0) !important;
    color: var(--bs-body-color, #d7dae0) !important;
}
/* Bootstrap's per-cell rule (`.table > :not(caption) > * > *`) reads
   --bs-table-color, "inherit" as its own fallback — inheriting from a
   custom property set on the ANCESTOR <table> should already reach every
   <th>/<td>, but this file has now been wrong trusting that kind of
   indirection on `body` and `.card` too (see those fixes above), so this
   is the same "stop trusting it, force it directly" treatment applied to
   table cells specifically, found from a live screenshot showing thead
   text nearly invisible against its own header background. */
[data-bs-theme="dark"] .table > :not(caption) > * > * {
    color: var(--bs-body-color, #d7dae0) !important;
    border-bottom-color: var(--bs-border-color, #3a4049) !important;
}
[data-bs-theme="dark"] .table > thead > tr > th {
    color: var(--bs-body-color, #d7dae0) !important;
    border-bottom-color: var(--bs-border-color, #3a4049) !important;
}
/* .text-muted/.text-body-secondary — Bootstrap's own utility for "de-
   emphasised" text (a booking's variation name, a pet's species/breed
   label, sub-notes throughout this component). Found nearly invisible in
   a live screenshot: whatever value it actually resolves to isn't reliably
   following --bs-secondary-color's retuning above (same trust-the-
   indirection problem, different consumer) — forced directly instead. */
[data-bs-theme="dark"] .text-muted,
[data-bs-theme="dark"] .text-body-secondary {
    color: var(--bs-secondary-color, #aeb3bc) !important;
}
[data-bs-theme="dark"] .alert:not(.alert-info):not(.alert-danger):not(.alert-warning):not(.alert-success) {
    background-color: var(--bs-tertiary-bg, #2c313b) !important;
    border-color: var(--bs-border-color, #3a4049) !important;
    color: var(--bs-body-color, #d7dae0) !important;
}

/* Secondary/neutral buttons + badges — Bootstrap's own --bs-secondary (the
   semantic colour .btn-outline-secondary/.btn-secondary/.badge.bg-secondary
   derive from) is a DIFFERENT variable family from the surface-tier ones
   above and is never retuned by anything, anywhere — at its stock
   light-mode value (mid-grey, built to read on white) it goes muddy on any
   dark surface here. Deliberately NOT retuning --bs-secondary itself (it
   also feeds .text-secondary/.border-secondary utilities elsewhere, with
   less predictable fallout); targeted component overrides instead.

   3.23.20: solid fill by default, not just on hover — reported live as
   unreadable against the Home page's paw-print background (3.16.2): a
   border-only outline button in a colour this close to --bs-body-bg
   visually disappears into a background that's already faintly textured,
   not just a flat colour. Every "outline" button on this platform now
   reads as a real, visible button at rest, not only once touched. */
[data-bs-theme="dark"] .btn-outline-secondary {
    background-color: var(--bs-tertiary-bg, #2c313b) !important;
    color: var(--bs-body-color, #d7dae0) !important;
    border-color: var(--bs-border-color, #3a4049) !important;
}
[data-bs-theme="dark"] .btn-outline-secondary:hover,
[data-bs-theme="dark"] .btn-outline-secondary:focus,
[data-bs-theme="dark"] .btn-outline-secondary:active {
    background-color: var(--bs-border-color, #3a4049) !important;
    border-color: var(--bs-border-color, #3a4049) !important;
    color: var(--bs-emphasis-color, #f5f6f8) !important;
}
[data-bs-theme="dark"] .btn-secondary {
    background-color: var(--bs-tertiary-bg, #2c313b) !important;
    border-color: var(--bs-border-color, #3a4049) !important;
    color: var(--bs-body-color, #d7dae0) !important;
}
[data-bs-theme="dark"] .btn-secondary:hover,
[data-bs-theme="dark"] .btn-secondary:focus,
[data-bs-theme="dark"] .btn-secondary:active {
    background-color: var(--bs-border-color, #3a4049) !important;
    border-color: var(--bs-border-color, #3a4049) !important;
    color: var(--bs-emphasis-color, #f5f6f8) !important;
}
[data-bs-theme="dark"] .badge.bg-secondary {
    background-color: var(--bs-tertiary-bg, #2c313b) !important;
    color: var(--bs-body-color, #d7dae0) !important;
}

/* Contextual alerts (.alert-info/-success/-warning/-danger) — same class of
   gap as --bs-secondary above, third time this exact bug shape has bitten a
   screenshot. Bootstrap 5.3's alerts read the "subtle" trio
   (--bs-*-bg-subtle/-border-subtle/-text-emphasis) that Bootstrap itself
   only retunes as part of ITS OWN compiled dark mode — which nothing here
   ever enabled (Cassiopeia's own dark-mode Sass flag is off in this
   codebase's reference copy of the template), so on a dark page these sat
   at their stock light-mode pastel values untouched: a pale blue "not yet
   assessed" notice on an otherwise-dark page. Retuned to Bootstrap's own
   documented dark-mode values (not invented — these are the values
   Bootstrap 5.3 itself ships for exactly this purpose) AND forced directly
   onto the alert selectors with !important, the same belt-and-braces
   pattern used everywhere else in this file — trusting "retune the
   variable, Bootstrap will pick it up" has been wrong twice already here
   (body, .card). --bs-light also retuned: anything using .bg-light/
   .text-bg-light (a common "neutral panel" utility) had the identical
   problem. */
/* 3.26.32: contextual alert banners disabled platform-wide per product
   decision — hidden outright rather than restyled. The dark-mode retuning
   below is now dead in practice (nothing left to retune once display:none
   applies) but left in place as harmless should the hide rule ever be
   reverted.

   Two distinct markup shapes needed covering, confirmed live via screenshot
   after the first pass missed one: (1) Bootstrap's own .alert-success/
   -danger/-warning/-info classes, used by this component's own templates
   (21 files under com_happypetconnect/*/tmpl), and (2) Joomla core's system
   messages ("You have been logged out", "Please login first", etc.), which
   render as a <joomla-alert type="success|error|warning|info|notice|message">
   custom element in #system-message-container, styled entirely by the
   [type="..."] attribute — no .alert-* class ever lands on that element, so
   the Bootstrap-class rule alone never touched it. */
.alert-success,
.alert-danger,
.alert-warning,
.alert-info {
    display: none !important;
}
#system-message-container joomla-alert {
    display: none !important;
}

[data-bs-theme="dark"] {
    --bs-info-bg-subtle: #032830;
    --bs-info-border-subtle: #055160;
    --bs-info-text-emphasis: #6edff6;
    --bs-success-bg-subtle: #051b11;
    --bs-success-border-subtle: #0a3622;
    --bs-success-text-emphasis: #75b798;
    --bs-warning-bg-subtle: #332701;
    --bs-warning-border-subtle: #664d03;
    --bs-warning-text-emphasis: #ffda6a;
    --bs-danger-bg-subtle: #2c0b0e;
    --bs-danger-border-subtle: #58151c;
    --bs-danger-text-emphasis: #ea868f;
    --bs-light: var(--bs-tertiary-bg);
    --bs-light-rgb: 44, 49, 59;
}
[data-bs-theme="dark"] .alert-info {
    background-color: var(--bs-info-bg-subtle, #032830) !important;
    border-color: var(--bs-info-border-subtle, #055160) !important;
    color: var(--bs-info-text-emphasis, #6edff6) !important;
}
[data-bs-theme="dark"] .alert-success {
    background-color: var(--bs-success-bg-subtle, #051b11) !important;
    border-color: var(--bs-success-border-subtle, #0a3622) !important;
    color: var(--bs-success-text-emphasis, #75b798) !important;
}
[data-bs-theme="dark"] .alert-warning {
    background-color: var(--bs-warning-bg-subtle, #332701) !important;
    border-color: var(--bs-warning-border-subtle, #664d03) !important;
    color: var(--bs-warning-text-emphasis, #ffda6a) !important;
}
[data-bs-theme="dark"] .alert-danger {
    background-color: var(--bs-danger-bg-subtle, #2c0b0e) !important;
    border-color: var(--bs-danger-border-subtle, #58151c) !important;
    color: var(--bs-danger-text-emphasis, #ea868f) !important;
}
[data-bs-theme="dark"] .bg-light,
[data-bs-theme="dark"] .text-bg-light {
    background-color: var(--bs-tertiary-bg, #2c313b) !important;
    color: var(--bs-body-color, #d7dae0) !important;
}

/* Table/list-group CONTEXT classes (.table-light, .table-warning, ...,
   .list-group-item-success, ...) — a real bug caught from live markup: a
   <tr class="table-light"> inside an already-dark-styled table was still
   rendering light. Different mechanism from the .table fix above, not the
   same bug again — Bootstrap sets --bs-table-bg DIRECTLY on the row/cell
   for these context classes, which shadows whatever the ancestor <table>
   has for that property. Inheritance always loses to a more specific
   element's own declaration, !important or not — !important only resolves
   conflicts on the SAME element, and a <table> and its <tr> are never the
   same element. So overriding .table's --bs-table-bg, however forcefully,
   was never going to reach a row that sets its own. Fixed by targeting
   every context variant directly, on the row/cell/list-item itself, with a
   background+text pairing that stays legible without losing the semantic
   colour entirely (a "light" row stays visually DISTINCT from a plain row,
   just a dark-mode-appropriate distinction instead of literal light grey).
   Bootstrap ships table-primary/-secondary/-success/-danger/-warning/-info/
   -light/-dark and the identical set for list-group-item-*; covering all
   of them now rather than one at a time as each happens to show up in a
   screenshot. */
[data-bs-theme="dark"] .table-primary,
[data-bs-theme="dark"] .table-primary > th,
[data-bs-theme="dark"] .table-primary > td {
    --bs-table-bg: color-mix(in srgb, var(--hpc-primary, #1f3c88) 22%, var(--bs-secondary-bg, #23272f)) !important;
    --bs-table-color: var(--bs-body-color, #d7dae0) !important;
    background-color: var(--bs-table-bg) !important;
    color: var(--bs-table-color) !important;
    border-color: var(--bs-border-color, #3a4049) !important;
}
[data-bs-theme="dark"] .table-secondary,
[data-bs-theme="dark"] .table-secondary > th,
[data-bs-theme="dark"] .table-secondary > td,
[data-bs-theme="dark"] .table-light,
[data-bs-theme="dark"] .table-light > th,
[data-bs-theme="dark"] .table-light > td,
[data-bs-theme="dark"] .table-active,
[data-bs-theme="dark"] .table-active > th,
[data-bs-theme="dark"] .table-active > td {
    --bs-table-bg: var(--bs-tertiary-bg, #2c313b) !important;
    --bs-table-color: var(--bs-body-color, #d7dae0) !important;
    background-color: var(--bs-table-bg) !important;
    color: var(--bs-table-color) !important;
    border-color: var(--bs-border-color, #3a4049) !important;
}
[data-bs-theme="dark"] .table-success,
[data-bs-theme="dark"] .table-success > th,
[data-bs-theme="dark"] .table-success > td {
    --bs-table-bg: var(--bs-success-bg-subtle, #051b11) !important;
    --bs-table-color: var(--bs-success-text-emphasis, #75b798) !important;
    background-color: var(--bs-table-bg) !important;
    color: var(--bs-table-color) !important;
    border-color: var(--bs-success-border-subtle, #0a3622) !important;
}
[data-bs-theme="dark"] .table-danger,
[data-bs-theme="dark"] .table-danger > th,
[data-bs-theme="dark"] .table-danger > td {
    --bs-table-bg: var(--bs-danger-bg-subtle, #2c0b0e) !important;
    --bs-table-color: var(--bs-danger-text-emphasis, #ea868f) !important;
    background-color: var(--bs-table-bg) !important;
    color: var(--bs-table-color) !important;
    border-color: var(--bs-danger-border-subtle, #58151c) !important;
}
[data-bs-theme="dark"] .table-warning,
[data-bs-theme="dark"] .table-warning > th,
[data-bs-theme="dark"] .table-warning > td {
    --bs-table-bg: var(--bs-warning-bg-subtle, #332701) !important;
    --bs-table-color: var(--bs-warning-text-emphasis, #ffda6a) !important;
    background-color: var(--bs-table-bg) !important;
    color: var(--bs-table-color) !important;
    border-color: var(--bs-warning-border-subtle, #664d03) !important;
}
[data-bs-theme="dark"] .table-info,
[data-bs-theme="dark"] .table-info > th,
[data-bs-theme="dark"] .table-info > td {
    --bs-table-bg: var(--bs-info-bg-subtle, #032830) !important;
    --bs-table-color: var(--bs-info-text-emphasis, #6edff6) !important;
    background-color: var(--bs-table-bg) !important;
    color: var(--bs-table-color) !important;
    border-color: var(--bs-info-border-subtle, #055160) !important;
}

/* Same context-class family, list-group flavour (booking/order lists that
   use list-group instead of a table hit the identical bug). */
[data-bs-theme="dark"] .list-group-item-light,
[data-bs-theme="dark"] .list-group-item-secondary {
    background-color: var(--bs-tertiary-bg, #2c313b) !important;
    color: var(--bs-body-color, #d7dae0) !important;
}
[data-bs-theme="dark"] .list-group-item-success {
    background-color: var(--bs-success-bg-subtle, #051b11) !important;
    color: var(--bs-success-text-emphasis, #75b798) !important;
}
[data-bs-theme="dark"] .list-group-item-danger {
    background-color: var(--bs-danger-bg-subtle, #2c0b0e) !important;
    color: var(--bs-danger-text-emphasis, #ea868f) !important;
}
[data-bs-theme="dark"] .list-group-item-warning {
    background-color: var(--bs-warning-bg-subtle, #332701) !important;
    color: var(--bs-warning-text-emphasis, #ffda6a) !important;
}
[data-bs-theme="dark"] .list-group-item-info {
    background-color: var(--bs-info-bg-subtle, #032830) !important;
    color: var(--bs-info-text-emphasis, #6edff6) !important;
}

/* ======================================================================
   Pet parent profile page (big_goose/html/com_users/profile/*.php — a real
   template override of Joomla core's com_users profile DISPLAY page, 3.12.9).
   Everything else on that page is plain .card/.list-group-flush/.text-muted,
   already covered by the dark-mode rules above — only the initial-letter
   avatar badge is genuinely new markup needing its own rule. */
/* 4.5rem, not the original 3rem — "make the circle 50% bigger" so a face is
   actually recognisable in it (3.25.8, alongside the avatar upload feature
   below). */
.hpc-profile-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 4.5rem;
    height: 4.5rem;
    border-radius: 50%;
    background: var(--hpc-primary);
    color: var(--hpc-primary-contrast);
    font-size: 1.875rem;
    font-weight: 600;
    flex-shrink: 0;
}
.hpc-profile-avatar-img {
    display: inline-block;
    width: 4.5rem;
    height: 4.5rem;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
}
/* Ring so the circle reads as sitting ON the banner rather than merging into
   it — box-sizing:border-box keeps the rendered circle at the same 4.5rem
   the camera badge's offsets were tuned against, instead of growing by the
   border width. */
.hpc-profile-avatar,
.hpc-profile-avatar-img {
    box-sizing: border-box;
    border: 4px solid var(--hpc-surface);
}
/* Positioning host for the avatar + its own upload trigger — an <a> can't
   contain another interactive element, so editing is a separate explicit
   button (.hpc-profile-edit-btn) rather than this control's own link. */
.hpc-profile-avatar-wrap {
    position: relative;
    display: inline-block;
}
.hpc-profile-avatar-edit {
    position: absolute;
    right: -.15rem;
    bottom: -.15rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 50%;
    background: var(--hpc-surface);
    color: var(--hpc-text);
    border: 2px solid var(--hpc-surface-sunken);
    cursor: pointer;
    font-size: .85rem;
    transition: transform .15s ease;
}
.hpc-profile-avatar-edit:hover,
.hpc-profile-avatar-edit:focus-visible {
    transform: scale(1.08);
}

/* Discord/Meta-profile shape: gradient banner + overlapping avatar + pill
   "chips" row, instead of the old flat card / whole-card-is-a-link design
   (see default.php's docblock). The edit page gets its own matching
   consolidated card + sticky Save/Cancel bar + live password checklist
   (see edit.php's docblock). */
/* 900px, not the original 720px: big_goose's .container-component fills 3 of
   its 4 main grid columns (~950px) whenever a right sidebar module is
   published but no left one is (see template.css's `body:not(.has-sidebar-
   left) .container-component { grid-column-start: main-start }` rule) — a
   720px card centred in that column left a large, oddly empty strip between
   the card and the sidebar menu. Widening close to the column's real width
   removes that gap instead of just shrinking it. */
.hpc-profile-card {
    max-width: 900px;
    /* Matches the .is-editing override below exactly (was 1.5rem — reported
       live: the card visibly shifts up when Edit Profile is tapped, since
       the two states used two different values here). Same number in both
       places now, so the card's top edge never moves when toggling. */
    margin-top: .5rem;
    overflow: hidden;
    /* Positioning context for .hpc-guide-fab, pinned to this card's
       bottom-left corner. */
    position: relative;
}
.hpc-profile-banner {
    height: 96px;
    background: var(--hpc-gradient-primary);
    position: relative;
}
@media (max-width: 575.98px) {
    .hpc-profile-banner {
        height: 72px;
    }
}
/* "Book a service" in the page-header action row is redundant here on
   phones — the mobile bottom bar's fixed 5-slot layout already carries a
   Book destination — so it's hidden only on this page, only at phone width
   (Bootstrap's md cutoff, matching the rest of the platform's mobile
   breakpoint). .hpc-btn-book is shared markup from _header-actions.php, so
   this rule stays scoped to .hpc-profile-page rather than hiding the
   button everywhere it's used. */
@media (max-width: 767.98px) {
    /* Extra .hpc-page-header-actions qualifier beats `.hpc-profile-page .btn`
       (site.css's shared button-sizing rule, further down this file) on
       specificity — that rule and a bare `.hpc-profile-page .hpc-btn-book`
       are equal specificity, and being later in the cascade it was silently
       winning and re-showing this button. */
    .hpc-profile-page .hpc-page-header-actions .hpc-btn-book {
        display: none;
    }
    /* Same reasoning as Book above — My Rewards duplicates the mobile bottom
       bar's own Rewards/dashboard destination, so it's redundant as a
       full-width row on this page at phone width. .hpc-btn-rewards is
       shared markup (_header-actions.php's 'rewards' case, also used on the
       My Pets page), so this stays scoped to .hpc-profile-page. */
    .hpc-profile-page .hpc-page-header-actions .hpc-btn-rewards {
        display: none;
    }
}
/* Frosted pill over the gradient rather than a solid button — reads on any
   --hpc-primary/--hpc-accent combination a customer instance picks, since
   it never needs its own contrasting fill colour. Shared base for both
   banner buttons (Edit Profile / Log Out); each side-specific class only
   sets which corner it sits in. */
.hpc-profile-banner-btn {
    position: absolute;
    top: .75rem;
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .4rem .9rem;
    border-radius: 999px;
    background: rgba(255, 255, 255, .18);
    border: 1px solid rgba(255, 255, 255, .4);
    color: #fff;
    font-size: .8rem;
    font-weight: 600;
    text-decoration: none;
    backdrop-filter: blur(4px);
    transition: background .15s ease, transform .15s ease;
}
/* Belt-and-braces for the JS-side logoutBtn.disabled toggle in
   profileedit.js — normally this button is also display:none (the
   .hpc-logout-slot hide rule) during a normal edit session, so this never
   renders in the ordinary case. Only matters if that hide rule ever fails
   to apply for some reason: a disabled control must still visibly read as
   inert, not identical to the live one sitting under it. */
.hpc-profile-banner-btn:disabled {
    opacity: .5;
    cursor: not-allowed;
    pointer-events: none;
}
.hpc-profile-banner-btn:hover,
.hpc-profile-banner-btn:focus-visible {
    background: rgba(255, 255, 255, .3);
    color: #fff;
    transform: translateY(-1px);
}
.hpc-profile-edit-btn,
.hpc-profile-logout-btn {
    padding: .3rem .7rem;
    font-size: .72rem;
}
/* Log Out on the left, Edit Profile on the right — deliberate swap from the
   class names' natural left-to-right reading order; positioning is driven
   entirely by these two rules so the markup order never has to match. Fixed
   `width` (not min-width) forces the two pills to be pixel-identical
   regardless of their different text length or any native <button> vs <a>
   chrome/appearance differences — a min-width floor alone left it up to
   each element's own intrinsic sizing, which is what actually produced the
   "Log Out looks bigger" mismatch. */
.hpc-profile-banner-btn {
    width: 9rem;
    justify-content: center;
    white-space: nowrap;
    box-sizing: border-box;
    appearance: none;
    -webkit-appearance: none;
}
.hpc-profile-edit-btn {
    right: .75rem;
}
.hpc-profile-logout-btn {
    left: .75rem;
}
/* The logout control used to be a <form><button> here, with the form kept
   out of the banner's box model via display:contents. The form is now
   hoisted OUTSIDE the banner entirely (see default.php's own comment on
   why — nested <form> elements are invalid HTML and the browser silently
   drops them, which was the actual root cause of the show/hide rule below
   never applying) and the button associates to it purely via the HTML5
   form="" attribute, so there's no wrapper element left to reset here. */
.hpc-profile-banner-btn.hpc-profile-logout-btn {
    /* font-family only — NOT the `font` shorthand. The shorthand resets
       font-size/weight/line-height too, and being two classes it outranks
       the .72rem font-size set on .hpc-profile-edit-btn/.hpc-profile-logout-
       btn above, silently kicking this button back to its UA-default 16px
       and inflating its height past the Edit Profile anchor next to it —
       confirmed via computed-style inspection (button: 16px/36.05px tall,
       anchor: 11.52px/29.36px tall, same padding). */
    font-family: inherit;
    cursor: pointer;
}
/* Pulls the avatar up so it overlaps the banner (Discord/Meta signature
   move) — negative margin sized to roughly half the avatar's 4.5rem height,
   not the whole thing, so the ring still sits mostly inside the card body. */
.hpc-profile-card-header {
    border-bottom: 1px solid var(--hpc-border-light);
    padding-top: 0;
    margin-top: -2.5rem;
}
.hpc-profile-username {
    display: block;
    color: var(--hpc-text-soft);
    font-size: .875rem;
}
.hpc-profile-chips {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: .5rem;
    margin-top: .85rem;
}
.hpc-profile-chip {
    display: inline-flex;
    align-items: baseline;
    gap: .3rem;
    padding: .4rem .85rem;
    border-radius: 999px;
    background: var(--hpc-surface-sunken);
    color: var(--hpc-text-soft);
    font-size: .8rem;
}
.hpc-profile-chip-value {
    font-weight: 700;
}
.hpc-profile-chip-theme {
    background: var(--hpc-primary-tint);
    color: var(--hpc-primary);
    font-weight: 600;
    text-transform: capitalize;
}
.hpc-profile-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 0 1.5rem;
    padding: 1rem 1.25rem 1.25rem;
}
@media (max-width: 575.98px) {
    .hpc-profile-grid {
        grid-template-columns: 1fr;
    }
}
.hpc-profile-row {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: .1rem;
    padding: .5rem .25rem;
}

/* Edit page — consolidated card, 2-up field grid, bottom-left action bar
   (deliberately NOT sticky-top any more — that competed with the header
   row's Book/My Pets/Dashboard actions for the same top-right space).
   3.30: enlarged from a thin, visually-disconnected row into a clear,
   properly-spaced action bar — Save reads as the obviously primary action,
   both buttons get comfortable tap targets, and the bar sits its own
   margin below the card rather than butting straight up against it. */
.hpc-edit-actions-bottom {
    display: flex;
    gap: .75rem;
    justify-content: flex-start;
    margin-top: 1.5rem;
    padding: 0;
}
.hpc-edit-actions-bottom .btn {
    min-height: 3rem;
    padding-inline: 1.5rem;
    font-size: .95rem;
    font-weight: 600;
}
@media (max-width: 575.98px) {
    .hpc-edit-actions-bottom {
        flex-direction: column-reverse;
    }
    .hpc-edit-actions-bottom .btn {
        width: 100%;
    }
}
.hpc-edit-section {
    padding: 1rem 1.25rem;
    border-bottom: 1px solid var(--hpc-border-light);
}
.hpc-edit-section:last-child {
    border-bottom: none;
}
.hpc-edit-section-title {
    font-size: .8rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--hpc-text-soft);
    margin-bottom: .75rem;
}
/* Fields laid out as two INDEPENDENT columns, each its own vertical flex
   stack (.hpc-edit-col), not a single row-paired CSS grid. Two earlier
   attempts at row-pairing via CSS-grid dense auto-placement both broke in
   production — a full-width checklist injected mid-sequence produced empty
   grid cells, and forcing it into its own row instead produced a doubled
   row-gap. A plain two-column flex layout has no auto-placement step to go
   wrong: each column just renders its own fields top to bottom, in DOM
   order — safe to run side-by-side at EVERY width, including phones, unlike
   the old dense grid. The two columns are a reading-order grouping (Left:
   Name/Phone/Password; Right: Username/Email/Theme), not a left-right field
   pairing, so each stays internally in its own natural top-to-bottom order
   regardless of viewport. */
.hpc-edit-columns {
    display: flex;
    flex-direction: row;
    align-items: flex-start;
    gap: 1rem;
}
@media (min-width: 480px) {
    .hpc-edit-columns {
        gap: 1.5rem;
    }
}
@media (min-width: 768px) {
    .hpc-edit-columns {
        gap: 2rem;
    }
}
.hpc-edit-col {
    display: flex;
    flex-direction: column;
    flex: 1 1 0;
    gap: 0;
    min-width: 0;
}
/* Legacy 2-up grid — still used for the rare "leftover" fieldset (anything
   not named in edit.php/registration default.php's explicit column lists,
   e.g. a future core/plugin field). Row-gap stays 0: vertical spacing comes
   from each Joomla-rendered field's own Bootstrap "mb-3" margin, not the
   grid — adding a row-gap on top of that doubles the spacing. */
.hpc-edit-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 0 .5rem;
}
@media (min-width: 480px) {
    .hpc-edit-grid {
        gap: 0 .75rem;
    }
}
@media (min-width: 768px) {
    .hpc-edit-grid {
        gap: 0 1.5rem;
    }
}
/* Small note under a field (e.g. "contact us to change your username") —
   same faint/small treatment as core's own field help text, just ours. */
.hpc-field-note {
    margin: -.65rem 0 .75rem;
    font-size: .8125rem;
    color: var(--hpc-text-faint);
}
.hpc-pw-checklist-wrap {
    margin: .5rem 0 1rem;
}
/* --- Appearance / theme segmented control (3.30) --------------------------
   theme_preference is an untouched `type="radio"` field (plg_user_hpcprofile
   /forms/hpcprofile.xml) — three real radio inputs, core's own RadioField
   layout, Bootstrap5 `.form-check`/`.form-check-label` markup (same
   convention this stylesheet already targets for the consent/comm
   checkboxes elsewhere on this page). This wrapper only restyles that
   existing markup into a compact segmented look — no field/name/JS change,
   so the save path is exactly what it was. `:has()` drives the "selected"
   segment so no click-handler JS is needed. If a future core update ever
   changes the wrapper element away from <fieldset>, this degrades to
   ordinary stacked radio buttons rather than breaking anything. */
/* 3.34.45 guessed at this markup shape twice (3.34.42's "must be a
   <fieldset>", 3.34.43's "no fieldset, style .hpc-theme-switch itself") and
   fixed it structurally instead of guessing a third tag/class name: `:has()`
   finds WHATEVER element directly contains the .form-check options and
   flexes that element specifically. Ground-truthed live since: the real
   wrapper is Joomla's own legacy `<div class="radio">`, matched here by the
   `*:has(> .form-check)` half of this selector — .hpc-theme-switch itself is
   the <fieldset> one level further out and is NOT that direct parent, so it
   gets reset to plain flow below instead of carrying its own copy of the
   pill styling (that produced a visible box-in-a-box: the fieldset's own
   background/shadow wrapping the already-styled .radio pill inside it). */
.hpc-theme-switch {
    margin: 0;
    padding: 0;
    border: 0;
}
.hpc-theme-switch *:has(> .form-check) {
    display: flex;
    gap: .3rem;
    margin: 0;
    padding: .25rem;
    border-radius: 999px;
    background: var(--hpc-surface-sunken);
    flex-wrap: wrap;
    /* Same recessed-groove treatment as .hpc-tab-switch (Profile/My Pets,
       Activity's Bookings/Orders) — border swapped for the inset shadow so
       both segmented controls read as the same component family. */
    box-shadow: inset 0 2px 4px rgba(15, 23, 42, .3),
                inset 0 -1px 0 rgba(255, 255, 255, .8);
}
[data-bs-theme="dark"] .hpc-theme-switch *:has(> .form-check) {
    box-shadow: inset 0 2px 4px rgba(0, 0, 0, .65),
                inset 0 1px 0 color-mix(in srgb, white 14%, transparent);
}
/* Joomla renders its own group label/legend ("Theme") ahead of the options —
   redundant with the section's own "Appearance" heading directly above this
   control (see the template), so it is hidden here. Both possible tag names
   covered (a <fieldset>'s heading is a <legend>, not a <label>). `label`
   stays a DIRECT-child-only match — a descendant match would also catch
   every per-option `.form-check-label`, which must stay visible; `legend`
   has no such conflict (never nested inside .form-check) so it is matched
   at any depth. */
.hpc-theme-switch > label,
.hpc-theme-switch legend {
    display: none;
}
.hpc-theme-switch .form-check {
    /* position:relative scopes the hidden input below to THIS box, not
       whatever positioned ancestor is nearest in the DOM. Without it, the
       input's containing block was .hpc-profile-card (position:sticky in
       edit mode — see that rule's own comment), so its "auto" position
       resolved somewhere relative to the CARD, not next to its own label.
       Reported live: clicking a theme button made the scroll region jump —
       root cause was the browser's focus scroll-into-view chasing that
       mispositioned, invisible target, landing well outside where the
       visible label actually sits. */
    position: relative;
    flex: 1 1 0;
    min-width: 0;
    margin: 0;
    padding: 0;
    text-align: center;
}
.hpc-theme-switch .form-check-input {
    position: absolute;
    top: 0;
    left: 0;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}
.hpc-theme-switch .form-check-label {
    /* Sized/shaped to match .hpc-tab-label exactly (Profile/My Pets,
       Activity's Bookings/Orders) — same pill segment, same component
       family, just driven by radio :checked instead of JS .is-active. */
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 2.75rem;
    margin: 0;
    padding: .4rem 1.1rem;
    border-radius: 999px;
    font-size: .85rem;
    font-weight: 600;
    line-height: 1.2;
    color: var(--hpc-text-soft);
    cursor: pointer;
    user-select: none;
    transition: background-color .15s ease, color .15s ease;
}
.hpc-theme-switch .form-check-label:hover {
    color: var(--hpc-text);
}
.hpc-theme-switch .form-check:has(.form-check-input:checked) .form-check-label {
    background: var(--hpc-primary);
    color: var(--hpc-primary-contrast);
}
.hpc-theme-switch .form-check-input:focus-visible ~ .form-check-label,
.hpc-theme-switch .form-check-input:focus-visible + .form-check-label {
    outline: 2px solid var(--hpc-primary);
    outline-offset: 2px;
}
@media (max-width: 420px) {
    .hpc-theme-switch,
    .hpc-theme-switch *:has(> .form-check) {
        flex-direction: column;
    }
}
/* The field's help text ("Applies across the whole site...") renders
   somewhere inside .hpc-theme-switch alongside the .form-check buttons,
   using Bootstrap's own .form-text colour — too dark/low-contrast against
   this page's dark surface. Matched as a descendant, not a direct child
   (3.34.45's direct-child assumption was wrong — still visibly unstyled
   live), and pulled fully out of the flex "pill" row it might be sitting
   inside via a hard reset, onto its own full-width line with an explicit
   theme-aware token instead of Bootstrap's default. */
.hpc-theme-switch .form-text {
    display: block;
    flex-basis: 100%;
    width: 100%;
    margin: .6rem 0 0 !important;
    padding: 0 !important;
    border: 0 !important;
    background: none !important;
    font-size: .8125rem;
    color: var(--hpc-text-soft) !important;
}
/* Every other field-description ".form-text" on this page — reported live:
   the "Start typing and pick your address..." text under Find your address
   didn't match the rest of the dark-mode page (same underlying issue as
   the theme-switch block above, just never generalised past that one
   spot). Bootstrap's own .form-text colour is too low-contrast against
   this page's dark surface; same themed token, applied page-wide instead
   of one field at a time. Lower specificity than the two more detailed
   overrides above/below (theme-switch, password) — those still win their
   own extra layout resets, but all three agree on the same colour. */
.hpc-profile-page .form-text {
    color: var(--hpc-text-soft) !important;
}
/* 3.34.81 — replaces the 3.34.77/79 approach of reskinning the real inputs
   to LOOK like the view-mode address row (never came out pixel-identical,
   only similar, however the CSS was tuned). Now two fully separate blocks
   exist in the DOM — #hpc-addr-fields (the real, normal editable inputs)
   and #hpc-addr-readout (literally the same .hpc-profile-row markup as
   the non-edit-mode view above) — and places.js swaps which is visible.
   ID selectors here are deliberate, not just convenient: they guarantee
   this wins over .hpc-profile-row's own `display: flex` (a bare class
   rule) regardless of source order, the same class of bug that made the
   3.34.77/78 Log Out hide rules unreliable. #hpc-addr-readout starts
   hidden — the DEFAULT visible state is #hpc-addr-fields, the manual-entry
   fallback if Places never confirms live (see places.js's own comment on
   why this can never be flipped ahead of a CONFIRMED-working
   autocomplete). */
#hpc-addr-readout {
    display: none;
}
.hpc-address-col {
    display: flex;
    flex-direction: column;
}
/* Address column + Appearance column, side by side (3.34.77 — was a
   generic 6-cell auto-flow grid mixing every address field in with
   Appearance; reworked once the address details became their own locked
   read-out block, see .hpc-address-col/.hpc-addr-readout above). Exactly 2
   grid children now: .hpc-address-col (Find your address + the locked
   read-out) in column 1, the Appearance field in column 2 — `align-items:
   start` keeps Appearance from being stretched to the taller left
   column's height (CSS Grid's row-stretch default, the same issue
   3.34.47 fixed for the old shape). */
.hpc-address-appearance-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    grid-auto-rows: min-content;
    align-items: start;
    gap: 0 1.5rem;
}
@media (max-width: 575.98px) {
    .hpc-address-appearance-grid {
        grid-template-columns: 1fr;
        /* Panels touch with no row-gap otherwise — the 0 above was fine
           for borderless fields but reads wrong once each side is boxed. */
        gap: 1.25rem;
    }
}
/* One panel class, used for both columns of the grid above. Border +
   radius + padding is the whole trick — the fields inside keep their
   existing markup/rules untouched. */
.hpc-field-panel {
    background: var(--hpc-surface);
    /* border: 1px solid var(--hpc-border); */
    border-radius: .875rem;
    padding: 1.1rem 1.15rem 1.25rem;
}
.hpc-address-appearance-grid .hpc-edit-field {
    min-width: 0;
}
.hpc-theme-field .form-label {
    display: block;
}
/* Core's own password field renders a static "Minimum Requirements —
   Characters: 12, Numbers: 1, ..." line under the input (Bootstrap5
   .form-text, aria-describedby'd from the input) — redundant now that the
   compact chips below cover the same thresholds live. 3.34.45: scoped via
   the template's own [data-hpc-pw-field] wrapper (one per password field)
   instead of :has(#jform_password1) — the :has() form only ever matched
   password1's own wrapper by construction, but relying on :has() support at
   all was an unnecessary risk for a rule this easy to scope directly. */
[data-hpc-pw-field] .form-text {
    display: none;
}
/* Compact password requirements checklist (3.30) — replaces the earlier
   "cramped outlined pills" (3.29.36 pass) and, before that, an even
   cramped row of tiny pills — both read as clutter. Plain vertical list,
   each row just a small tick + label, no boxes/borders/backgrounds. Shared
   by registration Step 2 and the profile edit page's password section;
   `.hpc-pw-chip`/`.hpc-pw-tick` class names kept as-is (renaming would
   touch passwordcheck.js's DOM contract for no benefit).
   3.34.44: a satisfied rule now collapses out of the list instead of just
   flipping muted→satisfied in place — explicit requirement, reversing the
   3.30 call to keep every row always visible. `gap` is not used on the
   flex container any more (a flex `gap` doesn't collapse for one child
   shrinking to zero height, leaving a stray blank band); each chip carries
   its own bottom margin instead, so height/opacity/margin can all collapse
   together. `max-height` (not `height`) is what a CSS transition can
   actually animate on an auto-sized element. */
.hpc-pw-chips-heading {
    margin: 0 0 .4rem;
    font-size: .8rem;
    font-weight: 600;
    color: var(--hpc-text-faint);
    transition: opacity .3s ease, max-height .3s ease, margin .3s ease;
    overflow: hidden;
    max-height: 1.4rem;
}
.hpc-pw-chips {
    display: flex;
    flex-direction: column;
}
.hpc-pw-chip {
    display: flex;
    align-items: center;
    gap: .55rem;
    font-size: .85rem;
    line-height: 1.3;
    color: var(--hpc-text-faint);
    max-height: 1.6rem;
    margin-bottom: .4rem;
    opacity: 1;
    overflow: hidden;
    transition: color .15s ease, opacity .3s ease, max-height .3s ease, margin .3s ease;
}
.hpc-pw-chip.is-met {
    color: var(--hpc-text);
    opacity: 0;
    max-height: 0;
    margin-bottom: 0;
}
/* Once every rule has faded out there's nothing left under the heading —
   fade it away too rather than leaving it labelling an empty list.
   passwordcheck.js already hides the whole wrap in that state
   (`wrap.hidden = allMet`); this covers the moment just before that, while
   the collapse transition is still playing. */
.hpc-pw-chips-heading:has(~ .hpc-pw-chips:not(:has(.hpc-pw-chip:not(.is-met)))) {
    opacity: 0;
    max-height: 0;
    margin: 0;
}
.hpc-pw-tick {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.05rem;
    height: 1.05rem;
    flex: 0 0 auto;
    border-radius: 50%;
    border: 1.5px solid var(--hpc-border);
    line-height: 1;
    transition: background-color .15s ease, border-color .15s ease;
}
.hpc-pw-chip.is-met .hpc-pw-tick {
    background: var(--hpc-success, #2e9e57);
    border-color: var(--hpc-success, #2e9e57);
}
.hpc-pw-tick::before {
    content: '';
}
.hpc-pw-chip.is-met .hpc-pw-tick::before {
    content: '\2713';
    display: block;
    font-size: .55rem;
    color: var(--hpc-surface, #fff);
}
@media (prefers-reduced-motion: reduce) {
    .hpc-pw-chip,
    .hpc-pw-chips-heading {
        transition: none;
    }
}

/* ======================================================================
   Pets & Rewards — card grid, thumbnails, points widgets (3.15.1).
   site/tmpl/pets/default.php + dashboard/default.php + the shared
   _pet-points-widget.php partial reference ~30 hpc-* classes that had ZERO
   CSS anywhere in this file (confirmed by grepping every class used against
   every selector actually defined) — not a tuning gap, entirely unstyled:
   the points ring rendered as bare text, the hairline was invisible, pet
   thumbnails had no aspect-ratio/crop treatment at all. This is the direct
   fix for the "dog's breakfast" report, and deliberately placed and built
   FIRST among this component's card-based views (ahead of the booking
   calendar section below, which already got its own dedicated pass in
   v3.13.0) — Pets is the identity surface every other feature hangs off,
   so it gets the CSS attention first.
   ====================================================================== */

/* Card lift + full-card click target — shared by every card grid in this
   section (Pets grid, memorialized section, dashboard pet cards). Built on
   the existing elevation tokens so both themes come for free. */
.hpc-card-interactive {
    position: relative;
    background: var(--hpc-surface);
    border-color: var(--hpc-border);
    box-shadow: var(--hpc-shadow-resting);
    transition: transform .15s ease, box-shadow .15s ease;
}
.hpc-card-interactive:hover,
.hpc-card-interactive:focus-within {
    transform: translateY(-3px);
    box-shadow: var(--hpc-shadow-lifted);
}
/* The whole-card click target on the dashboard's pet cards (3.19.16) is a
   plain div[role="button"], not a native control — cursor/focus-ring need
   spelling out explicitly since a div gets neither by default. */
.hpc-card-interactive[role="button"] {
    cursor: pointer;
}
.hpc-card-interactive[role="button"]:focus-visible {
    outline: 2px solid var(--hpc-primary);
    outline-offset: 2px;
}
/* NOTE: deliberately no `position: relative` here — the whole point of a
   stretched-link is that its ::after sizes against the nearest *positioned*
   ancestor. .hpc-card-interactive (the actual .card) already carries
   position:relative above; if this link were positioned too, its own
   (text-sized) box would become that ancestor instead, and the ::after would
   only ever cover the link text, not the card — exactly the "only the image/
   name is clickable" bug this fixes. */
.hpc-card-stretched-link::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;
}
/* Real bug (found from a live report — "not all cards are clickable"): the
   overlay above always paints above any non-positioned sibling regardless of
   DOM order, so it was silently swallowing taps on the Pets grid's OWN
   footer controls — the "Book" quick-action and the Lost/Memorialized card's
   Reactivate/Hide buttons — landing on the profile page instead of doing
   what the button said. Lifting those specific controls into their own
   stacking context (z-index above the overlay's 1) restores them without
   touching the overlay itself or any card that has no footer. */
.hpc-card-interactive .card-footer,
.hpc-card-interactive a.hcc-card-thumb {
    position: relative;
    z-index: 2;
}
/* A Lost/Memorialized pet's card is still fully usable (Reactivate/Hide) but
   visually reads as "not an active part of the roster" — a soft desaturated
   treatment rather than hiding anything. */
.hpc-pet-readonly {
    opacity: .78;
    filter: saturate(.7);
}

/* Pet card thumbnail (3.17.0: was a fixed 4:3 cover-crop — now sizes to the
   photo's own aspect ratio via object-fit:contain, so a portrait photo isn't
   cropped. 3.23.7: the box itself is a fixed height (not just capped) so
   every card's image slot — real photo, placeholder, or business logo — is
   the same height across a row; otherwise the content below (name/breed/
   age/points/footer button) starts at a different Y per card and rows never
   line up. object-fit:contain still letterboxes rather than cropping. */
.hcc-card-thumb {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 11rem;
    overflow: hidden;
    background: var(--hpc-surface-sunken);
    /* Must match the parent .card's own border-radius (--hpc-radius-lg,
       3.26) — this used the smaller --hpc-radius until now, so the photo's
       square top corners poked past the card's more-rounded edge ("strange
       radius" reported live). */
    border-top-left-radius: var(--hpc-radius-lg);
    border-top-right-radius: var(--hpc-radius-lg);
}
.hcc-card-thumb img {
    display: block;
    width: 100%;
    max-height: 100%;
    object-fit: contain;
    transition: transform .25s ease;
}
.hpc-card-interactive:hover .hcc-card-thumb img {
    transform: scale(1.045);
}
.hcc-card-thumb-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    font-size: 2.25rem;
    color: var(--hpc-text-faint);
}

/* My Pets + My Rewards dashboard pet-card grid — capped column count per
   breakpoint (max 2 per row on mobile, max 3 on desktop) that always
   CENTERS its row regardless of how many cards it holds, per explicit
   request ("no matter if min or max div should center"). A hard
   `grid-template-columns` (3.19.14) filled the full row width even with a
   single card, so 1 pet never centered — flex-wrap with a capped
   flex-basis is the direct fix: a row with fewer cards than the cap simply
   doesn't stretch to fill it, and `justify-content: center` centers
   whatever's actually on that row, 1 up to the cap. Shared by both grids
   using these same two class names: My Pets (site/tmpl/pets/default.php,
   incl. the memorialized section) and the My Rewards dashboard
   (dashboard/default.php). */
.hpc-pets-grid {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 1rem;
}
.hpc-pets-grid-item {
    flex: 0 1 calc(50% - .5rem);
    max-width: calc(50% - .5rem);
}
@media (min-width: 768px) {
    .hpc-pets-grid-item {
        flex-basis: calc(33.333% - .667rem);
        max-width: calc(33.333% - .667rem);
    }
}
/* Slightly tighter card body — the request specifically asked for the cards
   to be "a little more vertically compact" once the thumbnail stopped
   forcing a fixed crop height. */
.hpc-pets-grid-item .card-body,
.hpc-dash-pet-card .card-body {
    padding: .85rem 1rem;
}
.hpc-pets-grid-item .card-title {
    margin-bottom: .3rem;
}
.hpc-pets-grid-item ul.list-unstyled li {
    line-height: 1.3;
}
/* 3.23.6 tried anchoring the age/weight details + points widget to the
   BOTTOM of the card (margin-top: auto on a flex column) so they'd line up
   across a row regardless of how much text wrapped above them — confirmed
   live to be exactly backwards from what was actually wanted: it opened a
   large dead gap between the breed line and the age/weight list whenever a
   sibling card's photo forced this card taller via h-100. The real ask was
   the opposite — pet details (name/species/breed) and the points section
   directly BELOW them, anchored to the TOP as one compact block, with no
   gap. Plain top-down flow does that on its own; nothing needs to be
   pushed anywhere. Only the footer button (a sibling of .card-body, not
   inside it) still naturally lands at the bottom of the card. */
.hpc-pets-grid-item .card-body > ul.list-unstyled,
.hpc-dash-pet-card .card-body > .hpc-points-widget {
    margin-top: 0;
}

/* Memorialized-section divider/toggle (site/tmpl/pets/default.php) — full
   width of the active-pets block, doubling as the visual separator between
   the active grid above and the memorialized grid below (replaces the old
   header heart-icon toggle). A plain themed button, not a card. */
.hpc-pets-section-divider {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .5rem;
    width: 100%;
    margin-top: 1.5rem;
    padding: .6rem 1rem;
    background: var(--hpc-surface-sunken);
    border: 1px dashed var(--hpc-border);
    border-radius: var(--hpc-radius);
    color: var(--hpc-text-soft);
    font-size: .9rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color .15s ease, color .15s ease;
}
.hpc-pets-section-divider:hover,
.hpc-pets-section-divider:focus-visible {
    background: var(--hpc-surface);
    color: var(--hpc-text);
}
.hpc-pets-section-divider:focus-visible {
    outline: 2px solid var(--hpc-primary);
    outline-offset: 2px;
}

/* Star-tier glow/pulse (3.17.0). Progresses with the pet's CURRENT tier only
   (bronze -> silver -> gold), never regresses — driven purely by the tier
   computed from the pet's live point total, so there's nothing to persist
   server-side for "don't regress." The pulse is separate and temporary: JS
   (confetti.js) removes it the first time that tier's card has been opened,
   tracked client-side via localStorage since there's no "seen" column. */
/* 3.26 (retuned): first pass kept a 1px solid ring alongside the blur —
   still read as a drawn border, not a glow, on a live screenshot. Removed
   the ring outright: two pure blurred shadow layers only (a tighter one for
   definition near the edge, a wider one for the ambient halo), no hard line
   at any width. Also fixes the earlier specificity bug — this selector
   matches `.card`'s own specificity (paired with .hpc-card-interactive,
   which every glow-eligible card already carries), so it can't be silently
   replaced by the generic card rule above, and composes with --hpc-card-
   shadow (the same token that rule draws from) so the glow layers ON TOP of
   the card's normal elevation instead of replacing it. */
.hpc-star-glow.hpc-card-interactive {
    --hpc-glow-color: var(--hpc-star-bronze);
    box-shadow: 0 6px 16px -4px color-mix(in srgb, var(--hpc-glow-color) 55%, transparent),
                0 0 28px 4px color-mix(in srgb, var(--hpc-glow-color) 35%, transparent),
                var(--hpc-card-shadow);
}
.hpc-star-glow[data-hpc-tier="silver"] { --hpc-glow-color: var(--hpc-star-silver); }
.hpc-star-glow[data-hpc-tier="gold"]   { --hpc-glow-color: var(--hpc-star-gold); }
.hpc-star-glow.hpc-card-interactive:hover,
.hpc-star-glow.hpc-card-interactive:focus-within {
    box-shadow: 0 10px 22px -4px color-mix(in srgb, var(--hpc-glow-color) 70%, transparent),
                0 0 40px 8px color-mix(in srgb, var(--hpc-glow-color) 50%, transparent),
                var(--hpc-shadow-lifted);
}
.hpc-star-glow.hpc-pulse {
    animation: hpc-star-pulse 2.2s ease-in-out infinite;
}
@keyframes hpc-star-pulse {
    0%, 100% {
        box-shadow: 0 6px 16px -4px color-mix(in srgb, var(--hpc-glow-color) 55%, transparent),
                    0 0 28px 4px color-mix(in srgb, var(--hpc-glow-color) 35%, transparent),
                    var(--hpc-card-shadow);
    }
    50% {
        box-shadow: 0 10px 26px -4px color-mix(in srgb, var(--hpc-glow-color) 80%, transparent),
                    0 0 48px 10px color-mix(in srgb, var(--hpc-glow-color) 60%, transparent),
                    var(--hpc-shadow-lifted);
    }
}
@media (prefers-reduced-motion: reduce) {
    .hpc-star-glow.hpc-pulse {
        animation: none;
    }
}

/* First-reveal confetti (media/com_happypetconnect/js/confetti.js). Falls
   from the top of the viewport with randomized drift/rotation, cleaned up
   (JS-forced) after 3s — "no confetti after 3 seconds, screen should be
   clean" per the request. */
.hpc-confetti-layer {
    position: fixed;
    inset: 0;
    z-index: 1090;
    pointer-events: none;
    overflow: hidden;
}
.hpc-confetti-star {
    position: absolute;
    top: -2rem;
    animation-name: hpc-confetti-fall;
    animation-timing-function: ease-in;
    animation-fill-mode: forwards;
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .35));
}
@keyframes hpc-confetti-fall {
    0% {
        transform: translateY(0) translateX(0) rotate(0deg);
        opacity: 1;
    }
    100% {
        transform: translateY(105vh) translateX(var(--hpc-confetti-drift, 40px)) rotate(360deg);
        opacity: .15;
    }
}
@media (prefers-reduced-motion: reduce) {
    .hpc-confetti-layer {
        display: none;
    }
}

/* Dashboard ("My Rewards") pet-card avatar — a round crop distinct from the
   Pets grid's rectangular one, since this card's own layout puts it beside
   the name rather than above it. */
.hpc-dash-pet-thumb {
    width: 3.5rem;
    height: 3.5rem;
    border-radius: 50%;
    overflow: hidden;
    background: var(--hpc-surface-sunken);
    border: 1px solid var(--hpc-border);
    display: flex;
    align-items: center;
    justify-content: center;
}
.hpc-dash-pet-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.hpc-dash-pet-thumb-placeholder {
    font-size: 1.4rem;
    color: var(--hpc-text-faint);
}

/* Dashboard hero — total points + rules + bronze/silver/gold tally, laid out
   as an actual strip on the brand colour (never a hardcoded literal — see
   this component's own "platform product" rule) instead of the plain block
   it was rendering as with no CSS behind it at all. */
.hpc-stat-strip {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 1.25rem;
    background: var(--hpc-primary);
    color: var(--hpc-primary-contrast);
    border-radius: var(--hpc-radius);
    padding: 1.35rem 1.5rem;
    box-shadow: var(--hpc-shadow-lifted);
}
/* Dark mode (3.26): the site's single highest-visibility hero block gets the
   gradient + glow identity — matching a solid-fill CTA, this is decoration
   on top of an already-full-contrast element (white text stays white),
   never a substitute for contrast. */
[data-bs-theme="dark"] .hpc-stat-strip {
    background: var(--hpc-gradient-primary);
    box-shadow: var(--hpc-shadow-lifted), var(--hpc-glow-shadow);
}
/* 3.17.0: the strip is now itself a <button> opening the rewards modal —
   reset the button-default font/cursor, everything else is inherited. */
.hpc-stat-strip-btn {
    cursor: pointer;
    font: inherit;
    transition: transform .15s ease, box-shadow .15s ease;
}
.hpc-stat-strip-btn:hover,
.hpc-stat-strip-btn:focus-visible {
    transform: translateY(-2px);
    box-shadow: var(--hpc-shadow-resting), 0 0 0 3px rgba(255, 255, 255, .25);
}
.hpc-rewards-modal-body .hpc-stat-rules {
    color: var(--hpc-text-soft);
}
.hpc-rewards-ledger {
    max-height: 50vh;
    overflow-y: auto;
}
/* Pet-filter step (simplemodal.js) toggles the native `hidden` attribute on
   rows — but each row also carries Bootstrap's `.d-flex` utility, which is
   `!important` and (being an authored rule) beats the UA default `[hidden]
   { display: none }` regardless of attribute-selector specificity. Forced
   directly so the filter actually hides rows instead of silently no-op'ing. */
.hpc-rewards-ledger li[hidden] {
    display: none !important;
}
.hpc-stat-total {
    flex-shrink: 0;
    text-align: center;
    padding-right: 1.25rem;
    border-right: 1px solid rgba(255, 255, 255, .25);
}
.hpc-stat-total-value {
    display: block;
    font-size: 2.35rem;
    font-weight: 800;
    line-height: 1;
}
.hpc-stat-total-label {
    display: block;
    margin-top: .2rem;
    font-size: .68rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .06em;
    opacity: .85;
}
.hpc-stat-mid {
    flex: 1 1 240px;
    min-width: 0;
}
.hpc-stat-mid p {
    margin-bottom: .4rem;
}
.hpc-stat-rules {
    font-size: .85rem;
    opacity: .92;
}
.hpc-stat-history-btn {
    border-color: rgba(255, 255, 255, .55);
    color: #fff;
}
.hpc-stat-history-btn:hover,
.hpc-stat-history-btn:focus-visible {
    background: #fff;
    border-color: #fff;
    color: var(--hpc-primary);
}
@media (max-width: 575.98px) {
    .hpc-stat-strip {
        padding: 1.1rem;
        gap: .85rem;
    }
    .hpc-stat-total {
        border-right: none;
        padding-right: 0;
    }
}

/* Per-pet "progress to next star" widget (_pet-points-widget.php) — a
   conic-gradient ring reading --hpc-ring-pct/--hpc-ring-color, the two
   custom properties the template already emits inline per pet. Nothing ever
   turned those into an actual ring before this — it rendered as plain text
   sitting next to an invisible 0x0 element. */
.hpc-points-widget {
    padding-top: .75rem;
    border-top: 1px solid var(--hpc-border-light);
}
.hpc-points-widget-top {
    display: flex;
    align-items: center;
    gap: .75rem;
}
.hpc-points-ring {
    --hpc-ring-pct: 0;
    --hpc-ring-color: var(--hpc-primary);
    position: relative;
    flex-shrink: 0;
    width: 3.1rem;
    height: 3.1rem;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: conic-gradient(var(--hpc-ring-color) calc(var(--hpc-ring-pct) * 1%), var(--hpc-border) 0);
    transition: background .3s ease;
}
.hpc-points-ring::before {
    content: '';
    position: absolute;
    inset: 4px;
    border-radius: 50%;
    background: var(--hpc-surface);
}
.hpc-points-ring-mark {
    position: relative;
    z-index: 1;
    font-size: .72rem;
    font-weight: 700;
    color: var(--hpc-text-soft);
}
.hpc-points-ring-mark.hpc-star {
    font-size: 1.15rem;
    color: var(--hpc-ring-color);
}
.hpc-points-ring-mark sup {
    font-size: .55em;
}
.hpc-points-summary {
    display: flex;
    flex-direction: column;
    min-width: 0;
}
.hpc-points-value {
    font-weight: 700;
    font-size: .95rem;
}
.hpc-points-caption {
    font-size: .78rem;
}
.hpc-hairline {
    display: flex;
    gap: 3px;
    margin: .5rem 0 0;
    cursor: help;
}
.hpc-hairline-seg {
    position: relative;
    flex: 1 1 0;
    height: 4px;
    border-radius: 2px;
    background: var(--hpc-border);
    overflow: hidden;
}
.hpc-hairline-bronze { --hpc-seg-color: var(--hpc-star-bronze); }
.hpc-hairline-silver { --hpc-seg-color: var(--hpc-star-silver); }
.hpc-hairline-gold { --hpc-seg-color: var(--hpc-star-gold); }
.hpc-hairline-seg.is-earned {
    background: var(--hpc-seg-color);
}
.hpc-hairline-seg.is-current::after {
    content: '';
    position: absolute;
    inset: 0;
    width: var(--hpc-seg-fill, 0%);
    background: var(--hpc-seg-color);
    border-radius: inherit;
    transition: width .3s ease;
}
.hpc-points-widget-footer {
    font-size: .78rem;
}
/* Per-pet star-tier badges (3.23.11) — see _pet-points-widget.php's own
   comment: this is the bronze/silver/gold pill look the dashboard's "Total
   Points" banner used to show as an account-level tally (wrong — removed
   3.23.4), reapplied correctly per pet, right under the hairline. Earned =
   solid fill in the tier's own colour; the tier currently being chased gets
   a coloured outline so it reads as "next up," not equal to an already-won
   one; anything further out stays flat/muted so the row never looks broken
   or empty even at 0 points. */
.hpc-tier-badges {
    display: flex;
    width: 100%;
    gap: 3px; /* matches .hpc-hairline's own gap, so badges stay aligned under their segment */
    margin-top: .6rem;
}
.hpc-tier-badge {
    display: inline-flex;
    flex: 1;
    min-width: 0;
    align-items: center;
    justify-content: center;
    padding: .15rem .35rem;
    border-radius: 1rem;
    font-size: .68rem;
    font-weight: 600;
    line-height: 1.4;
    background: var(--hpc-surface-sunken);
    color: var(--hpc-text-soft);
    border: 1px solid var(--hpc-border);
}
.hpc-tier-badge-star {
    font-size: .8em;
}
.hpc-tier-badge-bronze .hpc-tier-badge-star { color: var(--hpc-star-bronze); }
.hpc-tier-badge-silver .hpc-tier-badge-star { color: var(--hpc-star-silver); }
.hpc-tier-badge-gold   .hpc-tier-badge-star { color: var(--hpc-star-gold); }
.hpc-tier-badge.is-current {
    border-color: var(--hpc-tier-badge-color, var(--hpc-primary));
    color: var(--hpc-text);
}
.hpc-tier-badge-bronze.is-current { --hpc-tier-badge-color: var(--hpc-star-bronze); }
.hpc-tier-badge-silver.is-current { --hpc-tier-badge-color: var(--hpc-star-silver); }
.hpc-tier-badge-gold.is-current   { --hpc-tier-badge-color: var(--hpc-star-gold); }
.hpc-tier-badge.is-earned {
    background: var(--hpc-tier-badge-color, var(--hpc-primary));
    border-color: var(--hpc-tier-badge-color, var(--hpc-primary));
    color: #fff;
}
.hpc-tier-badge-bronze.is-earned { --hpc-tier-badge-color: var(--hpc-star-bronze); }
.hpc-tier-badge-silver.is-earned { --hpc-tier-badge-color: var(--hpc-star-silver); }
.hpc-tier-badge-gold.is-earned   { --hpc-tier-badge-color: var(--hpc-star-gold); }
.hpc-tier-badge.is-earned .hpc-tier-badge-star { color: #fff; }

/* ======================================================================
   Store & product — grid card media, gallery hero/thumbnails, buy box.
   Same "confirmed missing by grep" story as the Pets/Rewards section above:
   these classes were referenced in store/default.php and product/default.php
   with zero CSS behind them.
   ====================================================================== */
.hcc-media-card {
    display: block;
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
}
.hcc-media-hero {
    display: block;
    aspect-ratio: 4 / 3;
    overflow: hidden;
    border-radius: var(--hpc-radius);
    background: var(--hpc-surface-sunken);
}
.hcc-media-hero img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    cursor: zoom-in;
}
.hcc-media-hero-empty {
    aspect-ratio: 4 / 3;
    background: var(--hpc-surface-sunken);
    color: var(--hpc-text-faint);
    font-size: 2.5rem;
}
.hcc-media-tile {
    width: 4.5rem;
    height: 4.5rem;
    object-fit: cover;
    cursor: pointer;
    opacity: .82;
    transition: opacity .15s ease, transform .15s ease;
}
.hcc-media-tile:hover,
.hcc-media-tile:focus-visible {
    opacity: 1;
    transform: translateY(-2px);
}
.hcc-buy-box .form-control {
    text-align: center;
}

/* Gallery lightbox (media/com_happypetconnect/js/lightbox.js) */
.hpc-lightbox-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1080;
    background: rgba(10, 12, 16, .92);
    display: flex;
    align-items: center;
    justify-content: center;
}
.hpc-lightbox-img-el {
    max-width: min(92vw, 1100px);
    max-height: 88vh;
    object-fit: contain;
    border-radius: var(--hpc-radius);
}
/* Desktop: a floating ~75% panel rather than the near-fullscreen sizing
   mobile keeps (appropriate there — a phone viewport IS the frame). A
   pointer-driven viewport has room to spare, so full-bleed read as
   "forced/stretched" rather than a deliberate lightbox. */
@media (min-width: 768px) {
    .hpc-lightbox-img-el {
        max-width: 75vw;
        max-height: 75vh;
        box-shadow: 0 20px 60px rgba(0, 0, 0, .5);
    }
}
.hpc-lightbox-btn {
    position: absolute;
    background: rgba(255, 255, 255, .12);
    color: #fff;
    border: 1px solid rgba(255, 255, 255, .3);
    border-radius: 50%;
    width: 2.75rem;
    height: 2.75rem;
    font-size: 1.6rem;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background .15s ease;
}
.hpc-lightbox-btn:hover,
.hpc-lightbox-btn:focus-visible {
    background: rgba(255, 255, 255, .25);
}
/* `.hpc-lightbox-btn`'s own authored `display: flex` above beats the
   browser's UA-default `[hidden]{display:none}` (an author rule always wins
   over a UA rule, regardless of selector specificity) — so JS setting
   `.hidden = true` on Prev/Next alone did nothing visually. Force it. */
.hpc-lightbox-btn[hidden] {
    display: none !important;
}
.hpc-lightbox-close {
    top: 1rem;
    right: 1rem;
}
.hpc-lightbox-prev {
    left: 1rem;
    top: 50%;
    transform: translateY(-50%);
}
.hpc-lightbox-next {
    right: 1rem;
    top: 50%;
    transform: translateY(-50%);
}
.hpc-lightbox-hint {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    opacity: 0;
}
.hpc-lightbox-hint.is-visible {
    opacity: 1;
    transition: opacity .25s ease;
}
.hpc-lightbox-hint-icon {
    font-size: 2.75rem;
    animation: hpc-lightbox-swipe 1s ease-in-out;
    filter: drop-shadow(0 2px 6px rgba(0, 0, 0, .5));
}
@keyframes hpc-lightbox-swipe {
    0%   { transform: translateX(30px); opacity: 0; }
    30%  { opacity: 1; }
    70%  { opacity: 1; }
    100% { transform: translateX(-30px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
    .hpc-lightbox-hint-icon {
        animation: none;
    }
}

/* ======================================================================
   Surface system (3.26) — the shared vocabulary behind the "soft-shadow
   light / glow-gradient dark" redesign. Two surface utilities + two accent
   utilities, all token-driven so a per-instance BrandingHelper override
   still flows through correctly:

   .hpc-surface-raised — a structural panel lifted off the page. Light mode:
   a soft neomorphic dual-shadow (--hpc-neo-shadow-out) on a --hpc-surface
   background. Dark mode: flat --hpc-surface + the existing
   --hpc-shadow-lifted (no neomorphic shadow on a near-black surface — see
   --hpc-neo-shadow-out's own comment in :root).

   .hpc-surface-inset — a control at rest (input fields). Light mode: the
   inverse, "pressed in" shadow (--hpc-neo-shadow-in). Dark mode: a plain 1px
   border instead of a fake inset shadow, which reads as noise on dark.

   .hpc-gradient-text / .hpc-glow — dark mode ONLY. Light mode intentionally
   never gets a gradient (that's the dark side's identity marker, softness
   there comes from shadow instead) — both are no-ops outside
   [data-bs-theme="dark"].
   ====================================================================== */
.hpc-surface-raised {
    background-color: var(--hpc-surface);
    border-radius: var(--hpc-radius-lg);
    box-shadow: var(--hpc-neo-shadow-out);
}
[data-bs-theme="dark"] .hpc-surface-raised {
    box-shadow: var(--hpc-shadow-lifted);
    border: 1px solid var(--hpc-border);
}
.hpc-surface-inset {
    background-color: var(--hpc-surface);
    border-radius: var(--hpc-radius);
    box-shadow: var(--hpc-neo-shadow-in);
    border: none;
}
[data-bs-theme="dark"] .hpc-surface-inset {
    box-shadow: none;
    border: 1px solid var(--hpc-border);
}
.hpc-gradient-text {
    display: inline-block;
}
[data-bs-theme="dark"] .hpc-gradient-text {
    background: var(--hpc-gradient-primary);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}
[data-bs-theme="dark"] .hpc-glow {
    box-shadow: var(--hpc-glow-shadow);
}

/* ======================================================================
   Button system — single source of truth (3.25.9, retuned 3.25.10). Before
   this, header-nav buttons used bare `.btn-sm` (Bootstrap stock: ~31px) and
   in-page CTAs (Add to cart, Save, upload triggers) used bare `.btn` (~38px)
   — two different, un-tokenised heights bouncing around the same pages —
   and individual pages (pet profile's mobile query, the pets-page header)
   each hand-rolled their own shrink/stack fix instead of sharing one.
   Collapses BOTH Bootstrap sizes to one token-driven height for every
   button inside a com_happypetconnect view OR either com_users override
   this platform ships (`.hpc-profile-wrap` = the PET profile page,
   site/tmpl/pet/profile.php; `.hpc-profile-page` = the ACCOUNT/"Pet Parent"
   profile, big_goose/html/com_users/profile/{default,edit}.php — easy to
   conflate, they are two different pages) — `.btn-sm`'s own smaller
   Bootstrap padding/font-size still computes, it's just overridden after by
   a rule with higher specificity, so it doesn't matter which size class a
   template uses. Scoped (not a bare `.btn` rule) so the rest of the site
   (articles, core Joomla forms) is untouched — site.css loads on every
   front-end page, not just this component's.

   40px/1rem/.9rem (3.25.9) read as a bigger jump than intended once seen
   next to the untouched "Pet Parent" page's stock ~31px buttons — retuned
   to sit between the two Bootstrap stock sizes rather than above both, and
   the "Pet Parent" page (missed in 3.25.9 — it lives outside this
   component's own tmpl/ entirely) is now actually in scope.

   `min-height`, not `height` — a fixed height risks clipping a button whose
   label wraps (translations run longer than English); min-height gets the
   same uniform floor without that failure mode. */
:root {
    --hpc-btn-height: 36px;
    --hpc-btn-padding-x: .875rem;
    --hpc-btn-font-size: .85rem;
}
[class^="com-happypetconnect-"] .btn,
.hpc-profile-wrap .btn,
.hpc-profile-page .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--hpc-btn-height);
    padding: 0 var(--hpc-btn-padding-x);
    font-size: var(--hpc-btn-font-size);
    line-height: 1.2;
}

/* Page header — title + action buttons. Shared by every view's top row
   (previously each template hand-wrote its own `d-flex justify-content-
   between align-items-center flex-wrap gap-2`, and only the Pets page ever
   got a mobile stacking fix — `.hpc-pets-header`, folded into this rule and
   removed as its own copy below). Below 768px the row stacks — heading on
   its own line, actions in a full-width wrapping row underneath it —
   instead of the two squeezing onto one line and crowding/overflowing. */
.hpc-page-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: .75rem;
}
/* One-line subtitle under a page's <h1> — currently only the Profile page
   uses it ("Manage your account and preferences."), but generic/tokenised
   so any other view's header can adopt it later. */
.hpc-page-subheading {
    margin-top: .2rem;
    font-size: .9rem;
    color: var(--hpc-text-faint);
}
.hpc-page-header-actions {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
}
@media (max-width: 767.98px) {
    .hpc-page-header {
        flex-direction: column;
        align-items: flex-start;
    }
    .hpc-page-header-actions {
        width: 100%;
    }
    .hpc-page-header-actions .btn {
        flex: 1 1 auto;
    }
    /* Back-to-My-Pets on the pet profile page is redundant on phones — the
       mobile back gesture/nav already gets you there, and as a full-width
       flex-1 bar (see rule above) it's the whole top row. Extra
       .hpc-page-header-actions qualifier for specificity parity with the
       .btn rule above, same fix as the profile-page Book button below. */
    .com-happypetconnect-pet-profile .hpc-page-header-actions .hpc-btn-back-to-pets {
        display: none;
    }
}

/* ======================================================================
   Segmented tab switcher — a small reusable component (`.hpc-tabs`, see
   mobilenav.js's generic click-wiring), used on the Activity page (My
   Bookings / My Orders) and the Profile page (My Profile / My Pets).
   Triggers are plain <button data-hpc-tab-trigger="KEY">; JS toggles
   `.is-active` on the trigger and the `hidden` attribute on the matching
   `[data-hpc-tab-panel="KEY"]`. The initial active/hidden state is set
   server-side (PHP) — this CSS only needs to style the two states, not
   decide which one is showing.
   ====================================================================== */
.hpc-tab-switch {
    display: inline-flex;
    gap: .25rem;
    padding: .25rem;
    /* margin-bottom: 1rem; */
    border-radius: 999px;
    background: var(--hpc-surface-sunken);
    /* Sunken-groove shadow so the track reads as recessed below the page,
       not just a flat grey pill. Own dedicated values rather than the
       shared --hpc-neo-shadow-in token — that token is tuned subtle for
       form inputs elsewhere and other callers shouldn't get pulled along
       if this needs tuning again. Top-edge shadow + bottom-edge highlight
       (or the reverse in dark mode) is what sells "recessed" — a single
       shadow alone reads as a drop shadow, not a groove. */
    box-shadow: inset 0 2px 4px rgba(15, 23, 42, .3),
                inset 0 -1px 0 rgba(255, 255, 255, .8);
}
[data-bs-theme="dark"] .hpc-tab-switch {
    box-shadow: inset 0 2px 4px rgba(0, 0, 0, .65),
                inset 0 1px 0 color-mix(in srgb, white 14%, transparent);
}
.hpc-tab-label {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 2.75rem;
    padding: .4rem 1.1rem;
    border: none;
    border-radius: 999px;
    background: none;
    color: var(--hpc-text-soft);
    font: inherit;
    font-size: .85rem;
    font-weight: 600;
    cursor: pointer;
    user-select: none;
    transition: background-color .12s ease, color .12s ease;
}
.hpc-tab-label:hover {
    color: var(--hpc-text);
}
.hpc-tab-label:focus-visible {
    outline: 2px solid var(--hpc-primary);
    outline-offset: 2px;
}
.hpc-tab-label.is-active {
    background: var(--hpc-primary);
    color: var(--hpc-primary-contrast);
}
.hpc-tab-panel[hidden] {
    display: none;
}

/* ======================================================================
   Home hub (Unified Navigation follow-on) — expandable hero built from an
   admin-picked article, "What's New" articles, Store/Services previews.
   Same token set as everywhere else, no new colours.
   ====================================================================== */
.hpc-home-article-full {
    background: var(--hpc-surface);
    border: 1px solid var(--hpc-border);
    border-radius: var(--hpc-radius-lg);
    box-shadow: var(--hpc-shadow-resting);
    padding: 1.75rem 2rem;
    color: var(--hpc-text-soft);
    line-height: 1.65;
}
.hpc-home-article-full img {
    max-width: 100%;
    height: auto;
    border-radius: var(--hpc-radius);
}

.hpc-home-hero {
    position: relative;
    overflow: hidden;
    background: var(--hpc-surface);
    border: 1px solid var(--hpc-border);
    border-radius: var(--hpc-radius-lg);
    box-shadow: var(--hpc-shadow-resting);
    padding: 1.5rem 1.75rem;
    margin-bottom: 2rem;
}
.hpc-home-hero-bg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    pointer-events: none;
}
/* Options -> Home Page -> Hero Image (HomeModel::getHeroImageUrl()). The
   image sits behind a fixed dark wash so the headline stays readable
   regardless of the photo's own tones or which theme the visitor is in —
   deliberately NOT theme-aware itself, since a bright hero photo needs the
   same legible-white-text treatment in light mode as in dark. Unset, the
   hero keeps its plain surface/border above untouched.

   No z-index anywhere in this trio (.hpc-home-hero-bg / -overlay /
   -content) — all three are positioned with z-index:auto, so they paint in
   plain DOM/tree order: photo, then wash, then content, each on top of the
   last. Simpler and less error-prone than juggling z-index numbers, and
   the earlier ::before-based version of this got that ordering backwards
   (a pseudo-element is generated as the FIRST child, painting UNDER a
   later real sibling at the same stacking level) — the photo painted over
   the wash and hid it completely. Keep this as real siblings in markup
   order, not a pseudo-element. */
.hpc-home-hero-has-image {
    border-color: transparent;
}
.hpc-home-hero-overlay {
    position: absolute;
    inset: 0;
    background: linear-gradient(190deg, rgba(10, 12, 20, .38), rgba(10, 12, 20, .82) 85%);
    pointer-events: none;
}
.hpc-home-hero-content {
    position: relative;
}
/* Targets .hpc-home-hero-content h1 generally, not just the one nested in
   .hpc-home-hero-head — the no-article-selected fallback heading
   (COM_HAPPYPETCONNECT_HOME_WELCOME_TITLE) renders as a direct child of
   .hpc-home-hero-content instead, and would otherwise keep the default
   dark heading colour over this same dark photo wash. */
.hpc-home-hero-has-image .hpc-home-hero-content h1,
.hpc-home-hero-has-image .hpc-home-hero-text,
.hpc-home-hero-has-image .text-muted {
    color: #fff;
}
.hpc-home-hero-text {
    max-height: 6.5rem;
    overflow-y: auto;
    overscroll-behavior: contain;
    position: relative;
    color: var(--hpc-text-soft);
    line-height: 1.6;
}
.hpc-home-hero-text:not(.is-expanded)::after {
    content: "";
    display: block;
    position: sticky;
    left: 0;
    right: 0;
    bottom: 0;
    height: 2.5rem;
    margin-top: -2.5rem;
    background: linear-gradient(to bottom, transparent, var(--hpc-surface));
    pointer-events: none;
}
.hpc-home-hero-text.is-expanded {
    /* REAL BUG fixed here: overflow-y:auto + overscroll-behavior:contain
       were left active on the expanded state too — a mobile-browser quirk
       (WebKit/Chrome touch handling) can lock the WHOLE PAGE's scroll
       gesture inside a still-`auto`-overflow box even once its content no
       longer overflows it, confirmed live (expanding the hero froze page
       scroll on mobile). Expanded means "no clamp, no internal scroll at
       all" — reset both so the box is a plain in-flow block and the page
       itself handles scrolling normally, same as any other content. */
    max-height: none;
    overflow: visible;
    overscroll-behavior: auto;
}
.hpc-home-hero-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}
.hpc-home-hero-head h1 {
    min-width: 0;
}
.hpc-home-hero-toggle {
    flex-shrink: 0;
    appearance: none;
    border: 1px solid var(--hpc-primary);
    border-radius: 999px;
    background: var(--hpc-primary);
    color: #fff;
    font-size: .85rem;
    font-weight: 600;
    /* Matches .hpc-profile-banner-btn's rendered height (~36px: .3rem padding
       @16px + .72rem font + browser default line-height ≈ 29-36px there) via
       an explicit box instead of copying its padding/font-size verbatim —
       this button sits on a plain surface, not a fixed-height banner strip,
       so it needs solid fill + real size to read as a control at all next
       to an <h1>, not just a height match. */
    min-height: 2.25rem;
    padding: 0 1.1rem;
    cursor: pointer;
    /* Space is reserved from first paint regardless of whether the article
       actually overflows — mobilenav.js toggles `visibility`, not the
       `hidden` attribute, specifically so revealing/hiding this button
       never shifts the text or CTA row below it (the "jump on load" this
       replaces — see mobilenav.js's own sync() comment). */
    visibility: hidden;
}
.hpc-home-hero-toggle:hover {
    background: var(--hpc-primary-tint);
    color: var(--hpc-primary);
}
/* Overrides the plain-surface fade-out mask above — matches the fixed dark
   wash .hpc-home-hero-has-image::before applies, not whichever theme's
   --hpc-surface happens to resolve to, so the fade never seams against a
   photo. */
.hpc-home-hero-has-image .hpc-home-hero-text:not(.is-expanded)::after {
    background: linear-gradient(to bottom, transparent, rgba(10, 12, 20, .82));
}

.hpc-home-section {
    margin-bottom: 2.25rem;
}
.hpc-home-section-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
    margin-bottom: 1rem;
}
.hpc-home-section-head a,
.hpc-home-section-head button {
    font: inherit;
    font-size: .82rem;
    font-weight: 600;
    color: var(--hpc-primary);
    text-decoration: none;
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
}

.hpc-home-articles-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
    gap: 1.1rem;
}
.hpc-home-article-card {
    display: block;
    background: var(--hpc-surface);
    border: 1px solid var(--hpc-border);
    border-radius: var(--hpc-radius);
    box-shadow: var(--hpc-shadow-resting);
    padding: 1.1rem 1.2rem;
    text-decoration: none;
    color: inherit;
    transition: transform .16s ease, box-shadow .16s ease;
}
.hpc-home-article-card:hover,
.hpc-home-article-card:focus-visible {
    transform: translateY(-3px);
    box-shadow: var(--hpc-shadow-lifted, var(--hpc-shadow-resting));
    color: inherit;
    text-decoration: none;
}
.hpc-home-article-found {
    display: inline-block;
    font-size: .68rem;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--bs-success-text-emphasis, #146c43);
    background-color: var(--bs-success-bg-subtle, #d1e7dd);
    border: 1px solid var(--bs-success-border-subtle, #a3cfbb);
    border-radius: 999px;
    padding: .15rem .55rem;
    margin-bottom: .5rem;
}
.hpc-home-article-date {
    display: block;
    font-size: .68rem;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--hpc-muted);
    margin-bottom: .35rem;
}
.hpc-home-article-title {
    margin: 0 0 .35rem;
    font-size: .98rem;
    font-weight: 700;
    color: var(--hpc-text);
}
.hpc-home-article-excerpt {
    margin: 0;
    font-size: .84rem;
    line-height: 1.5;
    color: var(--hpc-text-soft);
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.hpc-home-products-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 1rem;
}
.hpc-home-product-card {
    display: block;
    width: 100%;
    background: var(--hpc-surface);
    border: 1px solid var(--hpc-border);
    border-radius: var(--hpc-radius);
    box-shadow: var(--hpc-shadow-resting);
    overflow: hidden;
    text-decoration: none;
    color: inherit;
    /* Now a <button> (opens the quick-view modal, see home.js), not an <a> —
       these reset the UA button chrome that would otherwise fight the
       explicit background/border/etc above (default button font, centered
       text, own padding, pointer cursor missing on some browsers). */
    font: inherit;
    text-align: left;
    padding: 0;
    cursor: pointer;
    transition: transform .16s ease, box-shadow .16s ease;
}
.hpc-home-product-card:hover,
.hpc-home-product-card:focus-visible {
    transform: translateY(-3px);
    box-shadow: var(--hpc-shadow-lifted, var(--hpc-shadow-resting));
}
.hpc-home-product-media {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100px;
    background: var(--hpc-surface-sunken);
    color: var(--hpc-muted);
    font-size: 1.5rem;
}
.hpc-home-product-media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.hpc-home-product-name {
    display: block;
    padding: .65rem .8rem .8rem;
    font-size: .84rem;
    font-weight: 600;
    color: var(--hpc-text);
}

.hpc-home-services-grid {
    display: grid;
    /* auto-fit (not auto-fill) collapses empty tracks so existing cards
       stretch to fill the row — 1 service spans full width, N services
       share it evenly, resizing automatically as services are added. */
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1.1rem;
}
.hpc-home-service-card {
    display: block;
    background: var(--hpc-surface);
    border: 1px solid var(--hpc-border);
    border-radius: var(--hpc-radius);
    box-shadow: var(--hpc-shadow-resting);
    overflow: hidden;
    text-align: center;
    cursor: pointer;
    text-decoration: none;
    color: inherit;
    transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease;
}
.hpc-home-service-card:hover,
.hpc-home-service-card:focus-visible {
    border-color: var(--hpc-primary);
    color: inherit;
    text-decoration: none;
    transform: translateY(-3px);
    box-shadow: var(--hpc-shadow-lifted, var(--hpc-shadow-resting));
}
.hpc-home-service-tap {
    display: inline-block;
    font-size: .78rem;
    font-weight: 600;
    color: var(--hpc-primary);
}
/* Options -> Services -> Home Page Image (BookingModel::getServices() ->
   HomeModel image_url). Unset, falls back to a gradient built from the
   Service's own colour (--hpc-service-color, set inline per card) via
   color-mix() — the same technique --hpc-primary-tint already uses above,
   so a Service with no image still reads as deliberately coloured, never a
   blank/broken block. */
.hpc-home-service-art {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 5.5rem;
    background: linear-gradient(135deg,
        var(--hpc-service-color, var(--hpc-primary)),
        color-mix(in srgb, var(--hpc-service-color, var(--hpc-primary)) 55%, #000));
}
.hpc-home-service-art img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.hpc-home-service-icon {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 50%;
    background: rgba(255, 255, 255, .2);
    color: #fff;
    font-size: 1.1rem;
}
.hpc-home-service-body {
    display: block;
    padding: 1rem 1.1rem 1.15rem;
}
.hpc-home-service-card h3 {
    font-size: .95rem;
    font-weight: 700;
    margin: 0 0 .5rem;
}
@media (prefers-reduced-motion: reduce) {
    .hpc-home-service-card,
    .hpc-home-article-card,
    .hpc-home-product-card {
        transition: none;
    }
    .hpc-home-service-card:hover,
    .hpc-home-service-card:focus-visible,
    .hpc-home-article-card:hover,
    .hpc-home-article-card:focus-visible,
    .hpc-home-product-card:hover,
    .hpc-home-product-card:focus-visible {
        transform: none;
    }
}

/* ======================================================================
   Lists-as-cards — My Bookings / My Orders (3.17.0). A plain <table> never
   reads as "elegant" per the request, and each row now carries less data
   (Price dropped from Bookings per the request) — replaced with a vertical
   stack of row-cards built on the same elevation tokens as the Pets grid.
   ====================================================================== */
.hpc-list-cards {
    display: flex;
    flex-direction: column;
    gap: .65rem;
}
.hpc-list-card {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem 1.25rem;
    background: var(--hpc-surface);
    border: 1px solid var(--hpc-border-light);
    border-radius: var(--hpc-radius-lg);
    /* Same --hpc-card-shadow token the generic .card rule draws from (3.26)
       — a row-card is visually the same "raised panel" as a .card, just a
       different layout, so it should share the identical elevation. */
    box-shadow: var(--hpc-card-shadow);
    padding: .9rem 1.1rem;
    color: inherit;
}
a.hpc-list-card-link {
    text-decoration: none;
    transition: transform .15s ease, box-shadow .15s ease;
}
a.hpc-list-card-link:hover,
a.hpc-list-card-link:focus-visible {
    transform: translateY(-2px);
    box-shadow: var(--hpc-shadow-lifted);
}
.hpc-list-card-main {
    flex: 1 1 220px;
    min-width: 0;
}
.hpc-list-card-title {
    font-weight: 700;
    color: var(--hpc-text);
}
.hpc-list-card-sub {
    font-size: .85rem;
    color: var(--hpc-text-soft);
}
.hpc-list-card-when {
    flex: 0 0 auto;
    font-size: .88rem;
    color: var(--hpc-text-soft);
}
.hpc-list-card-status {
    flex: 0 0 auto;
    margin-left: auto;
}
@media (max-width: 575.98px) {
    .hpc-list-card {
        flex-direction: column;
        align-items: flex-start;
    }
    /* .hpc-list-card-main's base rule is `flex: 1 1 220px` — a flex-basis
       meant as a MINIMUM WIDTH for the row layout above. Flipping to
       flex-direction: column here rotates the main axis, so that same
       "220px" is read as a starting HEIGHT, and flex-grow: 1 then stretches
       it further to fill the column — a huge dead space between the
       title/sub text and the date/status line below it (confirmed live: a
       504x311px card on the Activity page for what should be a ~120px
       three-line card). Same bug, same fix already applied to
       .hpc-bookings-scroll .hpc-list-card-main (profile page's mini
       scroller) — needs no basis/grow at all in column mode, only as tall
       as its own two lines of text. */
    .hpc-list-card-main {
        flex: 0 1 auto;
        height: auto;
        min-height: 0;
    }
    .hpc-list-card-status {
        margin-left: 0;
    }
}
/* Compact variant (3.19.0) — Cart items, a "scaled down" reuse of the same
   pattern rather than a new one, per the request. */
.hpc-list-cards-compact .hpc-list-card {
    padding: .6rem .85rem;
    gap: .4rem .9rem;
}
.hpc-list-cards-compact .hpc-list-card-title {
    font-size: .92rem;
}

/* Unavailable cart line — the slot was taken (or the product sold out) after it
   was added, so the line is shown but excluded from the total and blocks
   checkout (see CartModel::getDetailedCart()). Deliberately dimmed rather than
   hidden: the buyer needs to see WHICH item went, especially when retrying a
   failed payment. Uses the existing danger token + a left accent, the same
   device as .hpc-list-card-highlight, so it reads correctly in both themes
   without its own dark-mode override. */
.hpc-list-card-unavailable {
    border-left: 3px solid var(--hpc-danger);
    opacity: .68;
}
.hpc-list-card-unavailable .hpc-list-card-title {
    text-decoration: line-through;
    text-decoration-color: var(--hpc-danger);
}
/* The remove button must stay fully legible — it is the one action that
   resolves this state, so it is exempt from the dimming above. */
.hpc-list-card-unavailable form,
.hpc-list-card-unavailable .badge {
    opacity: 1;
}

/* Upcoming-booking row accent (3.19.6, pet profile's Bookings & history —
   replaces the old bare <table class="table-light"> row, which never had a
   real dark-mode treatment of its own). A left accent bar keyed to the brand
   primary, not a full background tint, so it reads correctly against the
   card surface in either theme without needing its own light/dark override. */
.hpc-list-card-highlight {
    border-left: 3px solid var(--hpc-primary);
}

/* Cart page (3.19.0) — a centered, card-styled panel rather than a
   full-width table; reads as a "screen-centered modal" without actually
   gatekeeping a page that needs its own bookmarkable/navigable URL (the
   payment gateway return flow depends on it) behind client-side modal
   state — see this round's plan notes for why. */
.hpc-cart-panel {
    max-width: 900px;
    background: var(--hpc-surface);
    border: 1px solid var(--hpc-border);
    border-radius: var(--hpc-radius);
    box-shadow: var(--hpc-shadow-lifted);
    padding: 1.5rem 1.75rem;
}
@media (max-width: 767.98px) {
    .hpc-cart-panel {
        padding: 1.1rem 1rem;
    }
}
.hpc-cart-total {
    padding-top: .75rem;
    border-top: 1px solid var(--hpc-border-light);
}
.hcc-qty-box {
    flex: 0 0 auto;
}
.hcc-qty-box input[type="number"] {
    width: 4.5rem;
}

/* Cart items — single column at every width (tried a 2/3-col grid like My
   Pets, reverted: single column read better here). Kept as its own
   wrapper/item class pair (rather than plain .hpc-list-cards) so
   .hpc-cart-card below can target it specifically. */
.hpc-cart-grid {
    display: flex;
    flex-direction: column;
    gap: .65rem;
}
.hpc-cart-grid-item {
    display: flex;
}
/* .hpc-list-card's row layout only collapses to a column below 575.98px
   (see the plain-row usage on Bookings/Orders lists); Cart forces a
   compact column at every width instead, which is what actually fixed
   the "massive" cards — full-width rows with a wide empty gap between
   wrapped children. */
.hpc-cart-card {
    flex: 1 1 auto;
    flex-direction: column;
    align-items: stretch;
    gap: .35rem;
    padding: .7rem .85rem;
}
.hpc-cart-card .hpc-list-card-main,
.hpc-cart-card .hpc-list-card-when,
.hpc-cart-card .hpc-list-card-status {
    flex: 0 0 auto;
    margin-left: 0;
}
.hpc-cart-card .hpc-list-card-title {
    font-size: .92rem;
}
.hpc-cart-card .hcc-qty-box {
    width: 100%;
}
.hpc-cart-card .hcc-qty-box input[type="number"] {
    width: 3.5rem;
}
.hpc-cart-card .hcc-qty-box select {
    flex: 1 1 auto;
    min-width: 0;
}

/* ======================================================================
   Booking calendar — Month grid + slot side-panel (site/tmpl/booking/
   default.php, driven by media/com_happypetconnect/js/booking.js).
   Week view reuses the shared media/com_happypetconnect/css/timegrid.css
   (same file the admin availability calendar uses) — that file reads the
   same --hpc-* tokens defined in this file's :root/dark blocks above, so
   the two calendars stay visually consistent without a second copy of the
   values anywhere. DELIBERATELY NO DAY VIEW on the site side (see
   booking/default.php's view-toggle — a pet parent picks a date from a
   month or a week, a single day's column adds nothing a login-gated,
   one-slot-at-a-time booking flow needs; admin gets full Month/Week/Day,
   see admin.css).
   ====================================================================== */

/* Store/My Pets/Cart page title row. No longer sticky (see git history) —
   was position:sticky/top:0 (3.19.0) so it stayed reachable without
   scrolling back to the top of a long calendar, but stacking that against
   .hcc-book-controls' OWN sticky behavior below needed a live-measured
   offset between the two bars, which broke silently whenever either bar's
   real height changed and made them overlap instead of stack. Plain flow
   avoids the problem entirely, at the cost of this row scrolling away too. */
.hcc-booking-header {
    background: transparent;
    padding-top: .5rem;
    padding-bottom: .5rem;
    margin-bottom: .4rem;
}

.hcc-book-head,
.hcc-book-body {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 4px;
}
.hcc-book-head { margin-bottom: 4px; }
.hcc-book-dow {
    text-align: center;
    font-weight: 600;
    font-size: .75rem;
    color: var(--hpc-muted);
}
.hcc-book-cell {
    /* 3.19.0: was a rigid 1:1 square — trimmed ~15% off the height for a
       sleeker rectangle (width unchanged, height now 85% of width). */
    aspect-ratio: 1 / 0.85;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--hpc-border);
    border-radius: var(--hpc-radius);
    cursor: pointer;
    font-weight: 500;
    background: var(--hpc-surface);
    transition: background .12s ease, border-color .12s ease;
}
.hcc-book-cell[data-date]:hover,
.hcc-book-cell[data-date]:focus {
    border-color: var(--hpc-primary);
    background: var(--hpc-primary-tint);
    outline: none;
}
.hcc-book-empty { border: none; background: transparent; cursor: default; }
.hcc-book-past {
    color: var(--hpc-text-placeholder);
    background: var(--hpc-surface-sunken);
    cursor: not-allowed;
}
.hcc-book-today { border-color: var(--hpc-primary); }
.hcc-book-selected {
    background: var(--hpc-primary) !important;
    color: var(--hpc-primary-contrast);
    border-color: var(--hpc-primary);
}

.hcc-slot-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
    gap: .4rem;
    margin-bottom: .2rem;
}
/* !important: [data-bs-theme="dark"] .btn-outline-primary (above, 3.23.20)
   forces every .btn-outline-primary — .hcc-slot included — solid
   --hpc-primary in dark mode with its own !important, which otherwise
   always wins regardless of this rule's specificity and silently keeps a
   selected slot looking identical to every unselected one. Confirmed live:
   without !important here, tapping a slot changed nothing visually. */
.hcc-slot.active {
    background: var(--hpc-slot-selected) !important;
    border-color: var(--hpc-slot-selected) !important;
    color: #fff !important;
}

.hcc-price-box {
    margin-bottom: .35rem;
    padding-bottom: .25rem;
    border-bottom: 1px solid var(--hpc-border-light);
}
.hcc-price-final {
    font-size: 1.5rem;
    font-weight: 700;
}
.hcc-price-was {
    text-decoration: line-through;
    color: var(--hpc-text-faint);
    margin-left: .25rem;
}

/* #hcc-timegrid (Week view host) needs no layout rule of its own beyond a
   touch of breathing room above it — timegrid.css owns everything inside. */
#hcc-timegrid {
    margin-top: .25rem;
}

@media (max-width: 575.98px) {
    .hcc-book-cell { font-size: .85rem; }
    .hcc-price-final { font-size: 1.25rem; }
}

/* ======================================================================
   Mobile "Book a Service" refactor — day-strip date picker, 48px touch
   targets, edge-to-edge layout. Utility classes scoped under
   #hcc-booking so the generic-sounding names can't leak onto any other
   page; breakpoint matches this file's existing 767.98px convention
   (.hpc-page-header etc.) — the fields-row width-reveal above is a
   separate 1200px concern (desktop layout polish, not the phone line).
   ====================================================================== */
/* Each mobile-only element is hidden by default, then un-hidden per its own
   display type inside the media query below — a blanket `display:initial`
   would resolve to `inline` for these (7-col grid / block bar / icon
   button), collapsing the day strip's layout, so each gets its real value
   with a selector specific enough (id + 2 classes) to beat this default
   rule. `!important` on both sides is required because some of these
   elements (the month-modal card-header) also carry Bootstrap's `.d-flex`,
   itself `!important` — without matching `!important` here, `.d-flex`
   silently wins on every viewport regardless of selector specificity (see
   the identical `.hpc-rewards-ledger li[hidden]` note above). */
#hcc-booking .hcc-mobile-only { display: none !important; }
@media (max-width: 767.98px) {
    #hcc-booking .hcc-desktop-only { display: none !important; }
    #hcc-booking #hcc-day-strip-header.hcc-mobile-only { display: flex !important; }
    #hcc-booking #hcc-day-strip-area.hcc-mobile-only { display: block !important; }
    #hcc-booking #hcc-slots-inline.hcc-mobile-only { display: block !important; }
    #hcc-booking #hcc-month-modal .card-header.hcc-mobile-only { display: flex !important; }

    /* Edge-to-edge: drop the outer card chrome around the field row + nav
       row on phones — a bordered box nested inside the page's own edge
       reads as a redundant frame at this width. */
    #hcc-booking .hcc-book-controls {
        background: transparent;
        box-shadow: none;
        border-radius: 0;
        padding: 0 0 .5rem;
    }

    /* Selects span full width — both the real <select> (no-JS fallback) and
       customselect.js's .hpc-select-trigger replacement, which clones the
       select's own classes and needs the same rule. */
    #hcc-booking .hcc-field select,
    #hcc-booking .hcc-field .hpc-select-trigger {
        width: 100%;
    }

    /* 48x48 minimum touch target floor (WCAG 2.5.5) for every control this
       refactor introduces — day cards, the date-jump icon, and the now
       full-width selects all sit below this file's normal 36px
       --hpc-btn-height. `3rem`, not `48px` — a raw px floor stays fixed
       under the OS/browser "larger text" accessibility setting (which scales
       the root font-size, not physical pixels), so the card stopped growing
       with its own now-larger day-number text; rem keeps the whole card
       scaling together with it. */
    #hcc-booking .hcc-day-card,
    #hcc-booking #hcc-date-jump,
    #hcc-booking .hcc-field select,
    #hcc-booking .hcc-field .hpc-select-trigger,
    #hcc-booking #hcc-slots-inline .hcc-add {
        min-height: 3rem;
    }
    #hcc-booking #hcc-date-jump {
        min-width: 3rem;
    }
}

/* Mobile compact header — visible date range + jump icon, replacing the
   desktop nav row's prev/next/month-label (those stay for the Month/Week
   grid only, see .hcc-desktop-only above). Booking.js's pageDayStrip()
   keeps #hcc-day-strip-range's text in sync with whichever 7 days are
   actually showing. */
.hcc-day-strip-header {
    align-items: center;
    justify-content: space-between;
    margin-bottom: .5rem;
}
.hcc-day-strip-header strong {
    font-size: .95rem;
}

/* Drag viewport — clips the strip during a swipe so dragged-past content
   doesn't spill outside the card's rounded corners. touch-action:pan-y
   leaves vertical page scroll to the browser and reserves horizontal drag
   for the strip itself. */
.hcc-day-strip-viewport {
    overflow: hidden;
    touch-action: pan-y;
    cursor: grab;
    border-radius: var(--hpc-radius);
}
.hcc-day-strip-viewport.hcc-day-strip-dragging {
    cursor: grabbing;
}

/* Track — THREE day-strip pages side by side (prev/current/next), 300%
   wide, resting at -33.3333% so the current page fills the viewport.
   Dragging translates THIS, not an individual page — that's what makes
   adjacent days slide in from off-screen instead of the visible 7 cards
   just sliding within their own box and leaving blank space behind them
   (the bug a single-strip version of this had). booking.js keeps the
   prev/next pages rendered for whatever they'd need to show BEFORE any
   drag starts, so there's always real content behind the drag, not a
   blank page revealed partway through one. */
.hcc-day-strip-track {
    display: flex;
    width: 300%;
    transform: translateX(-33.3333%);
    transition: transform .22s ease;
}
.hcc-day-strip-track > .hcc-day-strip {
    flex: none;
    width: 33.3333%;
}

/* Day strip — the mobile date picker, standing in for the Month grid
   (Month/Week + Today are hidden via .hcc-desktop-only; the grid itself
   only reappears inside #hcc-month-modal when #hcc-date-jump is tapped).
   Clicking a card reuses state.selectedDate / renderSlots() exactly like a
   Month-grid cell click (see booking.js's renderDayStrip()/selectDate()) —
   same data, rendered into #hcc-slots-inline instead of a popup. */
/* Grid, not a scrollable flex row — always exactly 7 cards per page (see
   renderDayStrip()), so there is never anything to scroll TO; a fixed-width
   flex row with overflow-x just left dead space on wider phones and forced
   the same 3.25rem card width on narrow ones. Seven equal columns fill
   whatever width the page actually has. */
.hcc-day-strip {
    display: grid;
    /* minmax(0, 1fr), NOT plain 1fr — a bare `1fr` track's minimum is its
       content's natural (min-content) size, not 0, so once the viewport
       got narrow enough (browser zoom, mainly) the 7 tracks couldn't
       shrink past that any further and started overflowing/clipping
       instead of continuing to scale down. minmax(0, 1fr) lets a track
       shrink all the way, so the row always fits the real width it has. */
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: .4rem;
    padding: .2rem .1rem .35rem;
    margin-bottom: .1rem;
}
.hcc-day-card {
    /* Height tracks the column's own width (same idea as .hcc-book-cell's
       aspect-ratio in the month grid) instead of sitting at a flat
       min-height regardless of it — without this, a wider phone gave wider
       columns but the exact same card height, so cards looked squashed
       instead of actually scaling with the screen. min-height (above, in
       the touch-target rule) is the accessibility FLOOR; aspect-ratio is
       what makes it grow past that floor on a roomier screen. */
    aspect-ratio: 1 / 1.05;
    min-width: 0;   /* grid items default to min-width:auto (content-sized) —
                       same overflow trap as the track above, at the item level. */
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .15rem;
    border: 1px solid var(--hpc-border);
    border-radius: var(--hpc-radius);
    background: var(--hpc-surface);
    color: var(--hpc-text);
    cursor: pointer;
    overflow: hidden;
}
.hcc-day-card .hcc-day-dow {
    font-size: .62rem;
    font-weight: 600;
    color: var(--hpc-muted);
    text-transform: uppercase;
    letter-spacing: .03em;
}
.hcc-day-card .hcc-day-num {
    font-size: 1.05rem;
    font-weight: 700;
}
.hcc-day-card.hcc-day-today { border-color: var(--hpc-primary); }
.hcc-day-card.hcc-day-selected {
    background: var(--hpc-primary);
    border-color: var(--hpc-primary);
    color: var(--hpc-primary-contrast);
}
.hcc-day-card.hcc-day-selected .hcc-day-dow { color: rgba(255, 255, 255, .8); }
.hcc-day-card.hcc-day-past {
    color: var(--hpc-text-placeholder);
    background: var(--hpc-surface-sunken);
    cursor: not-allowed;
}

/* Date-jump icon — opens #hcc-month-modal (the real month grid, see below)
   via simplemodal.js's data-hpc-open-modal convention. */
#hcc-date-jump {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/* #hcc-month-modal — the month grid's ONLY container (single #hcc-cal, no
   duplicate DOM for booking.js to keep in sync). On mobile it's a real
   modal via the base .hpc-modal-backdrop rules (fixed, dimmed, centered
   card) — nothing extra needed here. On desktop it's forced permanently
   visible and stripped of every modal affordance so #hcc-calendar-area
   renders exactly where the old plain inline container used to, pixel for
   pixel: the [hidden] override matters because #hcc-date-jump (the only
   thing that would ever open/close it) is itself mobile-only, so this
   modal would otherwise sit permanently hidden on desktop. */
@media (min-width: 768px) {
    #hcc-booking #hcc-month-modal,
    #hcc-booking #hcc-month-modal[hidden] {
        display: block !important;
        position: static;
        inset: auto;
        background: none;
        padding: 0;
        z-index: auto;
    }
    #hcc-booking #hcc-month-modal .hpc-modal-dialog {
        max-width: none;
        width: 100%;
        box-shadow: none;
        background: transparent;
        border: none;
    }
    #hcc-booking #hcc-month-modal .card-body {
        padding: 0;
    }
}
/* [data-bs-theme="dark"] .card sets background-color with !important (the
   Bootstrap card-bg hotfix, above) — same specificity as, and loads after,
   the transparent .hpc-modal-dialog rule above, and !important always wins
   regardless of order. Left alone, the dialog silently re-paints itself
   var(--hpc-surface) on desktop-dark — the SAME colour as the centered
   .hcc-book-cal sitting inside it, so the two boxes have no visible seam
   and the whole area reads as one flat block instead of a card with room
   around it. Re-declared here, scoped + !important, to win that fight. */
[data-bs-theme="dark"] #hcc-booking #hcc-month-modal .hpc-modal-dialog {
    background: transparent !important;
}

/* Inline slot panel — mobile's replacement for #hcc-slot-modal. Same
   priceLine()/.hcc-slot-grid markup booking.js already builds, just
   rendered directly under the day strip instead of inside a popup, and
   themed off --hpc-surface/--hpc-text like the rest of the page instead of
   a Bootstrap .card's own background (the thing that read as "a light
   block" against a dark page). */
#hcc-slots-inline {
    border-top: 1px solid var(--hpc-border-light);
    padding-top: .7rem;
    margin-top: .2rem;
    color: var(--hpc-text);
}
.hcc-slots-inline-date {
    font-weight: 700;
    font-size: .9rem;
}
#hcc-slots-inline .hcc-slot-grid {
    grid-template-columns: repeat(3, 1fr);
    gap: .5rem;
}
/* Chip-sized, not "giant primary button" — explicit min-height/padding
   instead of this file's normal --hpc-btn-height (36-48px depending on
   context), which read as oversized once three fit per row. Colour is
   filled unconditionally (both themes, not just dark) — a deliberate
   departure from the site's generic outline-button convention, so a slot
   reads as a selectable chip on sight rather than a bordered button.
   var(--hpc-primary)/var(--hpc-primary-contrast), NOT a literal #238899 —
   this platform is multi-tenant (see CLAUDE.md) and --hpc-primary is this
   one instance's branded colour; a literal hex here would paint every
   OTHER business's pills with Hound Human Connection's teal regardless of
   their own branding. !important for the same reason as .active below:
   beats [data-bs-theme="dark"] .btn-outline-primary's own !important. */
#hcc-slots-inline .hcc-slot {
    border-radius: 999px;
    min-height: 40px;
    padding: .5rem .75rem;
    font-size: .85rem;
    font-weight: 600;
    background: var(--hpc-primary) !important;
    border-color: var(--hpc-primary) !important;
    color: var(--hpc-primary-contrast) !important;
}
/* Re-states .active explicitly: #hcc-slots-inline .hcc-slot (id-scoped,
   above) otherwise outranks the bare .hcc-slot.active rule on specificity
   alone, regardless of source order, and would keep a selected pill looking
   unselected. */
#hcc-slots-inline .hcc-slot.active {
    background: var(--hpc-slot-selected) !important;
    border-color: var(--hpc-slot-selected) !important;
    color: #fff !important;
}
/* Disabled/unavailable — muted, low-contrast on purpose (it's not a real
   choice), matching the dark surface underneath it rather than standing out
   as another actionable-looking chip. Not exercised by drawSlots() today
   (only bookable times are ever sent to the client), kept for when a
   partially-available day starts rendering fully-booked times as inert. */
#hcc-slots-inline .hcc-slot:disabled {
    background: var(--hpc-surface-sunken) !important;
    border-color: var(--hpc-border) !important;
    color: var(--hpc-text-placeholder) !important;
    opacity: 1;
}

/* Assessment hint — deliberately NOT .alert-info: every .alert-* class is
   display:none!important platform-wide (3.26.32 product decision), which
   left this exact message unable to render at all, in any theme, on any
   device. Its own class + token-driven colours instead. */
.hcc-hint-banner {
    display: flex;
    gap: .5rem;
    align-items: flex-start;
    border: 1px solid color-mix(in srgb, var(--hpc-primary) 35%, var(--hpc-border));
    background: var(--hpc-primary-tint);
    color: var(--hpc-text);
    border-radius: var(--hpc-radius);
    padding: .6rem .75rem;
    font-size: .85rem;
    line-height: 1.4;
}

/* ======================================================================
   CSS-class-driven modal base (3.15.0) — the SAME convention already used
   by the site pet-profile page's "Set Status" modal (site/tmpl/pet/
   profile.php, pet-status.js) and the admin Pet edit screen's identical
   markup. admin.css has carried this since that modal shipped; site.css
   never got its own copy, so that modal has been rendering fully unstyled
   on the site side this whole time — a real, previously-undetected gap,
   fixed here as a side effect of building the cart drawer on the exact same
   base rather than inventing a second modal pattern. Unlike admin.css's
   fixed-light copy, this one is dark-mode aware
   via the --hpc-* tokens already defined in this file's :root/dark blocks.
   ====================================================================== */
.hpc-modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, .5);
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: 4rem 1rem 1rem;
    z-index: 1055;
}
.hpc-modal-backdrop[hidden] { display: none; }
.hpc-modal-dialog {
    width: 100%;
    max-width: 420px;
    /* Same token the card/list-card system draws from (3.26) — a modal is
       the same "raised panel" concept, just positioned fixed. */
    box-shadow: var(--hpc-card-shadow);
}
body.hpc-modal-open { overflow: hidden; }
/* Wider variants for modals whose content needs more room than the 420px
   default (a multi-column slot grid, a scrollable ledger list). */
#hcc-slot-modal .hpc-modal-dialog,
#hpc-rewards-modal .hpc-modal-dialog,
#hpc-store-modal .hpc-modal-dialog {
    max-width: 560px;
}

/* ======================================================================
   Image crop modal (imagecrop.js) — pan/zoom-to-crop staging step shared
   by the pet photo upload and the account avatar upload. The frame IS the
   crop (see imagecrop.js's own docblock), so this is deliberately just a
   fixed square box with `overflow: hidden` — no separate drag-handle
   rectangle to draw.
   ====================================================================== */
.hpc-cropmodal-dialog {
    max-width: 380px;
}
.hpc-cropmodal-body {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .75rem;
}
.hpc-cropmodal-frame {
    position: relative;
    width: min(100%, 300px);
    aspect-ratio: 1 / 1;
    overflow: hidden;
    border-radius: .5rem;
    background: var(--hpc-surface-sunken);
    /* Own touch-action so a one-finger drag pans the image instead of the
       page trying to scroll/refresh underneath the modal. */
    touch-action: none;
    cursor: grab;
    border: 1px solid var(--hpc-border);
}
.hpc-cropmodal-frame:active {
    cursor: grabbing;
}
.hpc-cropmodal-frame-circle {
    border-radius: 50%;
}
.hpc-cropmodal-img {
    position: absolute;
    top: 0;
    left: 0;
    max-width: none;
    transform-origin: 0 0;
    user-select: none;
    -webkit-user-drag: none;
}
.hpc-cropmodal-zoom {
    display: flex;
    align-items: center;
    gap: .5rem;
    width: 100%;
}
.hpc-cropmodal-zoom-icon {
    font-size: .85rem;
    color: var(--hpc-muted);
    flex-shrink: 0;
    line-height: 1;
}
.hpc-cropmodal-zoom-icon-lg {
    font-size: 1.1rem;
}
.hpc-cropmodal-zoom-range {
    flex: 1;
}
.hpc-cropmodal-hint {
    text-align: center;
}

/* Shared skeleton-loader placeholder (3.25.9x) — a shaped, animated block
   standing in for content that hasn't arrived yet (an AJAX fetch: the cart
   drawer's line items, the store quick-view modal's "All Products" grid).
   Previews the real layout instead of a spinner/text line so nothing
   reflows once the fetch resolves — same "reserve the space up front"
   reasoning as the hero's own Expand Article button fix, applied to async
   content instead of a toggle. Respects prefers-reduced-motion. */
.hpc-skeleton {
    display: block;
    border-radius: var(--hpc-radius-sm, 6px);
    background: linear-gradient(
        100deg,
        var(--hpc-surface-sunken) 40%,
        color-mix(in srgb, var(--hpc-surface-sunken) 50%, var(--hpc-border)) 55%,
        var(--hpc-surface-sunken) 70%
    );
    background-size: 200% 100%;
    animation: hpc-skeleton-shimmer 1.4s ease-in-out infinite;
}
@keyframes hpc-skeleton-shimmer {
    0% { background-position: 150% 0; }
    100% { background-position: -50% 0; }
}
@media (prefers-reduced-motion: reduce) {
    .hpc-skeleton {
        animation: none;
        background: var(--hpc-surface-sunken);
    }
}
.hpc-skeleton-card {
    height: 100px;
    border-radius: var(--hpc-radius);
    margin-bottom: .5rem;
}
.hpc-skeleton-line {
    height: .85rem;
    width: 70%;
    margin: .65rem .8rem 0;
}
.hpc-skeleton-line + .hpc-skeleton-line {
    width: 45%;
    margin-top: .4rem;
    margin-bottom: .8rem;
}
.hpc-skeleton-row {
    height: 3.25rem;
    margin-bottom: .6rem;
}
.hpc-skeleton-text {
    height: .75rem;
    margin-bottom: .35rem;
}
.hpc-skeleton-text:last-child {
    margin-bottom: 0;
}
.hpc-skeleton-slot {
    height: 2.4rem;
}
/* Keeps the previous view's content in place (dimmed) while a re-fetch is
   in flight, instead of clearing it to a short placeholder first — the
   clear-then-refill pattern collapses the grid's height for a moment and
   snaps back once data lands, which is its own layout-shift source on
   month/week navigation, not just first paint. */
.hpc-grid-loading {
    opacity: .45;
    pointer-events: none;
}

/* Home Hub store quick-view modal (home.js) — one product's detail, or up to
   6 in a grid, inside the same dialog rather than a second modal. */
#hpc-store-modal .card-body {
    max-height: 70vh;
    overflow-y: auto;
}
.hpc-store-modal-media {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 200px;
    border-radius: var(--hpc-radius);
    background: var(--hpc-surface-sunken);
    color: var(--hpc-muted);
    font-size: 2rem;
    overflow: hidden;
    margin-bottom: 1rem;
}
.hpc-store-modal-media img {
    /* `contain`, not `cover` — a fixed 200px box with `cover` crops off
       whatever doesn't fit the box's aspect ratio (reported: the Dog
       Jacket photo showing cut off). `contain` always shows the whole
       image, letterboxed on the sunken background if the aspect differs. */
    width: 100%;
    height: 100%;
    object-fit: contain;
}
.hpc-store-modal-price {
    font-size: 1.4rem;
    font-weight: 700;
    color: var(--hpc-text);
    margin-bottom: .25rem;
}
.hpc-store-modal-full-link {
    display: block;
    text-align: center;
    font-size: .82rem;
    color: var(--hpc-primary);
    text-decoration: none;
}
.hpc-store-modal-browse-row {
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--hpc-border);
    text-align: center;
}
.hpc-store-modal-browse-row button {
    font: inherit;
    font-size: .85rem;
    font-weight: 600;
    color: var(--hpc-primary);
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
}
.hpc-store-modal-back {
    display: inline-flex;
    align-items: center;
    font: inherit;
    font-size: .85rem;
    font-weight: 600;
    color: var(--hpc-primary);
    background: none;
    border: none;
    padding: 0;
    margin-bottom: 1rem;
    cursor: pointer;
}

/* In-app PDF viewer (3.23.60 — see pdfviewer.js's own docblock: pdf.js
   renders the document onto plain <canvas> elements, so nothing here
   depends on the host WebView's own PDF support). Near-fullscreen, not the
   420px modal default — a document needs real room, not a small dialog. */
.hpc-pdf-modal-backdrop {
    padding: 1rem;
    align-items: stretch;
}
.hpc-pdf-modal-dialog {
    max-width: 900px;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    margin: 0 auto;
}
.hpc-pdf-modal-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    background: var(--hpc-surface-sunken);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .5rem;
    padding: .5rem;
}
.hpc-pdf-modal-status {
    color: var(--hpc-text-soft);
    padding: 2rem 1rem;
    text-align: center;
}
.hpc-pdf-modal-pages {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .5rem;
    width: 100%;
}
.hpc-pdf-modal-page {
    max-width: 100%;
    height: auto;
    box-shadow: var(--hpc-shadow-resting);
    background: #fff;
}

/* Post-add-to-cart utility bar (3.17.0) — replaces the old redirect-to-cart
   behaviour: adding a slot now surfaces this instead of navigating away, so
   browsing/booking more stays uninterrupted. */
.hpc-cart-toast {
    position: fixed;
    left: 50%;
    bottom: 1.25rem;
    transform: translateX(-50%);
    z-index: 1060;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .75rem;
    background: var(--hpc-surface);
    border: 1px solid var(--hpc-border);
    border-radius: var(--hpc-radius);
    box-shadow: var(--hpc-shadow-lifted);
    padding: .75rem 1rem;
    max-width: calc(100vw - 2rem);
}
.hpc-cart-toast[hidden] { display: none; }
.hpc-cart-toast-actions {
    display: flex;
    flex-wrap: wrap;
    gap: .4rem;
}
@media (max-width: 575.98px) {
    .hpc-cart-toast {
        left: 1rem;
        right: 1rem;
        bottom: calc(5.5rem + env(safe-area-inset-bottom, 0px));
        transform: none;
        max-width: none;
        flex-direction: column;
        align-items: stretch;
    }
}

/* ======================================================================
   Site-wide cart widget (3.25.18) — header-style trigger (desktop) + a
   floating action button (mobile, hidden until the cart has items — see
   cartdrawer.js's own docblock for why this is a FAB rather than a 6th
   bottom-bar slot) sharing one preview drawer, plus the AJAX add-to-cart
   toast. Injected site-wide by plg_system_hpctheme's
   HpcTheme::buildCartWidgetHtml(), same reach as the mobile chrome block
   below. Fixed-position rather than literally nested inside big_goose's own
   <header> markup — this file has no reliable hook into the forked
   template's header height/layout, and a fixed top-right control reads the
   same way visually without coupling to it. NOT visually verified against a
   live page (no browser in this dev sandbox) — check on deploy.
   ====================================================================== */
/* Solid brand-primary circle, same visual language as .hpc-cart-fab below —
   a live check flagged the original pale surface-coloured version as
   low-contrast against the gradient header and reading as "hidden in the
   corner" rather than a deliberate nav control. top:.9rem here is only the
   fallback for before cartdrawer.js's alignTriggerToHeader() runs (or if it
   can't find .header at all) — that JS re-centres it against the template
   header's own real rendered height, which isn't a fixed value (custom logo
   size, optional site-description row). right is pulled in from the bare
   viewport edge for the same "not glued into the corner" reason. */
.hpc-cart-trigger {
    display: none;
    position: fixed;
    top: .9rem;
    right: 1.5rem;
    z-index: 1045;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    border: none;
    border-radius: 9999px;
    background: var(--hpc-primary);
    color: var(--hpc-primary-contrast);
    box-shadow: var(--hpc-shadow-lifted);
    cursor: pointer;
    /* top/right above are only the pre-JS fallback — cartdrawer.js's
       alignTriggerToHeader() overwrites both against the real rendered
       header/main geometry. Left visible, that swap reads as the button
       flying in from the bare viewport edge on every load. Stay invisible
       until JS confirms it has placed the real position (.is-positioned,
       added by alignTriggerToHeader()), then fade in already correct. */
    opacity: 0;
    transition: opacity .15s ease-out;
}
.hpc-cart-trigger.is-positioned {
    opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
    .hpc-cart-trigger {
        transition: none;
    }
}
.hpc-cart-trigger:hover,
.hpc-cart-trigger:focus-visible {
    background: color-mix(in srgb, var(--hpc-primary) 85%, black);
}
.hpc-cart-trigger svg {
    width: 1.3rem;
    height: 1.3rem;
}
@media (min-width: 1200px) {
    .hpc-cart-trigger {
        display: inline-flex;
    }
}

.hpc-cart-fab {
    display: none;
    position: fixed;
    right: 1rem;
    bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
    z-index: 1045;
    align-items: center;
    justify-content: center;
    width: 3.25rem;
    height: 3.25rem;
    padding: 0;
    border: none;
    border-radius: 9999px;
    background: var(--hpc-primary);
    color: var(--hpc-primary-contrast);
    box-shadow: var(--hpc-shadow-lifted);
    cursor: pointer;
    animation: hpc-cart-fab-in .2s ease-out;
}
.hpc-cart-fab svg {
    width: 1.4rem;
    height: 1.4rem;
}
.hpc-cart-fab:not([hidden]) {
    display: inline-flex;
}
@media (min-width: 1200px) {
    .hpc-cart-fab {
        display: none !important;
    }
}
/* Clears the fixed bottom tab bar (see the mobile-chrome block below) and/or
   the fixed .footer (~L7965) when either is actually present — mirrors
   body:has(.hpc-mobile-chrome)'s own padding rule so the FAB never needs to
   know the other features' exact heights, just whether they rendered at all.
   Without this the FAB (fixed, z-index 1045) sat at a flat bottom:1rem and
   visually overlaid the footer band, since both are position:fixed in the
   same corner regardless of scroll position. */
body:has(.footer) .hpc-cart-fab {
    bottom: calc(1rem + var(--hpc-footer-height) + env(safe-area-inset-bottom, 0px));
}
body:has(.hpc-mobile-chrome) .hpc-cart-fab {
    bottom: calc(5rem + env(safe-area-inset-bottom, 0px));
}
body:has(.hpc-mobile-chrome):has(.footer) .hpc-cart-fab {
    bottom: calc(5rem + var(--hpc-footer-height) + env(safe-area-inset-bottom, 0px));
}
@keyframes hpc-cart-fab-in {
    from { transform: scale(.5); opacity: 0; }
    to   { transform: scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
    .hpc-cart-fab {
        animation: none;
    }
}

.hpc-cart-trigger .hpc-cart-widget-badge,
.hpc-cart-fab .hpc-cart-widget-badge {
    position: absolute;
    top: -.25rem;
    right: -.25rem;
    min-width: 1.2rem;
    height: 1.2rem;
    padding: 0 .3rem;
    font-size: .68rem;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* The preview drawer both the trigger and the FAB open — built on the same
   .hpc-modal-backdrop/.hpc-modal-dialog base the "Set Status" modal uses; a
   phone-width override below turns it into a bottom sheet.
   Restates the dimming overlay explicitly, scoped to this ID, rather than
   trusting the shared .hpc-modal-backdrop rule alone — a live screenshot
   showed this specific drawer opening fully see-through with no dimming at
   all, unlike the site's other .hpc-modal-backdrop dialogs (root cause not
   reproducible here, no browser in this dev sandbox — this is a targeted,
   deliberate belt-and-braces fix, not a guess dressed up as one). */
#hpc-cart-drawer-backdrop.hpc-modal-backdrop {
    background: rgba(0, 0, 0, .55);
}
.hpc-cart-drawer {
    max-width: 480px;
}
.hpc-cart-drawer-body {
    max-height: 50vh;
    overflow-y: auto;
}
.hpc-cart-drawer-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: .65rem;
}
.hpc-cart-drawer-line {
    display: flex;
    align-items: center;
    gap: .65rem;
}
.hpc-cart-drawer-line-unavailable {
    opacity: .55;
}
.hpc-cart-drawer-thumb {
    flex: 0 0 auto;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: var(--hpc-radius-sm);
    background: var(--hpc-surface-sunken);
    object-fit: cover;
}
.hpc-cart-drawer-thumb-icon {
    display: block;
}
.hpc-cart-drawer-line-main {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
}
.hpc-cart-drawer-line-name {
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.hpc-cart-drawer-line-sub {
    font-size: .8rem;
    color: var(--hpc-text-soft);
}
.hpc-cart-drawer-line-price {
    flex: 0 0 auto;
    font-weight: 700;
    white-space: nowrap;
}
/* Loading-state placeholder rows (cartdrawer.js's own load()) — same
   flex shape as a real .hpc-cart-drawer-line so the drawer doesn't resize
   once real lines replace them. A dedicated thumb size rather than reusing
   .hpc-cart-drawer-thumb: that class sets its own solid background, which
   (declared later in this file) would win over .hpc-skeleton's shimmer
   gradient if both classes were combined on one element. */
.hpc-cart-drawer-skeleton-thumb {
    flex: 0 0 auto;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: var(--hpc-radius-sm);
}
.hpc-cart-drawer-skeleton-main {
    flex: 1 1 auto;
    min-width: 0;
}
.hpc-cart-drawer-remove {
    flex: 0 0 auto;
    width: 1.6rem;
    height: 1.6rem;
    padding: 0;
    border: none;
    border-radius: 9999px;
    background: transparent;
    color: var(--hpc-text-soft);
    font-size: 1.15rem;
    line-height: 1;
    cursor: pointer;
}
.hpc-cart-drawer-remove:hover,
.hpc-cart-drawer-remove:focus-visible {
    background: var(--hpc-surface-sunken);
    color: var(--hpc-danger);
}
@media (max-width: 575.98px) {
    .hpc-cart-drawer-backdrop {
        align-items: flex-end;
        padding: 0;
    }
    .hpc-cart-drawer {
        max-width: none;
        width: 100%;
        border-radius: var(--hpc-radius-lg) var(--hpc-radius-lg) 0 0;
        max-height: 85vh;
        display: flex;
        flex-direction: column;
    }
    .hpc-cart-drawer-body {
        max-height: none;
    }
}

/* AJAX add-to-cart confirmation toast — reuses .hpc-cart-toast's positioning
   (see the booking calendar's own copy, just above) so both read as the same
   platform-wide affordance; only the thumbnail bit is new here. */
.hpc-cart-widget-toast,
.hpc-cart-toast {
    position: fixed;
    left: 50%;
    bottom: 1.25rem;
    transform: translateX(-50%);
    z-index: 1060;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .75rem;
    background: var(--hpc-surface);
    border: 1px solid var(--hpc-border);
    border-radius: var(--hpc-radius);
    box-shadow: var(--hpc-shadow-lifted);
    padding: .75rem 1rem;
    max-width: calc(100vw - 2rem);
}
.hpc-cart-widget-toast[hidden] {
    display: none;
}
.hpc-cart-widget-toast-thumb {
    flex: 0 0 auto;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: var(--hpc-radius-sm);
    background: var(--hpc-surface-sunken);
    overflow: hidden;
}
.hpc-cart-widget-toast-thumb[hidden] {
    display: none;
}
.hpc-cart-widget-toast-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.hpc-cart-widget-toast-text {
    font-weight: 600;
}
@media (max-width: 575.98px) {
    .hpc-cart-widget-toast {
        left: 1rem;
        right: 1rem;
        bottom: calc(5.5rem + env(safe-area-inset-bottom, 0px));
        transform: none;
        max-width: none;
    }
}

/* ======================================================================
   Mobile app chrome — a native-app-style, strict 5-tab bottom bar (My
   Rewards / Our Store / Book a Service / Activity / My Profile), no drawer,
   replacing Cassiopeia's own slide-out `offcanvas` mobile menu. Injected
   site-wide by plg_system_hpctheme's injectMobileChrome() (every front-end
   page, not just this component's own), driven by media/com_happypetconnect/
   js/mobilenav.js.

   BREAKPOINT: max-width 1199.98px (Bootstrap's `xl` cutoff) — deliberately
   wider than the usual phone-only breakpoint so iPad Pro landscape (1194px
   logical width) stays in "app chrome" mode rather than reverting to
   Cassiopeia's desktop nav; ≥1200px is completely untouched. The block below
   defaults to `display: none` and is only switched on inside the media
   query, so nothing here has any effect at desktop widths at all.
   ====================================================================== */
.hpc-mobilebar {
    display: none;
}

@media (max-width: 1199.98px) {
    /* Room for the fixed bottom bar so it never covers the page's own
       footer/last content — env() falls back to 0 on non-notch devices.
       Scoped to :has(.hpc-mobile-chrome) (3.19.19) so the "Mobile Site"
       Feature Toggle, off, doesn't leave dead padding behind once the
       chrome it was reserving room for stops rendering at all.
       3.34.49: also reserves --hpc-footer-height, now that the fixed
       .footer stacks above this bar (see the .footer rules ~L6780) instead
       of sitting in normal flow. */
    body:has(.hpc-mobile-chrome) {
        padding-bottom: calc(4rem + var(--hpc-footer-height) + env(safe-area-inset-bottom));
    }

    /* REMOVED (per explicit request): this block used to force-hide
       Cassiopeia's stock "Main Menu"/"Login Form" modules (and the
       hamburger/offcanvas/sidebar wrappers around them) whenever our own
       bottom bar was present. The Business wants to manage those modules
       themselves the normal Joomla way (Module Manager: unpublish, or
       unassign from a position) — our chrome is purely additive now, never
       suppressing anything on its own. Nothing else in this codebase reads
       or depends on .header-toggler/#offcanvas-mobile-menu/#sidebar/
       .sidebar/#form-login/ul.menu being hidden or present (confirmed: only
       cartdrawer.js touches `.header` itself, unrelated), so removing this
       is safe — if a Business does unpublish those modules, everything
       else keeps working exactly as before. */

    .hpc-mobilebar {
        display: flex;
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 1050;
        background: var(--hpc-surface);
        border-top: 1px solid var(--hpc-border);
        box-shadow: var(--hpc-shadow-lifted);
        padding: .25rem .25rem calc(.25rem + env(safe-area-inset-bottom));
    }
    .hpc-mobilebar-item {
        flex: 1 1 0;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: .15rem;
        padding: .35rem .25rem;
        color: var(--hpc-muted);
        text-decoration: none;
        font-size: .68rem;
        font-weight: 600;
        text-align: center;
        border-radius: var(--hpc-radius-sm);
        transition: color .12s ease;
    }
    .hpc-mobilebar-icon {
        display: flex;
        align-items: center;
        justify-content: center;
        position: relative;
    }
    .hpc-mobilebar-icon svg {
        width: 1.35rem;
        height: 1.35rem;
    }
    /* Cart item-count badge — same live count as the Booking/Store header
       Cart buttons already show (cartpulse.js), just overlaid on the icon
       here instead of appended after it. */
    .hpc-mobilebar-badge {
        position: absolute;
        top: -.3rem;
        right: -.5rem;
        min-width: 1.05rem;
        height: 1.05rem;
        padding: 0 .3rem;
        font-size: .6rem;
        line-height: 1.05rem;
    }
    .hpc-mobilebar-item:hover,
    .hpc-mobilebar-item:focus-visible,
    .hpc-mobilebar-item.is-active {
        color: var(--hpc-primary);
        outline: none;
    }

    /* Centre item ("Book a Service") — raised accent circle, native-app
       "primary action" styling. Only the icon box is pulled up (negative
       margin), never the whole flex item, so its label stays aligned with
       every other item's label instead of overlapping a neighbour. */
    .hpc-mobilebar-center .hpc-mobilebar-icon {
        width: 3rem;
        height: 3rem;
        margin-top: -1.4rem;
        border-radius: 50%;
        background: var(--hpc-primary);
        color: var(--hpc-primary-contrast);
        box-shadow: var(--hpc-shadow-resting);
        transition: background-color .12s ease;
    }
    .hpc-mobilebar-center:hover .hpc-mobilebar-icon,
    .hpc-mobilebar-center:focus-visible .hpc-mobilebar-icon,
    .hpc-mobilebar-center.is-active .hpc-mobilebar-icon {
        background: color-mix(in srgb, var(--hpc-primary) 85%, black);
    }
    .hpc-mobilebar-center.is-active {
        color: var(--hpc-muted); /* the raised circle carries the "active" colour instead */
    }
}

/* ======================================================================
   "Desktop Site" feature toggle off (FeatureHelper::desktopSiteEnabled()
   === false) — plg_system_hpctheme sets `data-force-mobile` on <html> (see
   HpcTheme::applyTheme()) instead of the earlier, confirmed-broken forced
   narrow <meta name="viewport"> tag (Cassiopeia's own template always
   overwrote it — see that method's own comment for the full render-order
   finding). This block mirrors the visibility-triggering declarations from
   the @media block directly above it, selector-for-selector, so a forced
   desktop-width "mobile" render is identical to a real narrow viewport.
   Deliberately NOT a full duplicate of that block — the cosmetic/position/
   colour rules above already apply unconditionally once `display` flips to
   flex, so only the declarations that actually flip visibility need a
   second copy here. Keep this block's selector list in sync with the
   @media block's if either one changes.
   ====================================================================== */
html[data-force-mobile] body:has(.hpc-mobile-chrome) {
    padding-bottom: calc(4rem + var(--hpc-footer-height) + env(safe-area-inset-bottom));
}

html[data-force-mobile] .hpc-mobilebar {
    display: flex !important;
}

/* Persistent header (3.25.9x) — the brand mark (also the Home link) must
   always stay reachable, on every page, without depending on the admin
   opting into Cassiopeia's own "Sticky Header" template style option
   (off by default; a Business that never finds/flips that setting would
   otherwise lose the header on scroll, per explicit report). Forced here
   unconditionally rather than relying on that param.
   `header.header` (element+class, specificity 0-1-1) rather than plain
   `.header` (0-1-0) — deliberately beats Cassiopeia's own `.container-header`
   rule of equal specificity regardless of stylesheet load order, same
   defensive-specificity reasoning as the dark-mode Bootstrap context-class
   overrides elsewhere in this file. `.container-header` already carries an
   opaque gradient background (big_goose's own template.css), so sticking it
   never reveals see-through content scrolling underneath. */
header.header {
    position: sticky;
    top: 0;
    z-index: 1040;
}

/* height:0 only from 1200px up: at that breakpoint the relocate script
   (HpcTheme::buildDesktopChromeHtml()) physically moves the real
   .navbar-brand node OUT of .header into .hpc-desktop-chrome, so .header has
   no visible content left and collapsing it is correct there. Below 1200px
   the brand row stays inside .header and must be counted in normal flow —
   collapsing it there used to make the logo float with zero reserved
   space, so the hero article and everything else painted underneath/behind
   it instead of below it. */
@media (min-width: 1200px) {
    header.header {
        height: 0px;
    }
}

/* Mobile brand row (< 1200px only — see the height:0 note above for what
   happens at 1200px+). Business name left, logo right, spanning the full
   row so the two sit at opposite edges instead of Cassiopeia's own
   .container-header .navbar-brand (template.css) inline-block/left-aligned
   default. Because .header is no longer height:0 on mobile (see above),
   this now takes real, reserved space — page content flows below it
   instead of underneath it. */
@media (max-width: 1199.98px) {
    /* header.header (0-1-1), not .header (0-1-0) — must outrank
       .container-header .navbar-brand (0-2-0, template.css) regardless of
       stylesheet load order; same defensive-specificity reasoning as
       header.header's own rule above. Targets .brand-logo, not .navbar-brand
       itself — the business-name span lives INSIDE the same <a> as the logo
       (index.php) so tapping the name also navigates home, not just the
       image, which means the name/logo space-between row is this anchor's
       own content, not a split across .navbar-brand's direct children. */
    header.header .navbar-brand {
        display: block;
        width: 100%;
    }
    header.header .brand-logo {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: .75rem;
        width: 100%;
        /* The business-name span is now inline text inside this anchor
           (moved here so tapping the name also navigates home) — without
           this it would pick up the browser's default link underline,
           template.css's .container-header .navbar-brand a rule sets colour
           only, never text-decoration. */
        text-decoration: none;
    }
    .header .brand-name-text {
        display: block;
        color: var(--hpc-text);
        font-weight: 600;
        font-size: 1.1rem;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    /* template.css's .container-header .site-description rule (0-2-0) hardcodes
       #fff, left over from the brand-gradient background this header no longer
       has (see header.header's own background override above) — dead-white
       text with no theme awareness. header.header (0-2-1) to reliably outrank
       it regardless of load order, same as .navbar-brand above. Optional param
       (Template Style "Site Description"), off by default. */
    header.header .site-description {
        color: var(--hpc-text-soft);
    }
    /* .brand-name-fallback (index.php): the actual brand mark when no logo
       image is uploaded — this instance's case. Same #fff-inherited-from-
       .container-header .navbar-brand bug as .site-description above, just
       via inheritance rather than a direct rule (the fallback span has no
       color of its own). Not scoped like .brand-name-text (which only shows
       alongside an uploaded logo) because this IS the whole brand mark when
       it renders — no separate image to pair it with. */
    header.header .brand-name-fallback,
    header.header .brand-logo-paw {
        color: var(--hpc-text);
    }
    /* Header background grade (mobile only — desktop's header has no visible
       fill once the brand mark relocates into .hpc-desktop-chrome, see the
       height:0 rule above). Left = the header's current flat colour
       (--hpc-surface), right = the page's own background (--bs-body-bg), so
       the bar visually dissolves into the page instead of ending in a hard
       edge. */
    header.header {
        background-image: linear-gradient(to right, var(--hpc-surface) 0%, var(--bs-body-bg) 100%);
    }
    /* Dark mode: swap the right-hand stop to --hpc-surface-sunken instead of
       --bs-body-bg. Both resolve to the same literal by default (site.css's
       dark block sets --hpc-surface-sunken: var(--bs-body-bg, ...)), but they
       diverge the moment a Business sets Options → Branding → "Dark mode
       background" (brand_dark_bg): BrandingHelper::buildCss() points that
       param at --hpc-surface-sunken (and --hpc-surface itself, via "Dark mode
       surface colour"/brand_dark_surface) but writes the body's own dark
       background as a literal `!important`, never updating --bs-body-bg — so
       a light-mode --bs-body-bg gradient would silently ignore a custom dark
       background while everything else on the page correctly re-themed. */
    [data-bs-theme="dark"] header.header {
        background-image: linear-gradient(to right, var(--hpc-surface) 0%, var(--hpc-surface-sunken) 100%);
    }
}

/* Business name label — hidden by default (desktop chrome shows only the
   logo image, never this text); the mobile media query above is the only
   place that turns it on. */
.brand-name-text {
    display: none;
}

/* Uploaded brand logo has no guaranteed aspect ratio or resolution — it's a
   free-form admin upload (BrandingHelper::logoUrl(), same field used for
   invoices/emails), not a pre-cropped template asset, so Cassiopeia's own
   .navbar-brand never constrains it. Cap it to a sane header height instead
   of trusting the source file. */
.header .brand-logo-img {
    display: block;
    height: 32px;
    max-height: 32px;
    width: auto;
    max-width: 100%;
    object-fit: contain;
}
/* Light/dark logo pair (BrandingHelper::logoUrl()/logoUrlDark()) — big_goose's
   index.php only emits BOTH <img> tags when a dark-specific logo is actually
   set and differs from the light one; when it does, this is what picks the
   right one for the live data-bs-theme attribute. Not scoped under
   [data-bs-theme="dark"] .header for the light rule — data-bs-theme is
   always set explicitly to "light" or "dark" (never absent, see
   plg_system_hpctheme), so hiding -dark by default and only showing it once
   dark is confirmed is the safer default if that ever changes. */
/* Also matched under .hpc-desktop-chrome, not just .header — at the
   >=1200px breakpoint buildDesktopChromeHtml()'s relocate script moves the
   real .navbar-brand node into the tab strip (see that method's docblock),
   so a .header-only selector would stop matching there and both images
   would fall back to visible (confirmed live in an earlier version of this
   feature that used a separate overlay: two stacked logos). Two ancestors,
   not a class swap on the images themselves, because it's the same DOM
   node either way — only which container holds it changes. */
.header .brand-logo-img-dark,
.hpc-desktop-chrome .brand-logo-img-dark {
    display: none;
}
[data-bs-theme="dark"] .header .brand-logo-img-light,
[data-bs-theme="dark"] .hpc-desktop-chrome .brand-logo-img-light {
    display: none;
}
[data-bs-theme="dark"] .header .brand-logo-img-dark,
[data-bs-theme="dark"] .hpc-desktop-chrome .brand-logo-img-dark {
    display: block;
}

/* ======================================================================
   Unified Navigation — desktop tab strip (3.25.58). Same 5 destinations as
   .hpc-mobilebar above, rendered by HpcTheme::buildDesktopTabsHtml() and
   spliced in right before </body> exactly like the mobile bar (see
   injectMobileChrome()). The two are mutually exclusive purely by breakpoint
   — this block defaults to `display: none` and only switches on at
   `min-width: 1200px`, the exact inverse of .hpc-mobilebar's own
   `max-width: 1199.98px` block, so there is never a width where both (or
   neither) are visible.

   REAL BUG fixed here (3.25.63): unlike .hpc-mobilebar, which is
   `position: fixed` and so never cares where the DOM injection point put
   it, this block had NO position rule at all — since it's spliced in right
   before </body>, it rendered exactly there: the very bottom of the page,
   after the footer. Confirmed live via screenshot (no tab strip anywhere
   near the header). HpcTheme::buildDesktopChromeHtml() now runs an inline
   script that relocates the node to right after the real `.header` element
   before first paint, so `position: sticky` here works correctly as a
   normal-flow element instead of needing a measured `top:` offset.

   Stock Joomla nav (Main Menu/Admin menu/Login Form, wherever a Business
   has them positioned) is deliberately left untouched — per explicit
   request, this chrome is purely additive, never suppressing anything on
   its own. A Business that wants those modules gone manages that
   themselves the normal Joomla way (Module Manager).
   ====================================================================== */
.hpc-desktop-chrome {
    display: none;
}

@media (min-width: 1200px) {
    /* Layout, fifth iteration — architectural fix, not another gap tweak.
       Every previous version (grid, flex-cluster at various gaps,
       edge-pin) docked the cart INSIDE this bar, as a sibling of
       .hpc-desktop-tabs. That's the actual root cause of "it was moving
       the tabs" — hiding/showing a flex sibling (see the cart's new
       hidden-when-empty behaviour, cartdrawer.js) changes what
       `justify-content: center` centres, so the tabs visibly shift left/
       right every time the cart appears or disappears.
       Fixed by moving the cart OUT of this bar entirely: it now docks
       into .header (the top brand bar, absolutely positioned top-right —
       see the `.header .hpc-cart-trigger` rule below cartdrawer.js docks
       into with dockTrigger()). Two completely separate rows: this one
       only ever contains tabs, so nothing here can ever move when the
       cart's visibility changes. */
    .hpc-desktop-chrome {
        display: flex;
        justify-content: center;
        align-items: center;
        position: sticky;
        top: 4.5rem; /* pre-JS fallback guess; overwritten immediately */
        z-index: 1030;
        background: var(--hpc-surface);
        border-bottom: 1px solid var(--hpc-border);
        box-shadow: var(--hpc-shadow-lifted);
        padding: 0 1rem;
        /* Explicit here (not left to derive from .hpc-desktop-tab-item's own
           min-height) so the bar's real height is a known, exact number —
           .hpc-desktop-chrome .brand-logo-img sizes itself directly off
           this value (74px bar - 2px top - 2px bottom = 70px logo). */
        min-height: 74px;
        /* REAL BUG fixed here (reported as "icons and names jumping
           around" / "some content loads before other"): this bar is
           spliced into the HTML right before </body>, then buildDesktopChromeHtml()'s
           inline script physically moves it up next to .header — that DOM
           move was happening visibly after the browser started rendering.
           Left visible throughout, that reads as the whole tab strip
           jumping into place on every load. Same fix as .hpc-cart-trigger:
           stay invisible until the relocate script confirms the move is
           done (.is-relocated, added at the end of that script), then
           reveal already in its final position — never a jump. */
        opacity: 0;
        transition: opacity .15s ease-out;
    }
    .hpc-desktop-chrome.is-relocated {
        opacity: 1;
    }
    @media (prefers-reduced-motion: reduce) {
        .hpc-desktop-chrome {
            transition: none;
        }
    }

    .hpc-desktop-tabs {
        display: flex;
        align-items: stretch;
        gap: .25rem;
    }

    /* Brand mark, relocated here from .header by buildDesktopChromeHtml()'s
       script at this same breakpoint (see that method's docblock). Absolute,
       not a flex sibling of .hpc-desktop-tabs — .hpc-desktop-chrome is
       already `position: sticky` above, which is enough to anchor this;
       staying out of flow means it can never disturb .hpc-desktop-tabs' own
       `justify-content: center` on the parent (an in-flow logo would shift
       what "center" means the moment it appeared/disappeared). Left-aligned
       to the same content-column edge the cart trigger uses on the other
       side of the header, so it doesn't just glue to the bare viewport edge
       on wide screens. */
    .hpc-desktop-chrome .navbar-brand {
        position: absolute;
        top: 50%;
        left: max(1.5rem, calc((100vw - 900px) / 2));
        transform: translateY(-50%);
        display: flex;
        align-items: center;
    }
    .hpc-desktop-chrome .brand-logo-img {
        height: 70px;
        max-height: 70px;
        width: auto;
        max-width: 100%;
        object-fit: contain;
    }

    /* The docked cart trigger — lives in .header now (see this block's own
       docblock above), absolutely positioned so it takes zero space in
       that row's normal flow: showing/hiding it can never push or resize
       anything else in the header, satisfying "nothing must shift" for
       real rather than just by observation. Overrides the base
       .hpc-cart-trigger fixed/top/right rule (descendant selector wins
       regardless of source order); that base rule remains the fallback
       for when .header isn't found at all (cartdrawer.js's
       alignTriggerToHeader() fallback path). */
    .header .hpc-cart-trigger {
        position: absolute;
        /* NOT 50% — .header is height:0 at this breakpoint (see the
           header.header height:0 rule above), so top:50% centres against
           nothing and translateY(-50%) pushes half the icon above the
           viewport ("joined the header and went half MIA", live report).
           .header sits flush above .hpc-desktop-chrome (the actual visible
           74px bar, see its min-height above), so centre against THAT bar's
           height instead. */
        top: calc(74px / 2);
        /* Aligns to the right edge of the centered 900px content container
           (see .container-component, ~L1350) that the page cards — including
           the "Expand Article" button — sit inside, so the trigger stays
           horizontally above that button at any viewport width instead of
           an arbitrary fixed/vw offset. */
        right: max(1.5rem, calc((100vw - 900px) / 2));
        left: auto;
        transform: translateY(-50%);
    }

    .hpc-desktop-tab-item {
        display: flex;
        align-items: center;
        gap: .5rem;
        padding: 1.35rem 1.1rem;
        min-height: 74px;
        color: var(--hpc-muted);
        text-decoration: none;
        font-size: .9rem;
        font-weight: 600;
        border-bottom: 2px solid transparent;
        transition: color .12s ease, border-color .12s ease;
    }

    .hpc-desktop-tab-icon {
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .hpc-desktop-tab-icon svg {
        width: 1.15rem;
        height: 1.15rem;
    }

    .hpc-desktop-tab-item:hover,
    .hpc-desktop-tab-item:focus-visible {
        color: var(--hpc-text);
        outline: none;
    }

    .hpc-desktop-tab-item.is-active {
        color: var(--hpc-primary);
        border-bottom-color: var(--hpc-primary);
    }
}

/* "Desktop Site" feature toggle off equivalent for forced-mobile: a
   `data-force-mobile` render must never show the desktop strip alongside
   the forced .hpc-mobilebar just above, even at a genuinely wide viewport. */
html[data-force-mobile] .hpc-desktop-chrome {
    display: none !important;
}

/* ======================================================================
   Pet profile page (3.17.0) — centered layout, photo container, details
   list, Pet Documents card grid, compact "Points earned" card.
   ====================================================================== */
.hpc-profile-wrap {
    max-width: 900px;
}

/* "Liquid" 2-track grid (3.25.2) — each card is pinned to a column (1 or 2)
   and an explicit `order` via its data-hpc-area, NOT DOM position, so the
   two tracks behave like independent masonry columns instead of paired
   rows: Photo/Bookings/Documents/Status stack down column 1, Details/
   Purchases/Points stack down column 2. align-items: start is required for
   that — without it Grid stretches every row to its tallest cell, which
   only makes sense when rows are meant to pair up (they no longer are).
   A card absent from the DOM (feature toggle off, e.g. Purchases/Points/
   Status) just leaves the rest of its column to ride up — plain Grid
   auto-placement, no JS needed for that part. `data-hpc-collapsed` is set
   by profilegrid.js only for the (currently unreachable, since Photo/
   Details/Bookings/Documents are unconditional) case where an entire
   column ends up with nothing in it — CSS alone can't detect "zero
   children with grid-column: 2" across siblings without a fragile :has()
   selector, so that specific check is left to the script. */
.hpc-profile-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1rem;
    align-items: start;
}
.hpc-profile-grid[data-hpc-collapsed] {
    grid-template-columns: 1fr;
}
.hpc-profile-grid [data-hpc-area="photo"]     { grid-column: 1; order: 1; }
.hpc-profile-grid [data-hpc-area="details"]   { grid-column: 2; order: 2; }
.hpc-profile-grid [data-hpc-area="bookings"]  { grid-column: 1; order: 3; }
.hpc-profile-grid [data-hpc-area="purchases"] { grid-column: 2; order: 4; }
.hpc-profile-grid [data-hpc-area="documents"] { grid-column: 1; order: 5; }
.hpc-profile-grid [data-hpc-area="points"]    { grid-column: 2; order: 6; }
.hpc-profile-grid [data-hpc-area="status"]    { grid-column: 1; order: 7; }

@media (max-width: 767px) {
    /* Numeric order 1-7 (not DOM order) drives mobile stacking too — a
       single grid-template-columns: 1fr collapses both tracks into one,
       `order` alone decides the sequence. */
    .hpc-profile-grid {
        grid-template-columns: 1fr;
    }
    .hpc-profile-grid [data-hpc-area] {
        grid-column: 1;
    }
}

/* Take Photo / Choose from Library sit flush against the bottom of the
   photo card. Previously relied on Grid stretching the row to match a
   taller sibling (align-items: stretch); now that align-items: start
   means the photo card is always exactly its own content height, this
   still anchors the upload control to the bottom of the fixed-height
   .hcc-pet-photo image area above it rather than the top. */
.hpc-profile-grid [data-hpc-area="photo"] .card-body {
    display: flex;
    align-items: flex-end;
}
.hpc-profile-grid [data-hpc-area="photo"] .card-body form {
    width: 100%;
}

.hcc-pet-photo {
    display: flex;
    align-items: center;
    justify-content: center;
    /* Square, not 4/3 — this box now sits in a half-width grid column at
       every screen size (see .hpc-profile-grid above), so a wide-ish ratio
       read as too short/cropped-feeling once the column itself got
       narrower; 1/1 gives it real height without depending on the column
       being full page width the way the old single-column layout was. */
    aspect-ratio: 1 / 1;
    /* Hard cap, not just aspect-ratio — a broken/oversized child (e.g. a
       failed photo request rendering raw response content instead of a
       small broken-image glyph, see profile.php's onerror handler) must
       never be able to blow this box past a sane height regardless of its
       own intrinsic size. Still scales dynamically with the column's own
       width via aspect-ratio below this cap — only the upper bound moved. */
    max-height: 400px;
    overflow: hidden;
    background: var(--hpc-surface-sunken);
    /* Must match the parent .card's own border-radius (--hpc-radius-lg,
       3.26) — same corner-mismatch bug as .hcc-card-thumb, just a separate
       class that got missed in that first pass. */
    border-top-left-radius: var(--hpc-radius-lg);
    border-top-right-radius: var(--hpc-radius-lg);
}
.hcc-pet-photo img {
    display: block;
    width: 100%;
    height: 100%;
    /* Fit the image into the container rather than stretch/crop it — the
       box stays a fixed shape (needed for a consistent card), the photo
       inside it stays true to its own aspect ratio, centered. */
    object-fit: contain;
}
.hcc-pet-photo-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    font-size: 3rem;
    color: var(--hpc-text-faint);
}

/* Pet details — column-first sub-grid (left column Species/Age/Gender,
   right column Breed/Weight/Spayed, per the wireframe's own grouping) plus
   free-text notes as their own full-width tiles below it. Each field is
   always rendered (missing values show an em dash — see profile.php's
   $genderLabel etc.) so the column split never shifts around. */
.hpc-details-subgrid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-auto-flow: column;
    grid-template-rows: repeat(3, auto);
    gap: .5rem;
}
.hpc-detail-tile {
    background: var(--hpc-surface-sunken);
    border-radius: var(--hpc-radius);
    padding: .5rem .65rem;
    text-align: center;
}
.hpc-detail-tile.hpc-detail-wide {
    text-align: left;
}

/* Bookings & history — scrolls internally past ~4 rows instead of growing
   the card (and the whole page) taller with every booking a pet racks up.
   This element is BOTH .hpc-list-cards (the flex-column list container)
   AND .hpc-bookings-scroll (see profile.php) — switched from flex to plain
   block flow here specifically, with margin-based spacing replacing flex
   `gap`, so a single short row can never be stretched to fill the column's
   full height the way a flex column's cross-axis sizing could. `height:
   auto !important` on the row itself is a second, redundant safety net for
   the same "never taller than its own content" guarantee. */
.hpc-bookings-scroll.hpc-list-cards {
    display: block;
    max-height: 280px;
    overflow-y: auto;
    padding-right: .25rem;
}
/* Real bug this "never taller than its own content" guarantee didn't
   actually cover: .hpc-list-card's own base layout (row, flex-wrap: wrap,
   align-items: center) is fine at full list width, but in this card's
   narrow half-column context (.hpc-profile-grid) the title/sub block and
   the price/status block don't fit side by side — they wrap onto two
   lines, and a WRAPPED flex container's default align-content (normal,
   which resolves to stretch when there's spare cross-axis room) spreads
   those two wrapped lines apart to fill whatever height the row ends up
   with, instead of packing them tight — that's the empty middle gap, not
   a min-height/flex:1 rule (there wasn't one). Fixed at the source: this
   row switches to a real vertical stack in this narrow context, so there's
   only ever one line's worth of content per line and nothing left to
   stretch apart. Scoped to .hpc-bookings-scroll specifically, not the
   shared .hpc-list-cards-compact class Cart also uses — Cart's rows stay
   full-width and genuinely want the side-by-side layout. */
.hpc-bookings-scroll .hpc-list-card {
    height: auto !important;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    flex-wrap: nowrap;
    gap: .35rem;
    padding: .65rem .75rem;
    margin-bottom: .5rem;
}
.hpc-bookings-scroll .hpc-list-card:last-child {
    margin-bottom: 0;
}
/* The bug that survived the previous pass: .hpc-list-card-main's base rule
   is `flex: 1 1 220px` — a flex-basis meant for the ROW layout, where 220px
   is a MINIMUM WIDTH so the title block doesn't get crushed next to the
   price. Switching this card to flex-direction: column (above) rotates the
   main axis, so that same "220px" is now read as a starting HEIGHT, and
   flex-grow: 1 then lets it stretch further to fill the column — exactly
   the tall-card-with-a-dead-middle bug, just relocated from the row to
   .hpc-list-card-main itself. It needs no basis/grow at all here; it should
   only ever be as tall as its own two lines of text. */
.hpc-bookings-scroll .hpc-list-card-main {
    flex: 0 1 auto;
    height: auto;
    min-height: 0;
}
.hpc-bookings-scroll .hpc-list-card-when {
    flex: 0 0 auto;
    margin-left: 0;
}

/* Compact scrollable preview of the same ledger the modal shows in full. */
.hpc-points-mini-history {
    max-height: 130px;
    overflow-y: auto;
}
.hpc-points-mini-history .list-group-item {
    padding: .3rem 0;
    font-size: .82rem;
}

/* Pet Documents — card grid (max 4 visible + "View all"), mime-based icon,
   fully clickable (image cards open the lightbox; PDFs open inline in a new
   tab; anything else downloads). */
.hpc-doc-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
    gap: .75rem;
}
.hpc-doc-card {
    text-align: center;
}
.hpc-doc-thumb {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 1 / 1;
    background: var(--hpc-surface-sunken);
    border: 1px solid var(--hpc-border);
    border-radius: var(--hpc-radius);
    overflow: hidden;
    color: var(--hpc-text-soft);
    text-decoration: none;
    transition: transform .15s ease, box-shadow .15s ease;
}
.hpc-doc-thumb:hover,
.hpc-doc-thumb:focus-visible {
    transform: translateY(-2px);
    box-shadow: var(--hpc-shadow-resting);
}
.hpc-doc-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.hpc-doc-icon {
    font-size: 2rem;
}
.hpc-doc-kind {
    position: absolute;
    margin-top: 2.5rem;
    font-size: .65rem;
    font-weight: 700;
    letter-spacing: .04em;
}
.hpc-doc-name {
    font-size: .75rem;
    margin-top: .35rem;
    color: var(--hpc-text-soft);
}
.hpc-doc-delete {
    font-size: .75rem;
}

.hpc-points-earned-card {
    cursor: pointer;
    font: inherit;
}

/* ==========================================================================
   MOBILE COMPACT MODE — direct response to "it looks super clunky, mobile
   devices are not as big as PCs, we need a compact feel ... not smaller,
   just more compact ... remove dead space". Every page's actual content
   sits inside one of this component's own wrapper divs
   (`<div class="com-happypetconnect-{view}">` — one per site view, always
   present, confirmed by grepping every template), which is what every rule
   below is scoped to via the `[class^="com-happypetconnect-"]` prefix
   selector — deliberately NOT a blind, page-wide override of Bootstrap's
   utility classes (mb-3/mb-4/gap-2/gap-3 etc. are reused by Cassiopeia and
   any other module/component sharing the same page; overriding them
   unscoped would tighten content this platform doesn't own). Phone-width
   only (max-width: 767.98px, Bootstrap's own `md` cutoff) — the 1199.98px
   breakpoint above is reserved for "is the app chrome active", a different
   question from "is the screen small enough that padding reads as waste".
   Sizes drop, spacing tightens — nothing here removes a feature or hides
   content, only the dead space around it. ========================== */
@media (max-width: 767.98px) {
    /* Page title row — every view opens with <h1 class="h3 mb-0"> plus a
       header action row; both were full desktop size regardless of screen
       width. */
    [class^="com-happypetconnect-"] h1.h3 {
        font-size: 1.25rem;
    }
    [class^="com-happypetconnect-"] h2.h5,
    [class^="com-happypetconnect-"] h2.h6 {
        font-size: 1rem;
    }

    /* Bootstrap's mb-3/mb-4 vertical rhythm (1rem/1.5rem) is the single
       biggest source of "dead space" stacking down a mobile page — every
       header row, card, and section break uses one or the other. Tightened
       only inside this platform's own markup, never touching Cassiopeia
       chrome or any other module sharing the page. */
    [class^="com-happypetconnect-"] .mb-4 {
        margin-bottom: 1rem !important;
    }
    [class^="com-happypetconnect-"] .mb-3 {
        margin-bottom: .65rem !important;
    }
    [class^="com-happypetconnect-"] .gap-2 {
        gap: .4rem !important;
    }
    [class^="com-happypetconnect-"] .gap-3 {
        gap: .6rem !important;
    }

    /* Card padding — Bootstrap's default --bs-card-spacer-y/-x (1rem each)
       reads as generous on a phone-width card. Overriding the variable
       (rather than card-body's padding directly) keeps card-header/
       card-footer, which read the same variable, tightened consistently
       too, with nothing to keep in sync by hand. */
    [class^="com-happypetconnect-"] .card {
        --bs-card-spacer-y: .65rem;
        --bs-card-spacer-x: .75rem;
    }

    /* The dashboard's "Total Points" hero strip — the single largest fixed
       block of padding on the site, sized for a desktop hero card. */
    .hpc-stat-strip {
        padding: .85rem 1rem;
    }
    .hpc-stat-total-value {
        font-size: 1.75rem;
    }

    /* Pet/product card grids and the Bookings/Orders row-card lists — same
       "smaller gap, tighter body" treatment as the card padding above. */
    .hpc-pets-grid {
        gap: .65rem;
    }
    .hpc-list-cards {
        gap: .5rem;
    }

    /* Modal dialogs (Set Status, My Rewards ledger, the cart drawer) —
       full desktop padding on a phone-width sheet was most of the reason
       they read as oversized rather than native-app-compact. */
    .hpc-modal-dialog .card-header,
    .hpc-modal-dialog .card-body,
    .hpc-modal-dialog .card-footer {
        padding: .75rem .9rem;
    }

    /* Pet profile — phone-width tightening on top of the structural grid
       defined outside this media query (.hpc-profile-grid, .hcc-pet-photo,
       .hpc-details-subgrid/.hpc-detail-tile, .hpc-bookings-scroll — all
       apply at every width; only spacing/sizing tweaks live here). */
    .hpc-profile-grid {
        gap: .6rem;
    }
    .hcc-pet-photo {
        /* Still square (see the base rule) and still scales dynamically
           with the column's own width — only the phone-width upper bound
           moved, slightly taller than before. */
        max-height: 300px;
    }
    .hcc-pet-photo-placeholder {
        font-size: 1.75rem;
    }
    .hpc-details-subgrid {
        gap: .35rem;
    }
    .hpc-detail-tile {
        padding: .3rem .4rem;
        line-height: 1.25;
    }
    .hpc-bookings-scroll.hpc-list-cards {
        max-height: 240px;
    }

    /* Pet Documents grid — smaller minimum tile so more fit per row on a
       narrow screen, same reasoning as the pets/product card grids above. */
    .hpc-doc-grid {
        grid-template-columns: repeat(auto-fill, minmax(82px, 1fr));
        gap: .5rem;
    }

    /* Card padding/margins on this page specifically go tighter than the
       site-wide compact defaults above — a live device screenshot showed
       even those still reading as "one big box per field group". */
    .hpc-profile-wrap .card {
        --bs-card-spacer-y: .5rem;
        --bs-card-spacer-x: .6rem;
    }
    .hpc-profile-wrap .card-header {
        font-size: .9rem;
    }
    .hpc-profile-wrap h1.h3 {
        font-size: 1.1rem;
    }
}

/* Pet edit form — Spayed/Neutered as a real settings row (title + live
   status on the left, a switch on the right), not a bare control floating
   in open space next to a text label — that read thin against the
   full-width, boxed look of every other field on this form (a live
   screenshot showed the gap plainly). Two earlier cuts at this also hand-
   rolled the switch itself (track/thumb via custom CSS); replaced here with
   Bootstrap's own `.form-switch` — a real, tested Bootstrap 5.3 primitive
   this whole site already runs on, toggled by the input's own native
   `:checked` state rather than a hand-written sibling/`:has()` rule reliant
   on guessing what Joomla's core checkbox-field layout renders (no local
   copy of that layout exists in this repo, and there's no browser here to
   check against it either — see this file's standing caveat on verifying
   Joomla-core-internals assumptions). The row itself is one big `<label
   for="...">` — click/tap anywhere in the row toggles it, not just the
   small switch control, the same larger-hit-area convention already used
   for this platform's card-stretched-link pattern. */
.hpc-switch-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    width: 100%;
    min-height: calc(1.5em + 1.05rem + 2px); /* match .form-control's rendered height */
    padding: .6rem .9rem;
    background: var(--hpc-surface-sunken);
    border: 1px solid var(--hpc-border);
    border-radius: var(--hpc-radius, .5rem);
    cursor: pointer;
    transition: border-color .15s ease;
}
.hpc-switch-row:hover {
    border-color: var(--hpc-primary);
}
.hpc-switch-row-text {
    display: flex;
    flex-direction: column;
    line-height: 1.3;
}
.hpc-switch-row-title {
    font-weight: 500;
    color: var(--bs-body-color);
}
.hpc-switch-row-desc {
    font-size: .85rem;
    color: var(--hpc-text-soft);
}
.hpc-switch-row-desc-on {
    display: none;
}
.hpc-switch-row:has(.form-check-input:checked) .hpc-switch-row-desc-off {
    display: none;
}
.hpc-switch-row:has(.form-check-input:checked) .hpc-switch-row-desc-on {
    display: inline;
}
.hpc-switch-row-control {
    flex: 0 0 auto;
    margin: 0;
    padding: 0;
}
.hpc-switch-row-control .form-check-input {
    width: 2.75rem;
    height: 1.5rem;
    margin: 0;
    cursor: pointer;
}
.hpc-switch-row-control .form-check-input:checked {
    background-color: var(--hpc-primary) !important;
    border-color: var(--hpc-primary) !important;
}
.hpc-switch-row-control .form-check-input:focus {
    box-shadow: 0 0 0 .2rem color-mix(in srgb, var(--hpc-primary) 25%, transparent) !important;
}

/* ======================================================================
   Registration page — big_goose/html/com_users/registration/default.php.
   Reuses .hpc-edit-form / .hpc-edit-grid / .hpc-profile-card from the
   profile-edit override above; only the consent card, strength meter and
   inline field feedback are new here. Tokens only, no colour literals —
   this page renders in dark mode too (theme_preference).
   ====================================================================== */

.hpc-register-shell {
    max-width: 600px;
    margin-inline: auto;
}
/* Step 3's two-column review/consent split (.hpc-review-grid, further down)
   needs more room than the single-column steps do to read as two real
   columns rather than two cramped ones — widened only while step 3 is the
   active step, via the wizard's own toggled .is-active class, so steps 1/2
   keep the narrower single-column measure they were tuned for. */
.hpc-register-shell:has(.hpc-wizard-step[data-step="3"].is-active) {
    max-width: 720px;
}

/* --- Auth heading (3.29.36) ---------------------------------------------
   Replaces the old "shy" sticky banner (a coloured --hpc-primary bar) that
   used to sit across the top of registration — explicitly removed, not
   hidden: no coloured background, no sticky/scroll behaviour, just a plain
   heading + one-line subtitle sitting directly in the page. Shared by both
   registration and login for identical typography/spacing on both auth
   pages. Not scoped under .hpc-register-page/.hpc-login-page since both
   pages use it identically. */
.hpc-auth-heading {
    text-align: center;
    max-width: 30rem;
    margin: 0 auto 1.25rem;
}
.hpc-auth-heading h1 {
    margin: 0 0 .35rem;
    font-size: 1.6rem;
    font-weight: 700;
    color: var(--hpc-text);
}
.hpc-auth-heading p {
    margin: 0;
    font-size: .95rem;
    color: var(--hpc-text-faint);
}

/* --- Stepper (3.29.36) ---------------------------------------------------
   Deliberately subtle — three small numbered markers connected by a thin
   line, never a large or visually dominant element. The active step is
   identified by BOTH a filled marker and aria-current="step" (set in JS),
   never by colour alone. */
.hpc-stepper {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: 0;
    list-style: none;
    margin: 0 auto 1rem;
    padding: 0;
    max-width: 26rem;
}
.hpc-stepper-step {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .3rem;
    flex: 1 1 0;
    min-width: 0;
    position: relative;
}
/* Connecting line, drawn as a pseudo-element on every step except the
   first — sits behind the marker circle (z-index) so it reads as one
   continuous line through the row, not three separate underlines. */
.hpc-stepper-step:not(:first-child)::before {
    content: '';
    position: absolute;
    top: .7rem;
    right: 50%;
    width: 100%;
    height: 2px;
    background: var(--hpc-border);
    z-index: 0;
}
.hpc-stepper-step.is-done:not(:first-child)::before {
    background: var(--hpc-primary);
}
.hpc-stepper-num {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.4rem;
    height: 1.4rem;
    border-radius: 50%;
    font-size: .75rem;
    font-weight: 700;
    background: var(--hpc-surface);
    border: 2px solid var(--hpc-border);
    color: var(--hpc-text-faint);
    transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.hpc-stepper-step.is-active .hpc-stepper-num {
    border-color: var(--hpc-primary);
    color: var(--hpc-primary);
}
/* Completed step shows a checkmark instead of its number — the digit stays
   in the DOM (colour: transparent, not display:none) so it's still announced
   to screen readers; sighted users see only the tick. */
.hpc-stepper-step.is-done .hpc-stepper-num {
    background: var(--hpc-primary);
    border-color: var(--hpc-primary);
    color: transparent;
}
.hpc-stepper-step.is-done .hpc-stepper-num::after {
    content: '\2713';
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--hpc-primary-contrast);
    font-size: .7rem;
    font-weight: 700;
}
.hpc-stepper-label {
    font-size: .7rem;
    color: var(--hpc-text-faint);
    white-space: nowrap;
}
.hpc-stepper-step.is-active .hpc-stepper-label {
    color: var(--hpc-text);
    font-weight: 600;
}
@media (max-width: 420px) {
    /* The label text is the first thing to go on very narrow phones — the
       numbered markers + connecting line alone still communicate progress. */
    .hpc-stepper-label {
        display: none;
    }
}

/* --- Wizard shell (3.30) --------------------------------------------------
   Replaces the old `.card.hpc-profile-card` wrapper around the three steps
   — an explicit "remove the nested boxes" requirement. Plain container,
   no border/background/shadow; `.hpc-edit-section`'s existing hairline
   bottom-border still separates sub-groups within a step. */
.hpc-wizard {
    padding: 0;
}

/* --- Wizard steps (3.30, stability pass) ---------------------------------
   All three steps exist in the DOM at once (see default.php's own note on
   why — a CAPTCHA widget in particular should only ever init once). JS
   toggles the `hidden` attribute + this class together; the animation only
   plays on `.is-active` picking up it fresh each time because `hidden`
   removes and re-adds the element to the render tree, which restarts a CSS
   animation naturally.
   Reduced to a short opacity-only fade (was translateY(14px) via the same
   keyframes .hpc-field-enter uses for individual fields) — the slide
   distance combined with the browser's own instant reflow when a
   differently-sized step swaps in read as a "jump", reported as glitchy.
   A plain fade has no directional motion to clash with that reflow. */
.hpc-wizard-step {
    animation: hpc-wizard-step-enter .2s ease-out;
}
@keyframes hpc-wizard-step-enter {
    from { opacity: 0; }
    to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
    .hpc-wizard-step {
        animation: none;
    }
}
.hpc-step-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    padding: 1.5rem 0 0;
    margin-top: 1.5rem;
}
@media (max-width: 575.98px) {
    .hpc-step-nav {
        flex-direction: column-reverse;
    }
    .hpc-step-nav .btn {
        width: 100%;
    }
}

/* --- Step 3 layout: identity left, verify + consent + submit right --------
   (3.34.44 — replaces a single stacked column where the review recap,
   CAPTCHA, and consent card all read as one undifferentiated wall). Single
   column below 700px: the whole point of the split is two segments reading
   side by side, which a phone-width column has no room for anyway — under
   that width this is just the previous stacked order. No boxed card around
   either column (kept flat, per the 3.30 "remove nested boxes" call below);
   the identity column is set apart with a hairline divider only — see the
   right column's own override block further down for why its INTERNAL
   dividers (the ones between captcha/consent/step-nav) are removed instead
   of kept, in favour of one consistent flex `gap`. */
.hpc-review-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1.75rem;
}
@media (min-width: 700px) {
    .hpc-review-grid {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr);
        align-items: start;
    }
    .hpc-review-col-identity {
        padding-right: 1.75rem;
        border-right: 1px solid var(--hpc-border-light);
    }
}
/* The right column stacks 2-3 pieces that used to be top-level sections of
   ONE long single-column step (leftover fields/CAPTCHA, the consent card,
   the step nav) — each still carrying the vertical padding/margin that made
   sense spread down a full-length page. Stacked inside this narrower
   column, those add up (1rem + 1rem + 1.5rem + 1.25rem ≈ 76px, confirmed
   live as a visibly broken gap between Captcha and "Before you continue").
   Zeroed here and replaced with ONE flex `gap`, so the column reads as a
   single connected block with consistent rhythm instead of stacked
   leftover spacing from a different layout. */
.hpc-review-col-identity > .hpc-edit-section,
.hpc-review-col-action > .hpc-edit-section {
    padding-top: 0;
    padding-bottom: 0;
}
/* 3.34.49: closes a leak the padding-zeroing above didn't catch — Captcha's
   OWN `.hpc-edit-field` wrapper carries `.hpc-register-page .hpc-edit-field
   { margin-bottom: 1.5rem }` (added for Step 1's field rhythm, further down
   this file), which isn't scoped to Step 1 and was landing here too. A flex
   item establishes its own formatting context, so that margin doesn't
   collapse into `.hpc-review-col-action`'s edge — it just ADDS to the
   column's own `gap`, doubling 1.5rem into 3rem. Zeroed here, at the exact
   selector that was leaking, rather than touching the Step 1 rule itself. */
.hpc-review-col-identity .hpc-edit-field,
.hpc-review-col-action .hpc-edit-field {
    margin-bottom: 0;
}
.hpc-review-col-action {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
}
.hpc-review-col-action .hpc-consent-card {
    margin-top: 0;
    padding-top: 0;
    border-top: none;
}
/* Back/Create account (3.34.49): moved OUT of .hpc-review-col-action in the
   markup — nested under just the action column, the row read as belonging
   to Captcha/Consent alone and sat off-centre under the narrower half of
   the page instead of closing the whole step. Now a sibling of
   `.hpc-review-grid`, spanning both columns. The base `.hpc-step-nav` rule
   (padding-top 1.5rem + margin-top 1.5rem = 3rem) was tuned for the single-
   column steps 1/2; here it would double up on top of the grid's own
   1.75rem `gap` below, so this collapses it to one clean value instead. */
.hpc-step-nav-review {
    margin-top: 1.75rem;
    padding-top: 0;
}
/* Confirmed via the interactive slider preview (3.34.65) — a matched
   min-width on both buttons rather than letting Back size to its shorter
   text and Create account to its longer one, which read as an unbalanced
   pair. `min-width`, not `width`: still a normal flex row, so this can
   shrink and the existing @media (max-width:575.98px) stack-to-full-width
   rule (base .hpc-step-nav, untouched) keeps working on phones. */
.hpc-step-back,
.hpc-register-submit {
    min-width: 14rem;
}

/* --- Review summary (Step 3) ---------------------------------------------
   Read-only name/email/mobile recap — deliberately never the password. */
.hpc-review-summary {
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: .6rem;
}
.hpc-review-row {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
}
.hpc-review-row dt {
    font-size: .8rem;
    color: var(--hpc-text-faint);
}
.hpc-review-row dd {
    margin: 0;
    font-weight: 600;
    color: var(--hpc-text);
    text-align: right;
    overflow-wrap: anywhere;
}

/* --- Inline validation errors (3.29.36) -----------------------------------
   Only ever added to the DOM by JS after a field has been touched (blurred
   invalid, or a Continue/Create-account attempt) — see validateField() in
   default.php. Never present at page load. */
.hpc-field-error {
    margin: .3rem 0 0;
    font-size: .8125rem;
    color: var(--hpc-danger);
}
.form-control.is-invalid,
.form-check-input.is-invalid {
    border-color: var(--hpc-danger) !important;
    box-shadow: 0 0 0 .15rem color-mix(in srgb, var(--hpc-danger) 20%, transparent) !important;
}

/* A Terms & Conditions/Privacy Policy trigger inside consent label text —
   reads and behaves like the plain-text link it replaced (target=_blank
   anchor), just opening the shared modal instead of a new tab. */
.hpc-inline-link {
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    font: inherit;
    color: var(--hpc-primary);
    text-decoration: underline;
    cursor: pointer;
}

/* T&C/Privacy modal — plain iframe onto the existing configured link. */
.hpc-legal-modal {
    width: 100%;
    max-width: 640px;
}
.hpc-legal-modal .card-body {
    padding: 0;
}
.hpc-legal-modal iframe {
    display: block;
    width: 100%;
    height: min(70vh, 640px);
    border: 0;
}

/* "Already have an account? Log in" — plain text under the wizard, not a
   button, on both auth pages. */
.hpc-auth-switch {
    text-align: center;
    margin: 1.25rem 0 0;
    font-size: .9rem;
    color: var(--hpc-text-faint);
    display: none;
}
.hpc-auth-switch a {
    /* No accent colour here either — explicit, repeated "no teal, EVER"
       requirement, same as the field labels. NOT --hpc-primary: that token
       (#1f3c88 navy) has no dark-mode override anywhere in this file, so it
       reads as low-contrast on this dark background — which is what made
       this link look uninviting in the first place. Plain, high-contrast
       and unmistakably a link from weight + underline alone, no colour
       coding needed. */
    color: inherit;
    font-weight: 700;
    text-decoration: underline;
    text-underline-offset: .15em;
}

/* The leftover-fields fallback section in registration/default.php still
   used the old 2-column .hpc-edit-grid from before the 3.27.0 single-column
   redesign — the only field that ever lands there is Captcha, so it sat in
   a 2-up grid with an empty, awkward second column. Full width keeps it
   consistent with every other field on this page. */
.hpc-register-page .hpc-edit-grid {
    grid-template-columns: 1fr;
}
/* No outer card any more (3.30) — .hpc-edit-section's card-oriented
   horizontal padding would just read as unexplained indentation without
   one, so it's zeroed here. The hairline border-bottom between sections is
   ALSO removed (explicit ask: "all these break lines... look tacky") — Step
   3 in particular stacks Review summary / Captcha / Consent as three
   sibling .hpc-edit-section blocks, so that border was firing three times
   down one step. */
.hpc-register-page .hpc-edit-section {
    padding-left: 0;
    padding-right: 0;
    border-bottom: none;
}

/* Comfortable tap targets on phones; 16px prevents iOS Safari zoom-on-focus.
   Colour/size are repeated here with a scoped !important:
   confirmed live (3.29.32) that the typed value was rendering both too dark
   and too small on these two pages — a Bootstrap/dark-mode cascade these
   overrides sit downstream of was winning on specificity even though this
   block comes later in the file. Same fix category as the dark-mode
   .form-control hotfix elsewhere in this file (see its own comment). */
.hpc-register-page .form-control,
.hpc-register-page .form-select,
.hpc-login-page .form-control,
.hpc-login-page .form-select {
    min-height: 3rem;
    /* Symmetric, so the typed value sits dead centre in the box. It used to
       be deliberately top-heavy (1.05rem / .35rem) purely to reserve room
       for the floating label that sat inside the box; the label is now a
       normal element ABOVE the box, so that reserved space had nothing left
       in it and just pushed the value low. */
    padding-top: .5rem;
    padding-bottom: .5rem;
    font-size: 1rem !important;
    color: var(--hpc-text) !important;
}
/* 3.34.45: registration-only bump, layered on top of the block above rather
   than edited in place — login's 1rem is untouched (never part of the
   complaint). Needs its own !important for the same reason the block above
   does (see its comment): without one, this loses to the very rule it's
   trying to override, same specificity or not. */
.hpc-register-page .form-control,
.hpc-register-page .form-select {
    font-size: 1.0625rem !important;
}

/* Login's two fields (username/password) centered on request — floating
   label positioning above is unaffected since that's vertical padding,
   this is only the typed value's horizontal alignment. login/default.php
   (a separate, competing full-page rewrite with its own #form-login id)
   is gone — core's own trivial login/default.php dispatcher now delegates
   straight to this override's default_login.php, whose actual form id is
   #com-users-login__form. Scoped to that id rather than .hpc-register-page
   so this never bleeds onto the registration page. */
/* #com-users-login__form .form-control {
    text-align: center;
} */

.hpc-register-page .hpc-edit-field,
.hpc-login-page .hpc-edit-field {
    /* Login's own fields also render inside .hpc-edit-field (confirmed live
       via DevTools — NOT the .control-group structure this file's login
       section assumed), so the floating label's absolute positioning had no
       correct anchor here at all — it was positioning against whatever
       positioned ancestor it found further up the tree, not this field. */
    position: relative;
    margin-bottom: 1rem;
}
/* 3.34.45: Step 1's field rhythm opened up on request ("space it a little
   more... name from surname from username from email from mobile") — 1rem
   read as cramped once the page was seen live. Registration-only override
   (login keeps the tighter 1rem — its two fields were never the complaint)
   layered on top of the shared rule above rather than editing it, so this
   doesn't also affect login or the profile page reusing the same class. */
.hpc-register-page .hpc-edit-field {
    margin-bottom: 1.5rem;
}
/* Edge/Chromium's native password reveal + clear icons render INSIDE the
   input's own box, on top of our floating-label overlay and padding —
   confirmed live as garbled overlapping text ("Hide"/"Passwo…" with the
   ambient background showing through). Registration/Profile provide no
   custom reveal toggle of their own, so the native one is simply disabled
   rather than fought with z-index tricks. Login is untouched/out of scope
   (see this file's own login section) even though its password field could
   show the same native artifact — not this pass's problem to fix. */
.hpc-register-page input[type="password"]::-ms-reveal,
.hpc-register-page input[type="password"]::-ms-clear,
.hpc-profile-page input[type="password"]::-ms-reveal,
.hpc-profile-page input[type="password"]::-ms-clear {
    display: none;
}
/* Login has no per-field .hpc-edit-field wrapper — its two fields
   (username/password) come straight out of core's own
   $form->renderFieldset('credentials', …). Ground-truthed from the 6.1.2
   core source (see default_login.php's header comment): despite this being
   Joomla 6, $field->renderField() still uses the LEGACY B/C layout
   (joomla.form.renderfield — Bootstrap 2/3's `.control-group` >
   `.control-label` + `.controls`, not the Bootstrap5 `.mb-3`/`.form-label`
   shape used elsewhere on this site), because Cassiopeia (and this fork)
   ships no site-side override of that layout — only the admin Atum
   template does. That wrapper — .control-group — is reused here as the
   floating-label positioning context instead of introducing a parallel
   markup convention for a two-field form. Registration's Joomla-rendered
   fields (username/e-mail/password/confirm) get the same .control-group
   treatment below; only First name/Surname are our own hand-written
   .form-label markup. */
.hpc-login-page .control-group {
    position: relative;
    margin-bottom: 1rem;
}

/* --- Static labels above the input -------------------------------------
   NOT a floating label any more. The floating version was absolutely
   positioned over the input and vertically anchored to its wrapper
   (.hpc-edit-field / .control-group) — but that wrapper GROWS whenever an
   inline feedback or error <p> is appended below the input ("Looks good.",
   "Please include an '@'…"), so the label kept sliding out of position the
   moment a field gained a message. Every attempt to pin it (line-height
   matched to the box, top:50%, a fixed half-height offset) fixed one state
   and broke another, because the label's position was coupled to markup
   that changes at runtime.

   A label in NORMAL FLOW above its input cannot drift: nothing rendered
   after the input can move it, there is no overlap with the typed value to
   tune, and no focus/filled/autofill state has to be tracked at all. This
   is option (a) from the brief ("labels sit cleanly ABOVE the input box
   with adequate spacing").

   Two label shapes are covered: our own hand-written `.form-label` (First
   name/Surname), and core's `.control-group > .control-label > label`
   (Joomla-rendered fields on both pages). Consent checkboxes / remember-me
   use `.form-check-label`, so they are untouched by this block. */
.hpc-register-page .hpc-edit-field .form-label,
.hpc-register-fields .hpc-edit-field .control-label label,
.hpc-login-page .control-group .control-label label,
.hpc-login-page .hpc-edit-field .form-label {
    display: block;
    margin: 0 0 .35rem;
    padding: 0;
    font-size: .8125rem;
    font-weight: 500;
    line-height: 1.3;
    color: var(--hpc-text-faint);
}
/* 3.34.45: "a little more size on the text" — registration-only, same
   reasoning as the spacing override above (login untouched). Labels up one
   step (.8125rem -> .9rem) and the typed value up from 1rem -> 1.0625rem
   (overrides the shared !important rule below by matching its own
   specificity + coming later in source order, not by adding a second
   !important). */
.hpc-register-page .hpc-edit-field .form-label {
    font-size: .9rem;
}
/* No focus / filled / autofill / reduced-motion state rules any more — a
   static label has no states to track and nothing to animate, which is the
   entire point of moving off the floating version. The required "*" inside
   the label markup inherits the colour above. */
/* Core's own `.control-label` div (login/registration's Joomla-rendered
   fields) and `.controls` div carry no Bootstrap CSS of their own on this
   Bootstrap5-only site — they're unstyled generic wrapper divs — so
   `.controls` needs nothing extra; the label above simply leaves it as the
   only thing occupying normal flow. Scoped to `.hpc-register-fields`, not
   the whole page, so the leftover-fieldset fallback (Captcha, rendered in
   `.hpc-edit-grid` outside this container — the only field that ever lands
   there) never gets its label wrenched out of flow and laid over the
   widget; it keeps Joomla's plain default look instead. */
.hpc-register-fields .hpc-edit-field .control-label,
.hpc-login-page .control-group .control-label {
    margin: 0;
}

/* Password1's "Minimum Requirements — Characters: 12, …" line — confirmed
   live (3.29.33) this is NOT the field's .form-text description at all.
   registration.xml sets rules="true" on password1, which makes Joomla's
   password field LAYOUT (layouts/joomla/form/field/password.php, ground-
   truthed from the 6.1.2 core source) render a wholly separate
   `<div id="{name}-rules" class="small text-muted">Minimum Requirements —
   …</div>` BEFORE the actual `.password-group` (input), both still inside
   the same `.controls` div. That div sitting in normal flow at the top of
   `.controls` — the exact spot the floated-out label overlays — is what
   caused the visible text collision; hiding the wrong selector (.form-text)
   left it untouched and also pushed the real input down below it. Matched
   by its `-rules`-suffixed id rather than `.small.text-muted`, which are
   generic enough to risk matching something unrelated later. */
.hpc-password1-field [id$="-rules"] {
    display: none;
}

/* --- Ambient background (3.29.31) --------------------------------------
   Slow-drifting blurred colour blobs behind the card on both auth pages —
   the "2026 feels alive" touch. `.hpc-ambient-bg` is `position: fixed` and
   sits BEFORE the sticky shy header in document order but is deliberately
   never made a positioning/overflow ancestor of anything — `position:
   fixed` needs no positioned or overflow:hidden container to work, which
   matters here specifically because an overflow:hidden ancestor is exactly
   what would break the registration page's `position: sticky` shy header.
   `z-index: -1` keeps it behind all real content without needing every
   other element on the page to be pulled into a stacking context.
   Token-driven (--hpc-primary/--hpc-accent mixed with transparent), no
   literal colours, so it reads correctly in dark mode. `filter: blur()` is
   cheap here since only `transform` is animated, not the blur radius
   itself. */
.hpc-ambient-bg {
    position: fixed;
    inset: 0;
    z-index: -1;
    overflow: hidden;
    pointer-events: none;
}
.hpc-ambient-bg span {
    position: absolute;
    width: 42vmax;
    height: 42vmax;
    border-radius: 50%;
    filter: blur(70px);
    opacity: .28;
    will-change: transform;
}
[data-bs-theme="dark"] .hpc-ambient-bg span {
    opacity: .16;
}
.hpc-ambient-bg span:nth-child(1) {
    top: -18vmax;
    left: -12vmax;
    background: var(--hpc-primary);
    animation: hpc-orb-drift-a 26s ease-in-out infinite;
}
.hpc-ambient-bg span:nth-child(2) {
    bottom: -20vmax;
    right: -14vmax;
    background: var(--hpc-primary);
    animation: hpc-orb-drift-b 32s ease-in-out infinite;
}
.hpc-ambient-bg span:nth-child(3) {
    top: 40%;
    left: 60%;
    width: 30vmax;
    height: 30vmax;
    background: var(--hpc-success, #2e9e57);
    animation: hpc-orb-drift-c 22s ease-in-out infinite;
}
@keyframes hpc-orb-drift-a {
    0%, 100% { transform: translate(0, 0) scale(1); }
    50%      { transform: translate(6vmax, 8vmax) scale(1.12); }
}
@keyframes hpc-orb-drift-b {
    0%, 100% { transform: translate(0, 0) scale(1); }
    50%      { transform: translate(-7vmax, -5vmax) scale(1.08); }
}
@keyframes hpc-orb-drift-c {
    0%, 100% { transform: translate(0, 0) scale(1); }
    50%      { transform: translate(-5vmax, 6vmax) scale(.92); }
}
@media (prefers-reduced-motion: reduce) {
    .hpc-ambient-bg span {
        animation: none;
    }
}
/* Hidden on narrow phones — at that viewport the blobs' 30-42vmax radius is
   large enough relative to the card that the blur reads as a colour cast
   over the whole screen rather than an ambient background detail. */
@media (max-width: 480px) {
    .hpc-ambient-bg {
        display: none;
    }
}
/* Registration-only dampener (3.30): the redesign asks for the background
   to stay subtle and never compete with the (now un-boxed) content. A new
   override layered on top of the shared rule above — .hpc-ambient-bg's own
   base opacity is untouched so login's identical background is unaffected. */
.hpc-register-page .hpc-ambient-bg span {
    opacity: .16;
}
[data-bs-theme="dark"] .hpc-register-page .hpc-ambient-bg span {
    opacity: .09;
}

/* --- Entrance motion (3.29.31) ------------------------------------------
   Each field row fades/slides up on load, staggered by a short delay per
   row — `animation-fill-mode: backwards` holds the 0% state during the
   delay so nothing flashes at full opacity before its turn. Pure CSS (no
   JS): it only needs to run once, on initial paint, which is exactly what
   a plain `animation` on page load already does without any trigger logic.
   Registration's rows are `.hpc-field-pair` / `.hpc-edit-field` /
   `.hpc-pw-checklist-wrap` direct children of `.hpc-register-fields`, plus
   the consent card and the action row below the card, which sit outside it;
   login's rows are the `.control-group` fields inside `.com-users-login__input`
   (see the .control-group comment above — core's legacy field layout, not
   `.mb-3`), plus the remember-me row and submit row. Both lists are covered
   explicitly rather than with a generic `> *` — a generic selector would
   also catch the hidden CSRF/return input Joomla's renderControlFields()
   appends, animating something invisible for no reason. */
@keyframes hpc-field-enter {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
}
.hpc-register-fields > .hpc-field-pair,
.hpc-register-fields > .hpc-edit-field,
.hpc-register-fields > .hpc-pw-checklist-wrap,
.hpc-consent-card,
.hpc-register-page .hpc-edit-actions-bottom,
.com-users-login__input > .control-group,
.com-users-login__remember,
.com-users-login__submit {
    animation: hpc-field-enter .5s cubic-bezier(.16, 1, .3, 1) backwards;
}
.hpc-register-fields > :nth-child(1) { animation-delay: .02s; }
.hpc-register-fields > :nth-child(2) { animation-delay: .08s; }
.hpc-register-fields > :nth-child(3) { animation-delay: .14s; }
.hpc-register-fields > :nth-child(4) { animation-delay: .20s; }
.hpc-register-fields > :nth-child(5) { animation-delay: .26s; }
.hpc-register-fields > :nth-child(6) { animation-delay: .32s; }
.hpc-register-fields > :nth-child(n+7) { animation-delay: .36s; }
.hpc-consent-card                     { animation-delay: .40s; }
.hpc-register-page .hpc-edit-actions-bottom { animation-delay: .46s; }
.com-users-login__input > :nth-child(1) { animation-delay: .02s; }
.com-users-login__input > :nth-child(2) { animation-delay: .1s; }
.com-users-login__remember              { animation-delay: .18s; }
.com-users-login__submit                { animation-delay: .26s; }
.hpc-login-fields                       { animation: hpc-field-enter .45s cubic-bezier(.16, 1, .3, 1) backwards; }
.hpc-login-remind                       { animation: hpc-field-enter .45s cubic-bezier(.16, 1, .3, 1) .32s backwards; }
@media (prefers-reduced-motion: reduce) {
    .hpc-register-fields > .hpc-field-pair,
    .hpc-register-fields > .hpc-edit-field,
    .hpc-register-fields > .hpc-pw-checklist-wrap,
    .hpc-consent-card,
    .hpc-register-page .hpc-edit-actions-bottom,
    .com-users-login__input > .control-group,
    .com-users-login__remember,
    .com-users-login__submit,
    .hpc-login-fields,
    .hpc-login-remind {
        animation: none;
    }
}

/* --- Login page ----------------------------------------------------------
   big_goose/html/com_users/login/default_login.php. No outer card (aligned,
   by explicit request, with registration's own 3.30 card removal — login
   was previously deliberately EXCLUDED from that redesign; that exclusion
   is now reversed, so the old LOGIN-ONLY glassmorphic-card rule that used
   to live here is gone, not just hidden). Reuses the floating-label block
   above for username/password and .hpc-ambient-bg/.hpc-field-enter for the
   same motion language as registration — built to feel like the same
   product, not a reskinned Joomla core page next to a bespoke one. */
.hpc-login-page {
    max-width: 460px;
    margin-inline: auto;
}
.hpc-login-page .com-users-login__input .control-group {
    margin-bottom: 1rem;
}
/* Remember-me + "Forgot password?" on one row (3.29.34) — previously
   remember-me sat in the card while reset/remind lived in a visually
   separate list-group panel below it, flagged live as "disconnected
   floating elements". */
.hpc-login-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin: .25rem 0 1.1rem;
}
.hpc-login-page .com-users-login__remember {
    margin: 0;
}
.hpc-login-page .com-users-login__remember .form-check-input:checked {
    background-color: var(--hpc-primary);
    border-color: var(--hpc-primary);
}
.hpc-login-forgot {
    font-size: .875rem;
    color: var(--hpc-primary);
    white-space: nowrap;
}
.hpc-login-submit {
    width: 100%;
    min-height: 2.75rem;
}
.hpc-login-page .com-users-login__submit + .com-users-login__submit {
    margin-top: .5rem;
}
/* "Forgot your username?" — relocated inside the card (3.29.34), directly
   under the submit button, rather than a separate panel below it. */
.hpc-login-remind {
    margin: .9rem 0 0;
    text-align: center;
    font-size: .8125rem;
}
.hpc-login-remind a {
    color: var(--hpc-text-faint);
}
@media (max-width: 575.98px) {
    .hpc-login-page {
        max-width: none;
    }
}

/* --- Consent (3.30: flattened) -----------------------------------------
   Previously a boxed, blue-left-bordered "card" — explicitly one of the
   nested boxes the redesign asks to remove. Now a plain section, same
   treatment as any other .hpc-edit-section (a hairline top border, no
   background/box), so it reads as part of the one continuous form. */
.hpc-consent-card {
    margin-top: 1.5rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--hpc-border-light);
}

.hpc-consent-title {
    margin: 0 0 .75rem;
    color: var(--hpc-text);
    font-weight: 600;
}

.hpc-consent-check {
    display: flex;
    align-items: flex-start;
    gap: .65rem;
    margin: 0;
    padding: .4rem 0;
    min-height: 2.5rem;
}

.hpc-consent-check + .hpc-consent-check {
    border-top: 1px solid color-mix(in srgb, var(--hpc-primary) 14%, transparent);
}

/* Bootstrap floats .form-check-input and pads the label; this row is flex
   instead, so both are reset to sit on the shared baseline. */
.hpc-consent-check .form-check-input {
    float: none;
    margin: .15rem 0 0;
    flex: 0 0 auto;
    width: 1.15rem;
    height: 1.15rem;
}

.hpc-consent-check .form-check-label {
    padding-left: 0;
    line-height: 1.45;
    cursor: pointer;
}

.hpc-consent-check .form-check-input.is-invalid {
    border-color: var(--hpc-danger);
    box-shadow: 0 0 0 .2rem color-mix(in srgb, var(--hpc-danger) 25%, transparent);
}

.hpc-consent-error {
    margin: .6rem 0 0;
    color: var(--hpc-danger);
    font-size: .875rem;
    font-weight: 500;
}

/* --- Password strength meter ----------------------------------------- */
.hpc-pw-strength {
    display: flex;
    align-items: center;
    gap: .6rem;
    margin-top: -.35rem;
    margin-bottom: .5rem;
}

.hpc-pw-strength-track {
    flex: 1 1 auto;
    height: 5px;
    border-radius: 999px;
    background: var(--hpc-border);
    overflow: hidden;
}

.hpc-pw-strength-bar {
    display: block;
    width: 0;
    height: 100%;
    border-radius: 999px;
    background: var(--hpc-danger);
    transition: width .18s ease, background-color .18s ease;
}

.hpc-pw-strength-label {
    flex: 0 0 auto;
    min-width: 4.5rem;
    font-size: .8rem;
    color: var(--hpc-text-faint);
}

.hpc-pw-strength[data-score="2"] .hpc-pw-strength-bar { background: #d98324; }
.hpc-pw-strength[data-score="3"] .hpc-pw-strength-bar { background: #b0a02b; }
.hpc-pw-strength[data-score="4"] .hpc-pw-strength-bar { background: var(--hpc-success, #2e9e57); }

@media (prefers-reduced-motion: reduce) {
    .hpc-pw-strength-bar { transition: none; }
}

/* --- Inline field feedback ------------------------------------------- */
.hpc-inline-feedback {
    margin: .3rem 0 0;
    font-size: .8125rem;
    min-height: 1.1em;
    color: var(--hpc-text-faint);
}

/* No message yet (page just loaded, or the field hasn't been touched) means
   nothing to reserve space for — the min-height above only earns its keep
   once there's actually a line of text to hold steady. */
.hpc-inline-feedback:empty {
    margin: 0;
    min-height: 0;
}

.hpc-inline-feedback.is-ok  { color: var(--hpc-success, #2e9e57); }
.hpc-inline-feedback.is-bad { color: var(--hpc-danger); }

.hpc-required-note {
    margin-top: .85rem;
    font-size: .8125rem;
    color: var(--hpc-text-faint);
}

/* Sits above the form instead of below it (3.27.33) — tighter bottom margin
   than the trailing-note spacing above, no top margin since the shy header
   already carries its own bottom margin. */
.hpc-required-note-top {
    margin: 0 0 1rem;
}

/* Full-width, stacked actions on phones — a 50/50 split leaves both buttons
   too narrow for their labels. */
@media (max-width: 575.98px) {
    .hpc-register-page .hpc-edit-actions-bottom {
        display: flex;
        flex-direction: column;
        gap: .6rem;
    }
    .hpc-register-page .hpc-edit-actions-bottom .btn {
        width: 100%;
    }
    .hpc-consent-card {
        padding: .9rem 1rem;
    }
}

/* ==========================================================================
   Mandatory profile completion (3.27.0)
   Address / consent / communication-preference sections on the profile edit
   page, plus the callout shown when the gate has redirected the user here.
   Tokens only — no colour literals, so this themes correctly in dark mode.
   ========================================================================== */

/* The "why am I here" callout. Bootstrap's .alert-warning sets its own
   --bs-alert-* custom properties on the element itself, which shadow any
   ancestor theme variables regardless of !important — so the palette is
   restated here on the element rather than inherited. */
.hpc-gate-callout {
    margin-bottom: 1.25rem;
    border-radius: var(--hpc-radius, .5rem);
}
.hpc-gate-list {
    padding-left: 1.1rem;
    font-size: .9rem;
}
.hpc-gate-list li + li {
    margin-top: .2rem;
}

/* The Places autocomplete box spans both grid columns — it is a lookup for
   the fields beneath it, not a peer of them. */
.hpc-address-search {
    grid-column: 1 / -1;
}
/* Google renders its suggestion dropdown in a .pac-container appended to
   <body>, OUTSIDE this form's DOM — so it cannot inherit anything from here
   and must be themed globally. Without an explicit z-index it also renders
   BEHIND any positioned card/modal on the page. */
.pac-container {
    z-index: 2000;
    background: var(--hpc-surface, #fff);
    border-radius: var(--hpc-radius, .5rem);
    box-shadow: var(--hpc-shadow-lg, 0 .5rem 1.5rem rgba(0, 0, 0, .18));
    font-family: inherit;
}
.pac-item {
    padding: .35rem .6rem;
    color: var(--hpc-text-faint);
    border-top-color: var(--hpc-border);
    cursor: pointer;
}
.pac-item:hover,
.pac-item-selected {
    background: var(--hpc-surface-alt, rgba(0, 0, 0, .04));
}
.pac-item-query,
.pac-matched {
    color: var(--hpc-text);
}

/* Consent rows. An outstanding consent is a live checkbox; one already on
   file renders as a tick plus the recorded date (never a pre-ticked box —
   see the template for why). */
.hpc-consent-row + .hpc-consent-row {
    margin-top: .6rem;
}
.hpc-consent-row.is-outstanding {
    padding: .6rem .75rem;
    background: var(--hpc-surface-alt, rgba(0, 0, 0, .03));
    border: 1px solid var(--hpc-border);
    border-radius: var(--hpc-radius, .5rem);
}
.hpc-consent-row .form-check-input.is-invalid {
    border-color: var(--hpc-danger, #dc3545);
}
.hpc-consent-done {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .4rem;
    font-size: .9rem;
    color: var(--hpc-text-faint);
}
.hpc-consent-tick::before {
    content: "✓";
    color: var(--hpc-success, #198754);
    font-weight: 700;
}
.hpc-consent-date {
    font-size: .8125rem;
    color: var(--hpc-text-faint);
}

/* Communication preferences. Rendered from CommPreferenceService's registry,
   so this must lay out an arbitrary number of channels and categories. */
.hpc-comm-channel + .hpc-comm-channel {
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--hpc-border);
}
.hpc-comm-channel-title {
    margin-bottom: .5rem;
    color: var(--hpc-text-faint);
    text-transform: uppercase;
    letter-spacing: .04em;
    font-size: .75rem;
}
/* REQUIRED/OPTIONAL group labels (3.30) — split the previously interleaved
   category list so it's obvious at a glance which preferences cannot be
   turned off, without needing to read every row's own "Required" chip. */
.hpc-comm-group-title {
    margin: .75rem 0 .1rem;
    font-size: .7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--hpc-text-soft);
}
.hpc-comm-group-title-optional {
    margin-top: 1rem;
}
.hpc-comm-option {
    padding-top: .4rem;
    padding-bottom: .4rem;
}
.hpc-comm-option .form-check-label {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .4rem;
}
.hpc-comm-label {
    font-weight: 600;
}
/* A mandatory category is shown ticked and disabled rather than hidden — the
   user should see that transactional mail exists and know it is compulsory,
   not discover it by receiving something they never opted into. */
.hpc-comm-required {
    padding: .05rem .4rem;
    font-size: .6875rem;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--hpc-text-faint);
    background: var(--hpc-surface-alt, rgba(0, 0, 0, .05));
    border-radius: 999px;
}
.hpc-comm-desc {
    flex-basis: 100%;
    font-size: .8125rem;
    color: var(--hpc-text-faint);
}

/* Profile DISPLAY view sections (3.27.0) — consent + comms summary shown
   below the details grid. Reuses .hpc-consent-* from the edit page above
   where the treatment is identical; only the display-only states are new. */
.hpc-profile-section {
    padding: 1rem 1.25rem;
    border-top: 1px solid var(--hpc-border);
}
.hpc-profile-section-title {
    margin: 0 0 .6rem;
    font-size: .75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--hpc-text-faint);
}
/* An outstanding consent on the display page. Warning-toned rather than
   error-toned: the user is not in trouble, there is simply something to do. */
.hpc-consent-pending {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .4rem;
    font-size: .9rem;
}
.hpc-consent-warn::before {
    content: "!";
    display: inline-block;
    width: 1.05rem;
    height: 1.05rem;
    line-height: 1.05rem;
    text-align: center;
    font-weight: 700;
    font-size: .75rem;
    color: var(--hpc-surface, #fff);
    background: var(--hpc-warning, #f0ad4e);
    border-radius: 50%;
}
.hpc-consent-action {
    font-weight: 600;
}
/* Read-only comms summary: every category listed, the inactive ones muted,
   so "what am I NOT getting" is answerable at a glance rather than only
   showing what is switched on. */
.hpc-comm-summary {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .5rem;
}
.hpc-comm-summary + .hpc-comm-summary {
    margin-top: .5rem;
}
.hpc-comm-summary-channel {
    font-size: .8125rem;
    font-weight: 600;
    color: var(--hpc-text-faint);
}
.hpc-comm-summary-list {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem;
}
.hpc-comm-pill {
    padding: .1rem .5rem;
    font-size: .75rem;
    border-radius: 999px;
    border: 1px solid var(--hpc-border);
    color: var(--hpc-text-faint);
    opacity: .55;
}
.hpc-comm-pill.is-on {
    opacity: 1;
    color: var(--hpc-text);
    border-color: transparent;
    background: var(--hpc-surface-alt, rgba(0, 0, 0, .06));
}

/* Profile edit: single-column layout + concertina sections (3.27.0).
   The two-column .hpc-edit-columns is still used by the REGISTRATION page —
   do not delete those rules on the strength of this page no longer using
   them. */
.hpc-edit-single {
    display: flex;
    flex-direction: column;
}
/* Cap the line length on wide screens. A single column of inputs stretched
   to a full-width card reads as broken; the card itself stays full width so
   the section dividers still span it. */
.hpc-edit-single > .hpc-edit-field,
.hpc-edit-single > .hpc-pw-checklist-wrap {
    max-width: 34rem;
}

/* Concertina, built on native <details>/<summary> — no JS, keyboard and
   screen-reader accessible for free, and it cannot fail closed if a script
   doesn't load. That matters: the address is mandatory, so a section that
   refused to open would be a hard lockout. */
/* 3.30: enlarged from a thin .35rem-padding row with a near-invisible 8px
   chevron — flagged as "hiding important information behind a tiny arrow"
   (Address is mandatory; Password is a real security action, not trivia).
   The whole row is still the native <summary> click/keyboard target, just
   sized and weighted to read as an obvious control rather than a hairline. */
/* 3.34.45: horizontal padding moved onto .hpc-collapse itself (was 0 on
   both the summary and the body) — on the merged profile page these
   collapses sit directly under a plain wrapper div with no padding of its
   own (unlike Agreements' old .hpc-edit-section, which had 1.25rem), so the
   icon sat flush against the card's left edge and the chevron flush against
   its right edge. Matches .hpc-edit-section's own 1.25rem so every section
   on the card lines up. */
.hpc-collapse {
    padding: 0 1.25rem;
}
.hpc-collapse > .hpc-collapse-summary {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .75rem 0;
    cursor: pointer;
    list-style: none;
}
/* Both are needed: ::marker is the standard, ::-webkit-details-marker is the
   older WebKit/Safari spelling. */
.hpc-collapse > .hpc-collapse-summary::marker,
.hpc-collapse > .hpc-collapse-summary::-webkit-details-marker {
    display: none;
    content: "";
}
.hpc-collapse > .hpc-collapse-summary:focus-visible {
    outline: 2px solid var(--hpc-primary, #0d6efd);
    outline-offset: 2px;
    border-radius: var(--hpc-radius, .5rem);
}
.hpc-collapse-title {
    font-size: .8rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--hpc-text-faint);
    white-space: nowrap;
}
/* One-line summary of the collapsed content (Password section, and
   Address's own empty-state fallback). Truncates rather than wrapping, so
   the closed header stays exactly one row tall. */
.hpc-collapse-preview {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: .9rem;
    color: var(--hpc-text-faint);
}
/* Hidden once open — the fields below are then the content, and leaving a
   stale summary above them reads as a duplicate. */
.hpc-collapse[open] > .hpc-collapse-summary .hpc-collapse-preview,
.hpc-collapse[open] > .hpc-collapse-summary .hpc-address-preview {
    display: none;
}
/* Explicit "Edit"/chevron control — an obvious affordance next to the
   chevron rather than the chevron being the only clickable-looking thing. */
.hpc-collapse-edit {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: .35rem;
    font-size: .8125rem;
    font-weight: 600;
    color: var(--hpc-primary);
    white-space: nowrap;
}
/* 3.34.45: `margin-left: auto` pins the chevron to the row's far right on
   its own — previously it relied on .hpc-collapse-preview's `flex: 1 1 auto`
   to push it there, which only worked while the preview was visible. Once
   open, the preview is hidden (display:none, just below) and disappears
   from the flex layout entirely, so with nothing left to push against, the
   chevron collapsed back to sit right next to the title instead of staying
   in place — exactly the reported bug. */
.hpc-collapse-chevron {
    flex: 0 0 auto;
    margin-left: auto;
    width: .5rem;
    height: .5rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg);
    /* Slow, deliberate spin on open/close — more than a quarter turn, for
       the "wow" a plain quick flip doesn't give. */
    transition: transform .6s ease-in-out;
}
/* -495deg, not -135deg: CSS transitions interpolate the literal numeric
   angle, not the shortest visual path, so a bigger (but equivalent) target
   angle is what actually produces a longer spin. -495 ≡ -135 (mod 360), so
   the chevron still lands pointing exactly straight down at rest — the
   45→-495 transition just sweeps 540° (one and a half full turns) getting
   there instead of 180°. Requested as "450°"; 450 isn't a multiple-of-360
   offset from -135 (it would land the chevron pointing the wrong way at
   rest), so 540° is the nearest sweep that both spins big AND ends correct. */
.hpc-collapse[open] > .hpc-collapse-summary .hpc-collapse-chevron {
    transform: rotate(-495deg);
}
.hpc-collapse-body {
    padding-top: .75rem;
}
@media (prefers-reduced-motion: reduce) {
    .hpc-collapse-chevron {
        transition: none;
    }
}

/* --- Address summary (3.30) ------------------------------------------
   Address is mandatory, so its collapsed state reads as real address text
   (each line on its own row, like a mailing label) rather than a single
   ellipsis-truncated line — the .hpc-collapse-preview treatment above still
   covers the "no address on file" fallback text. */
.hpc-address-summary {
    align-items: flex-start;
}
.hpc-address-preview {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    padding-top: .1rem;
}
.hpc-address-line {
    font-size: .9rem;
    color: var(--hpc-text);
    line-height: 1.4;
}

/* Registration layout (3.27.0) — single column with one deliberate two-up row
   for First name + Surname. Replaces the two-column split, whose two halves
   drifted out of vertical alignment as soon as inline feedback appeared under
   a field on one side only.
   3.34.44: the row is now UNCONDITIONAL, not gated behind a min-width — First
   name/Surname sit side by side on a phone the same as they do on desktop
   (explicit requirement; the two floating-label inputs stay comfortably
   readable down to the narrowest supported phone width). */
.hpc-field-pair {
    display: flex;
    flex-direction: row;
    gap: .6rem;
}
/* Equal halves. min-width:0 stops a long value from forcing one side wider
   than the other — flex items default to min-width:auto, which refuses to
   shrink below their content. */
.hpc-field-pair > .hpc-edit-field {
    flex: 1 1 0;
    min-width: 0;
}
@media (min-width: 576px) {
    .hpc-field-pair {
        gap: 1rem;
    }
}

/* --- Registration Country Code toggle (feature flag, off by default) -----
   Only rendered when Options -> Feature Toggles -> "Registration Country
   Code" is on; the free-text phone input (default) needs none of this. */
.hpc-phone-row {
    display: flex;
    align-items: stretch;
    gap: .5rem;
}
.hpc-phone-prefix {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    padding: 0 .75rem;
    border: 1px solid var(--hpc-border);
    border-radius: var(--hpc-radius, .6rem);
    background: var(--hpc-surface-sunken);
    color: var(--hpc-text-faint);
    font-size: .9rem;
    font-weight: 600;
    white-space: nowrap;
}
.hpc-phone-row .form-control {
    flex: 1 1 auto;
    min-width: 0;
}
/* Password and its meter/chips span the full column width. .hpc-edit-single
   caps its children at 34rem on the profile page; registration deliberately
   opts out for these, since a password is the longest thing typed here and
   the strength bar needs the room to read as a meter. */
.hpc-register-fields > .hpc-edit-field,
.hpc-register-fields > .hpc-pw-checklist-wrap,
.hpc-register-fields > .hpc-field-pair {
    max-width: none;
}


/* ======================================================================
   My Profile — edit-in-place (3.34.42)
   Both the read-only view and the real Joomla edit form render in the same
   response now (default.php); this is purely the show/hide + new-component
   CSS the merge needs. Tokens only, same as everything else on this page —
   both themes already resolve correctly with no extra work here.
   ====================================================================== */

/* The whole toggle mechanism: two classes, shown/hidden by whether
   .hpc-profile-page carries .is-editing. profileedit.js only ever adds/
   removes that one class — Save/Cancel are real submits, not JS. */
.hpc-profile-page:not(.is-editing) .hpc-edit-only { display: none; }
.hpc-profile-page.is-editing .hpc-view-only { display: none; }
/* The Logout slot: visible in plain view mode AND while gated (Cancel is
   withheld then — see the template). Hidden only for a normal, non-gated
   edit session, where Cancel occupies this slot instead.
   .hpc-logout-slot is now a class on the BUTTON itself (it used to be on a
   <form> wrapping the button — that form was nested inside #member-profile,
   which is invalid HTML; the browser silently dropped the nested <form>
   tag from the DOM entirely, so this selector's ancestor never existed and
   the rule never matched, no matter what deployed. See default.php's own
   comment on the fix — button now associates to its (relocated, outside
   #member-profile) form via the HTML5 form="" attribute instead. */
.hpc-profile-page.is-editing:not(.is-gated) .hpc-logout-slot,
.hpc-profile-page.is-editing:not(.is-gated) .hpc-guide-fab { 
    display: none; 
}

/* Pinned banner, scrollable body (3.34.56, reworked 3.34.60) — edit mode
   only. Save/Cancel and the avatar upload live in .hpc-profile-banner; on a
   long edit (Address, Appearance, Agreements, Comm Prefs, Password, 2FA)
   scrolling the whole card carried those controls away with it. Fixed by
   capping the card's own height and making .hpc-profile-scroll (everything
   from the banner colour picker down — see the template) the one part that
   scrolls internally, while the banner + avatar/name/chips header keep
   their natural height and never move.

   3.34.60 — the original version only capped max-height in normal document
   flow, which left the card's TOP wherever the page header/tabs/gate
   banner above it happened to push it — a variable, unmeasurable amount.
   Reported live: the inner region scrolled fine, but once it was
   exhausted, part of the (still off-screen) card required a SECOND, outer
   page scroll to reach — the max-height cap was real, but nothing pinned
   WHERE the capped box sat, so its bottom edge could land below the fold.
   Fixed with `position: sticky; top: .5rem` — once scrolled to that fixed,
   KNOWN point, the box's bottom edge is `.5rem + max-height`, a fixed point
   too, so the whole capped card is always fully on-screen once reached and
   the inner scroll alone can reach every bit of it. `top` is a small
   reasoned guess (no browser here — see CLAUDE.md): if this platform's
   optional sticky site header is ever turned on, tune `top` upward so the
   card sticks just below it rather than under/behind it. */
/* 3.34.69 — outer-page scroll lock while editing. Reported live: touching
   ANY field or button inside the sticky card made the whole page jump.
   Cause: the card is only visually pinned (position:sticky) — the page
   underneath is still a real, independently-scrollable ancestor, and
   focusing an input runs the browser's default scroll-into-view walk over
   EVERY scrollable ancestor, not just .hpc-profile-scroll. That second
   scroll fights the sticky recalculation on the same frame — the jump.
   profileedit.js adds this class to <html>+<body> for as long as editing
   is open (both, since Safari anchors the scroll lock to <html> while
   Chrome/Firefox honour it on <body>), removing the outer page as a
   scrollable ancestor entirely so there is nothing left for the browser to
   fight over. Card-visibility on entry is handled in JS (scrollIntoView
   BEFORE this class is added — locking freezes wherever the page already
   was, so without that the card could freeze off-screen). */
html.hpc-edit-lock,
body.hpc-edit-lock {
    overflow: hidden;
}

.hpc-profile-page.is-editing .hpc-profile-card {
    display: flex;
    flex-direction: column;
    /* Now the SAME value as the base (non-editing) .hpc-profile-card rule
       above — kept as an explicit override (not left to inherit) so the
       two stay in sync deliberately rather than by accident if one is ever
       tuned again. Also matches the sticky `top` below, so the gap stays
       visually constant whichever state you're in. */
    margin-top: .5rem;
    /* 3.34.67 — position:sticky RE-ADDED. A live DevTools Layout check
       (3.34.66 round) confirmed the box model here is exact:
       banner(96px) + header(56.98px) + .hpc-profile-scroll(389.02px) =
       542px, matching the 34rem cap almost to the pixel — nothing is
       clipped, nothing miscalculated. What WAS happening: without sticky,
       this card is a completely normal block, so scrolling the OUTER page
       moves the banner off-screen exactly like any non-pinned element —
       that was misread as a "clipping" bug when it was really "this was
       never actually pinned since 3.34.63 removed sticky over an earlier,
       unconfirmed failure." Retrying sticky now that the underlying sizing
       is proven correct — the earlier failure may have been this exact
       same normal-scroll behaviour, misdiagnosed as sticky not engaging.
       If the banner still scrolls fully out of view after this, that is
       real evidence sticky itself is the problem, not the box model. */
    position: sticky;
    top: .5rem;
    z-index: 20;
    /* 3.34.82 — was a fixed 34rem regardless of the actual viewport
       (reported live: plenty of unused space below the card on a taller
       screen/browser window, since 34rem is a fixed ceiling far short of
       most desktop viewport heights). Now scales with the real viewport
       instead: `top: .5rem` + an equal .5rem reserved below is 1rem
       consumed total, so max-height fills whatever's left. `vh` line
       first as a fallback for browsers without `dvh` support, `dvh`
       second so it wins where available (accounts for mobile browser
       chrome show/hide, `vh` alone does not) — same progressive-
       enhancement pattern already used elsewhere on this page. No upper
       cap: on a very tall viewport the card can now grow past what
       34rem ever allowed, which is the point. */
    max-height: calc(100vh - 1rem);
    max-height: calc(100dvh - 1rem);
    overflow: hidden;
}
/* 3.34.83 — the calc above doesn't know about the fixed mobile bottom bar
   (.hpc-mobilebar, ~4rem tall + the device's own safe-area inset), which
   sits on TOP of everything (position:fixed, z-index above this card) —
   reported live: the card's dynamic height ran the full viewport, so its
   bottom edge sat right under the bar instead of stopping above it. Same
   breakpoint and `:has(.hpc-mobile-chrome)` gating already used for the
   body's own bottom padding just above in this file, so this only
   subtracts space when the bar is actually rendered (the "Mobile Site"
   Feature Toggle off case needs no adjustment here either). */
@media (max-width: 1199.98px) {
    body:has(.hpc-mobile-chrome) .hpc-profile-page.is-editing .hpc-profile-card {
        max-height: calc(100vh - 1rem - 4rem - env(safe-area-inset-bottom));
        max-height: calc(100dvh - 1rem - 4rem - env(safe-area-inset-bottom));
    }
}
.hpc-profile-page.is-editing .hpc-profile-banner {
    flex-shrink: 0;
}
.hpc-profile-page.is-editing .hpc-profile-card-header {
    flex-shrink: 0;
    /* Was the full card-body padding — the identity text (h2/username/
       chips) is hidden while editing now, so the avatar alone doesn't need
       as much air around it. */
    padding-top: .6rem;
    padding-bottom: .5rem;
    /* 3.34.66: the base (non-editing) rule pulls this whole block up
       -2.5rem to overlap the banner — a deliberate "avatar straddles the
       cover photo" look that only works with the ORIGINAL full-size
       avatar/header. In edit mode's shrunk, flex-shrink:0 header that same
       -2.5rem overlap is far too aggressive: reported live as clipping the
       avatar and throwing off where the scroll region visually starts.
       Overridden back to a normal, non-overlapping position here. */
    margin-top: 0;
}
.hpc-profile-page.is-editing .hpc-profile-avatar-img,
.hpc-profile-page.is-editing .hpc-profile-avatar {
    width: 2.4rem;
    height: 2.4rem;
}
.hpc-profile-page.is-editing .hpc-profile-avatar {
    font-size: 1rem;
}
.hpc-profile-page.is-editing .hpc-profile-avatar-edit {
    width: 1.15rem;
    height: 1.15rem;
    font-size: .6rem;
}
.hpc-profile-page.is-editing .hpc-profile-scroll {
    /* min-height:0 overrides the flex item default of `auto`, which would
       otherwise refuse to shrink below its content's natural height and
       defeat the whole point — the card would just grow past max-height
       instead of this region scrolling internally. */
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    /* Reported live: content looked narrower/shifted in edit mode vs view
       mode. Cause: a scrollbar takes its width OUT of the content box it
       sits in, and view mode never scrolls (no scrollbar) while edit mode
       often does — so the same content rendered ~10px narrower the moment
       it needed to scroll, reading as a resize. `stable` reserves that
       gutter unconditionally, whether or not the content currently
       overflows, so the width never changes based on scroll state.
       Chromium/Firefox only (no Safari support as of this writing) — on
       Safari this is a no-op, not a regression: falls back to exactly
       today's before-this-fix behaviour, not something worse. */
    scrollbar-gutter: stable;
}
/* Deliberate, scoped exception to the site-wide hidden-scrollbar rule
   above (the `*`/`*::-webkit-scrollbar` block, explicitly "do not swap
   without a fresh discussion") — a class selector beats a bare `*`
   regardless of source order, so no !important is needed to win here.
   This one specific scrollbar IS the affordance: without it, a mouse user
   has no visual sign the pinned banner is hiding more content below, since
   they've already lost the usual drag-thumb cue everywhere else on the
   site. Plain grey (--hpc-text-faint), not a brand/accent colour — this is
   a system-chrome affordance, not a themed UI element, and should read as
   an ordinary OS scrollbar. ::-webkit-scrollbar-button hides the up/down
   arrow buttons some browsers still draw on an unstyled/thin scrollbar. */
.hpc-profile-page.is-editing .hpc-profile-scroll {
    scrollbar-width: thin;
    scrollbar-color: var(--hpc-text-faint) var(--hpc-surface-sunken);
}
.hpc-profile-page.is-editing .hpc-profile-scroll::-webkit-scrollbar {
    display: block;
    width: 10px;
}
.hpc-profile-page.is-editing .hpc-profile-scroll::-webkit-scrollbar-button {
    display: none;
    height: 0;
    width: 0;
}
.hpc-profile-page.is-editing .hpc-profile-scroll::-webkit-scrollbar-track {
    background: var(--hpc-surface-sunken);
}
.hpc-profile-page.is-editing .hpc-profile-scroll::-webkit-scrollbar-thumb {
    background-color: var(--hpc-text-faint);
    border-radius: 999px;
    border: 2px solid var(--hpc-surface-sunken);
    background-clip: padding-box;
}
.hpc-profile-page.is-editing .hpc-profile-scroll::-webkit-scrollbar-thumb:hover {
    background-color: var(--hpc-text-soft);
}

/* "Scroll for more" hint pill — a coloured scrollbar alone wasn't obvious
   enough (every OTHER scrollbar on this platform is deliberately hidden, so
   there's no learned expectation this one differs; touch/trackpad never
   show one at all). Floats over the bottom edge of the scroll region,
   pointer-events:none so it never blocks a tap/click on the real content
   under it. `.is-visible` is added/removed by profileedit.js based on
   actual scroll position — hidden entirely if the content is short enough
   not to overflow, and once you've scrolled to the bottom there's nothing
   left to hint at. Starts hidden (opacity/transform) so there's no flash
   before JS measures the content on load. */
.hpc-scroll-hint {
    position: absolute;
    left: 50%;
    bottom: .85rem;
    transform: translate(-50%, 6px);
    z-index: 5;
    display: flex;
    align-items: center;
    gap: .4rem;
    padding: .4rem .85rem;
    border-radius: 999px;
    /* 3.34.64: was --hpc-text on --hpc-surface — an inverted, one-off
       colour scheme unrelated to anything else on the page. Now the SAME
       chip look as .hpc-profile-chip ("3 Pets" / "Joined") just above it. */
    background: var(--hpc-surface);
    border: 1px solid var(--hpc-border);
    color: var(--hpc-text-soft);
    font-size: .75rem;
    font-weight: 700;
    letter-spacing: .01em;
    white-space: nowrap;
    box-shadow: 0 6px 16px -4px rgba(0, 0, 0, .25), 0 2px 6px rgba(0, 0, 0, .3);
    pointer-events: none;
    opacity: 0;
    transition: opacity .2s ease, transform .2s ease;
}
.hpc-scroll-hint.is-visible {
    opacity: 1;
    transform: translate(-50%, 0);
    animation: hpc-scroll-hint-bob 1.6s ease-in-out infinite;
}
.hpc-scroll-hint-chevron {
    display: inline-block;
    font-size: .65rem;
    line-height: 1;
    animation: hpc-scroll-hint-chevron 1.6s ease-in-out infinite;
}
@keyframes hpc-scroll-hint-bob {
    0%, 100% { transform: translate(-50%, 0); }
    50% { transform: translate(-50%, -4px); }
}
@keyframes hpc-scroll-hint-chevron {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(2px); }
}
@media (prefers-reduced-motion: reduce) {
    .hpc-scroll-hint,
    .hpc-scroll-hint.is-visible {
        animation: none;
        transition: opacity .2s ease;
    }
    .hpc-scroll-hint-chevron {
        animation: none;
    }
}
/* No separator hairlines between sections on this page (approved mockup) —
   spacing alone does the work. Scoped to .hpc-profile-page so the shared
   .hpc-edit-section rule (registration/login still use it) is untouched. */
.hpc-profile-page .hpc-edit-section {
    border-bottom: none;
}

/* Banner-colour picker (3.34.62 — replaces the 6-swatch-circle + native
   <input type="color"> version from 3.34.42). Two bars: Default (always
   left, always .hpc-gradient-primary) and Custom (right) — the whole
   spectrum, expands on press to swallow the row, collapses back to a solid
   swatch on release. The spectrum fill on .hpc-banner-bar-custom is
   deliberately literal (not tokens) — same reasoning the old swatch colours
   used: it's meant to look identical regardless of theme. --custom is set
   inline by the template (server-rendered initial state) and live by
   profileedit.js while dragging/after commit. */
.hpc-banner-picker {
    position: relative;
    display: flex;
    align-items: stretch;
    gap: .6rem;
    height: 3rem;
}
.hpc-banner-bar {
    /* `<button>` as a flex item is a known cross-browser inconsistency
       spot — some engines size it to content instead of honouring
       align-items:stretch/flex-basis unless box-sizing/line-height are
       pinned explicitly. Reported live as rendering several times its
       intended size (3rem/. 8rem font clearly not what was showing) —
       these are the properties that close that gap. */
    box-sizing: border-box;
    flex: 1 1 0;
    align-self: stretch;
    min-width: 0;
    max-height: 3rem;
    line-height: 1;
    margin: 0;
    border-radius: 999px;
    border: 2px solid transparent;
    cursor: pointer;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: inherit;
    font-size: .8rem;
    font-weight: 700;
    color: #fff;
    text-shadow: 0 1px 3px rgba(0, 0, 0, .45);
    transition: flex-grow .28s cubic-bezier(.2, .8, .3, 1), border-color .15s ease, transform .15s ease;
    overflow: hidden;
    -webkit-user-select: none;
    user-select: none;
    padding: 0 .5rem;
}
.hpc-banner-bar:active {
    transform: scale(.98);
}
.hpc-banner-bar:focus-visible {
    outline: 2px solid var(--hpc-primary);
    outline-offset: 2px;
}
.hpc-banner-bar-default {
    background: var(--hpc-gradient-primary);
}
.hpc-banner-bar-default.is-selected {
    border-color: var(--hpc-surface);
    box-shadow: 0 0 0 2px var(--hpc-primary);
}
.hpc-banner-bar-custom {
    background: linear-gradient(
        90deg,
        hsl(0 90% 55%), hsl(60 90% 55%), hsl(120 90% 45%),
        hsl(180 90% 45%), hsl(240 90% 60%), hsl(300 90% 55%), hsl(360 90% 55%)
    );
    /* Stops the browser from treating a press-and-drag on this bar as a
       page scroll/swipe gesture before pointermove ever fires — reported
       live as "does not slide, it exits" on mobile. Without this, touch
       input defaults to panning the page; pointer events alone don't
       override that. */
    touch-action: none;
}
.hpc-banner-bar-custom.has-color {
    background: var(--custom, var(--hpc-gradient-primary));
}
/* While actively picking, ALWAYS show the spectrum — reported live: with
   .has-color's solid fill winning even mid-drag, there was no way to see
   where on the spectrum you were relative to where you started. Three
   classes beats .has-color's two, so this wins on specificity alone, no
   !important needed. */
.hpc-banner-picker.is-expanded .hpc-banner-bar-custom {
    background: linear-gradient(
        90deg,
        hsl(0 90% 55%), hsl(60 90% 55%), hsl(120 90% 45%),
        hsl(180 90% 45%), hsl(240 90% 60%), hsl(300 90% 55%), hsl(360 90% 55%)
    );
}
.hpc-banner-bar-custom.is-selected {
    border-color: var(--hpc-surface);
    box-shadow: 0 0 0 2px var(--custom, var(--hpc-primary));
}
/* Expanded: the custom bar grows to swallow both bars' combined width; the
   default bar fades out rather than reflowing, so nothing jumps. */
.hpc-banner-picker.is-expanded .hpc-banner-bar-default {
    flex-grow: 0;
    width: 0;
    min-width: 0;
    padding: 0;
    border: none;
    opacity: 0;
    pointer-events: none;
}
.hpc-banner-picker.is-expanded .hpc-banner-bar-custom {
    flex-grow: 1;
}
.hpc-banner-bar-label {
    position: relative;
    z-index: 2;
    pointer-events: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    transition: opacity .12s ease;
}
.hpc-banner-picker.is-expanded .hpc-banner-bar-custom .hpc-banner-bar-label {
    opacity: 0;
}
.hpc-banner-hue-thumb {
    position: absolute;
    top: 50%;
    left: 0;
    width: 2.4rem;
    height: 2.4rem;
    border-radius: 50%;
    background: #fff;
    border: 3px solid var(--custom, var(--hpc-primary));
    box-shadow: 0 4px 12px rgba(0, 0, 0, .4);
    transform: translate(-50%, -50%);
    pointer-events: none;
    opacity: 0;
    transition: opacity .15s ease;
    z-index: 3;
}
.hpc-banner-picker.is-expanded .hpc-banner-hue-thumb {
    opacity: 1;
}
.hpc-banner-hint {
    margin: .6rem 0 0;
    font-size: .8125rem;
    color: var(--hpc-text-faint);
}

/* List-row: icon box + label/sub + trailing control — Agreements (already-
   agreed state) and the Communication Preferences / Password & Security
   collapse headers all use this, matching the approved mockup exactly. */
.hpc-list-row {
    display: flex;
    align-items: center;
    gap: .75rem;
    text-decoration: none;
    color: inherit;
    width: 100%;
}
a.hpc-list-row:hover .hpc-list-label {
    text-decoration: underline;
}
.hpc-list-icon {
    width: 2.1rem;
    height: 2.1rem;
    border-radius: .6rem;
    background: var(--hpc-surface-sunken);
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    color: var(--hpc-primary);
}
.hpc-list-icon svg {
    width: 1.05rem;
    height: 1.05rem;
}
.hpc-list-body {
    flex: 1 1 auto;
    min-width: 0;
}
.hpc-list-label {
    font-size: .9rem;
    font-weight: 600;
    color: var(--hpc-text);
}
.hpc-list-sub {
    display: block;
    font-size: .75rem;
    color: var(--hpc-text-faint);
    margin-top: .1rem;
}
/* Consent "done" row reuses .hpc-list-row wholesale — .hpc-consent-row still
   carries the outstanding/is-outstanding treatment for the live-checkbox
   case, untouched. */
a.hpc-consent-row.hpc-list-row {
    padding: .5rem 0;
}

/* Password show/hide eye toggle — button injected by profileedit.js next to
   each core-rendered password input. */
.hpc-pw-input-wrap {
    position: relative;
    display: block;
}
.hpc-pw-input-wrap input[type="password"],
.hpc-pw-input-wrap input[type="text"] {
    padding-right: 2.4rem;
}
.hpc-pw-eye {
    position: absolute;
    right: .4rem;
    top: 50%;
    transform: translateY(-50%);
    background: none;
    border: none;
    color: var(--hpc-text-faint);
    cursor: pointer;
    padding: .3rem;
    display: flex;
    border-radius: .35rem;
}
.hpc-pw-eye:hover {
    color: var(--hpc-text);
}
.hpc-pw-eye svg {
    width: 1rem;
    height: 1rem;
}

/* ======================================================================
   Guided Tour onboarding (3.29.x) — see tour.js and HpcTheme::linkTourScript()/
   buildTourSteps(). Spotlight design (per the reference screenshot supplied):
   a uniform dark scrim over the WHOLE screen with a soft circular "hole" cut
   out of it directly over the current step's target — CSS `mask-image`, not a
   gradient background — so the real target is left fully undimmed/crisp right
   at the tap point, fading smoothly back to full dim a short distance out. A
   separate soft white glow (`.hpc-tour-glow`, mix-blend-mode: screen) sits at
   the same spot to actively brighten it, matching the reference's halo rather
   than merely "not dimmed." Back/Skip live ON the card itself (see
   .hpc-tour-card-controls) rather than a page-fixed top bar, so they travel
   with the card instead of competing with whatever the page's own header/nav
   is doing at a given moment.

   The card is DELIBERATELY theme-independent (a fixed dark glass panel, never
   --hpc-surface/--hpc-text) rather than following the site's own light/dark
   mode — it always sits on this same dark scrim regardless of which mode the
   underlying page is in, so a fixed dark-glass-plus-light-text card is what
   stays readable in BOTH modes, rather than what best matches either one.

   "Tour Finished/Skipped" confirmation reuses .hpc-modal-backdrop/
   .hpc-modal-dialog instead (a plain centred dialog, no spotlight involved).
   ====================================================================== */
.hpc-tour-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(10, 14, 26, .72);
    z-index: 1300;
    /* No CSS transition here deliberately — mask-image transitions on a
       radial-gradient are unreliable across browsers (several don't
       interpolate the gradient smoothly at all, just jump). tour.js's
       animateSpotlight() drives the mask by hand instead (requestAnimationFrame,
       eased), repainting it every frame alongside the glow/hit so all three
       move in lockstep on every browser regardless of mask-image transition
       support. Fallback for a browser with no mask-image support at all: the
       scrim simply stays solid, no cutout — degrades to a plain dim overlay,
       never breaks the tour. */
}
@supports not ((mask-image: none) or (-webkit-mask-image: none)) {
    .hpc-tour-backdrop {
        background: rgba(10, 14, 26, .6);
    }
}
body.hpc-tour-open {
    overflow: hidden;
}
/* Soft white halo brightening the spotlighted area beyond just "undimmed" —
   screen-blended so it lifts whatever's beneath (backdrop or real page)
   rather than painting a flat white disc over it. Purely decorative
   (pointer-events: none); .hpc-tour-hit below is the actual tap target. */
.hpc-tour-glow {
    position: fixed;
    z-index: 1301;
    border-radius: 50%;
    /* Fallback only — tour.js's positionSpotlight() overwrites this inline on
       every render with a gradient sized to the CURRENT target's own real
       footprint (a fully transparent hole out to the target's edge, THEN the
       bright ring), so the glow never washes out whatever icon/label sits
       under it regardless of how big that element is. This static version
       only matters for the split second before the first render runs. */
    background: radial-gradient(circle,
        rgba(255, 255, 255, 0) 0px,
        rgba(255, 255, 255, 0) 30%,
        rgba(255, 255, 255, .6) 55%,
        rgba(255, 255, 255, .22) 75%,
        rgba(255, 255, 255, 0) 100%);
    mix-blend-mode: screen;
    pointer-events: none;
    animation: hpc-tour-pulse 1.8s ease-in-out infinite;
    /* Position/size are animated by hand too (tour.js's animateSpotlight()),
       same reasoning as .hpc-tour-backdrop above — kept in one rAF loop with
       the mask so the glow and the "hole" it sits inside never drift out of
       sync with each other mid-transition. No CSS transition here; the pulse
       keyframes above are independent of that and still apply. */
}
@keyframes hpc-tour-pulse {
    0%, 100% { opacity: .85; transform: scale(1); }
    50%      { opacity: 1; transform: scale(1.045); }
}
@media (prefers-reduced-motion: reduce) {
    .hpc-tour-glow {
        animation: none;
    }
}
/* The actual tap target — an invisible <button> exactly over the highlighted
   element (never the real link underneath, so tapping it can never navigate
   away mid-tour). cursor:pointer is the only visual cue besides the glow's
   own pulse; deliberately borderless/transparent per "an invisible section
   where they should tap their finger." Position/size animated by hand, same
   rAF loop as the glow/backdrop — see .hpc-tour-glow's own comment. */
.hpc-tour-hit {
    position: fixed;
    z-index: 1302;
    background: transparent;
    border: 0;
    border-radius: 50%;
    padding: 0;
    cursor: pointer;
}
/* Back (left, hidden on step 1) / Skip Tour (right) now live IN the card
   itself, not a fixed top bar — a page-top-fixed bar sat wherever the real
   page nav/header happened to be at the time (3.29.x), and a header/nav
   reshuffle (HONK — header hidden, nav promoted to act as header) put the
   two in each other's way. Anchoring Back/Skip to the card instead means
   they always travel with it, independent of whatever the underlying page
   chrome is doing. */
.hpc-tour-card-controls {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin: -.15rem -.15rem .6rem;
}
.hpc-tour-back {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.9rem;
    height: 1.9rem;
    background: rgba(255, 255, 255, .14);
    border: 0;
    border-radius: 999px;
    color: #fff;
    cursor: pointer;
}
.hpc-tour-back svg {
    width: 16px;
    height: 16px;
}
.hpc-tour-back[hidden] {
    visibility: hidden;
}
.hpc-tour-skip {
    margin-left: auto;
    padding: 0;
    background: none;
    border: 0;
    font-size: .75rem;
    font-weight: 600;
    letter-spacing: .01em;
    text-transform: uppercase;
    color: #fff;
    text-decoration: underline;
    cursor: pointer;
}
.hpc-tour-skip:hover,
.hpc-tour-skip:focus-visible {
    color: rgba(255, 255, 255, .8);
}
/* The popover card — deliberately theme-independent, see this section's own
   docblock above for why. */
.hpc-tour-card {
    position: fixed;
    z-index: 1303;
    width: min(300px, calc(100vw - 24px));
    background: rgba(15, 18, 28, .92);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    color: #fff;
    border-radius: var(--hpc-radius-lg);
    box-shadow: 0 20px 40px -12px rgba(0, 0, 0, .5);
    padding: 1rem 1.1rem;
    /* Plain numeric top/left, so (unlike mask-image) a CSS transition handles
       this reliably everywhere — matches the 320ms/ease-out feel of the
       spotlight's own hand-rolled rAF tween (tour.js's animateSpotlight()) so
       the card glides to the next step in step with the glow, not ahead of or
       behind it. */
    transition: top .32s cubic-bezier(.33, 1, .68, 1), left .32s cubic-bezier(.33, 1, .68, 1);
}
@media (prefers-reduced-motion: reduce) {
    .hpc-tour-card {
        transition: none;
    }
}
.hpc-tour-card-title,
.hpc-tour-card-body {
    /* Crossfaded across a step change by goToStep() (tour.js) — an instant text
       swap while the card/spotlight are still gliding to the next step reads
       as a mismatch, so the text fades out/in a little quicker than the
       320ms position glide instead of snapping. */
    transition: opacity .13s ease;
}
@media (prefers-reduced-motion: reduce) {
    .hpc-tour-card-title,
    .hpc-tour-card-body {
        transition: none;
    }
}
.hpc-tour-card-title {
    font-size: .95rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .01em;
    margin: 0 0 .5rem;
}
.hpc-tour-card-body {
    font-size: .85rem;
    line-height: 1.4;
    color: rgba(255, 255, 255, .85);
    margin: 0;
    padding-left: .6rem;
    border-left: 2px solid rgba(255, 255, 255, .3);
}
/* Step count + "tap to continue" hint are announced to screen readers only —
   the reference design relies on the glow/pulse alone to communicate
   interactivity, with no visible on-card chrome besides title/body. */
.hpc-tour-card-step,
.hpc-tour-card-hint {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* Confirmation modal ("Tour Finished!"/"Tour Skipped.") — centred on the
   shared .hpc-modal-backdrop base (full click-catching overlay), but the
   dialog itself is restyled to match the tour card's own dark-glass look
   rather than the plain default .card — same reasoning as .hpc-tour-card's
   own docblock (deliberately theme-independent, always readable regardless
   of the site's light/dark mode), plus a small status icon and better
   spacing so this reads as a deliberate close to the tour instead of a
   generic system dialog. Sits above the tour's own layer since it only ever
   appears after teardown() has already removed the backdrop/glow/card. */
.hpc-tour-done-backdrop {
    z-index: 1305;
    background: rgba(6, 8, 16, .78);
}
.hpc-tour-done-dialog {
    max-width: 340px;
    background: rgba(15, 18, 28, .95);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border: 1px solid rgba(255, 255, 255, .08);
    border-radius: var(--hpc-radius-lg);
    box-shadow: 0 24px 48px -16px rgba(0, 0, 0, .65);
}
.hpc-tour-done-dialog .card-body {
    padding: 2rem 1.5rem 1.5rem;
}
.hpc-tour-done-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 3rem;
    height: 3rem;
    margin: 0 auto 1rem;
    border-radius: 50%;
    background: var(--hpc-gradient-primary);
    color: #fff;
}
.hpc-tour-done-icon svg {
    width: 22px;
    height: 22px;
}
.hpc-tour-done-dialog h3 {
    color: #fff;
    font-weight: 700;
    font-size: 1.15rem;
}
.hpc-tour-done-dialog p {
    color: rgba(255, 255, 255, .78);
    font-size: .87rem;
    line-height: 1.45;
}
.hpc-tour-done-dialog .btn-primary {
    min-width: 8rem;
}

/* Guide button — relaunches the tour. Round, icon-only, pinned to the
   bottom-left corner of .hpc-profile-card (which supplies the
   position:relative context) rather than sitting in the page-header action
   row, where it crowded Pets/Rewards/Book into a 4-wide bar on mobile.
   rem-sized fixed-diameter icon button — nothing here needs to reflow, so
   this is exempt from the "no position:absolute + px" rule: that rule
   guards against fixed px content that can't shrink for narrow viewports. */
.hpc-guide-fab {
    position: absolute;
    left: .85rem;
    bottom: .85rem;
    width: 2.75rem;
    height: 2.75rem;
    min-width: 2.75rem;
    min-height: 2.75rem;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    border-radius: 9999px;
    background: var(--hpc-primary);
    color: var(--hpc-primary-contrast);
    box-shadow: var(--hpc-shadow-lifted);
    cursor: pointer;
    z-index: 2;
}
.hpc-guide-fab svg {
    width: 1.15rem;
    height: 1.15rem;
}
.hpc-guide-fab:hover,
.hpc-guide-fab:focus-visible {
    background: color-mix(in srgb, var(--hpc-primary) 85%, black);
}

/* Checkout failure reasons on the cart page (Cart\HtmlView::$checkoutErrors).
   NOT an .alert-* box on purpose: those are hidden platform-wide, which is how a
   refused checkout came to present as a dead Pay button with no explanation. Built
   from tokens only, so it themes with the rest of the site in dark mode. */
.hpc-checkout-error {
    border: 1px solid var(--hpc-danger);
    border-left-width: 4px;
    border-radius: var(--hpc-radius, .5rem);
    background: color-mix(in srgb, var(--hpc-danger) 8%, var(--hpc-surface));
    color: var(--hpc-body-color, inherit);
    padding: .75rem 1rem;
}
.hpc-checkout-error ul {
    padding-left: 1.1rem;
}

/* "Next available" status line on the booking calendar. Plain inline text rather than an
   .alert-* box, which is hidden platform-wide. Tokens only, so it themes in dark mode. */
/* Boxed to match the btn-outline-secondary/btn-outline-primary controls either
   side of it (same surface/border tokens, same btn-sm-ish height) instead of
   sitting as bare text between two buttons. Fixed-width so "Aug 2026" vs
   "Sep 2026" never nudges the prev/next buttons — gap-2 already fixes button
   spacing, this fixes the label's own footprint and centers its text in it. */
#hcc-month-label {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 6.5rem;
    min-height: var(--hpc-btn-height);
    padding: 0 var(--hpc-btn-padding-x);
    border: 1px solid var(--hpc-border);
    border-radius: var(--bs-border-radius, .375rem);
    background: var(--hpc-surface);
    font-size: var(--hpc-btn-font-size);
    text-align: center;
}

/* Fixed footer, pinned to the browser viewport bottom on every page (3.34.49
   — explicit product decision, overriding the earlier "never fixed" stance:
   a fixed footer is expected UX and the mobile-bar conflict noted below is
   handled by stacking, not by avoiding position:fixed altogether).

   Superseded: the previous "sticky-when-short" approach kept the footer in
   normal document flow and used a flex-grown `.site-grid` to push it to the
   viewport bottom only on short pages. That flex-grow was ALSO the root
   cause of a separate bug — .site-grid is a CSS Grid with 8 auto-sized rows
   (banner/top-a/top-b/comp/...), and Grid's default align-content:normal
   stretches all of them to fill flex-grown height equally, shoving `comp`
   (the actual page content, row 4) down on short pages. Removing the flex
   machinery here fixes that at the root — no band-aid needed on .site-grid.

   Height is fixed (not padding-driven) so the space reserved below the page
   content (`body`'s padding-bottom, below) can be calculated exactly instead
   of guessed. Single line of text — overflow:hidden, no wrap. */
:root {
    --hpc-footer-height: 2.25rem;
}
.footer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1040; /* below .hpc-mobilebar's 1050 so the bar stays on top when both render */
    height: var(--hpc-footer-height);
    display: flex;
    align-items: center;
    overflow: hidden;
    /* Same gradient as the header (.container-header, big_goose's own
       template.css ~L13701) rather than --hpc-surface — a plain white bar
       read as visually unrelated to the rest of the chrome (reported live:
       "does not match the site header color"). Same tokens, so a per-
       instance brand colour recolours both together automatically. */
    background-color: var(--big_goose-color-primary);
    background-image: linear-gradient(135deg, var(--big_goose-color-primary) 0%, var(--big_goose-color-hover) 100%);
    /* Was a hardcoded #fff — fine only by accident (brand primary happens to
       be dark in the one instance this shipped on). A light brand colour
       would make this unreadable; var(--hpc-text) is the same token every
       other themed surface uses, so it's guaranteed legible in both modes
       against --hpc-surface-toned surfaces, but note this background is
       still the brand gradient, not a surface token, so a light brand colour
       can still collide with light-mode --hpc-text. Flagged, not solved —
       revisit if a light-brand instance reports low contrast here. */
    color: var(--hpc-text);
    /* Shy footer (3.34.49): slides fully off-screen on scroll-down, back in
       on scroll-up/at-top — see mobilenav.js for the class toggle. Transform
       (not display/visibility) so the slide is what animates, and it moves
       correctly regardless of the mobile breakpoint's `bottom` offset below
       since translateY is relative to the element's own box either way. */
    transform: translateY(0);
    transition: transform .25s ease;
}
.footer.hpc-footer-hidden {
    transform: translateY(100%);
}
.footer .grid-child {
    width: 100%;
    padding-top: 0;
    padding-bottom: 0;
    color: var(--hpc-text);
}
.footer a {
    color: var(--hpc-text);
    text-decoration: underline;
}
body {
    padding-bottom: var(--hpc-footer-height);
}

/* <=1199.98px (and the forced-mobile equivalent): stack the footer ABOVE
   the fixed Unified Navigation bottom bar instead of sitting under/behind
   it — the two are both position:fixed at the same edge, so without this
   they'd overlap. The matching body padding-bottom that reserves room for
   both the bar AND this footer lives with the bar's own rules (~L4167 and
   ~L4284), not duplicated here. */
@media (max-width: 1199.98px) {
    body:has(.hpc-mobile-chrome) .footer {
        bottom: calc(4rem + env(safe-area-inset-bottom));
    }
}
html[data-force-mobile] body:has(.hpc-mobile-chrome) .footer {
    bottom: calc(4rem + env(safe-area-inset-bottom));
}
