/* ============================================================
   STOREFRONT — public booking-engine chrome (G-6)
   ============================================================
   Loaded ONLY on storefront requests (see App.razor), last in the cascade.

   It is an ADDITIVE layer on top of the shared brand/theme tokens: every colour, radius and font here
   resolves to a token defined in brand-tokens.css / theme-tokens.css. Nothing is hardcoded, so a re-skin
   of the brand file re-skins the storefront too, and this file never forks the design system.

   LIGHT-ONLY: the storefront carries no theme toggle (it is the tenant's brand surface), and App.razor
   pins data-bs-theme="light" for these requests. There are therefore no [data-bs-theme="dark"] rules
   below by design — not by omission.
   ============================================================ */

/* ---------- Tenant brand palette ----------
   The storefront draws its accents from --sf-*, NEVER from --brand-* directly. That indirection is the
   whole mechanism: StorefrontLayout sets --sf-primary / --sf-secondary (and their computed text colours)
   as an INLINE style on .sf-shell for the resolved tenant, so a hotelier re-colours the booking engine
   with data instead of a per-deployment CSS edit. Admin keeps reading --brand-* and stays Roomzy blue.

   Inline rather than a <style> in the head on purpose: a head-injected block depends on HeadOutlet, and
   when that misbehaves it fails SILENTLY (page renders, branding quietly reverts to platform blue). The
   attribute is emitted by the very element it styles, so if the shell renders, the palette is there.

   The :root defaults below are the Roomzy ramp, so a tenant that set nothing still gets a finished page;
   the inline style overrides them on the shell, and custom properties inherit down the whole subtree.

   Only the two SEEDS are stored. The hover/tint steps are derived here with color-mix(), so the hotelier
   picks two colours instead of nine and the ramp can never drift out of sync with the seed. Mixing toward
   black/white keeps both a light and a dark brand working: the step always moves away from the seed. */
:root {
    --sf-primary: var(--brand-700, #1E40AF);
    --sf-on-primary: #FFFFFF;
    --sf-secondary: var(--brand-900, #172554);
    --sf-on-secondary: #FFFFFF;
}

.sf-shell {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    /* Full-bleed hero + an absolutely-positioned guest panel can each poke a pixel or two past the viewport
       on a narrow phone, which the browser answers with a horizontal rubber-band "shake". clip (not
       hidden/auto) contains that WITHOUT creating a scroll container, so the sticky header keeps working. */
    overflow-x: clip;
    /* Derived on the SAME element the inline tenant style lands on: custom properties resolve against that
       element's cascaded value and inline wins, so these mixes see the tenant seed, not the :root default.
       Scoped to .sf-shell and deliberately NOT to the platform landing page, which stays Roomzy-branded. */
    --sf-primary-hover: color-mix(in srgb, var(--sf-primary) 85%, black);
    --sf-primary-soft: color-mix(in srgb, var(--sf-primary) 12%, white);
    --sf-primary-border: color-mix(in srgb, var(--sf-primary) 35%, white);
    --sf-secondary-hover: color-mix(in srgb, var(--sf-secondary) 85%, black);

    /* Neutral text/border scale for the storefront, fixed ONCE here so contrast is a token concern,
       not a per-page one (owner: "contrast too low on EVERY page - a tokens problem"). --sf-muted lands
       at ~7:1 on white (was gray-500 #6B7280 ~4.8:1).

       BORDERS ARE NEUTRAL GRAY ON PURPOSE. --bs-border-color drives every Bootstrap control
       (.form-control/.form-select), card and table; --border-color drives the custom .sf-* edges. Both
       are a pure achromatic gray (equal R=G=B, or black-alpha) - NOT a blue-gray like the earlier
       #98a2b3 / rgba(17,24,39,...): a tinted neutral would fight the tenant's brand colour once customer
       theming lands, so the chrome greys must stay hue-free. The weight sits between Bootstrap's too-faint
       #dee2e6 default and the too-heavy first attempt (~#bdbdbd, the achromatic match of the owner's
       #b4c0d3). Scoped to .sf-shell so the platform landing page and admin keep their own scale. */
    --sf-text: #111827;
    --sf-muted: #4b5563;
    --sf-muted-strong: #374151;
    --border-color: rgba(0, 0, 0, .26);
    --bs-border-color: #bdbdbd;
    /* Hero height, ONE variable shared by the landing and the results page so the hero never changes size
       between them (owner: shrinking it would crop too much of an image-heavy property photo). Deliberately
       below 100vh - exactly the legacy 80vh trick - so the first results always peek under the fold and the
       auto-scroll then slides them up. Tune here; both pages follow. */
    --sf-hero-h: 80vh;
}

/* Bootstrap's primary button, re-pointed at the tenant seed for storefront requests only. The LABEL colour
   comes from --sf-on-primary, computed server-side by luminance (BrandColor.OnColor): the hotelier picks
   the fill, so a pale brand gets dark text instead of unreadable white. */
.sf-shell .btn-primary {
    --bs-btn-bg: var(--sf-primary);
    --bs-btn-border-color: var(--sf-primary);
    --bs-btn-color: var(--sf-on-primary);
    --bs-btn-hover-bg: var(--sf-primary-hover);
    --bs-btn-hover-border-color: var(--sf-primary-hover);
    --bs-btn-hover-color: var(--sf-on-primary);
    --bs-btn-active-bg: var(--sf-primary-hover);
    --bs-btn-active-border-color: var(--sf-primary-hover);
    --bs-btn-active-color: var(--sf-on-primary);
    --bs-btn-disabled-bg: var(--sf-primary);
    --bs-btn-disabled-border-color: var(--sf-primary);
    --bs-btn-disabled-color: var(--sf-on-primary);
}

.sf-shell .btn-outline-primary {
    --bs-btn-color: var(--sf-primary);
    --bs-btn-border-color: var(--sf-primary);
    --bs-btn-hover-bg: var(--sf-primary);
    --bs-btn-hover-border-color: var(--sf-primary);
    --bs-btn-hover-color: var(--sf-on-primary);
    --bs-btn-active-bg: var(--sf-primary);
    --bs-btn-active-border-color: var(--sf-primary);
    --bs-btn-active-color: var(--sf-on-primary);
}

/* Bootstrap's secondary button, re-pointed at the tenant's second brand seed. Previously unstyled here, so
   every "back"/"view cart"/"cancel" action on the storefront (there are many - Cart, Checkout, Check-in,
   listing results) rendered Bootstrap's stock gray regardless of the tenant's brand, and a filled variant
   would have inherited Bootstrap's own --bs-btn-color assumption instead of the computed contrast-safe
   label (decision #201, item 8: WCAG contrast already computed server-side via BrandColor.OnColor(), this
   was simply never wired to the secondary variant). */
.sf-shell .btn-secondary {
    --bs-btn-bg: var(--sf-secondary);
    --bs-btn-border-color: var(--sf-secondary);
    --bs-btn-color: var(--sf-on-secondary);
    --bs-btn-hover-bg: var(--sf-secondary-hover);
    --bs-btn-hover-border-color: var(--sf-secondary-hover);
    --bs-btn-hover-color: var(--sf-on-secondary);
    --bs-btn-active-bg: var(--sf-secondary-hover);
    --bs-btn-active-border-color: var(--sf-secondary-hover);
    --bs-btn-active-color: var(--sf-on-secondary);
    --bs-btn-disabled-bg: var(--sf-secondary);
    --bs-btn-disabled-border-color: var(--sf-secondary);
    --bs-btn-disabled-color: var(--sf-on-secondary);
}

.sf-shell .btn-outline-secondary {
    --bs-btn-color: var(--sf-secondary);
    --bs-btn-border-color: var(--sf-secondary);
    --bs-btn-hover-bg: var(--sf-secondary);
    --bs-btn-hover-border-color: var(--sf-secondary);
    --bs-btn-hover-color: var(--sf-on-secondary);
    --bs-btn-active-bg: var(--sf-secondary);
    --bs-btn-active-border-color: var(--sf-secondary);
    --bs-btn-active-color: var(--sf-on-secondary);
}

/* Focus ring: Bootstrap's default is its own blue, which would clash with a non-blue tenant brand. */
.sf-shell .btn:focus-visible,
.sf-shell .form-control:focus,
.sf-shell .form-select:focus {
    border-color: var(--sf-primary);
    box-shadow: 0 0 0 .25rem color-mix(in srgb, var(--sf-primary) 30%, transparent);
}

/* ---------- flatpickr (date picker) ----------
   flatpickr appends .flatpickr-calendar to <body>, OUTSIDE .sf-shell, so these are unscoped selectors -
   safe because storefront.css is only linked on storefront pages. flatpickr's own "disabled" and
   adjacent-month days default to ~10% ink (rgba(57,57,57,.1)), which left past/out-of-window dates almost
   invisible. Lifted to a neutral, clearly-legible-but-still-muted gray so the guest can READ the dates
   they cannot pick, without them looking selectable. Achromatic, to stay clear of the tenant brand hue. */
.flatpickr-day.flatpickr-disabled,
.flatpickr-day.flatpickr-disabled:hover,
.flatpickr-day.prevMonthDay,
.flatpickr-day.nextMonthDay {
    color: #9aa0a6;
}

/* ---------- Header ---------- */

.sf-header {
    background: #fff;
    border-bottom: 1px solid var(--border-color, rgba(0, 0, 0, .08));
    position: sticky;
    top: 0;
    z-index: 1030;
}

.sf-brand {
    text-decoration: none;
    color: inherit;
}

/* Header icon actions (cart, admin gear): borderless on purpose, so the glyph sits clean in the chrome
   instead of inside a boxed outline button. Colour follows the brand secondary; hover just dims it, which
   stays legible against ANY tenant brand rather than betting on a second colour. */
.sf-icon-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: .25rem;
    color: var(--sf-secondary, #495057);
    font-size: 1.2rem;
    line-height: 1;
    text-decoration: none;
    border: 0;
    background: transparent;
}

.sf-icon-link:hover,
.sf-icon-link:focus-visible {
    opacity: .7;
}

/* Cart glyph from the bundled SVG, tinted via mask so it inherits the icon colour (currentColor) instead of
   shipping a fixed fill - the same trick keeps it crisp at any size. */
.sf-cart-svg {
    display: inline-block;
    width: 1.4rem;
    height: 1.4rem;
    background-color: currentColor;
    -webkit-mask: url('/css/Roomzy/svg/airport-cart.svg') center / contain no-repeat;
    mask: url('/css/Roomzy/svg/airport-cart.svg') center / contain no-repeat;
}

/* Rooms-in-cart count as a small pill on the glyph. Rendered only when > 0, so an empty cart stays quiet.
   The white ring lifts it off the (white) header cleanly even when it overlaps the icon. */
.sf-cart-badge {
    position: absolute;
    top: -.3rem;
    right: -.35rem;
    min-width: 1.05rem;
    height: 1.05rem;
    padding: 0 .3rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: .68rem;
    font-weight: 700;
    line-height: 1;
    color: var(--sf-on-primary, #fff);
    background: var(--sf-primary, #0d6efd);
    border-radius: 999px;
    box-shadow: 0 0 0 2px #fff;
}

/* Mini-cart line thumbnail. Fills its col-4 cell at a fixed height and crops (object-fit) so a portrait or
   a panorama both read as a tidy tile - and, crucially, never overflows the narrow offcanvas into a
   horizontal scroll (owner: the photo was pushing the property details off-screen). */
.sf-minicart-photo {
    width: 100%;
    height: 4.5rem;
    object-fit: cover;
    display: block;
}

/* "Add to booking" button glyph, tinted via mask so it inherits the button's text colour (white on the
   primary button). Sized in em so it tracks the label, and nudged onto the text baseline. */
.sf-add-icon {
    display: inline-block;
    width: 1.1em;
    height: 1.1em;
    vertical-align: -.15em;
    background-color: currentColor;
    -webkit-mask: url('/css/Roomzy/svg/luggage-add-fill.svg') center / contain no-repeat;
    mask: url('/css/Roomzy/svg/luggage-add-fill.svg') center / contain no-repeat;
}

.sf-brand-logo {
    height: 36px;
    min-height: 50px;
    width: auto;
    /* Tenant logos are arbitrary aspect ratios; cap the box and let the image letterbox inside it rather
       than letting a wide wordmark blow out the header. */
    max-width: 180px;
    object-fit: contain;
}

/* ---------- Main ---------- */

.sf-main {
    flex: 1 0 auto;
}

/* ---------- Hero ---------- */

.sf-hero {
    background-size: cover;
    background-position: center;
    /* Guards the default gradient AND any tenant photo that fails to load: without a fallback colour the
       white overlay text would land on a white page and vanish. Uses the tenant seed, so a property with
       no hero photo still shows its own brand rather than Roomzy blue. Safe with any seed: the overlay
       below is a dark scrim, so the title stays white-on-dark whatever colour sits underneath. */
    background-color: var(--sf-primary);
}

.sf-hero-overlay {
    /* Scrim: tenant photography is unpredictable, so the title must stay legible over a bright sky. */
    background: linear-gradient(180deg, rgba(0, 0, 0, .35), rgba(0, 0, 0, .55));
    min-height: var(--sf-hero-h, 80vh);
    display: flex;
    /* Anchor the content to the LOWER part of the hero (legacy sits the search box ~50px off the bottom, not
       vertically centred) so a fresh search still leaves the top of the photo, and the results peeking below
       80vh, visible. */
    align-items: flex-end;
}

.sf-hero-title {
    font-weight: 700;
    font-size: clamp(1.9rem, 4vw, 3.25rem);
    text-shadow: 0 2px 12px rgba(0, 0, 0, .35);
    margin-bottom: .5rem;
}

.sf-hero-subtitle {
    font-size: clamp(1rem, 1.6vw, 1.25rem);
    opacity: .95;
    margin-bottom: 0;
}

/* ---------- Sections ---------- */

.sf-section-title {
    font-size: 1.5rem;
    font-weight: 600;
    color: var(--sf-primary);
    margin-bottom: .75rem;
}

.sf-section-text {
    line-height: 1.7;
    /* Long-form prose: measure cap disabled on purpose (owner call, UX) - the description now uses the
       full available width instead of wrapping at 70 characters. */
    /* max-width: 70ch; */
}

/* ---------- Platform landing ---------- */
/* Shown at "/" on the platform host. Chrome-less by design (StorefrontLayout renders no header/footer
   without a tenant), so this section IS the whole page. */

.pl-hero {
    /* Full viewport height: with no header or footer around it, anything less leaves dead space below.
       min-height (not height) so the content can still push the box taller on a small phone in landscape. */
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 2rem 1rem;
    background-size: cover;
    background-position: center;
    /* Fallback if the hero SVG fails to load: without it the white text would land on a white page. */
    background-color: var(--brand-700);
    color: #fff;
}

.pl-hero-inner {
    max-width: 44rem;
}

.pl-logo {
    /* clamp scales the mark with the viewport instead of stepping at breakpoints. */
    width: clamp(180px, 32vw, 300px);
    height: auto;
    margin-bottom: 2rem;
}

.pl-title {
    font-weight: 700;
    font-size: clamp(1.6rem, 3.6vw, 2.75rem);
    line-height: 1.25;
    text-shadow: 0 2px 12px rgba(0, 0, 0, .3);
    margin-bottom: 1rem;
}

.pl-subtitle {
    font-size: clamp(1rem, 1.6vw, 1.2rem);
    line-height: 1.6;
    opacity: .92;
    margin-bottom: 2.25rem;
}

.pl-cta {
    /* Deep brand ink on the white button: Bootstrap's default dark grey would read as disabled here. */
    color: var(--brand-700);
    font-weight: 600;
    border-radius: 999px;
}

.pl-cta:hover,
.pl-cta:focus {
    color: var(--brand-900);
}

/* ---------- Footer ---------- */

/* Legacy parity: a NEUTRAL light-grey surface, not a brand-coloured one. Deliberate -- the footer carries
   the tenant logo and the payment-methods artwork, both authored for a light background. A dark/brand
   footer would force per-tenant logo variants (or an invert filter that mangles coloured marks), which is
   exactly the problem the legacy design avoided. */
.sf-footer {
    background: #f1f2f6;
    /* Literal colours, NOT var(--bs-*): those Bootstrap runtime variables are not guaranteed to be defined
       in this cascade, and when they resolve to nothing the fallback silently loses to an inherited value --
       which is how the footer text became unreadable on the grey. Contrast on #f1f2f6 is ~8:1 (AAA). */
    color: #4b5563;
    margin-top: auto;
}

.sf-footer-title {
    font-weight: 600;
    color: #111827;
    margin-bottom: .5rem;
    font-size: 1.05rem;
}

.sf-footer-text {
    font-size: .925rem;
    font-style: normal; /* <address> italicises by default. */
    color: #4b5563;
}

.sf-footer-row,
.sf-footer address {
    color: #4b5563;
}

/* Links pick up the tenant's brand colour (legacy: the e-mail/phone lines are the accent on the grey). */
.sf-footer a {
    color: var(--sf-primary);
    text-decoration: none;
    font-weight: 500;
}

.sf-footer a:hover,
.sf-footer a:focus {
    color: var(--sf-primary-hover);
    text-decoration: underline;
}

.sf-social {
    font-size: 1.35rem;
}

.sf-footer-bottom {
    color: #4b5563;
}

.sf-footer-bottom strong {
    color: #111827;
}

/* The tenant logo is authored for a light background, which is why the footer stays neutral: it can be
   dropped in at a fixed height with no filter/invert that would wreck a coloured mark. */
.sf-footer-logo {
    height: 44px;
    width: auto;
    max-width: 100%;
}

/* Icon + value pair, mirroring the legacy .icon-text rows. The icon column is fixed so multi-line values
   (the address) stay aligned instead of wrapping under the glyph. */
.sf-footer-row {
    display: flex;
    align-items: flex-start;
    gap: .5rem;
    margin-bottom: .4rem;
    font-size: .925rem;
}

.sf-footer-row > i {
    line-height: 1.5;
    opacity: .8;
}

.sf-footer-payment-icon {
    height: 34px;
    width: auto;
}

/* ============================================================
   SEARCH FORM + RESULTS (stage B2)
   ============================================================
   Same rules as everything above: colours come from the --sf-* tenant palette or the shared tokens, never
   hardcoded, and there are no dark-mode rules because the storefront is pinned light. */

/* ---------- Search form ---------- */

/* The hero panel floats the form over the tenant's photography. Legacy keeps it TRANSLUCENT so the photo
   reads through the box (background: rgb(255,255,255,0.2)); the actual inputs inside stay opaque and
   high-contrast, so only the frame is see-through. */
.sf-hero-search {
    max-width: 760px;
    margin: 1.5rem auto 0;
    background: rgba(255, 255, 255, .2);
    border-radius: .75rem;
    padding: 1rem;
    box-shadow: 0 .5rem 1.5rem rgba(0, 0, 0, .18);
}

/* Bootstrap's default invalid-feedback red (#dc3545) muddies into the dark scrim/photo behind the
   translucent hero panel. Brighten it and add a shadow so the "select your dates" / "max N guests"
   messages read clearly over any background. Scoped to the hero panel; the toolbar on the results
   page inherits it too, which is fine (same translucent treatment). */
.sf-hero-search .invalid-feedback {
    color: #ffd0d4;
    font-weight: 600;
    text-shadow: 0 1px 3px rgba(0, 0, 0, .55);
}

/* On the results page the same component sits on a plain background, so it drops the float treatment and
   reads as a toolbar instead of a hero card. */
.sf-search-label {
    font-size: .8rem;
    font-weight: 600;
    margin-bottom: .25rem;
    /* White over the translucent panel + dark scrim; the shadow keeps it legible over a bright photo. */
    color: #fff;
    text-shadow: 0 1px 3px rgba(0, 0, 0, .45);
}

/* Full-height submit so it lines up with the inputs on desktop; the w-100 in markup handles mobile. */
.sf-search-submit {
    height: calc(1.5em + .75rem + 2px);
    white-space: nowrap;
}

.sf-search-promo-toggle {
    font-size: .85rem;
    cursor: pointer;
    /* On the translucent hero panel the brand colour can vanish into a dark photo, so the toggle is white
       with a shadow to stay legible over any background. */
    color: #fff;
    text-shadow: 0 1px 3px rgba(0, 0, 0, .45);
}

/* ---------- Guest selector ---------- */

/* Built on <details>, so the open/closed state is the browser's - no JS, and it still works with the
   enhancement script blocked. */
.sf-guests-disclosure {
    position: relative;
}

.sf-guests-toggle {
    cursor: pointer;
    list-style: none;
    user-select: none;
}

/* Remove the native disclosure triangle in both engines; the chevron in the markup replaces it. */
.sf-guests-toggle::-webkit-details-marker {
    display: none;
}

.sf-guests-toggle::marker {
    content: '';
}

.sf-guests-disclosure[open] .sf-guests-caret {
    transform: rotate(180deg);
}

.sf-guests-caret {
    transition: transform .15s ease;
}

/* Absolute so opening the panel does not push the submit button down and shift the layout mid-interaction.
   z-index sits above the sticky header's stacking context but below modals. */
.sf-guests-panel {
    position: absolute;
    z-index: 1040;
    top: calc(100% + .25rem);
    left: 0;
    right: 0;
    /* Cap at the container width so the panel never forces horizontal overflow on a narrow phone. */
    min-width: min(260px, 100%);
    background: #fff;
    border: 1px solid var(--border-color, rgba(0, 0, 0, .12));
    border-radius: .5rem;
    box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .12);
    padding: .75rem;
}

.sf-guests-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: .4rem 0;
}

.sf-guests-label {
    font-weight: 600;
    font-size: .925rem;
}

.sf-guests-hint,
.sf-guests-note {
    font-size: .78rem;
    color: var(--sf-muted);
}

.sf-guests-note {
    margin-top: .5rem;
}

.sf-stepper {
    display: flex;
    align-items: center;
    gap: .35rem;
}

/* Fixed width so the row does not reflow as the value grows from 1 to 10 digits-wise. The native spinner
   is hidden because the +/- buttons are the affordance here. */
.sf-stepper-input {
    width: 3.5rem;
    text-align: center;
    -moz-appearance: textfield;
}

.sf-stepper-input::-webkit-outer-spin-button,
.sf-stepper-input::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

/* ---------- Result cards ---------- */

.sf-listing-card {
    border: 1px solid var(--border-color, rgba(0, 0, 0, .08));
    border-radius: .75rem;
    /* NOTE: no overflow:hidden here. It would clip the per-room guest panel (.sf-guests-panel), which is
       absolutely positioned and drops BELOW the card body - an ancestor overflow:hidden clips it no matter
       how high its z-index. The photo's corner-rounding that overflow:hidden used to provide is done on the
       image itself (.sf-listing-photo) instead. */
    /* Soft neutral drop shadow (legacy parity). Biased DOWNWARD, not uniform: a positive y-offset with a
       slightly negative spread pulls the blur off the top/side edges so the card reads as lifted with the
       shadow pooling under its bottom edge. Achromatic (black-alpha) like the borders, so it never tints
       the tenant palette. */
    box-shadow: 0 .4rem .7rem -.15rem rgba(0, 0, 0, .12);
}

/* Fixed height + cover so a row of cards keeps one baseline regardless of the aspect ratios a hotelier
   uploaded; without it the grid visibly jitters between listings. */
.sf-listing-photo {
    width: 100%;
    height: 100%;
    min-height: 200px;
    object-fit: cover;
    display: block;
    /* Round to the card corners the image actually touches. Mobile-first: the photo is stacked ON TOP
       (full width), so round the two TOP corners. The md override below re-rounds for the side-by-side
       layout where the photo is the LEFT column. */
    border-top-left-radius: .75rem;
    border-top-right-radius: .75rem;
}

@media (min-width: 768px) {
    /* Side-by-side layout: the photo is the LEFT column, so round top-left + bottom-left and drop the
       top-right rounding it carried while stacked. */
    .sf-listing-photo {
        border-top-right-radius: 0;
        border-bottom-left-radius: .75rem;
    }
}

.sf-listing-title {
    color: var(--sf-secondary);
}

/* The card title is a link, but it must still READ as a heading: underline only on hover/focus, and a
   visible focus ring because the photo link above it has no text of its own. */
.sf-listing-title-link {
    color: inherit;
    text-decoration: none;
}

.sf-listing-title-link:hover,
.sf-listing-title-link:focus-visible {
    color: var(--sf-primary);
    text-decoration: underline;
}

.sf-listing-meta {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem;
    margin-top: .25rem;
}

.sf-listing-tag {
    font-size: .78rem;
    padding: .15rem .5rem;
    border-radius: 1rem;
    background: var(--sf-primary-soft);
    color: var(--sf-secondary);
    white-space: nowrap;
}

/* Scarcity only; kept amber rather than red so it reads as urgency, not as an error. */
.sf-listing-tag-warn {
    background: #FEF3C7;
    color: #92400E;
}

/* Feature chips (adults-only, pets, smoking, scarcity) as Bootstrap subtle-contextual pill badges, so
   the allow/deny state reads at a glance - the port of the legacy is-*-light tags. Only sizing and the
   icon/text gap are ours; the colours come entirely from Bootstrap's bg-*-subtle / text-*-emphasis pairs. */
.sf-listing-badge {
    font-size: .74rem;
    font-weight: 600;
    display: inline-flex;
    align-items: center;
    gap: .25rem;
}

/* Cart line remove control: a comfortable, obvious tap target (the bi-trash glyph alone was too small to
   hit reliably on a phone). Square, centred, with a soft danger-tinted hover so it reads as destructive. */
.sf-cart-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2.5rem;
    min-height: 2.5rem;
    border-radius: .5rem;
}

.sf-cart-remove:hover,
.sf-cart-remove:focus-visible {
    background-color: var(--bs-danger-bg-subtle, #f8d7da);
}

.sf-listing-desc {
    font-size: .9rem;
    color: #4B5563;
}

.sf-listing-price-from,
.sf-listing-price-note {
    font-size: .75rem;
    color: var(--sf-muted);
}

.sf-listing-price-amount {
    font-size: 1.35rem;
    font-weight: 700;
    color: var(--sf-primary);
    white-space: nowrap;
}

/* ---------- Money (amount + ISO currency code) ---------- */

/* One rendering for every storefront price (StorefrontMoney.FormatHtml). The ISO code trails the amount and
   is deliberately smaller and muted, sized in `em` so it stays proportionally smaller than whatever font the
   surrounding price uses - a big headline or an inline summary line. We show the code, not a symbol, because
   "$" is ambiguous across USD/CAD/AUD on a multi-currency SaaS. */
.sf-money-code {
    font-size: .7em;
    font-weight: 600;
    color: var(--sf-muted);
    margin-left: .15em;
    white-space: nowrap;
}

/* ---------- Rate plan chooser ---------- */

/* The whole row is the label, so the entire card area is a hit target - a 16px radio is a poor one on
   touch. :has() gives the selected state without any JS. */
.sf-plan {
    display: flex;
    align-items: center;
    gap: .75rem;
    width: 100%;
    padding: .6rem .75rem;
    margin-bottom: .4rem;
    border: 1px solid var(--border-color, rgba(0, 0, 0, .12));
    border-radius: .5rem;
    cursor: pointer;
}

.sf-plan:has(.sf-plan-radio:checked) {
    border-color: var(--sf-primary);
    background: var(--sf-primary-soft);
}

.sf-plan-body {
    flex: 1 1 auto;
    min-width: 0;
}

.sf-plan-name {
    display: block;
    font-weight: 600;
    font-size: .925rem;
}

.sf-plan-tags {
    display: flex;
    flex-wrap: wrap;
    gap: .3rem;
    margin-top: .2rem;
}

.sf-plan-tag {
    font-size: .72rem;
    padding: .1rem .4rem;
    border-radius: .25rem;
    background: rgba(0, 0, 0, .04);
    color: #374151;
    white-space: nowrap;
}

.sf-plan-tag-ok {
    background: #DCFCE7;
    color: #166534;
}

.sf-plan-tag-muted {
    background: rgba(0, 0, 0, .06);
    color: var(--sf-muted);
}

.sf-plan-price {
    text-align: right;
    white-space: nowrap;
}

.sf-plan-total {
    display: block;
    font-weight: 700;
}

.sf-plan-nightly {
    display: block;
    font-size: .72rem;
    color: var(--sf-muted);
}

/* ---------- Per-card refinement (B3) ---------- */

/* The selected plan's detail sits between the dropdown and the action, so the guest reads WHAT they picked
   and WHAT it costs in one glance instead of re-opening the select to check. */
.sf-plan-selected {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    flex-wrap: wrap;
}

.sf-listing-refine-label {
    font-size: .78rem;
    font-weight: 600;
    color: var(--sf-muted);
    margin-bottom: .2rem;
}

/* The explicit "Update price" button is the JS-off reprice trigger. Once storefront-search.js takes over
   the reprice on change it adds .sf-refine-live and the button becomes redundant, so it is hidden - kept in
   the DOM (not removed) so a mid-session script failure still leaves a working control. */
.sf-refine-live .sf-refine-submit {
    display: none;
}

/* While an in-place reprice fetch is in flight the pricing figures are briefly stale, so the pricing region
   is dimmed as a quiet "recalculating" cue. Only the pricing region is dimmed - the refinement inputs stay
   fully interactive, so the guest can keep adjusting while a fetch resolves. */
.sf-card-busy [data-sf-region="pricing"] {
    opacity: .55;
    transition: opacity .12s ease-in-out;
}

/* ---------- Card 3-column layout (legacy _ListingItem parity) ---------- */

/* The pricing column reads as a distinct panel on wide screens, so a hairline separates it from the
   information column - the same visual split the legacy Bulma card had between its middle and right areas. */
@media (min-width: 992px) {
    .sf-card-pricing {
        border-left: 1px solid var(--border-color, rgba(0, 0, 0, .08));
    }
}

.sf-price-heading {
    font-size: .72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--sf-muted);
}

.sf-card-plan-name .sf-plan-tag-name {
    background: var(--sf-primary-soft);
    color: var(--sf-secondary);
}

/* Room-quantity input is a small numeric field, not a full-width control - it holds a 1-2 digit count. */
.sf-card-units-input {
    max-width: 6rem;
}

.sf-card-dates {
    margin-top: .25rem;
    font-size: .95rem;
    color: var(--sf-secondary);
}

.sf-card-date + .sf-card-date {
    margin-top: .15rem;
}

.sf-card-date-label {
    color: var(--sf-muted);
}

.sf-card-date-value {
    font-weight: 600;
}

/* The "includes taxes and fees" reassurance sits under the price, muted so it informs without competing
   with the total above it. */
.sf-taxes-note {
    font-size: .72rem;
    color: var(--sf-muted);
}

.sf-card-plan-tags {
    display: flex;
    flex-wrap: wrap;
    gap: .3rem;
}

/* On desktop the action sits to the right, under the price it commits to (legacy). */
.sf-card-action {
    display: flex;
    justify-content: flex-end;
}

/* On desktop the pricing column is right-aligned, so its tag rows must hug the right edge too. */
@media (min-width: 992px) {
    .sf-card-plan-tags {
        justify-content: flex-end;
    }
}

/* Phones: stack and centre, matching the legacy has-text-centered-mobile treatment so the card reads as a
   single tidy column instead of a left-ragged one. The action button goes full width for an easy tap. */
@media (max-width: 991.98px) {
    .sf-card-pricing {
        text-align: center;
    }

    .sf-card-plan-tags {
        justify-content: center;
    }

    .sf-card-action {
        justify-content: stretch;
    }

    .sf-card-book-btn {
        width: 100%;
    }
}

/* ---------- Unavailable cards ---------- */

/* Muted so they inform without competing with bookable results. Deliberately NOT display:none - the reason
   ("3-night minimum") is often the difference between a lost search and an adjusted one. */
.sf-listing-card-unavailable {
    opacity: .78;
    background: rgba(0, 0, 0, .015);
}

.sf-listing-photo-muted {
    filter: grayscale(1);
}

.sf-unavailable-badge {
    background: #E5E7EB;
    color: #374151;
}

.sf-unavailable-reason {
    font-size: .875rem;
    color: #4B5563;
}

.sf-unavailable-plans {
    list-style: none;
    padding-left: 0;
    font-size: .8rem;
}

.sf-unavailable-plans li {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    padding: .15rem 0;
    border-top: 1px dashed var(--border-color);
}

.sf-unavailable-plan-reason {
    color: var(--sf-muted);
    text-align: right;
}

/* ---------- Listing details page (C3) ---------- */

.sf-listing-details .sf-listing-header h1 {
    color: var(--sf-secondary);
}

.sf-listing-facts {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
    margin-bottom: 1.25rem;
}

.sf-listing-fact {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    font-size: .85rem;
    padding: .3rem .7rem;
    border-radius: 1rem;
    background: var(--sf-primary-soft);
    color: var(--sf-secondary);
    white-space: nowrap;
}

.sf-listing-section {
    margin-bottom: 1.75rem;
}

.sf-listing-section h2 {
    color: var(--sf-secondary);
    margin-bottom: .6rem;
}

.sf-listing-body {
    color: #4B5563;
    line-height: 1.7;
}

/* Two columns on wide screens so a long amenity list does not become a page-long ribbon; one column when
   there is no room for two. */
.sf-listing-amenities {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: .4rem 1rem;
    margin-bottom: 0;
}

.sf-listing-amenities li {
    display: flex;
    align-items: center;
    gap: .5rem;
    font-size: .9rem;
    color: #4B5563;
}

.sf-listing-amenities i {
    color: var(--sf-primary);
}

/* ---------- Booking panel ---------- */

/* Sticky only where there is a second column to stick beside. On phones the panel is a normal block: a
   sticky card there would permanently occupy the viewport the guest is reading with. */
@media (min-width: 992px) {
    .sf-booking-sticky {
        position: sticky;
        top: 1rem;
    }
}

.sf-booking-price-amount {
    font-size: 1.6rem;
    font-weight: 700;
    color: var(--sf-primary);
    white-space: nowrap;
}

.sf-booking-price-note,
.sf-booking-prompt {
    font-size: .8rem;
    color: var(--sf-muted);
    margin-bottom: 0;
}

/* ---------- Gallery (CSS-only, no circuit) ---------- */

.sf-gallery {
    margin-bottom: 1.5rem;
}

.sf-gallery-empty {
    height: 240px;
    border-radius: .75rem;
    background: var(--sf-primary-soft);
    color: var(--sf-secondary);
    font-size: 2rem;
}

/* The stage is a fixed-ratio box and every slide is stacked absolutely inside it, so switching photos never
   reflows the page - which is what keeps the layout stable without any script. */
.sf-gallery-stage {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 9;
    border-radius: .75rem;
    overflow: hidden;
    background: var(--sf-primary-soft);
}

.sf-gallery-slide {
    position: absolute;
    inset: 0;
    margin: 0;
    opacity: 0;
    /* visibility (not display) so the transition can run and so a hidden slide stays out of the a11y tree. */
    visibility: hidden;
    transition: opacity .2s ease-in-out;
}

.sf-gallery-slide img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* THE WHOLE MECHANISM. The checked radio selects its matching slide through the general sibling combinator;
   no JavaScript, no circuit, and it keeps working with scripting disabled. */
.sf-gallery-radio:nth-of-type(1):checked ~ .sf-gallery-stage .sf-gallery-slide-0,
.sf-gallery-radio:nth-of-type(2):checked ~ .sf-gallery-stage .sf-gallery-slide-1,
.sf-gallery-radio:nth-of-type(3):checked ~ .sf-gallery-stage .sf-gallery-slide-2,
.sf-gallery-radio:nth-of-type(4):checked ~ .sf-gallery-stage .sf-gallery-slide-3,
.sf-gallery-radio:nth-of-type(5):checked ~ .sf-gallery-stage .sf-gallery-slide-4,
.sf-gallery-radio:nth-of-type(6):checked ~ .sf-gallery-stage .sf-gallery-slide-5,
.sf-gallery-radio:nth-of-type(7):checked ~ .sf-gallery-stage .sf-gallery-slide-6,
.sf-gallery-radio:nth-of-type(8):checked ~ .sf-gallery-stage .sf-gallery-slide-7,
.sf-gallery-radio:nth-of-type(9):checked ~ .sf-gallery-stage .sf-gallery-slide-8,
.sf-gallery-radio:nth-of-type(10):checked ~ .sf-gallery-stage .sf-gallery-slide-9,
.sf-gallery-radio:nth-of-type(11):checked ~ .sf-gallery-stage .sf-gallery-slide-10,
.sf-gallery-radio:nth-of-type(12):checked ~ .sf-gallery-stage .sf-gallery-slide-11,
.sf-gallery-radio:nth-of-type(13):checked ~ .sf-gallery-stage .sf-gallery-slide-12,
.sf-gallery-radio:nth-of-type(14):checked ~ .sf-gallery-stage .sf-gallery-slide-13,
.sf-gallery-radio:nth-of-type(15):checked ~ .sf-gallery-stage .sf-gallery-slide-14,
.sf-gallery-radio:nth-of-type(16):checked ~ .sf-gallery-stage .sf-gallery-slide-15,
.sf-gallery-radio:nth-of-type(17):checked ~ .sf-gallery-stage .sf-gallery-slide-16,
.sf-gallery-radio:nth-of-type(18):checked ~ .sf-gallery-stage .sf-gallery-slide-17,
.sf-gallery-radio:nth-of-type(19):checked ~ .sf-gallery-stage .sf-gallery-slide-18,
.sf-gallery-radio:nth-of-type(20):checked ~ .sf-gallery-stage .sf-gallery-slide-19 {
    opacity: 1;
    visibility: visible;
}

/* SAFETY NET past the enumerated pairs. A listing with more photos than rules would otherwise show an empty
   stage; this keeps the cover visible, so it degrades to "cover only" rather than to a blank box. It is
   deliberately weaker than the rules above (no :checked on the radio), so it never fights them. */
.sf-gallery-stage .sf-gallery-slide-0 {
    opacity: 1;
    visibility: visible;
}

/* Horizontal scroller rather than a wrapping grid: the strip must not push the booking panel down the page
   on a listing with many photos. */
.sf-gallery-thumbs {
    display: flex;
    gap: .5rem;
    margin-top: .5rem;
    overflow-x: auto;
    padding-bottom: .25rem;
}

.sf-gallery-thumb {
    flex: 0 0 auto;
    width: 84px;
    height: 60px;
    border-radius: .4rem;
    overflow: hidden;
    cursor: pointer;
    opacity: .65;
    border: 2px solid transparent;
    transition: opacity .15s ease-in-out;
}

.sf-gallery-thumb:hover {
    opacity: 1;
}

.sf-gallery-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* The selected thumb, and a visible focus ring on the RADIO's label - the radios themselves are visually
   hidden, so without this a keyboard user would have no idea where they are. */
.sf-gallery-radio:nth-of-type(1):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-0,
.sf-gallery-radio:nth-of-type(2):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-1,
.sf-gallery-radio:nth-of-type(3):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-2,
.sf-gallery-radio:nth-of-type(4):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-3,
.sf-gallery-radio:nth-of-type(5):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-4,
.sf-gallery-radio:nth-of-type(6):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-5,
.sf-gallery-radio:nth-of-type(7):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-6,
.sf-gallery-radio:nth-of-type(8):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-7,
.sf-gallery-radio:nth-of-type(9):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-8,
.sf-gallery-radio:nth-of-type(10):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-9,
.sf-gallery-radio:nth-of-type(11):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-10,
.sf-gallery-radio:nth-of-type(12):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-11,
.sf-gallery-radio:nth-of-type(13):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-12,
.sf-gallery-radio:nth-of-type(14):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-13,
.sf-gallery-radio:nth-of-type(15):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-14,
.sf-gallery-radio:nth-of-type(16):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-15,
.sf-gallery-radio:nth-of-type(17):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-16,
.sf-gallery-radio:nth-of-type(18):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-17,
.sf-gallery-radio:nth-of-type(19):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-18,
.sf-gallery-radio:nth-of-type(20):checked ~ .sf-gallery-thumbs .sf-gallery-thumb-19 {
    opacity: 1;
    border-color: var(--sf-primary);
}

.sf-gallery-radio:focus-visible ~ .sf-gallery-thumbs .sf-gallery-thumb {
    outline: 2px solid var(--sf-primary);
    outline-offset: 2px;
}

/* Respect the visitor's motion preference - a cross-fade is decoration, not information. */
@media (prefers-reduced-motion: reduce) {
    .sf-gallery-slide,
    .sf-gallery-thumb {
        transition: none;
    }
}

/* ---------- Narrow screens ---------- */


@media (max-width: 767.98px) {
    /* The photo column is full width on mobile, so a cover-cropped full-height image would eat the fold. */
    .sf-listing-photo {
        min-height: 160px;
        max-height: 200px;
    }

    .sf-plan {
        flex-wrap: wrap;
    }

    .sf-plan-price {
        width: 100%;
        text-align: left;
    }

    .sf-hero-search {
        padding: .75rem;
    }

    /* Breathing room so the translucent search box does not sit flush against the phone edges (the default
       Bootstrap container gutter alone reads as too tight over a full-bleed photo). */
    .sf-hero-overlay {
        padding-left: .75rem;
        padding-right: .75rem;
    }
}

/* ------------------------------------------------------------------------------------------------------
   Guest online check-in wizard stepper (Plan 1, Phase 4).

   Light-only, tokens only (--sf-*), no hardcoded brand colour: the connector and the current/done dots
   draw from --sf-primary so the wizard wears the tenant's brand exactly like the buttons do. The step
   labels hide below the sm breakpoint (the markup adds d-none d-sm-block) so the numbered dots alone
   carry the progress on a phone, and the connector line runs behind the dots via a ::before on each step.
   ------------------------------------------------------------------------------------------------------ */
.gci-stepper {
    counter-reset: gci;
    margin: 0;
    padding: 0;
}

.gci-step {
    position: relative;
    min-width: 0;
}

/* The connector line between dots, drawn on every step except the first. */
.gci-step + .gci-step::before {
    content: "";
    position: absolute;
    top: 1rem;
    left: -50%;
    width: 100%;
    height: 2px;
    background: var(--bs-border-color, #dee2e6);
    z-index: 0;
}

.gci-step.is-current + .gci-step::before,
.gci-step.is-done::before {
    background: var(--sf-primary, #0d6efd);
}

.gci-step__dot {
    position: relative;
    z-index: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    border: 2px solid var(--bs-border-color, #dee2e6);
    background: #fff;
    color: var(--sf-muted, #6c757d);
    font-weight: 600;
    font-size: .875rem;
}

.gci-step.is-current .gci-step__dot {
    border-color: var(--sf-primary, #0d6efd);
    color: var(--sf-primary, #0d6efd);
}

.gci-step.is-done .gci-step__dot {
    border-color: var(--sf-primary, #0d6efd);
    background: var(--sf-primary, #0d6efd);
    color: var(--sf-on-primary, #fff);
}

.gci-step__label {
    display: block;
    margin-top: .375rem;
    color: var(--sf-muted, #6c757d);
}

.gci-step.is-current .gci-step__label {
    color: var(--sf-text, #111827);
    font-weight: 600;
}
