/*
 * components.css — shared component class library.
 *
 * Named design decisions composed from raw tokens. Components consume
 * recipes; themes override recipes. Every rule below reads only from
 * recipe / token custom properties — no literal hex or px decisions
 * belong here.
 */

.eyebrow {
    display: block;
    font-size: 10.5px;
    letter-spacing: var(--type-eyebrow-letter-spacing);
    text-transform: uppercase;
    font-weight: var(--type-eyebrow-weight);
    color: var(--eyebrow-colour);
    margin-bottom: 4px;
}

.eyebrow--muted {
    color: var(--kohsanuk-text-muted);
}

.section-title {
    font-family: var(--type-display-font);
    font-size: 18px;
    font-weight: var(--type-display-weight);
    letter-spacing: var(--type-display-letter-spacing);
    color: var(--kohsanuk-text);
    line-height: 1.3;
    margin: 0;
}

.card {
    background: var(--surface-card);
    border: var(--border-card);
    border-radius: var(--kohsanuk-radius-xl);
    box-shadow: var(--shadow-card);
    transition: border-color var(--ease), box-shadow var(--ease);
}

.card--collapsed {
    display: flex;
    align-items: center;
    gap: 12px;
}

.card--selected,
.card--has-picks {
    background: var(--surface-card-selected);
    background-image: var(--gradient-card-selected);
    border: var(--border-card-selected);
    padding-left: 18px;
    box-shadow: var(--shadow-card-selected);
    position: relative;
}

.card--selected::before,
.card--has-picks::before {
    content: '';
    position: absolute;
    left: -1px;
    top: 14px;
    bottom: 14px;
    width: var(--border-accent-bar-width);
    background: var(--border-accent-bar-colour);
    border-radius: 0 4px 4px 0;
}

.card__head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 10px;
}

.card__info {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: baseline;
    gap: 8px;
    flex-wrap: wrap;
}

.pill {
    display: inline-block;
    padding: 2px 8px;
    border-radius: var(--kohsanuk-radius-pill);
    font-size: 10.5px;
    font-weight: var(--type-eyebrow-weight);
    letter-spacing: .05em;
    text-transform: uppercase;
}

.pill--paid {
    background: var(--status-paid-bg);
    color: var(--status-paid-text);
}

.pill--unpaid {
    background: var(--status-unpaid-bg);
    color: var(--status-unpaid-text);
}

.pill--hold {
    background: var(--status-hold-bg);
    color: var(--status-hold-text);
}

.pill--neutral {
    background: var(--status-neutral-bg);
    color: var(--status-neutral-text);
}

.pill--selected {
    background: var(--kohsanuk-action);
    color: var(--button-primary-fg);
    padding: 2px 6px;
    font-size: 9.5px;
    letter-spacing: .08em;
    border-radius: 4px;
}

.counter {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    user-select: none;
}

.counter__btn {
    width: 28px;
    height: 28px;
    line-height: 26px;
    text-align: center;
    border-radius: var(--kohsanuk-radius-md);
    border: 1px solid var(--kohsanuk-border-subtle);
    background: var(--kohsanuk-surface);
    color: var(--kohsanuk-text-muted);
    font-size: 16px;
    font-weight: 600;
    cursor: pointer;
    padding: 0;
}

.counter__btn--add {
    background: var(--kohsanuk-action);
    border-color: var(--kohsanuk-action);
    color: #fff;
}

.counter__btn--add-outline {
    background: var(--kohsanuk-surface);
    color: var(--kohsanuk-action);
    border-color: var(--kohsanuk-action);
}

.counter__val {
    min-width: 22px;
    text-align: center;
    font-weight: 700;
    color: var(--kohsanuk-text);
    font-size: 14px;
}

.counter__val--has {
    color: var(--kohsanuk-action);
}

.option-row {
    background: var(--surface-field);
    border: 1px solid var(--kohsanuk-border-accent-soft);
    border-radius: var(--kohsanuk-radius-lg);
    padding: 8px 12px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 10px;
    margin-top: 6px;
}

.option-row--extra {
    border: var(--border-extra-dashed);
}

.refund-bar {
    display: flex;
    height: 6px;
    border-radius: var(--kohsanuk-radius-pill);
    overflow: hidden;
}

.refund-bar__tier--full {
    background: var(--refund-full);
    flex: 1;
}

.refund-bar__tier--half {
    background: var(--refund-half);
    flex: 1;
}

.refund-bar__tier--none {
    background: var(--refund-none);
    flex: 1;
}

.button {
    font-family: var(--type-body-font);
    font-size: 13.5px;
    font-weight: 600;
    border-radius: var(--kohsanuk-radius-md);
    padding: 7px 16px;
    border: 1px solid;
    cursor: pointer;
    transition: background var(--ease), color var(--ease);
    display: inline-block;
    text-decoration: none;
    text-align: center;
}

.button--primary {
    background: var(--button-primary-bg);
    color: var(--button-primary-fg);
    border-color: var(--button-primary-bg);
}

.button--primary:hover {
    background: var(--button-primary-bg-hover);
    border-color: var(--button-primary-bg-hover);
    color: var(--button-primary-fg);
}

.button--outline {
    background: transparent;
    color: var(--button-outline-fg);
    border-color: var(--button-outline-border);
}

.button--outline:hover {
    background: var(--button-outline-bg-hover);
    color: var(--button-outline-fg);
    border-color: var(--button-outline-border);
}

/* ─── Theme-aware base ────────────────────────────────────────────────────
   Apply page-bg + text recipes at body level. Uses html body to outrank
   bootstrap-styles.min.css `body` rule, so light themes keep the same
   white-ish bg they had via Bootstrap default while dark themes (Midnight
   Lagoon) actually pick up the dark page colour. */
html body {
    background-color: var(--kohsanuk-bg-page);
    color: var(--kohsanuk-text);
}

/* Common content wrappers used by the public layout (Bootstrap admin
   theme leaves these white via .page-container / .content-wrapper /
   .content). Push the theme surface through them so dark mode doesn't
   leave bright bands across the page. Scoped to non-admin to keep the
   admin console unchanged (admin pages don't load this file anyway). */
.page-container,
.content-wrapper,
.page-content,
.content {
    background-color: transparent;
    color: var(--kohsanuk-text);
}


/* ─── Bootstrap btn-warning theme bridge ──────────────────────────────────
   The compiled Bootstrap theme hardcodes .btn-warning as the brand orange
   (#ff5722). The booking wizard's jquery-steps plugin renders its Next /
   Previous controls as `.btn-warning`, so they were stuck as orange in
   every theme. Re-route the colour through the theme action token so
   they recolour with the theme. */
.btn.btn-warning {
    background-color: var(--kohsanuk-action);
    border-color: var(--kohsanuk-action);
    color: var(--button-primary-fg, #fff);
}
.btn.btn-warning:hover,
.btn.btn-warning:focus,
.btn.btn-warning:active,
.btn.btn-warning.active,
.btn.btn-warning.focus,
.open > .dropdown-toggle.btn-warning {
    background-color: var(--kohsanuk-action-dark);
    border-color: var(--kohsanuk-action-dark);
    color: var(--button-primary-fg, #fff);
}

/* Bootstrap btn-primary is hardcoded to teal #0097a7 in bootstrap-styles —
   the legacy brand colour. Re-route through --kohsanuk-brand so each
   theme swaps it (teal in Classic, theme brand elsewhere). Distinct from
   .btn-warning, which is the booking-orange ACTION. Used by review-sort
   pills and other secondary CTAs. */
.btn.btn-primary {
    background-color: var(--kohsanuk-brand);
    border-color: var(--kohsanuk-brand);
    color: #fff;
}
.btn.btn-primary:hover,
.btn.btn-primary:focus,
.btn.btn-primary:active,
.btn.btn-primary.active,
.btn.btn-primary.focus,
.open > .dropdown-toggle.btn-primary {
    background-color: var(--kohsanuk-brand);
    border-color: var(--kohsanuk-brand);
    color: #fff;
    filter: brightness(0.88);
}


/* ============================================================================
   DOSSIER — the unified summary-card system.
   Single component used four ways across the booking flow:
     1. Payment page right rail        (Order summary)
     2. Voucher page                   (Trip dossier)
     3. Booking wizard right rail      (Order summary, live-updated by wizard JS)
     4. Tour detail right rail         (3 stacked variants: TOC, Book Now, Specs)
   DRY: one set of rules + token-driven so all 6 themes recolour without forks.

   Variant content lives in the blade templates; this file owns the visual
   contract: cream-50 head with eyebrow + Fraunces title, white body of icon
   rows with brand-coloured icons, cream-50 amount band with Fraunces serial.
   ============================================================================ */
.dossier {
    background: var(--kohsanuk-surface);
    border: 1px solid var(--kohsanuk-rule);
    border-radius: 20px;
    overflow: hidden;
    font-family: 'Public Sans', 'Maven Pro', system-ui, sans-serif;
    color: var(--kohsanuk-text);
}

.dossier__head {
    padding: 22px 26px 18px;
    background: var(--kohsanuk-cream-50);
    border-bottom: 1px solid var(--kohsanuk-rule);
}
.dossier__eyebrow {
    font: 600 10px/1 'Public Sans', sans-serif;
    letter-spacing: .24em;
    text-transform: uppercase;
    color: var(--kohsanuk-brand-dark);
    margin: 0 0 6px;
}
.dossier__title {
    font: 600 19px/1.22 'Fraunces', Georgia, serif;
    margin: 0;
    letter-spacing: -.01em;
    color: var(--kohsanuk-text);
}
.dossier__sub {
    margin: 4px 0 0;
    font: 400 12.5px/1.4 'Public Sans', sans-serif;
    color: var(--kohsanuk-text-soft);
}

.dossier__body {
    margin: 0;
    padding: 18px 26px 22px;
    display: flex;
    flex-direction: column;
    gap: 14px;
}
.dossier__row {
    display: flex;
    align-items: flex-start;
    gap: 14px;
    font-size: 13.5px;
    margin: 0;
    padding: 0;
    border: none;
}
/* jQuery 2.2.4 .show() writes inline `display: block` on the row when the
   wizard reveals previously hidden facts (e.g. tour duration). Snap back
   to flex so icon/label/value stay horizontal. See order-sidebar comment. */
.dossier__row[style*="display: block"],
.dossier__row[style*="display:block"] {
    display: flex !important;
}
.dossier__row .ico {
    flex: 0 0 18px;
    height: 18px;
    color: var(--kohsanuk-brand);
    font-size: 16px;
    line-height: 1;
    margin-top: 2px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.dossier__row svg.ico,
.dossier__row > svg {
    width: 18px;
    height: 18px;
    flex: 0 0 18px;
    color: var(--kohsanuk-brand);
    margin-top: 1px;
}
.dossier__row dt {
    flex: 0 0 92px;
    color: var(--kohsanuk-text-soft);
    font-size: 12.5px;
    margin: 0;
    padding: 0;
}
.dossier__row dd {
    margin: 0;
    padding: 0;
    color: var(--kohsanuk-text);
    font-weight: 500;
    flex: 1;
    min-width: 0;
    /* Wrap at word boundaries (no mid-word breaks). overflow-wrap covers
       the rare unbreakable long token. */
    word-break: normal;
    overflow-wrap: break-word;
}
.dossier__row dd small {
    display: block;
    color: var(--kohsanuk-text-muted);
    font-size: 11.5px;
    font-weight: 400;
    margin-top: 1px;
}
.dossier__row img {
    vertical-align: middle;
    height: 14px;
    width: auto;
    margin-right: 2px;
}

/* ─── Route-first tour summary (desktop rail + mobile card) ──────────────
   The trip's From → To becomes the emphasised hero at the top of the
   Summary pane; vessel specs drop to a quiet "The boat" block below.
   Consumers: tours/_dossier-rail.blade.php, tours/summary.blade.php.
   ──────────────────────────────────────────────────────────────────────── */
.dossier__group {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin: 0;
    padding: 0;
}
.dossier__route {
    position: relative;
    margin: 0;
    padding: 16px 16px 16px 14px;
    background: var(--kohsanuk-brand-tint);
    border: 1px solid var(--kohsanuk-brand-soft);
    border-radius: 14px;
    display: grid;
    grid-template-columns: 30px 1fr;
    column-gap: 14px;
    row-gap: 14px;
}
/* Dashed connector running between the two route markers. */
.dossier__route::before {
    content: '';
    position: absolute;
    left: 29px;
    top: 32px;
    bottom: 32px;
    width: 2px;
    background: repeating-linear-gradient(var(--kohsanuk-brand-soft) 0 5px, transparent 5px 9px);
}
.dossier__route--single::before { display: none; }
.route-marker {
    grid-column: 1;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    z-index: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--kohsanuk-surface);
    border: 2px solid var(--kohsanuk-brand);
    color: var(--kohsanuk-brand);
    font-size: 15px;
}
.route-marker--to {
    background: var(--kohsanuk-brand);
    border-color: var(--kohsanuk-brand);
    color: #fff;
}
.route-text { grid-column: 2; min-width: 0; align-self: center; }
.route-eyebrow {
    display: block;
    font: 700 9.5px/1 'Public Sans', sans-serif;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--kohsanuk-brand-dark);
    margin: 0 0 3px;
}
.route-place {
    display: block;
    font: 600 17px/1.2 'Fraunces', Georgia, serif;
    color: var(--kohsanuk-text);
    letter-spacing: -.01em;
    overflow-wrap: break-word;
}
/* "The boat" divider label above the de-emphasised vessel block. */
.dossier__sub-label {
    margin: 0;
    padding-top: 14px;
    border-top: 1px solid var(--kohsanuk-rule);
    font: 700 9.5px/1.3 'Public Sans', sans-serif;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--kohsanuk-text-muted);
    display: flex;
    align-items: center;
    gap: 8px;
}
.dossier__sub-label i { font-size: 14px; color: var(--kohsanuk-text-muted); }
/* De-emphasised vessel rows: muted icons + lighter values. */
.dossier__vessel .dossier__row .ico { color: var(--kohsanuk-text-muted); }
.dossier__vessel .dossier__row dt { color: var(--kohsanuk-text-muted); }
.dossier__vessel .dossier__row dd { color: var(--kohsanuk-text-soft); font-weight: 500; }
@media (max-width: 380px) {
    .route-place { font-size: 15.5px; }
}

/* "Your selection" panel — dynamic list of picked options across the
   booking-wizard steps. Driven by update_dossier_picks() in summary_info.js
   on every count change so the user sees their cart take shape live, even
   when the underlying card on the wizard page is collapsed (single-expand
   model means only one package per step is open at a time). */
.dossier__picks {
    padding: 14px 26px 18px;
    border-top: 1px solid var(--kohsanuk-rule);
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.dossier__picks[hidden] { display: none; }
.dossier__picks-eyebrow {
    margin: 0;
    font: 600 10px/1 'Public Sans', sans-serif;
    letter-spacing: .22em;
    text-transform: uppercase;
    color: var(--kohsanuk-brand-dark);
}
.dossier__picks-group { display: flex; flex-direction: column; gap: 6px; }
.dossier__picks-group[hidden] { display: none; }
.dossier__picks-section {
    margin: 0;
    font: 600 9.5px/1 'Public Sans', sans-serif;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--kohsanuk-text-muted, #8aa0b0);
}
.dossier__picks-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.dossier__picks-pkg { padding: 0; margin: 0; list-style: none; }
.dossier__picks-pkg-name {
    font: 600 13px/1.3 'Public Sans', sans-serif;
    color: var(--kohsanuk-text);
    margin-bottom: 2px;
}
.dossier__picks-opts {
    list-style: none;
    margin: 0;
    padding: 0 0 0 4px;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.dossier__picks-opt {
    display: grid;
    grid-template-columns: 1fr auto auto;
    gap: 8px;
    align-items: baseline;
    font: 400 12.5px/1.4 'Public Sans', sans-serif;
    color: var(--kohsanuk-text-soft);
}
.dossier__picks-opt-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.dossier__picks-opt-qty {
    font-variant-numeric: tabular-nums;
    color: var(--kohsanuk-text-muted, #8aa0b0);
    font-weight: 600;
}
.dossier__picks-opt-price {
    font-variant-numeric: tabular-nums;
    color: var(--kohsanuk-text);
    font-weight: 600;
    white-space: nowrap;
}

.dossier__status {
    display: flex;
    gap: 8px;
    padding: 14px 26px;
    border-top: 1px solid var(--kohsanuk-rule);
    align-items: center;
    flex-wrap: wrap;
}
.dossier__status .label {
    padding: 4px 10px;
    border-radius: 999px;
    font: 600 10.5px/1.4 'Public Sans', sans-serif;
    letter-spacing: .08em;
    text-transform: uppercase;
}
.dossier__status .label-info { background: #dbeef2; color: var(--kohsanuk-brand-dark); }
.dossier__status .label-default { background: #eef0f2; color: #566876; }
.dossier__status .label-success { background: #d9efde; color: #1f6f3a; }
.dossier__status .label-danger { background: #fadcdc; color: #842029; }
.dossier__status .label-warning { background: #fde9c6; color: #8a5a17; }

.dossier__amount {
    padding: 18px 26px 22px;
    border-top: 1px solid var(--kohsanuk-rule);
    background: var(--kohsanuk-cream-50);
}
.dossier__amount__label {
    display: block;
    font: 600 10px/1.2 'Public Sans', sans-serif;
    letter-spacing: .22em;
    text-transform: uppercase;
    color: var(--kohsanuk-text-soft);
    margin-bottom: 4px;
    text-align: right;
}
.dossier__amount__value {
    display: block;
    font: 600 28px/1 'Fraunces', Georgia, serif;
    color: var(--kohsanuk-action-dark);
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.dossier__amount__hint {
    display: block;
    font: italic 400 12.5px/1.4 'Fraunces', Georgia, serif;
    color: var(--kohsanuk-text-soft);
    text-align: right;
    margin-top: 4px;
}
/* Legacy wizard live-calc table used inside .dossier__amount on the wizard sidebar. */
.dossier__amount table.table { width: 100%; border-collapse: collapse; margin: 0; }
.dossier__amount table.table td {
    padding: 4px 0;
    border: none;
    font-size: 13px;
    color: var(--kohsanuk-text-soft);
}
.dossier__amount table.table td.text-right {
    text-align: right;
    color: var(--kohsanuk-text);
    font-weight: 500;
}
.dossier__amount table.table h4,
.dossier__amount table.table h5,
.dossier__amount table.table h6 { margin: 0; font-weight: 600; }
.dossier__amount table.table h4 {
    font: 600 22px/1 'Fraunces', Georgia, serif;
    color: var(--kohsanuk-action-dark);
}
.dossier__amount table.table .text-warning { color: var(--kohsanuk-action-dark) !important; }
.dossier__amount table.table .text-success { color: #1f6f3a !important; }

/* ─── DOSSIER VARIANT: collapsible (mobile payment-step order recap) ──────
   Reuses the whole .dossier contract; the head doubles as an expand/collapse
   toggle (a <button>), and everything below it lives in .dossier__collapse-region
   which hides until opened. While collapsed the head shows a running total
   (.dossier__head-total) so the customer still sees the headline figure.
   Consumer: booking/order-recap-mobile.blade.php. */
.dossier--collapsible { margin: 0 0 18px; }
.dossier--collapsible > .dossier__head {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 12px;
    text-align: left;
    cursor: pointer;
    border: 0;
    font: inherit;
    color: inherit;
}
.dossier--collapsible:not(.is-open) > .dossier__head { border-bottom: 0; }
.dossier--collapsible > .dossier__head:focus-visible {
    outline: 2px solid var(--kohsanuk-brand);
    outline-offset: -2px;
}
.dossier--collapsible .dossier__head-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.dossier--collapsible .dossier__title {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.dossier--collapsible .dossier__head-total {
    font: 600 17px/1 'Fraunces', Georgia, serif;
    color: var(--kohsanuk-action-dark);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.dossier--collapsible.is-open .dossier__head-total { display: none; }
.dossier--collapsible .dossier__chev {
    flex: 0 0 auto;
    color: var(--kohsanuk-text-muted);
    transition: transform .2s ease;
}
.dossier--collapsible.is-open .dossier__chev { transform: rotate(180deg); }
.dossier--collapsible:not(.is-open) .dossier__collapse-region { display: none; }

/* When a .dossier sits inside the legacy .sidebar-content wrapper (sidebar
   shells used on tour detail + booking wizard), drop the wrapper's white bg
   and padding so the dossier reads as a free-standing card. */
.sidebar.sidebar-opposite.sidebar-default:has(.dossier) { background: transparent !important; border: none !important; }
.sidebar-content:has(.dossier) { padding: 0 !important; }

/* Every dossier consumer (wizard sidebar, payment page, voucher, tour
   detail rail) now uses the single .dossier prefix — the legacy
   .trip-dossier migration bridge has been removed. */


/* ─── DOSSIER VARIANT: TOC (Table of Contents) ───────────────────────────
   Used by the tour detail page right rail. Body becomes a vertical link
   list rather than a row of label/value pairs.
   ──────────────────────────────────────────────────────────────────────── */
.dossier--toc .dossier__body {
    gap: 0;
    padding: 14px 18px;
}
.dossier__link {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 12px;
    border-radius: 10px;
    text-decoration: none;
    color: var(--kohsanuk-text-soft);
    font: 500 13.5px/1.3 'Public Sans', sans-serif;
    transition: background var(--ease), color var(--ease);
}
.dossier__link:hover {
    background: var(--kohsanuk-cream-100);
    color: var(--kohsanuk-brand-dark);
    text-decoration: none;
}
.dossier__link.is-active,
.dossier__link.tour-toc-link-active { /* tour-toc.js scrollspy state */
    background: var(--kohsanuk-brand-tint);
    color: var(--kohsanuk-brand-dark);
    font-weight: 600;
}
.dossier__link svg {
    width: 14px;
    height: 14px;
    color: var(--kohsanuk-brand);
    opacity: .5;
    flex-shrink: 0;
}
.dossier__link:hover svg,
.dossier__link.is-active svg,
.dossier__link.tour-toc-link-active svg { opacity: 1; }


/* ─── DOSSIER VARIANT: Book Now ──────────────────────────────────────────
   Used by the tour detail page right rail. Price + Chat (ghost) + Book Now
   (orange CTA) on a single dossier card.
   ──────────────────────────────────────────────────────────────────────── */
.dossier--book .dossier__price {
    padding: 18px 26px 0;
    display: flex;
    align-items: baseline;
    gap: 10px;
    flex-wrap: wrap;
}
.dossier--book .dossier__price-val {
    font: 600 28px/1 'Fraunces', Georgia, serif;
    color: var(--kohsanuk-brand-dark);
    font-variant-numeric: tabular-nums;
    letter-spacing: -.01em;
}
.dossier--book .dossier__price-sfx {
    font: 500 13px/1 'Public Sans', sans-serif;
    color: var(--kohsanuk-text-soft);
    margin-left: auto;
}
.dossier--book .dossier__buttons {
    padding: 18px 26px 22px;
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 10px;
    align-items: stretch;
}

.btn-dossier-ghost {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 13px 18px;
    border-radius: 12px;
    background: var(--kohsanuk-surface);
    border: 1px solid var(--kohsanuk-rule);
    color: var(--kohsanuk-brand-dark);
    font: 600 14px/1 'Public Sans', sans-serif;
    text-decoration: none;
    cursor: pointer;
    transition: background var(--ease), border-color var(--ease);
}
.btn-dossier-ghost:hover {
    border-color: var(--kohsanuk-brand);
    background: var(--kohsanuk-cream-100);
    color: var(--kohsanuk-brand-dark);
    text-decoration: none;
}
.btn-dossier-ghost i,
.btn-dossier-ghost svg { font-size: 16px; width: 16px; height: 16px; color: var(--kohsanuk-brand); flex-shrink: 0; }

.btn-dossier-cta {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 13px 22px;
    border-radius: 12px;
    background: var(--kohsanuk-action);
    color: #fff;
    border: none;
    cursor: pointer;
    font: 600 15px/1 'Public Sans', sans-serif;
    letter-spacing: .02em;
    text-decoration: none;
    box-shadow: 0 8px 18px var(--kohsanuk-action-shadow, rgba(253,126,20,.30));
    transition: background var(--ease), transform var(--ease), box-shadow var(--ease);
}
.btn-dossier-cta:hover {
    background: var(--kohsanuk-action-dark);
    transform: translateY(-1px);
    color: #fff;
    text-decoration: none;
}
.btn-dossier-cta i,
.btn-dossier-cta svg { font-size: 16px; width: 16px; height: 16px; color: #fff; flex-shrink: 0; }


/* ─── DOSSIER stacked-rail layout helper ─────────────────────────────────
   Wrap multiple stacked dossier cards in .dossier-rail for consistent
   spacing + a stagger-in animation. Used on the tour detail right rail.
   ──────────────────────────────────────────────────────────────────────── */
.dossier-rail {
    display: flex;
    flex-direction: column;
    gap: 16px;
}
.dossier-rail .dossier {
    opacity: 0;
    transform: translateY(8px);
    animation: dossier-rise .55s cubic-bezier(.2,.7,.2,1) forwards;
}
.dossier-rail .dossier:nth-child(1) { animation-delay: .04s; }
.dossier-rail .dossier:nth-child(2) { animation-delay: .12s; }
.dossier-rail .dossier:nth-child(3) { animation-delay: .20s; }
@keyframes dossier-rise {
    to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
    .dossier-rail .dossier { animation: none; opacity: 1; transform: none; }
}

@media (max-width: 768px) {
    .dossier { border-radius: 16px; }
    .dossier__head { padding: 18px 20px 14px; }
    .dossier__body { padding: 14px 20px 18px; }
    .dossier__amount { padding: 14px 20px 18px; }
    .dossier__amount__value { font-size: 24px; }
    .dossier--book .dossier__price { padding: 14px 20px 0; }
    .dossier--book .dossier__buttons { padding: 14px 20px 18px; grid-template-columns: 1fr; }
}


/* ─── DOSSIER VARIANT: Unified TOC + Summary tabs with shared footer ─────
   Used by the tour detail right rail to show ON THIS PAGE links and
   SUMMARY specs as switchable tabs inside ONE card, with a permanent
   Price + Book Now footer below.
   ──────────────────────────────────────────────────────────────────────── */
.dossier--unified .dossier__head--tabs {
    /* Override the default head: keep the cream-50 background, but lay
       the tab buttons out as a horizontal pair. Bottom rule comes from
       the parent rule already. */
    display: flex;
    gap: 6px;
    padding: 10px 12px;
}
.dossier__tab {
    flex: 1 1 0;
    appearance: none;
    background: transparent;
    border: 0;
    border-radius: 10px;
    padding: 10px 14px;
    font: 600 11px/1 'Public Sans', sans-serif;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--kohsanuk-text-soft);
    cursor: pointer;
    transition: color var(--ease), background var(--ease), box-shadow var(--ease);
}
.dossier__tab:hover { color: var(--kohsanuk-text); }
.dossier__tab.is-active {
    color: var(--kohsanuk-brand-dark);
    background: var(--kohsanuk-surface);
    box-shadow: 0 2px 6px rgba(15, 30, 45, .08);
}

.dossier__panes {
    background: var(--kohsanuk-surface);
}
.dossier__pane { display: none; }
.dossier__pane.is-active { display: flex; }   /* .dossier__body sets `flex-direction: column` */
/* The summary pane wraps one <dl class="dossier__body"> per content tab
   (child tours / packages). switchSummary() in page-details.js flips
   is-active between variants in sync with the active tab; these rules must
   come after .dossier__body so the display:none wins on inactive ones. */
.dossier__pane--summary.is-active { flex-direction: column; }
.dossier__summary-variant { display: none; }
.dossier__summary-variant.is-active { display: flex; }

.dossier__footer {
    border-top: 1px solid var(--kohsanuk-rule);
    background: var(--kohsanuk-cream-50);
    padding: 18px 26px 22px;
    display: flex;
    flex-direction: column;
    gap: 14px;
}
.dossier__footer-eyebrow {
    font: 600 10px/1 'Public Sans', sans-serif;
    letter-spacing: .24em;
    text-transform: uppercase;
    color: var(--kohsanuk-brand-dark);
    margin: 0 0 4px;
}
/* Reuse the existing .dossier--book price + buttons styling inside the
   unified card's footer. Scope rules so the spacing fits the smaller
   footer container (no extra horizontal padding — the footer owns it). */
.dossier--unified .dossier__footer .dossier__price {
    padding: 0;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
}
.dossier--unified .dossier__footer .dossier__price-val {
    /* 22px so the typical "25,000 - 70,000 THB" range fits a single line
       inside the 360px sidebar without wrapping the currency suffix. */
    font: 600 22px/1.1 'Fraunces', Georgia, serif;
    color: var(--kohsanuk-brand-dark);
    font-variant-numeric: tabular-nums;
    letter-spacing: -.01em;
    white-space: nowrap;
}
/* Very narrow rails — allow wrapping so we don't overflow. */
@media (max-width: 380px) {
    .dossier--unified .dossier__footer .dossier__price-val {
        font-size: 20px;
        white-space: normal;
    }
}
/* Group/private split price (tours offering both types). One
   .dossier__price-variant per tab variant is pre-rendered in the footer;
   switchSummary() (page-details.js) flips is-active — only the active
   variant that actually has a price is shown. */
.dossier--unified .dossier__footer .dossier__price-variant { display: none; }
.dossier--unified .dossier__footer .dossier__price-variant.is-active.has-price { display: flex; }
.dossier__price-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 10px;
    width: 100%;
}
.dossier__price-row-label {
    font: 500 13px/1.4 'Maven Pro', system-ui, sans-serif;
    color: var(--kohsanuk-text-soft);
    white-space: nowrap;
}
.dossier--unified .dossier__footer .dossier__price-val--split {
    /* Smaller than the single-line price (two amounts must share the rail)
       and in Space Grotesk — the face used for ALL split price amounts
       (rail, cards, sticky CTA). */
    font: 600 16px/1.3 'Space Grotesk', 'Maven Pro', system-ui, sans-serif;
    letter-spacing: 0;
}
.dossier__price-val--split small {
    font: 400 11.5px/1 'Maven Pro', system-ui, sans-serif;
    color: var(--kohsanuk-text-muted);
    margin-left: 2px;
}

/* Group/private split price on the Vue index cards (tours/yachts listings,
   legacy Bootstrap panels) — same two-line pattern as the Blade tour-card. */
/* Equal-height footer: the grid is float-based (col-md-3/4 inside .row),
   so rows only wrap cleanly while every card is the same height. Reserve
   the full two-line split height on ALL grid cards, split or single.
   61px = two .card-price-split-row lines (~41px, 13.5px bold at 1.5) plus
   the pt-10/pb-10 utility padding — border-box, so min-height must
   include the padding. Single-line cards center inside the same box. */
.card-price-area {
    min-height: 61px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}
.card-price-area > .list-inline {
    float: none; /* pull-right is a float — inert on flex items; flex handles alignment */
    margin-bottom: 0;
}
.card-price-split {
    display: inline-flex;
    flex-direction: column;
    line-height: 1.5;
    vertical-align: middle;
}
.card-price-split-row {
    font-size: 12px;
    color: var(--kohsanuk-text-soft);
    white-space: nowrap;
}
.card-price-split-row b {
    font-family: 'Space Grotesk', system-ui, sans-serif;
    font-size: 13.5px;
    font-weight: 700;
    color: var(--kohsanuk-brand-dark); /* tour-page price colour — one identity everywhere */
}
.card-price-split-row small {
    font-size: 10.5px;
    color: var(--kohsanuk-text-muted);
}
.dossier--unified .dossier__footer .dossier__buttons {
    padding: 0;
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 10px;
    align-items: stretch;
}

/* ─── Spacing above the rail so it doesn't kiss the trust-signals strip
   that sits right above. The legacy .sidebar-content / .sidebar-fixed
   structure gave us no breathing room. */
.dossier-rail--unified {
    margin-top: 24px;
}

@media (max-width: 768px) {
    .dossier--unified .dossier__head--tabs { padding: 8px 10px; }
    .dossier__tab { padding: 9px 10px; letter-spacing: .14em; }
    .dossier__footer { padding: 14px 20px 18px; }
    .dossier--unified .dossier__footer .dossier__buttons { grid-template-columns: 1fr; }
}

/* ═══ Unified site navbar (parts/site-navbar.blade.php) ═══════════════════
   One navbar for every page family — public pages, booking flow and the
   admin console. Framework-free (no Bootstrap 3/4 dependency) so the same
   include works on both legacy stacks. All colours come from the
   --kohsanuk-* tokens, so each site theme retints it automatically.

   --ks-nav-h is declared at :root because page chrome OUTSIDE the navbar
   (layouts/app content padding, tour-page sticky sidebar offsets) aligns
   to the bar height too. */
:root { --ks-nav-h: 64px; }
@media (max-width: 991.98px) { :root { --ks-nav-h: 56px; } }

.ks-nav {
    position: fixed; top: 0; left: 0; right: 0; z-index: 1030;
    height: var(--ks-nav-h);
    background: var(--kohsanuk-surface);
    box-shadow: 0 1px 0 var(--kohsanuk-rule), 0 4px 16px rgba(18, 41, 57, 0.05);
    transition: background .25s ease, box-shadow .25s ease;
    font-family: var(--kohsanuk-font-sans);
}
.ks-nav *, .ks-nav *::before, .ks-nav *::after { box-sizing: border-box; }
.ks-nav [hidden] { display: none !important; }
.ks-nav__inner {
    max-width: 1400px; margin: 0 auto; height: 100%;
    display: flex; align-items: center; gap: 8px;
    padding: 0 24px;
}

/* ─── Wordmark ── */
.ks-nav__brand {
    display: flex; flex-direction: column; justify-content: center;
    text-decoration: none; margin-right: 28px; line-height: 1.05;
}
.ks-nav__brand:hover, .ks-nav__brand:focus { text-decoration: none; }
.ks-nav__brand-name {
    font-family: var(--kohsanuk-font-serif);
    font-size: 23px; font-weight: 700; letter-spacing: .01em;
    color: var(--kohsanuk-text); white-space: nowrap;
}
.ks-nav__brand-name em { font-style: normal; color: var(--kohsanuk-brand); }
.ks-nav__brand-tag {
    font-size: 9.5px; font-weight: 600; letter-spacing: .24em;
    text-transform: uppercase; color: var(--kohsanuk-text-muted);
    margin-top: 3px; white-space: nowrap;
}

/* ─── Primary links (desktop) ── */
.ks-nav__links { display: flex; align-items: center; gap: 2px; height: 100%; }
.ks-nav__item { position: relative; height: 100%; display: flex; align-items: center; }
.ks-nav__utils .ks-nav__item { height: auto; }
.ks-nav__link {
    position: relative; display: inline-flex; align-items: center; gap: 5px;
    height: 100%; padding: 0 14px;
    font-size: 12.5px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase;
    color: var(--kohsanuk-text-soft); text-decoration: none;
    transition: color .15s ease;
    background: none; border: 0; cursor: pointer; font-family: inherit;
}
.ks-nav__link::after {
    content: ""; position: absolute; left: 14px; right: 14px; bottom: 14px;
    height: 2px; background: var(--kohsanuk-brand);
    transform: scaleX(0); transform-origin: left; transition: transform .2s ease;
}
.ks-nav__link:hover, .ks-nav__link:focus,
.ks-nav__link[aria-expanded="true"], .ks-nav__link.is-active {
    color: var(--kohsanuk-brand); text-decoration: none;
}
.ks-nav__link:hover::after, .ks-nav__link.is-active::after { transform: scaleX(1); }
.ks-nav__caret {
    width: 8px; height: 8px; flex: none;
    border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
    transform: rotate(45deg) translateY(-2px); transition: transform .2s ease;
}
.ks-nav__link[aria-expanded="true"] .ks-nav__caret,
.ks-nav__pill[aria-expanded="true"] .ks-nav__caret { transform: rotate(225deg) translateY(-1px); }

/* ─── Utility cluster (desktop right side) ── */
.ks-nav__utils { display: flex; align-items: center; gap: 2px; margin-left: auto; }
/* extra breathing room between the currency and language pills only — a
   blanket container gap here would double up with .ks-nav__divider's own
   margin on every divider-adjacent pair, blowing out those gaps instead */
.ks-nav__utils .ks-nav__item + .ks-nav__item { margin-left: 8px; }
.ks-nav__divider { width: 1px; height: 22px; background: var(--kohsanuk-rule); margin: 0 10px; flex: none; }
.ks-nav__icon-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 38px; height: 38px; border-radius: 50%; flex: none;
    color: var(--kohsanuk-text-soft); text-decoration: none;
    font-size: 16px; line-height: 1;
    transition: background .15s ease, color .15s ease;
    background: none; border: 0; cursor: pointer; padding: 0;
}
.ks-nav__icon-btn:hover, .ks-nav__icon-btn:focus {
    background: var(--kohsanuk-brand-tint); color: var(--kohsanuk-brand); text-decoration: none;
}
.ks-nav__icon-btn--wa { color: #25D366; }
.ks-nav__icon-btn--wa:hover, .ks-nav__icon-btn--wa:focus { background: rgba(37, 211, 102, .12); color: #25D366; }
.ks-nav__pill {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 8px 12px; border-radius: var(--kohsanuk-radius-pill);
    font-size: 12.5px; font-weight: 600; letter-spacing: .04em;
    color: var(--kohsanuk-text-soft);
    background: none; border: 0; cursor: pointer; font-family: inherit;
    transition: background .15s ease, color .15s ease;
}
.ks-nav__pill:hover, .ks-nav__pill:focus, .ks-nav__pill[aria-expanded="true"] {
    background: var(--kohsanuk-brand-tint); color: var(--kohsanuk-brand);
}
.ks-nav__avatar {
    width: 30px; height: 30px; border-radius: 50%; flex: none;
    background: var(--kohsanuk-brand); color: #fff; object-fit: cover;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 13px; font-weight: 700; font-style: normal;
}
.ks-nav__avatar--sm { width: 28px; height: 28px; font-size: 12px; }
.ks-nav__avatar--lg { width: 38px; height: 38px; font-size: 15px; }
.ks-nav__login {
    display: inline-flex; align-items: center; gap: 7px;
    padding: 9px 18px; border-radius: var(--kohsanuk-radius-pill);
    font-size: 12.5px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
    color: var(--kohsanuk-brand); text-decoration: none; white-space: nowrap;
    border: 1.5px solid var(--kohsanuk-brand-soft);
    transition: background .15s ease, color .15s ease, border-color .15s ease;
}
.ks-nav__login:hover, .ks-nav__login:focus {
    background: var(--kohsanuk-brand); color: #fff; border-color: var(--kohsanuk-brand);
    text-decoration: none;
}

/* ─── Dropdown cards ── */
.ks-dd {
    position: absolute; top: calc(100% - 8px); left: 0; min-width: 230px;
    background: var(--kohsanuk-surface); border-radius: var(--kohsanuk-radius-xl);
    box-shadow: var(--kohsanuk-shadow-popup);
    border: 1px solid var(--kohsanuk-border-subtle);
    padding: 8px; z-index: 1031;
    opacity: 0; visibility: hidden; transform: translateY(6px);
    transition: opacity .18s ease, transform .18s ease, visibility .18s;
}
.ks-nav__utils .ks-dd { top: calc(100% + 10px); }
.ks-dd--right { left: auto; right: 0; }
.ks-dd--scroll { max-height: 380px; overflow-y: auto; min-width: 260px; }
/* A ~30-currency list inside an 8px-padded, 380px-max-height box gives no
   visual signal that it's scrollable — this pinned, faded hint (sticky
   inside the scroll container, so it rides along at the bottom) makes the
   "more content below" affordance explicit instead of relying on the
   thin native scrollbar alone. Rendered as a real (translated, via
   trans('translate-tours.currency-dropdown-scroll-hint')) element in the
   Blade markup rather than CSS `content`, so it's localized like every
   other navbar string instead of being permanently English. */
.ks-dd__scroll-hint {
    position: sticky; bottom: -8px; left: 0; right: 0;
    margin-top: 4px; padding: 12px 0 8px;
    background: linear-gradient(to top, var(--kohsanuk-surface) 55%, transparent);
    text-align: center; font-size: 10px; font-weight: 700;
    letter-spacing: .1em; text-transform: uppercase;
    color: var(--kohsanuk-text-muted); pointer-events: none;
}
.ks-dd.is-open { opacity: 1; visibility: visible; transform: translateY(0); }
.ks-dd__label {
    padding: 8px 12px 6px;
    font-size: 10px; font-weight: 700; letter-spacing: .18em; text-transform: uppercase;
    color: var(--kohsanuk-text-muted);
}
.ks-dd__item {
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    padding: 9px 12px; border-radius: var(--kohsanuk-radius-lg);
    font-size: 14px; color: var(--kohsanuk-text); text-decoration: none;
    transition: background .12s ease, color .12s ease;
}
.ks-dd__item:hover, .ks-dd__item:focus { background: var(--kohsanuk-brand); color: #fff; text-decoration: none; }
.ks-dd__item:hover .ks-nav-muted, .ks-dd__item:focus .ks-nav-muted { color: rgba(255, 255, 255, .75); }
.ks-dd__item.is-current { color: var(--kohsanuk-brand); font-weight: 600; }
.ks-dd__item.is-current::after { content: "\2713"; }
.ks-dd__item.is-current:hover, .ks-dd__item.is-current:focus {
    background: var(--kohsanuk-brand-tint); color: var(--kohsanuk-brand);
}
.ks-nav-muted { color: var(--kohsanuk-text-muted); font-size: 12.5px; }

/* ─── Flags (positions; sprite URL set inline in the blade) ── */
.ks-flag {
    display: inline-block; width: 16px; height: 11px; flex: none;
    background-repeat: no-repeat; margin-right: 7px; vertical-align: -1px;
}
.ks-flag--cn, .ks-flag--zh-CN { background-position: 0 0; }
.ks-flag--de { background-position: -16px 0; }
.ks-flag--en { background-position: -32px 0; }
.ks-flag--fr { background-position: -48px 0; }
.ks-flag--ru { background-position: -64px 0; }
.ks-flag--th { background-position: -80px 0; }
.ks-flag--ja { background-position: -96px 0; }
.ks-flag--ko { background-position: -112px 0; }

/* ─── Hero (homepage) — transparent until scrolled, desktop only ── */
@media (min-width: 992px) {
    .ks-nav--hero.is-clear {
        background: linear-gradient(rgba(10, 30, 42, .45), rgba(10, 30, 42, 0));
        box-shadow: none;
    }
    .ks-nav--hero.is-clear .ks-nav__brand-name,
    .ks-nav--hero.is-clear .ks-nav__brand-name em { color: #fff; }
    .ks-nav--hero.is-clear .ks-nav__brand-tag { color: rgba(255, 255, 255, .75); }
    .ks-nav--hero.is-clear .ks-nav__link { color: rgba(255, 255, 255, .92); }
    .ks-nav--hero.is-clear .ks-nav__link::after { background: #fff; }
    .ks-nav--hero.is-clear .ks-nav__link:hover,
    .ks-nav--hero.is-clear .ks-nav__link[aria-expanded="true"],
    .ks-nav--hero.is-clear .ks-nav__link.is-active { color: #fff; }
    .ks-nav--hero.is-clear .ks-nav__pill { color: rgba(255, 255, 255, .92); }
    .ks-nav--hero.is-clear .ks-nav__pill:hover,
    .ks-nav--hero.is-clear .ks-nav__pill[aria-expanded="true"] { background: rgba(255, 255, 255, .14); color: #fff; }
    .ks-nav--hero.is-clear .ks-nav__icon-btn { color: rgba(255, 255, 255, .92); }
    .ks-nav--hero.is-clear .ks-nav__icon-btn:hover { background: rgba(255, 255, 255, .14); color: #fff; }
    .ks-nav--hero.is-clear .ks-nav__icon-btn--wa { color: #fff; }
    .ks-nav--hero.is-clear .ks-nav__divider { background: rgba(255, 255, 255, .3); }
    .ks-nav--hero.is-clear .ks-nav__login { color: #fff; border-color: rgba(255, 255, 255, .55); }
    .ks-nav--hero.is-clear .ks-nav__login:hover { background: #fff; color: var(--kohsanuk-text); border-color: #fff; }
}

/* ─── Console (admin) — dark variant ── */
.ks-nav--console { background: #263238; box-shadow: 0 1px 0 rgba(255, 255, 255, .08); }
.ks-nav--console .ks-nav__brand-name,
.ks-nav--console .ks-nav__brand-name em { color: #fff; }
.ks-nav--console .ks-nav__brand-tag { color: rgba(255, 255, 255, .6); }
.ks-nav--console .ks-nav__pill { color: rgba(255, 255, 255, .85); }
.ks-nav--console .ks-nav__pill:hover,
.ks-nav--console .ks-nav__pill[aria-expanded="true"] { background: rgba(255, 255, 255, .12); color: #fff; }
.ks-nav--console .ks-nav__divider { background: rgba(255, 255, 255, .2); }
.ks-nav--console .ks-nav__burger { color: #fff; }

/* ─── Mobile bar ── */
.ks-nav__burger { display: none; }
.ks-nav__m-utils { display: none; }
@media (max-width: 991.98px) {
    .ks-nav__inner { padding: 0 6px 0 4px; gap: 2px; }
    .ks-nav__links, .ks-nav__utils { display: none; }
    .ks-nav__brand { margin-right: 0; }
    .ks-nav__brand-name { font-size: 20px; }
    .ks-nav__brand-tag { display: none; }
    .ks-nav__burger {
        display: inline-flex; align-items: center; justify-content: center;
        width: 48px; height: 48px; flex: none;
        background: none; border: 0; cursor: pointer; padding: 0;
        color: var(--kohsanuk-text); font-size: 20px;
    }
    /* gap (unlike the desktop .ks-nav__utils, which sets gap: 2px) gives
       the pills/icons in this row real visual separation instead of
       sitting flush against each other. */
    .ks-nav__m-utils { display: flex; align-items: center; margin-left: auto; gap: 10px; }
    /* 38px matches the desktop .ks-nav__icon-btn baseline (components.css
       .ks-nav__icon-btn) rather than shrinking below it — this is the
       account/login entry point on a touch-only surface, so it keeps the
       desktop's own click-target size even though other elements in this
       row give up more space to fit 320px. */
    .ks-nav__m-utils .ks-nav__icon-btn { width: 38px; height: 38px; font-size: 15px; }
    /* The quick-search icon is optional (most pages hide it; some — about,
       categories, explore, subindex — unhide it via JS when #fs-toggle is
       present) and is the lowest-priority control in this row, so it alone
       gets a smaller floor size when the 320px budget has to absorb a 5th
       element it wasn't otherwise sized for. 24px is the WCAG 2.5.8 (AA)
       minimum target size. */
    .ks-nav__m-utils [data-ks-search] { width: 24px; height: 24px; font-size: 13px; }
    .ks-nav__m-utils .ks-nav__item { height: auto; }
    .ks-nav__m-utils .ks-nav__pill { padding: 5px 2px; font-size: 11.5px; }
    /* Mobile dropdowns break out of the narrow trigger pill's own box
       (the default .ks-dd--right anchor) and instead position relative to
       the viewport: the currency/language pills are not the rightmost
       element in this row like their desktop counterparts are, so a
       .ks-dd--scroll panel (min-width 260px) anchored to a ~50-70px-wide
       pill overflowed off the left edge of the screen on real phone
       widths. Fixed positioning with symmetric left/right insets is also
       robust to which pill triggered it and to future width changes in
       either pill. */
    .ks-nav__m-utils .ks-dd {
        position: fixed; left: 12px; right: 12px; width: auto; min-width: 0;
        top: calc(var(--ks-nav-h) + 8px);
    }
}

/* Narrow phones: the mobile bar's fixed-width content (hamburger + both
   utility pills + the avatar/login icon) leaves very little slack once the
   wordmark is added back in — trim the wordmark itself a bit further so
   nothing overflows past the viewport edge at 320px. Matches this file's
   existing 380px narrow-phone breakpoint convention used elsewhere. */
@media (max-width: 380px) {
    .ks-nav__brand-name { font-size: 14px; }
}

/* Wordmark: binary show/hide, never a clipped/ellipsis fragment — below
   this width "Koh Sanuk" + both utility pills + the avatar icon would
   start crowding the bar, so the whole wordmark drops rather than
   truncating mid-word. In practice this rarely if ever triggers on real
   devices (320px phones still fit comfortably); it's a safety net for
   oversized accessibility text settings / in-app browser chrome eating
   viewport width. */
@media (max-width: 300px) {
    .ks-nav__brand { display: none; }
}

/* ─── Mobile drawer ── */
.ks-scrim {
    position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1040;
    background: rgba(13, 32, 44, .5);
    opacity: 0; visibility: hidden; transition: opacity .25s ease, visibility .25s;
}
.ks-scrim.is-open { opacity: 1; visibility: visible; }
.ks-drawer {
    position: fixed; top: 0; bottom: 0; left: 0; z-index: 1050;
    width: min(86vw, 340px);
    background: var(--kohsanuk-surface);
    display: flex; flex-direction: column;
    transform: translateX(-102%);
    transition: transform .28s cubic-bezier(.32, .72, .28, 1);
    box-shadow: 12px 0 40px rgba(13, 32, 44, .18);
    font-family: var(--kohsanuk-font-sans);
}
.ks-drawer *, .ks-drawer *::before, .ks-drawer *::after { box-sizing: border-box; }
.ks-drawer.is-open { transform: translateX(0); }
body.ks-drawer-locked { overflow: hidden; }
.ks-drawer__head {
    display: flex; align-items: center; justify-content: space-between;
    padding: 8px 8px 8px 20px; flex: none;
    border-bottom: 1px solid var(--kohsanuk-border-subtle);
}
.ks-nav__brand-name--drawer { font-size: 19px; }
.ks-drawer__close {
    width: 44px; height: 44px; background: none; border: 0; cursor: pointer;
    color: var(--kohsanuk-text-soft); font-size: 24px; line-height: 1; padding: 0;
}
.ks-drawer__body { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; padding-bottom: 12px; }

.ks-drawer__user {
    display: flex; align-items: center; gap: 12px;
    width: 100%; text-align: left; font-family: inherit; cursor: pointer;
    padding: 14px 20px; background: var(--kohsanuk-cream-50);
    border: 0; border-bottom: 1px solid var(--kohsanuk-border-subtle);
}
.ks-drawer__user-text { flex: 1; min-width: 0; }
.ks-drawer__user-name { display: block; font-weight: 700; font-size: 15px; color: var(--kohsanuk-text); }
.ks-drawer__user-hint { display: block; font-size: 12px; color: var(--kohsanuk-text-muted); margin-top: 1px; }

.ks-drawer__section-label {
    padding: 18px 20px 6px;
    font-size: 10.5px; font-weight: 700; letter-spacing: .2em; text-transform: uppercase;
    color: var(--kohsanuk-text-muted);
}
.ks-drawer__item {
    display: flex; align-items: center; gap: 13px;
    min-height: 48px; padding: 0 20px;
    font-size: 15px; color: var(--kohsanuk-text); text-decoration: none;
    background: none; border: 0; width: 100%; cursor: pointer;
    font-family: inherit; text-align: left;
}
.ks-drawer__item:hover, .ks-drawer__item:focus { color: var(--kohsanuk-text); text-decoration: none; }
.ks-drawer__item:active { background: var(--kohsanuk-brand-tint); }
.ks-drawer__item.is-active {
    color: var(--kohsanuk-brand); font-weight: 600;
    box-shadow: inset 3px 0 0 var(--kohsanuk-brand);
}
.ks-drawer__ico { width: 20px; text-align: center; color: var(--kohsanuk-text-muted); font-size: 15px; flex: none; }
.ks-drawer__chev {
    margin-left: auto; width: 8px; height: 8px; flex: none;
    border-right: 1.5px solid var(--kohsanuk-text-muted); border-bottom: 1.5px solid var(--kohsanuk-text-muted);
    transform: rotate(45deg); transition: transform .2s ease;
}
[aria-expanded="true"] > .ks-drawer__chev,
[aria-expanded="true"] > .ks-drawer__value > .ks-drawer__chev { transform: rotate(225deg); }
.ks-drawer__value {
    margin-left: auto; font-size: 13.5px; font-weight: 600; color: var(--kohsanuk-brand);
    display: flex; align-items: center; gap: 8px;
}
.ks-acc { max-height: 0; overflow: hidden; transition: max-height .25s ease; background: var(--kohsanuk-cream-50); }
.ks-acc.is-open { max-height: 480px; }
.ks-acc--scroll.is-open { overflow-y: auto; }
.ks-acc .ks-drawer__item {
    padding-left: 53px; font-size: 14.5px; min-height: 44px;
    color: var(--kohsanuk-text-soft); justify-content: space-between;
}
.ks-acc .ks-drawer__item.is-current { color: var(--kohsanuk-brand); font-weight: 600; }
.ks-acc .ks-drawer__item.is-current::after { content: "\2713"; }
.ks-acc .ks-drawer__sub-label {
    padding: 10px 20px 4px 53px;
    font-size: 10px; font-weight: 700; letter-spacing: .18em; text-transform: uppercase;
    color: var(--kohsanuk-text-muted);
}
.ks-drawer__rule { height: 1px; background: var(--kohsanuk-border-subtle); margin: 10px 0 0; }
.ks-drawer__foot {
    padding: 14px 20px calc(16px + env(safe-area-inset-bottom)); flex: none;
    border-top: 1px solid var(--kohsanuk-border-subtle);
}
.ks-drawer__wa {
    display: flex; align-items: center; justify-content: center; gap: 10px;
    height: 48px; border-radius: var(--kohsanuk-radius-pill);
    background: #25D366; color: #fff; text-decoration: none;
    font-size: 15px; font-weight: 700;
}
.ks-drawer__wa:hover, .ks-drawer__wa:focus { color: #fff; text-decoration: none; background: #1fb959; }
.ks-drawer__wa .fa { font-size: 18px; }
.ks-drawer__login-cta {
    display: flex; align-items: center; justify-content: center; gap: 10px;
    height: 48px; border-radius: var(--kohsanuk-radius-pill); margin: 16px 20px 4px;
    background: var(--kohsanuk-brand); color: #fff; text-decoration: none;
    font-size: 15px; font-weight: 700;
}
.ks-drawer__login-cta:hover, .ks-drawer__login-cta:focus {
    color: #fff; text-decoration: none; background: var(--kohsanuk-brand-dark);
}

/* ─── iOS auto-zoom guard (mobile) ────────────────────────────────────
   iOS Safari zooms the whole page when a focused field's font-size is under
   16px, which on small screens shifts the layout and hides sticky controls.
   Across the site nearly every input is set below 16px by a class-level rule
   (.form-control 15px, .cp-contact__input 14px, etc.), so this raises text
   inputs / textareas / selects to a 16px floor at phone widths.

   The `:not(#ai-search-input)` lifts each selector's specificity to (1,0,1)
   so it beats those class-level rules (0,1,0) that a bare `input` selector
   could not — WITHOUT !important, so inline-styled fields still win and
   nothing intentionally larger is shrunk. It also exempts the oversized hero
   search (#ai-search-input — 1.8rem in the quick-search widget); the
   homepage's own #ai-search-input is raised in web-home.blade.php. */
@media (max-width: 767.98px) {
    input:not(#ai-search-input),
    textarea:not(#ai-search-input),
    select:not(#ai-search-input) {
        font-size: 16px;
    }
}

/* ═══════════════════════════════════════════════════════════════════════
   ADVANCED SEARCH — ONE control set, ONE engine, THREE surfaces.
   Ported from the approved mock theme-verify/mocks/hero-search-B.html
   (its <style id="ks-core"> block).

     surface 1  HOME (/)          .ks-hero-search             — liquid glass over the hero photo
     surface 2  /search           .ks-refine + .ks-solid      — inline refine bar, opaque
     surface 3  every other page  .ks-pop    + .ks-solid      — the popup, opaque

   The popup is the ONLY surface that is a popup. Home and /search render the
   form INLINE and must suppress layouts/web-app.blade.php's automatic dialog
   mount, or the page carries two search UIs.

   PALETTE — theme tokens, not the mock's literals. The mock was a standalone
   file with no access to the theme layer, so it inlined classic.css's values
   (--brand #0097a7 / --brand-dark #006d7a / --action #fd7e14). Binding the
   tokens is pixel-identical to the sign-off under classic (the :root baseline)
   AND lets the form re-theme with the other five palettes.

   TWO EXCEPTIONS, both deliberate:
     · GLASS whites stay literal rgba(255,255,255,…). They are specular light on
       a photograph, not a themed surface — they must NOT follow a theme that
       darkens --kohsanuk-surface, or the glass stops reading as glass.
     · ALPHA teals stay literal rgba(0,151,167,…) / rgba(0,109,122,…). The
       obvious tokens --kohsanuk-brand-soft / -tint are declared ONLY in
       themes/classic.css; tokens.css falls them through to
       --kohsanuk-accent-soft / -tint, which is CORAL under midnight-lagoon and
       driftwood. Consuming them would turn this form's teal gauge orange under
       two themes. Fixing that means editing those two theme files in BOTH
       mirrors — out of scope here, so the literals stay and this comment is the
       flag. Do NOT "tidy" them into brand-soft/-tint without doing that first.

   DATA-URI STROKES stay literal too: a data: URI cannot read a CSS custom
   property. White on glass, --kohsanuk-text-soft's #506880 on the opaque skins.
   ═══════════════════════════════════════════════════════════════════════ */

/* ─── Component custom properties ──────────────────────────────────────
   Declared at :root, not on .ks-hero-search, because .ks-menu--glass is
   appended to <body> by select2 and therefore inherits from NOTHING inside
   the panel. Same reason --ks-nav-h is at :root above. */
:root {
    /* ─── INK ON GLASS ──────────────────────────────────────────────────
       THREE surfaces in this system are white glass carrying DARK content
       rather than white content: the price gauge, a FOCUSED (inverted) field,
       and the open dropdown panel. They share one ink, declared once here, so
       the reading is identical across all three and a future correction lands
       in one place instead of three.

       "Glass" is a property of the SURFACE; it does not oblige the content to
       invert. Measured over the hero photo blurred to match the panel's own
       backdrop-filter, white text reads 1.6–2.4:1 on these fills and ink reads
       6.2–15:1 — which is the whole reason the inversion is legal.

       KNOWN AND PRE-EXISTING: these are THEME tokens painted onto a LITERAL
       white fill (see the palette note in this section's header for why the
       fill has to be literal). Under midnight-lagoon --kohsanuk-text is
       #e8eded, so all three go light-on-white. That hazard shipped with
       --ks-price-fg; routing every consumer through one token is what turns
       the eventual fix into a one-line change rather than a hunt. */
    --ks-g-ink:             var(--kohsanuk-text);
    --ks-g-ink-soft:        var(--kohsanuk-text-soft);
    --ks-g-ink-muted:       var(--kohsanuk-text-muted);

    /* GLASS PANEL — production's .hero-filters, unchanged: flat white glass. */
    --ks-g-panel-bg:        rgba(255, 255, 255, 0.15);
    --ks-g-panel-bg-mob:    rgba(255, 255, 255, 0.18);
    --ks-g-panel-blur:      blur(14px);
    --ks-g-panel-radius:    var(--kohsanuk-radius-xl);
    --ks-g-row-bg:          rgba(255, 255, 255, 0.30);
    /* the three declarations that make it read as GLASS rather than as a
       translucent rectangle: a lit upper lip, a defined edge, and lift */
    --ks-g-panel-edge:      rgba(255, 255, 255, 0.30);
    --ks-g-panel-lip:       inset 0 1px 0 rgba(255, 255, 255, 0.45);
    --ks-g-panel-lift:      0 20px 46px -22px rgba(6, 30, 44, 0.55);

    --ks-g-field-bg:        rgba(255, 255, 255, 0.16);
    --ks-g-field-bg-hover:  rgba(255, 255, 255, 0.24);
    --ks-g-field-border:    rgba(255, 255, 255, 0.45);
    --ks-g-field-fg:        #ffffff;
    --ks-g-field-ph:        rgba(255, 255, 255, 0.75);

    /* ─── FOCUS INVERT ──────────────────────────────────────────────────
       RECOVERED FROM PRODUCTION, and widened. origin/master's
       web-home.blade.php carries this inside an @media (max-width: 767.98px)
       block, scoped to `#ai-search-input:focus` alone (added by PR #892): idle,
       the field is transparent glass with white text; focused — i.e. while you
       are typing — it flips to a solid white field with dark text so what you
       type is crisply legible over the bright hero water.

       Widened on two axes, both deliberate:
         · EVERY GLASS CONTROL, not one id. #ai-search-input no longer exists,
           and the problem it solved was never that input's alone — the number
           input and the select2 controls sit on the same photograph and had the
           same failure. Applied here against the ks-* classes.
         · EVERY WIDTH, not just the phone. A control that changes material
           below 768px and not above it is two designs; the hero water is just
           as bright on a desktop.

       This is the field FILL inverting. It COMPOSES with --ks-g-focus-ring (the
       white glow), which is a different signal — "this control has focus"
       rather than "you can read what you type here" — and is unchanged.

       NO !important anywhere. Production needed it only to beat the inline
       style="background-color:…" on that one <input>; this form carries no
       inline field styling, so ordinary specificity does the whole job. */
    --ks-g-field-focus-bg:  rgba(255, 255, 255, 0.92);
    --ks-g-field-focus-fg:  var(--ks-g-ink);
    --ks-g-field-focus-ph:  var(--ks-g-ink-soft);

    --ks-g-label-fg:        #ffffff;
    --ks-g-label-shadow:    0 1px 2px rgba(0, 0, 0, 0.30);

    /* WHITE GLOW ONLY. This is production's own hero focus, unchanged
       (origin/master web-home.blade.php #ai-search-input:focus), widened 2 -> 3px
       so it still carries against the near-white sand at the foot of the photo.
       An earlier revision added a dark inner hairline for that legibility; it
       bought it at the cost of a black band around a glass control, which is not
       the house style. No dark band. */
    --ks-g-focus-ring:      0 0 0 3px rgba(255, 255, 255, 0.72);
    --ks-g-hairline:        rgba(255, 255, 255, 0.22);

    /* PRICE GAUGE, GLASS. .42 not .90: at .90 it read as an opaque chip sitting
       ON the panel — a different material. At .42 the photo transmits through it
       and it frosts via the panel above rather than a filter of its own.
       Text stays INK and does not invert: measured over the four hero luminances
       white reads 1.59–2.39:1 here, ink reads 7.8–10.5:1. "Glass" is a property
       of the surface; it does not oblige the content to invert. */
    --ks-price-bg:          rgba(255, 255, 255, 0.42);
    --ks-price-edge:        rgba(255, 255, 255, 0.55);
    --ks-price-fg:          var(--ks-g-ink);
    --ks-price-muted:       rgba(0, 109, 122, 0.85);
    --ks-price-lip:         inset 0 1px 0 rgba(255, 255, 255, 0.62);
    --ks-price-lift:        0 10px 22px -12px rgba(6, 30, 44, 0.42);

    /* rail deepened from .14, where the white handle measured 1.40:1 against it
       — a 1.4.11 failure on an interactive control */
    --ks-price-track:       rgba(0, 151, 167, 0.32);
    --ks-price-fill:        var(--kohsanuk-action);
    --ks-price-fill-lip:    inset 0 1px 0 rgba(255, 255, 255, 0.45);
    --ks-price-thumb:       #ffffff;
    /* the RING, not the fill, delineates the handle */
    --ks-price-thumb-ring:  0 0 0 2px var(--kohsanuk-brand), 0 2px 7px rgba(0, 109, 122, 0.32);
    /* an orange focus ring is luminance-degenerate on glass (1.13–1.36:1) —
       replaced by a dual ring that reads against ITSELF on any backdrop */
    --ks-price-thumb-focus: 0 0 0 2px rgba(5, 27, 41, 0.68), 0 0 0 5px rgba(255, 255, 255, 0.78), 0 3px 10px rgba(6, 30, 44, 0.45);

    /* HISTOGRAM — LUMINANCE-coded, not hue-coded. Teal-in-range against
       faint-teal-out-of-range measures ~3.6:1 and survives every backdrop.
       ORANGE stays reserved for the selected span on the rail directly below,
       where it is unambiguous. NO GREY anywhere. */
    --ks-price-bar:         rgba(0, 151, 167, 0.26);
    --ks-price-bar-lit:     var(--kohsanuk-brand);
    --ks-price-hist-base:   rgba(0, 151, 167, 0.24);

    /* OPEN DROPDOWN PANEL — WHITE glass: the same material as the form the menu
       belongs to, and the same material as the price gauge one band lower.

       SUPERSEDES the mock's ink glass (rgba(10,34,50,.78)), which shipped in
       #1053 and which the user rejected on sight: over the bright hero it reads
       as a near-black slab dropped onto a white-glass form, and the control
       changes material between its closed and open states instead of staying
       one object.

       The mock's case for inverting was that "a light-tinted menu has no floor:
       white rows on rgba(255,255,255,.14) over sand measure 1.10:1". That is an
       argument against WHITE TEXT on light glass, not against light glass. On
       INK this panel measures 11.7:1 over the darkest blurred hero region,
       14.2:1 over the brightest and 14.8:1 over pure white — and the price
       gauge already makes exactly this trade (see --ks-price-bg, which keeps
       ink on white glass for the same reason). The menu now agrees with the
       gauge instead of contradicting it.

       .82 AND NOT the gauge's .42: this panel is body-appended, floats over
       whatever the page happens to supply, and carries eight rows rather than
       one reading. .82 is the lowest fill at which the 14px count column
       (--ks-g-ink-soft) still clears 4.5:1 on every backdrop measured — 4.53
       darkest hero / 5.47 brightest / 5.72 cream chrome / 5.78 pure white. The
       blur is doing visible work at that fill, so it still reads as glass. */
    --ks-g-menu-bg:         rgba(255, 255, 255, 0.82);
    --ks-g-menu-blur:       blur(20px);
    /* A TEAL hairline, not a white one. The white edge the ink panel used is
       invisible against the sand and against the cream page chrome — which is
       precisely where this panel overflows to once it leaves the hero. */
    --ks-g-menu-edge:       rgba(0, 109, 122, 0.20);
    --ks-g-menu-lip:        inset 0 1px 0 rgba(255, 255, 255, 0.85);
    /* same shadow colour as the panel's own --ks-g-panel-lift, softened: the
       ink panel's near-black lift reads as grime under a light one */
    --ks-g-menu-lift:       0 24px 54px -18px rgba(6, 30, 44, 0.45);
    --ks-g-menu-fg:         var(--ks-g-ink);
    --ks-g-menu-count:      var(--ks-g-ink-soft);
    --ks-g-menu-disabled:   var(--ks-g-ink-muted);
    --ks-g-menu-rule:       rgba(0, 109, 122, 0.16);
    /* HIGHLIGHTED is the CURSOR (teal) and SELECTED is the PICK (orange) — the
       same split the solid menu draws, restated in alpha so the panel stays
       glass rather than gaining opaque rows. */
    --ks-g-menu-hi-bg:      rgba(0, 151, 167, 0.16);
    --ks-g-menu-hi-bar:     inset 3px 0 0 var(--kohsanuk-brand);
    /* action-DARK, not action: the solid menu's note now applies here verbatim
       — #fd7e14 measures 2.57:1 on white and misses 3:1. */
    --ks-g-menu-sel-bar:    inset 3px 0 0 var(--kohsanuk-action-dark);
    --ks-g-menu-sel-bg:     rgba(253, 126, 20, 0.18);
}

/* No backdrop-filter support (older Firefox, some embedded webviews): every
   translucent surface densifies, so nothing depends on the blur. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    :root {
        --ks-g-panel-bg:     rgba(255, 255, 255, 0.34);
        --ks-g-panel-bg-mob: rgba(255, 255, 255, 0.38);
        --ks-g-menu-bg:      rgba(255, 255, 255, 0.96);
        --ks-price-bg:       rgba(255, 255, 255, 0.62);
    }
}

/* ─── THE SHARED FIELD RECIPE lives further down ───────────────────────
   The site-wide field recipe — hoisted out of .cp-contact__input — is declared
   at the head of the .ks-solid section below, NOT here, and it
   HAS to be there. It carries `.ks-solid .select2-selection--single` in its
   selector list, which ties the glass skin's
   `.ks-hero-search .select2-selection--single` at (0,2,0); on surfaces 2 and 3
   the form is nested INSIDE .ks-solid and still carries .ks-hero-search, so
   both match the same element and only SOURCE ORDER separates them. Declared
   up here it lost, and every select2 control on /search and in the popup
   rendered with the glass skin: translucent white fill and white text on a
   white dialog — invisible, which is the exact failure this rework exists to
   fix. Verified in a browser, not reasoned about. Do not move it back up. */

/* ─── SURFACE 1: the glass panel ────────────────────────────────────────
   The ONLY element in this system that blurs, plus .ks-menu--glass. Both are
   top-level panels with no position:fixed descendants and no popover children.
   backdrop-filter on a FIELD or on a popover ancestor makes it a containing
   block for position:fixed in Chromium — the exact bug reverted at
   booking/wizard-v2-styles.blade.php:990-994. Do not add it anywhere else.

   Craft over production's flat .hero-filters: a specular top edge (real glass
   catches light on its upper lip), a saturating backdrop so the turquoise glows
   THROUGH the panel instead of being greyed by it, and a lift shadow so it
   floats above the photo instead of being pasted onto it. The panel is NOT
   tinted downward into navy to rescue white labels over the sand — a tint that
   changes down the surface reads as a fogged, dirty pane. Glass is uniform or
   it is grime; the legibility fix belongs to the photograph's scrim. */
.ks-hero-search {
    width: 100%;
    max-width: 1040px;
    margin: 20px auto 0;
    padding: 16px;
    background: var(--ks-g-panel-bg);
    -webkit-backdrop-filter: var(--ks-g-panel-blur) saturate(1.2);
    backdrop-filter: var(--ks-g-panel-blur) saturate(1.2);
    border: 1px solid var(--ks-g-panel-edge);
    border-radius: var(--ks-g-panel-radius);
    box-shadow: var(--ks-g-panel-lip), var(--ks-g-panel-lift);
    font-family: var(--kohsanuk-font-sans);
    text-align: left;
}

/* STRUCTURAL HIDE. applicable() drives which controls exist for the picked type
   through the `hidden` PROPERTY on .ks-f / .ks-band, and paint() does the same
   for the hint / reset / notice. !important so no present or future layout
   declaration can outrank it — .ks-f{display:flex} is (0,1,0) and would tie a
   bare .ks-f[hidden] on nothing but source order. This is the trap the old
   .ks-grid2{display:grid} vs [data-search-adv-hidden]{display:none} pair fell
   into, and it is why the mode toggle is gone rather than merely unused. */
.ks-hero-search [hidden],
.ks-refine [hidden],
.ks-pop [hidden] { display: none !important; }

/* ─── BANDS: the three questions the form asks ─────────────────────────── */
.ks-band { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; }
/* production's .search-box, to the pixel — rgba(255,255,255,.30), 8px, 10px pad,
   10px gap — plus the same lit upper lip the outer panel carries, so the two
   glass surfaces belong to one material family. */
.ks-band--q {
    padding: 10px; gap: 10px;
    background: var(--ks-g-row-bg);
    border-radius: var(--kohsanuk-radius-lg);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.38);
}
.ks-band--where { margin-top: 14px; }
/* BAND 1b — type (on /search only) + category. It sits between the free-text band and
   the fold, and it is the one band that is NOT inside .ks-adv: these are the primary
   filters and must not be hidden behind the mobile "More filters" disclosure. */
.ks-band--what {
    margin-top: 14px;
    /* TOP-ALIGNED, for the reason .ks-band--how below already is: this band mixes a ~76px
       select with the ~137px price card, and .ks-band's own `align-items: flex-end` then
       drops the short controls to the row's baseline — measured 62px of label misalignment
       on the popup, with a hole above category. Top-aligning puts every label on one line
       and pushes the slack to the bottom, where it reads as breathing room. */
    align-items: flex-start;
}
/* Band 3 mixes a 48px select with the ~110px price card. Bottom-aligning them
   left a hole ABOVE the short controls and floated their labels mid-panel.
   Top-aligning puts every label on one line across the band — the whole bet of
   this direction — and pushes the slack to the baseline, where it reads as
   breathing room instead of a gap. */
.ks-band--how {
    margin-top: 14px; padding-top: 14px;
    border-top: 1px solid var(--ks-g-hairline);
    align-items: flex-start;
}

.ks-f { display: flex; flex-direction: column; min-width: 0; }
.ks-f--q      { flex: 3 1 320px; }
.ks-f--type   { flex: 1 1 214px; }
/* Matches --type: on the popup the two sit side by side on the same row when there is
   width for it, and wrap to one each below ~460px of band. */
.ks-f--category { flex: 1 1 214px; }
.ks-f--go     { flex: 0 0 auto; }
/* PAIRED ROWS. type+category and boat+guests each share a row when the surface is wide
   enough (the popup, and the widened /search sidebar); price, overnight and mode take
   their own full row. This is the two-column rework, driven by flex-basis rather than a
   grid so the popup and the sidebar reuse one mechanism and it collapses to one column
   for free when the band is narrow. */
.ks-f--route  { flex: 1 1 100%; }   /* full row; from+to pair INSIDE it via .ks-route */
.ks-f--mode   { flex: 1 1 100%; }   /* travel style, own row (has no natural partner) */
.ks-f--guests { flex: 1 1 200px; }  /* pairs with boat */
.ks-f--boat   { flex: 1 1 200px; }
.ks-f--price  { flex: 1 1 100%; }   /* the gauge is wide; own row */
.ks-f--overnight { flex: 1 1 100%; } /* the checkbox spans, own row */

/* ─── THE ADVANCED DISCLOSURE (every width) ───────────────────────────────
   Everything after the Search button folds behind one "Advanced" toggle. It
   never persists, never hides the free-text box, and never detaches anything —
   .ks-adv is HIDDEN, so every control stays in the DOM and still submits.

   display:contents is load-bearing twice over:
     · it keeps the wrapper out of the box tree, so the bands inside lay out as
       if .ks-adv were not there — desktop is byte-identical to no wrapper;
     · an AUTHOR display declaration beats the UA's [hidden]{display:none}
       regardless of specificity, so a stale `hidden` attribute left behind by
       the toggle can never blank the advanced controls on desktop. That is a
       real failure mode: the mock scopes .ks-adv[hidden] to the phone query
       only, which silently relies on JS stripping the attribute on resize. */
.ks-adv { display: contents; }
/* NO force-open block above 768px any more. The fold was a phone-only concession that
   desktop overrode; the form is simple-by-default at every width now (type / category /
   price outside it, the other six facets in it), so [hidden] has to mean hidden
   everywhere. display:contents keeps the OPEN fold contributing no box of its own, which
   is what lets its bands lay out as if the wrapper were not there. */
/* THE ADVANCED BUTTON, at every width — its styling used to live entirely inside a
   (max-width: 767.98px) block because the fold was phone-only. */
.ks-more {
    display: flex; align-items: center; justify-content: center; gap: 8px;
    width: 100%; min-height: 44px; margin-top: 12px; padding: 0 14px;
    background: var(--ks-g-field-bg);
    border: 1px solid var(--ks-g-field-border);
    border-radius: var(--kohsanuk-radius-lg);
    color: #fff; font: 700 12.5px/1 var(--kohsanuk-font-sans);
    letter-spacing: 0.06em; text-transform: uppercase; cursor: pointer;
}
.ks-more:focus-visible { outline: none; box-shadow: var(--ks-g-focus-ring); }
.ks-more__ic { display: block; width: 12px; height: 8px; transition: transform 0.2s ease; }
.ks-more[aria-expanded="true"] .ks-more__ic { transform: rotate(180deg); }
/* the opaque surfaces get the same fold, in their own skin */
.ks-solid .ks-more {
    background: var(--kohsanuk-surface);
    border: 1px solid var(--kohsanuk-rule);
    color: var(--kohsanuk-text);
}
.ks-solid .ks-more:focus-visible {
    outline: none;
    border-color: var(--kohsanuk-action);
    box-shadow: 0 0 0 3px var(--kohsanuk-action-soft);
}

/* PX, NOT REM, THROUGHOUT THIS FORM — the shared-base twin of the note on
   .ks-refine .ks-g-lab further down, and the reason the POPUP's type was broken.
   bootstrap-styles.min.css ships html{font-size:10px} and loads on every surface
   carrying the search popup (/tours, /yachts, /explore, /contact,
   layouts/subindex) and on /search — but NOT on web-home. So one shared partial
   rendered at two scales: these sizes resolved at 16px in the hero and at 62.5%
   of that everywhere else, which put the popup's Search button at 11px and its
   field labels at 8px. Restated as the rem figure x16 — the size the design asked
   for and the size the hero was already getting — so the popup now matches the
   hero and web-home renders byte-identically. .ks-refine keeps its own smaller
   overrides; that surface is deliberately denser. */
/* ─── LABEL ─────────────────────────────────────────────────────────────── */
.ks-g-lab {
    display: block; margin-bottom: 8px;
    color: var(--ks-g-label-fg);
    font-size: 12.8px; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.5px;
    text-shadow: var(--ks-g-label-shadow);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ─── SHARED FIELD BASE (glass) ─────────────────────────────────────────
   16px is the iOS auto-zoom floor. Declared here at (0,1,0) and NEVER inline:
   the site-wide guard above is input:not(#ai-search-input) at (1,0,1) with no
   !important, so an inline font-size would defeat it outright. */
.ks-g-inp,
.ks-g-sel {
    display: block; width: 100%; min-height: 48px; padding: 0 14px;
    background-color: var(--ks-g-field-bg);
    border: 1px solid var(--ks-g-field-border);
    border-radius: var(--kohsanuk-radius-lg);
    color: var(--ks-g-field-fg);
    font-family: inherit; font-size: 16px; outline: 0;
    transition: background-color var(--kohsanuk-ease), border-color var(--kohsanuk-ease), box-shadow var(--kohsanuk-ease);
}
.ks-g-inp::placeholder { color: var(--ks-g-field-ph); opacity: 1; }
.ks-g-inp:hover, .ks-g-sel:hover { background-color: var(--ks-g-field-bg-hover); }
/* :focus and :focus-visible carry the SAME ring. Chrome matches :focus-visible
   on a mouse-clicked <select>, so a weaker :focus fallback looks fine there —
   Safari and Firefox do not, and a fallback would serve a different ring on
   exactly the browsers that need it most. One ring, no fallback. */
/* THE FOCUS INVERT (see --ks-g-field-focus-bg). The ring is unchanged; the two
   new declarations are the FILL flipping to solid white with ink on it.

   SPECIFICITY IS LOAD-BEARING AND IS EXACTLY (0,2,0), the same as the .ks-solid
   resting rules further down — which are LATER in the file and therefore win on
   surfaces 2 and 3. That is what keeps the invert off the opaque skins, where a
   white field flipping to a slightly different white would be noise, and where
   a themed dark surface would flip to white and break. Scoping this to
   `.ks-hero-search` would raise it to (0,3,0), beat .ks-solid, and leak the
   invert onto /search and the popup. Do not add a scope. */
.ks-g-inp:focus, .ks-g-sel:focus,
.ks-g-inp:focus-visible, .ks-g-sel:focus-visible {
    background-color: var(--ks-g-field-focus-bg);
    border-color: #ffffff;
    color: var(--ks-g-field-focus-fg);
    box-shadow: var(--ks-g-focus-ring);
}
/* (0,2,1), tying .ks-solid .ks-g-inp::placeholder below, which wins on source
   order — the same arrangement as the rule above. */
.ks-g-inp:focus::placeholder { color: var(--ks-g-field-focus-ph); }

/* ─── THE OVERNIGHT CHECKBOX ──────────────────────────────────────────────
   A styled checkbox that replaced the "Trip length" <select>. The real <input> is
   visually hidden but stays focusable and stays the thing that submits (name=overnight,
   value=1); .ks-check__box is the painted box, .ks-check__tx the two-line label, and
   .ks-check__n the live count. The whole row is the <label>, so a click anywhere toggles.
   Skinned for both the glass hero and the opaque surfaces via .ks-solid, like the fields. */
.ks-check {
    /* flex-direction:row overrides the .ks-f base (column): this element carries BOTH
       classes (ks-f ks-f--overnight ks-check), and without this the box and label stack
       and centre instead of sitting box-left / text-right. */
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 12px;
    cursor: pointer;
    user-select: none;
    padding: 12px 14px;
    border: 1px solid var(--ks-g-field-border);
    border-radius: var(--kohsanuk-radius-lg);
    background: var(--ks-g-field-bg);
    transition: border-color .15s ease, background-color .15s ease;
}
.ks-check:hover { border-color: var(--kohsanuk-brand); }
.ks-check__in {
    position: absolute;
    width: 1px; height: 1px;
    opacity: 0;
    margin: 0;
    pointer-events: none;
}
.ks-check__box {
    flex: 0 0 22px;
    width: 22px; height: 22px;
    border-radius: 6px;
    border: 2px solid var(--ks-g-field-border);
    display: grid;
    place-items: center;
    color: #fff;
    transition: background-color .15s ease, border-color .15s ease;
}
.ks-check__box svg { width: 13px; height: 13px; opacity: 0; transition: opacity .15s ease; }
.ks-check__in:checked + .ks-check__box {
    background: var(--kohsanuk-brand);
    border-color: var(--kohsanuk-brand);
}
.ks-check__in:checked + .ks-check__box svg { opacity: 1; }
/* Keyboard focus lands on the hidden input; ring the painted box instead. */
.ks-check__in:focus-visible + .ks-check__box {
    outline: none;
    box-shadow: var(--ks-g-focus-ring);
}
.ks-check__tx { min-width: 0; line-height: 1.25; }
.ks-check__tx b { display: block; font-weight: 600; font-size: 14.5px; color: var(--kohsanuk-text); }
.ks-check__tx small { color: var(--kohsanuk-text-soft); font-size: 12px; }
.ks-check__n { margin-left: auto; color: var(--kohsanuk-brand-dark); font-size: 13px; font-weight: 600; }
.ks-check__n:empty { display: none; }
/* Checked = the same brand tint the active facets use, so "on" reads at a glance. */
.ks-check:has(.ks-check__in:checked) { border-color: var(--kohsanuk-brand); background: var(--kohsanuk-brand-soft); }

/* EVERY <select> in this form MUST also carry class="browser-default". MDB's
   bs4theme.min.css ships a bare select{display:none!important} and it loads on
   web-home, explore-index, categories-index, contact and layouts/subindex —
   omitting it makes the control INVISIBLE on exactly those pages. /search does
   not load MDB, which is why this class of bug survives review there. */
.ks-g-sel {
    -webkit-appearance: none; -moz-appearance: none; appearance: none;
    padding-right: 38px;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8'><path d='M1 1l5 5 5-5' stroke='%23ffffff' stroke-width='2' fill='none' stroke-linecap='round'/></svg>");
    background-repeat: no-repeat;
    background-position: right 14px center;
    text-overflow: ellipsis;
}
/* REQUIRED — Win/Linux Chrome inherits the translucent field background into
   the native popup list, where it is unreadable. */
.ks-g-sel option { color: var(--kohsanuk-text); background: var(--kohsanuk-surface); }
.ks-g-sel option:disabled { color: var(--kohsanuk-text-muted); }

/* Guests is a NUMERIC INPUT, not a dropdown. Chrome's spinner is painted from
   the text colour, which is white here and invisible against the glass; the
   field is inputmode="numeric" and typed, so the spinner is removed rather
   than recoloured. Firefox's is suppressed by appearance:textfield. */
.ks-g-inp[type="number"] { -moz-appearance: textfield; appearance: textfield; }
.ks-g-inp[type="number"]::-webkit-outer-spin-button,
.ks-g-inp[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* FREE-TEXT INPUT — magnifier affordance, WHITE stroke on glass. */
.ks-g-inp--search {
    padding-left: 42px;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%23ffffff' stroke-width='2'><circle cx='7' cy='7' r='5.2'/><path d='M11 11l4 4' stroke-linecap='round'/></svg>");
    background-repeat: no-repeat; background-position: 14px center;
}
.ks-hint {
    margin: 6px 0 0; font-size: 12px;
    color: rgba(255, 255, 255, 0.88);
    text-shadow: var(--ks-g-label-shadow);
}

/* ─── THE ROUTE PAIR ────────────────────────────────────────────────────
   Origin and destination are not two independent dropdowns — origin DICTATES
   destination — but they are drawn as two ordinary, FULLY ROUNDED fields with a
   real gap between them. No connector glyph and no joined/half-open boxes: the
   directional arrow was removed as redundant, and a welded pair read as a third
   greyed-out control sitting between them. Because the legs no longer share a
   border there is nothing to overlap, so the z-index lift the joined version
   needed is gone too. */
.ks-route { display: flex; align-items: flex-end; gap: 14px; }
.ks-route__leg { display: flex; flex-direction: column; flex: 1 1 0; min-width: 0; }
/* Safety net only: the arrow element is not part of this design. Kept as a
   one-line kill so a stale partial that still emits it cannot reintroduce the
   glyph, rather than as ~20 lines of dead styling for it. */
.ks-route__arrow { display: none; }

/* ─── ORANGE ACTION BUTTON ──────────────────────────────────────────────── */
.ks-g-go {
    flex: 0 0 auto; min-height: 48px; padding: 12px 26px;
    background: var(--kohsanuk-action);
    border: none; border-radius: var(--kohsanuk-radius-lg);
    color: #ffffff; font-family: inherit; font-weight: 700; font-size: 17.6px;
    cursor: pointer; white-space: nowrap;
    transition: background var(--kohsanuk-ease);
}
.ks-g-go:hover, .ks-g-go:focus { background: var(--kohsanuk-action-dark); color: #fff; }
.ks-g-go:focus-visible { outline: none; box-shadow: var(--ks-g-focus-ring); }
.ks-g-go[disabled] { opacity: 0.55; cursor: not-allowed; }

/* ─── LIQUID-GLASS PRICE GAUGE ──────────────────────────────────────────
   The gauge wears the same outer .ks-g-lab as every other field, so band 3's
   labels sit on one line and its controls on the next. The card holds only the
   reading: value, distribution, range.

   NO backdrop-filter of its own. It would (a) break the panel-only rule, (b)
   re-filter an ALREADY-filtered result — .ks-hero-search frosts the photo
   before this card ever sees it — and (c) make .ks-price a containing block for
   position:fixed descendants in Chromium. The card inherits the panel's frost
   for free. */
.ks-price {
    padding: 9px 14px 7px;
    background: var(--ks-price-bg);
    border: 1px solid var(--ks-price-edge);
    border-radius: var(--kohsanuk-radius-lg);
    color: var(--ks-price-fg);
    box-shadow: var(--ks-price-lip), var(--ks-price-lift);
}
.ks-price__hd { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; margin-bottom: 4px; }
.ks-price__l {
    min-width: 0; font-size: 11.2px; font-weight: 600; letter-spacing: 0.02em;
    color: var(--ks-price-muted);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ks-price__v {
    font-family: var(--kohsanuk-font-serif); font-size: 18px; font-weight: 600;
    color: var(--ks-price-fg); font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* Georgia has no ฿ and the fallback glyph collides with the following digit. */
.ks-price__v b, .ks-price__l b { font-family: var(--kohsanuk-font-sans); font-weight: inherit; padding-right: 1px; }
/* Histogram bins are FIXED-WIDTH price bands, never equal-count deciles —
   deciles are why the superseded sparkline was ten identical blocks carrying no
   information. Heights use a SQRT scale: these distributions have a long thin
   tail which on a linear scale rounds every bin past the first few to a 1px
   stub. (Both are the engine's job; the CSS just has to not fight them.) */
.ks-price__hist {
    display: flex; align-items: flex-end; gap: 2px; height: 24px; margin-bottom: 4px;
    border-bottom: 1px solid var(--ks-price-hist-base);
}
.ks-price__hist i {
    flex: 1; min-height: 2px; background: var(--ks-price-bar);
    border-radius: 2px 2px 0 0; transition: background var(--kohsanuk-ease);
}
.ks-price__hist i.lit { background: var(--ks-price-bar-lit); }
.ks-price__sl { position: relative; display: block; height: 32px; touch-action: pan-y; }
.ks-price__tr { position: absolute; left: 0; right: 0; top: 14px; height: 4px; background: var(--ks-price-track); border-radius: 2px; }
.ks-price__fl { position: absolute; top: 14px; height: 4px; background: var(--ks-price-fill); border-radius: 2px; box-shadow: var(--ks-price-fill-lip); }
/* pointer-events:none on the input + all on the thumb is what lets two ranges
   overlap on one track and still both be grabbable. */
/* `border: 0` is NOT tidiness — it is the fix for the white rectangle that was
   drawn across the middle of the gauge. MDB's public/css/bs4theme.min.css ships
   a bare `input[type="range"] { border: 1px solid #fff; margin: 15px 0; … }`,
   and bs4theme loads on web-home / explore-index / categories-index / contact /
   subindex — i.e. on exactly the GLASS surface and nowhere else. The two range
   inputs stretch the full width of the card, so that white hairline painted a
   full-width white box around the histogram and rail. Same family of trap as the
   `select { display: none !important }` in the same stylesheet, and it survives
   review for the same reason: /search does not load MDB, so the gauge looks
   perfect there. This rule already out-specifies it at (0,2,1) — it simply had
   nothing to say about `border`, and an undeclared property is not a reset. */
.ks-price__sl input[type="range"] {
    position: absolute; left: 0; top: 3px; width: 100%; height: 26px; margin: 0;
    border: 0;
    -webkit-appearance: none; appearance: none; background: none; pointer-events: none;
}
/* The track must be re-declared for every INTERACTION STATE, not just the rest
   state, and the reason is a specificity TIE — not a shortfall. Compare:

     ours  .ks-price__sl input[type="range"]::-webkit-slider-runnable-track
           .ks-price__sl + [type="range"]      = 2   input + ::track = 2  → (0,2,2)
     MDB   input[type=range]:focus::-webkit-slider-runnable-track
           [type=range] + :focus              = 2   input + ::track = 2  → (0,2,2)

   Dead even, so the winner is decided by source order — and web-home.blade.php
   loads @include('parts.theme-links') BEFORE css/bs4theme.min.css, so MDB wins.
   The moment the handle takes focus, MDB's `background:#ccc` repaints the track
   and, because OUR `height:26px` still applies (MDB's :focus rule says nothing
   about height), it paints as a full-width 26px GREY SLAB that buries the rail,
   the fill and the histogram baseline. It only appears on click, which is why it
   reads as a JS bug and why a synthetic mousedown never reproduces it: dispatching
   an event does not move focus, so :focus never matches.

   Naming the states lifts us to (0,3,2) and wins outright — no !important needed.
   Keep -webkit- and -moz- in SEPARATE rules: one invalid selector voids an entire
   selector list, so a merged list would be dropped wholesale by both engines. */
.ks-price__sl input[type="range"]::-webkit-slider-runnable-track,
.ks-price__sl input[type="range"]:hover::-webkit-slider-runnable-track,
.ks-price__sl input[type="range"]:focus::-webkit-slider-runnable-track,
.ks-price__sl input[type="range"]:focus-visible::-webkit-slider-runnable-track,
.ks-price__sl input[type="range"]:active::-webkit-slider-runnable-track { background: none; border: none; height: 26px; }
.ks-price__sl input[type="range"]::-moz-range-track,
.ks-price__sl input[type="range"]:hover::-moz-range-track,
.ks-price__sl input[type="range"]:focus::-moz-range-track,
.ks-price__sl input[type="range"]:focus-visible::-moz-range-track,
.ks-price__sl input[type="range"]:active::-moz-range-track { background: none; border: none; height: 26px; }
/* `margin: 0` is the fix for the handles floating ABOVE the rail. MDB ships
   `input[type=range]::-webkit-slider-thumb { margin: -5px 0 0 }` to re-centre a
   14px thumb on its own 3px track. Our thumb is 26px on a 26px track and is
   already centred, so that -5px is pure error — and it applied because our rule,
   though it out-specifies MDB's at (0,2,2) vs (0,1,2), never MENTIONED margin,
   and an undeclared property is not a reset. Measured: handle centre sat 5.5px
   above the rail on the glass hero and 5.4px above in the popup, while /search —
   which loads no MDB — was already correct at 0.6px. Declaring margin here is
   what makes all three surfaces agree.
   `transition` is declared for the same reason: MDB's bare `transition:.3s`
   (i.e. `all`) leaked onto the thumb, so the focus ring animated at a different
   speed on MDB pages than on /search. Naming the property pins both surfaces. */
.ks-price__sl input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none; pointer-events: all;
    width: 26px; height: 26px; margin: 0; border-radius: 50%;
    background: var(--ks-price-thumb); box-shadow: var(--ks-price-thumb-ring); cursor: grab;
    transition: box-shadow var(--kohsanuk-ease);
}
.ks-price__sl input[type="range"]::-moz-range-thumb {
    pointer-events: all; width: 26px; height: 26px; margin-top: 0; border: none; border-radius: 50%;
    background: var(--ks-price-thumb); box-shadow: var(--ks-price-thumb-ring); cursor: grab;
    transition: box-shadow var(--kohsanuk-ease);
}
.ks-price__sl input[type="range"]:focus-visible::-webkit-slider-thumb { box-shadow: var(--ks-price-thumb-focus); }
.ks-price__sl input[type="range"]:focus-visible::-moz-range-thumb { box-shadow: var(--ks-price-thumb-focus); }
/* Kept for screen readers (wired via aria-describedby) but removed from the
   visual field: a two-handled slider explains itself, and the line cost 18px of
   a vertical budget that has none to spare. */
.ks-price__meta { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; }

/* ─── FOOTER: count / reset / notice ────────────────────────────────────
   The last line in the panel, so it sits lowest on the sand and is the first
   thing to wash out. It carries no field background to hide behind, so it gets
   a deeper text-shadow than the labels above it — the one place where a heavier
   shadow is doing real work rather than decorating. */
.ks-foot { display: flex; justify-content: space-between; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 12px; }
.ks-g-count { color: #fff; font-size: 14.4px; font-weight: 500; text-shadow: 0 1px 3px rgba(5, 27, 41, 0.55), 0 0 10px rgba(5, 27, 41, 0.30); }
.ks-g-count b { font-weight: 700; }
.ks-g-reset {
    color: #fff; font-size: 13.6px; font-weight: 500;
    text-decoration: underline; text-underline-offset: 3px;
    text-decoration-color: rgba(255, 255, 255, 0.55);
    text-shadow: 0 1px 3px rgba(5, 27, 41, 0.55), 0 0 10px rgba(5, 27, 41, 0.30);
}
.ks-g-reset:hover, .ks-g-reset:focus { color: #fff; text-decoration-color: #fff; }
.ks-g-notice {
    display: flex; align-items: center; gap: 12px; margin-top: 12px;
    padding: 10px 14px; border-radius: var(--kohsanuk-radius-lg);
    background: rgba(255, 255, 255, 0.92); color: var(--kohsanuk-text); font-size: 13.6px;
}
.ks-g-notice button {
    padding: 0; background: none; border: none; font: inherit;
    color: var(--kohsanuk-brand-dark); font-weight: 700; text-decoration: underline; cursor: pointer;
}

/* ═══ SELECT2 — THE CLOSED CONTROL ══════════════════════════════════════
   Every class below is one select2 4.0.3 really emits
   (resources/assets/js/jquery-ctrl/select2.min.js).

   EVERY RULE IS SCOPED, AND THE SCOPE IS LOAD-BEARING — do not "simplify" these
   selectors back to bare .select2-* ones. Two independent reasons:

   1. OUTWARD. components.css is not a frontend-only stylesheet. It ships via
      parts/theme-links -> parts/head, and layouts/console-app.blade.php includes
      parts/head — so anything here also lands on all ~44 admin console pages, the
      booking wizard and booking/my-order-edit, every one of which runs select2 on
      controls this design has never seen (console tour edit's optgroup'd
      "Tour categories" multiselect, for one). Unscoped, this block silently
      restyled them: `.select2-results__option{display:flex;justify-content:
      space-between}` renders an <optgroup> heading and its options side by side
      instead of stacked.

   2. INWARD. The claim this block used to open with — "the only select2
      stylesheet the customer-facing site has" — is false. public/css/
      bootstrap-styles.min.css carries a complete select2 skin (39 selectors) and
      parts/head.blade.php loads it AFTER parts/theme-links. At equal specificity
      the later sheet wins, so the bare rules lost to it on every page that
      includes parts/head: `.select2-selection--single{display:block;height:40px;
      padding:7px 0}` beat this design's flex/48px, and the intended layout only
      ever appeared on home / explore / contact / subindex, which include
      theme-links WITHOUT head. Scoping raises specificity to 0,2,0+ and wins
      regardless of source order — the same edit fixes both directions.

   .ks-hero-search is the right hook because form.blade.php puts that class on the
   <form> on ALL THREE surfaces; .ks-solid only distinguishes the skin.

   The CLOSED control is a descendant of the form, so ordinary scoping reaches it
   and each surface skins it by ancestor. The OPEN panel is NOT: select2 appends
   the dropdown to <body>, so no ancestor selector can ever match it and the only
   hook is dropdownCssClass. That single fact drives the split between the
   ancestor-scoped rules here and the .ks-menu classes below, and it is why this
   repo previously gave up and left dropdown panels unstyled
   (booking/my-order-edit.blade.php:72-73 says so in as many words). */
.ks-hero-search .select2-container { position: relative; display: inline-block; width: 100%; vertical-align: middle; }
.ks-hero-search .select2-container .selection { display: block; }
.ks-hero-search .select2-selection--single {
    display: flex; align-items: center; width: 100%; min-height: 48px;
    border-radius: var(--kohsanuk-radius-lg);
    cursor: pointer; outline: 0; -webkit-user-select: none; user-select: none;
    transition: background-color var(--kohsanuk-ease), border-color var(--kohsanuk-ease), box-shadow var(--kohsanuk-ease);
}
/* Three classes deep, not two: bootstrap-styles' own
   `.select2-selection--single .select2-selection__rendered{padding-left:12px}` is
   already 0,2,0 and loads later, so a 0,2,0 rule here would tie and lose. */
.ks-hero-search .select2-selection--single .select2-selection__rendered {
    flex: 1; min-width: 0; padding: 0 4px 0 14px;
    font-size: 16px;  /* iOS floor. NEVER inline — the site's guard has no !important. */
    font-weight: 400; line-height: 1.2;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ks-hero-search .select2-selection--single .select2-selection__arrow {
    flex: 0 0 38px; align-self: stretch;
    display: flex; align-items: center; justify-content: center; pointer-events: none;
}
/* select2 draws its own <b> caret and bootstrap-styles bolts an icomoon glyph onto
   :after; this system draws the same chevron the native .ks-g-sel uses, so one
   glyph serves open and closed. initSelect2() injects the <svg>. */
.ks-hero-search .select2-selection--single .select2-selection__arrow b { display: none; }
.ks-hero-search .select2-selection--single .select2-selection__arrow:after { content: none; }
.ks-hero-search .select2-selection__arrow svg { display: block; width: 12px; height: 8px; transition: transform var(--kohsanuk-ease); }
.ks-hero-search .select2-container--open .select2-selection__arrow svg { transform: rotate(180deg); }
.ks-hero-search .select2-hidden-accessible {
    position: absolute !important; width: 1px !important; height: 1px !important;
    padding: 0 !important; margin: -1px !important; overflow: hidden !important;
    clip: rect(0 0 0 0) !important; white-space: nowrap !important; border: 0 !important;
}

/* glass skin, applied by ancestor (surface 1) */
.ks-hero-search .select2-selection--single {
    background-color: var(--ks-g-field-bg);
    border: 1px solid var(--ks-g-field-border);
    color: var(--ks-g-field-fg);
}
.ks-hero-search .select2-selection--single:hover { background-color: var(--ks-g-field-bg-hover); }
.ks-hero-search .select2-selection__arrow { color: #ffffff; }
.ks-hero-search .select2-selection__placeholder { color: var(--ks-g-field-ph); }
/* THE FOCUS INVERT, select2's half. Unlike the native rule above this one is
   already (0,3,0) — it has to be, to beat bootstrap-styles — so source order
   canNOT protect the opaque skins from it. The three declarations it would leak
   (fill, ink, placeholder ink) are therefore RESTATED on .ks-solid's own focus
   rule below, at equal specificity and later, where they are no-ops that pin
   the opaque field to its resting white. See the note there. */
.ks-hero-search .select2-container--focus .select2-selection--single,
.ks-hero-search .select2-container--open .select2-selection--single {
    background-color: var(--ks-g-field-focus-bg);
    border-color: #ffffff;
    color: var(--ks-g-field-focus-fg);
    box-shadow: var(--ks-g-focus-ring);
}
/* The rendered value inherits `color` from the box above, so it needs no rule of
   its own. The placeholder and the chevron are painted separately and do:
   white-on-white is invisible. Both land on --kohsanuk-text-soft, which is
   ALREADY what .ks-solid paints them — so although these two selectors do reach
   surfaces 2 and 3, they change nothing there. */
.ks-hero-search .select2-container--focus .select2-selection__placeholder,
.ks-hero-search .select2-container--open .select2-selection__placeholder { color: var(--ks-g-field-focus-ph); }
.ks-hero-search .select2-container--focus .select2-selection__arrow,
.ks-hero-search .select2-container--open .select2-selection__arrow { color: var(--ks-g-ink-soft); }

/* ═══ SELECT2 — THE OPEN PANEL ══════════════════════════════════════════
   Body-appended, so no ANCESTOR selector can reach it — but it is not therefore
   unscopable. initSelect2() passes dropdownCssClass: 'ks-menu ' + skin, so the
   panel carries a marker class of this design's own: .ks-menu identifies "a panel
   belonging to the search form" and .ks-menu--glass / .ks-menu--solid pick the
   surface. Scoping on .ks-menu is what keeps these rules off the admin console's
   select2 panels, and what lets them out-specify bootstrap-styles.min.css, which
   also skins .select2-dropdown and loads later (see the block header above). */
.ks-menu.select2-dropdown {
    position: absolute; z-index: 9999;
    display: flex; flex-direction: column; overflow: hidden;
    font-family: var(--kohsanuk-font-sans);
    /* select2 sizes the panel to the control (a half-column ~230px), which ellipsised long
       place names ("Angthong Marine Park", "Koh Tan & Koh Matsum"). min-width floors the
       panel wider than the control so names read in full; max-width caps it so a narrow
       viewport can't clip it. The panel is body-appended and grows rightward over the
       results, so this is safe on /search, the popup and full-width mobile alike. */
    min-width: 300px;
    max-width: min(94vw, 460px);
}
.ks-menu .select2-results { display: flex; flex-direction: column; min-height: 0; }
.ks-menu .select2-results__options {
    margin: 0; padding: 0; list-style: none;
    max-height: min(46vh, 320px); overflow-y: auto; overscroll-behavior: contain;
}
.ks-menu .select2-results__option {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    min-height: 44px;  /* tap target */
    padding: 11px 14px;
    font-size: 16px;   /* iOS floor. NEVER inline. */
    cursor: pointer;
}
.ks-menu .ks-menu__lab { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ks-menu .ks-menu__count { flex: 0 0 auto; font-size: 14px; font-variant-numeric: tabular-nums; }
/* NO search box inside dropdown panels — a hard requirement of this design, so
   it is enforced here rather than left to a JS option. select2 4.0.3 ALWAYS
   renders this element for a single select and relies on its own stylesheet to
   hide it when minimumResultsForSearch is Infinity; that stylesheet is not
   loaded on the frontend (see the block header above), so without this rule the
   box appears in every dropdown regardless of how the control is configured. */
.ks-menu .select2-search--dropdown { display: none; }

/* ─── GLASS MENU (surface 1) ────────────────────────────────────────────
   PERMITTED backdrop-filter: this element IS a panel — top level, body-appended,
   no position:fixed descendants, not an ancestor of any popover. */
/* .ks-menu.ks-menu--glass, not .ks-menu--glass alone: bootstrap-styles.min.css sets
   .select2-dropdown{background-color:#fff} at 0,1,0 and loads later, which would
   paint the glass panel opaque white on any page that includes parts/head. */
.ks-menu.ks-menu--glass {
    background: var(--ks-g-menu-bg);
    -webkit-backdrop-filter: var(--ks-g-menu-blur) saturate(1.15);
    backdrop-filter: var(--ks-g-menu-blur) saturate(1.15);
    border: 1px solid var(--ks-g-menu-edge);
    border-radius: var(--ks-g-panel-radius);
    box-shadow: var(--ks-g-menu-lip), var(--ks-g-menu-lift);
    color: var(--ks-g-menu-fg);
}
/* bootstrap-styles' .select2-results__option+.select2-results__option{margin-top:1px}
   is 0,2,0 and later; this design draws its separators with borders, not gaps. */
.ks-menu .select2-results__option + .select2-results__option { margin-top: 0; }
.ks-menu--glass .select2-results__option {
    background: transparent;
    color: var(--ks-g-menu-fg);
    border-bottom: 1px solid var(--ks-g-menu-rule);
}
.ks-menu--glass .select2-results__option:last-child { border-bottom: 0; }
.ks-menu--glass .ks-menu__count { color: var(--ks-g-menu-count); }
.ks-menu--glass .select2-results__option[aria-disabled="true"] { color: var(--ks-g-menu-disabled); cursor: not-allowed; }
/* a zero-count option is NOT disabled — it is the "ghost" entry that preserves a
   pick which fell out of the rebuilt list, so it must stay selectable-away-from */
.ks-menu--glass .select2-results__option.ks-menu__opt--empty .ks-menu__lab { opacity: 0.62; }
.ks-menu--glass .select2-results__option.ks-menu__opt--empty .ks-menu__count { opacity: 0.62; font-style: italic; }
/* SELECTED — three redundant cues; the 3px bar is the one that clears 3:1
   against the menu on every backdrop (4.38 water / 3.40 sand / 3.13 white) */
/* `color` is NOT redundant with the row rule above — see the note on the solid
   twin of this rule, which is where the missing declaration was doing visible
   damage. Declared on both skins so neither can regress. */
.ks-menu--glass .select2-results__option[aria-selected="true"] {
    background: var(--ks-g-menu-sel-bg);
    box-shadow: var(--ks-g-menu-sel-bar);
    color: var(--ks-g-menu-fg);
    font-weight: 600;
}
.ks-menu--glass .select2-results__option[aria-selected="true"] .ks-menu__lab::after {
    content: "\2713"; margin-left: 8px; font-weight: 700; color: var(--kohsanuk-action-dark);
}
/* HIGHLIGHTED — mouse hover AND the keyboard cursor share one treatment */
.ks-menu--glass .select2-results__option--highlighted {
    background: var(--ks-g-menu-hi-bg);
    box-shadow: var(--ks-g-menu-hi-bar);
}
/* Both at once: the row keeps the cursor's wash but the BAR states the pick,
   because selected is the stronger fact. Two 3px bars stacked would be one bar
   with a hidden second, so this states the winner instead of composing them. */
.ks-menu--glass .select2-results__option--highlighted[aria-selected="true"] {
    background: var(--ks-g-menu-hi-bg);
    box-shadow: var(--ks-g-menu-sel-bar);
}

/* ─── SOLID MENU (surfaces 2 and 3) ─────────────────────────────────────
   A SEPARATE class, not an ancestor override: the dropdown is body-appended and
   lives OUTSIDE .ks-pop / .ks-refine, so `.ks-pop .select2-dropdown` would match
   nothing. This is the trap the whole architecture is built around. */
.ks-menu.ks-menu--solid {
    background: var(--kohsanuk-surface);
    border: 1px solid var(--kohsanuk-rule);
    border-radius: var(--kohsanuk-radius-lg);
    box-shadow: 0 18px 44px -18px rgba(6, 30, 44, 0.28);
    color: var(--kohsanuk-text);
}
.ks-menu--solid .select2-results__option { color: var(--kohsanuk-text); border-bottom: 1px solid var(--kohsanuk-rule); }
.ks-menu--solid .select2-results__option:last-child { border-bottom: 0; }
.ks-menu--solid .ks-menu__count { color: var(--kohsanuk-text-muted); }
.ks-menu--solid .select2-results__option[aria-disabled="true"] { color: var(--kohsanuk-text-muted); cursor: not-allowed; }
.ks-menu--solid .select2-results__option.ks-menu__opt--empty .ks-menu__lab { opacity: 0.6; }
.ks-menu--solid .select2-results__option.ks-menu__opt--empty .ks-menu__count { opacity: 0.6; font-style: italic; }
/* SELECTED — action-DARK, not plain action: #fd7e14 measures 2.57:1 on white
   and misses 3:1. Orange here is the SELECTION marker, which is the one job the
   palette rule reserves it for. */
/* SEPARATE BUG, FIXED HERE because this is the rule that owns the state.
   bootstrap-styles.min.css ships `.select2-results__option[aria-selected="true"]
   { color: #fff; background: #0097a7 }` at (0,2,0) and loads AFTER components.css.
   This rule is (0,3,0), so it took the background and the bar — but it never
   declared `color`, and an undeclared property does not override anything. The
   result shipped in #1053: on /search and in the popup the SELECTED row of every
   dropdown was white text on this rule's own near-white tint, i.e. invisible;
   only the orange check gave the pick away. Home never showed it because
   bootstrap-styles does not load there — the same asymmetry that hides the MDB
   traps, in the opposite direction. */
.ks-menu--solid .select2-results__option[aria-selected="true"] {
    background: var(--kohsanuk-action-tint);
    box-shadow: inset 3px 0 0 var(--kohsanuk-action-dark);
    color: var(--kohsanuk-text);
    font-weight: 600;
}
.ks-menu--solid .select2-results__option[aria-selected="true"] .ks-menu__lab::after {
    content: "\2713"; margin-left: 8px; font-weight: 700; color: var(--kohsanuk-action-dark);
}
/* HIGHLIGHTED is the CURSOR, not a selection — teal, so the two never blur. */
.ks-menu--solid .select2-results__option--highlighted {
    background: var(--kohsanuk-cream-100);
    box-shadow: inset 3px 0 0 var(--kohsanuk-brand);
}
.ks-menu--solid .select2-results__option--highlighted[aria-selected="true"] {
    background: var(--kohsanuk-cream-100);
    box-shadow: inset 3px 0 0 var(--kohsanuk-action-dark);
}

/* ═══ .ks-solid — THE OPAQUE SKIN, SHARED BY SURFACES 2 AND 3 ═══════════
   Deliberately a CLASS both hosts apply, not `.ks-pop .X` bound to the dialog.
   There are two non-photographic surfaces — the /search refine bar and the
   popup — and they need byte-identical field treatment. One solid skin, two
   hosts. (CLAUDE.md: no copy-paste.)

   The field recipe itself is NOT repeated here: .ks-solid's three control
   selectors are listed in the SHARED FIELD RECIPE rule at the top of this
   section, which is the single shared declaration block. Only the things that
   genuinely differ from glass appear below.

   NOTE the box-shadow resets: the hero panel carries a specular lip and a lift
   shadow and the band-1 row carries a lip of its own. Neither means anything on
   an opaque surface — the lift shadow would smear a dark halo INSIDE the dialog
   — so both are cancelled rather than merely repainted. */
/* ─── SHARED FIELD RECIPE (declaration) ─────────────────────────────────
   DRY. The site already had exactly one customer-facing form recipe:
   .cp-contact__input / .cp-contact__textarea in public/assets/css/category-pages.css,
   used by the contact form. It is hoisted here VERBATIM, via the same tokens,
   as the single source of truth:

       background: var(--kohsanuk-surface);   (WHITE — not a cream fill)
       border:     1px solid var(--kohsanuk-rule);
       border-radius: 6px;                    (= --kohsanuk-radius-md)
       :focus ->  border-color: var(--kohsanuk-action);
                  box-shadow:   0 0 0 3px var(--kohsanuk-action-soft);

   That recipe already satisfies the standing rules: WHITE not grey, and ORANGE
   ONLY AS THE FOCUS MARKER — never a resting border. Earlier revisions of the
   mock invented a cream fill, then invented teal resting borders; both were
   wrong and both were rejected. NB the mock's own .ks-solid :focus rules still
   carry the superseded TEAL ring while the comment above them specifies the
   orange one — the comment is the approved intent and is what ships here.

   >>> NEXT CONSUMER: public/assets/css/category-pages.css.
       .cp-contact__input / .cp-contact__textarea (~line 614) still declare these
       same five properties inline. Fold them in by adding those two selectors to
       the rule below and deleting the duplicated declarations there. Not done in
       this change only because category-pages.css is outside its file scope.
       Until then this is the canonical copy; do not edit one without the other.

       There is deliberately NO opt-in class here. An earlier revision carried a
       `.koh-field` lead selector for that future consumer to point at, and it
       went out with zero elements matching it in the entire repo — a name three
       comments referred to and nothing rendered. The recipe is claimed by being
       LISTED in the selector list below, which is how its live consumers already
       claim it; a class with no markup is not a plan, it is dead CSS.

   The search form's OPAQUE surfaces consume it by being listed in the same
   selector list — one declaration block, zero duplication. The GLASS surface
   deliberately does NOT: white-on-white is not a field over a photograph.

   background-COLOR, never the `background` shorthand: .ks-g-sel and
   .ks-g-inp--search inherit a background-image (chevron / magnifier) plus its
   no-repeat and position from their base rule. The shorthand resets those to
   initial, which tiles the icon across the whole field. */
.ks-solid .ks-g-inp,
.ks-solid .ks-g-sel,
.ks-solid .select2-selection--single {
    background-color: var(--kohsanuk-surface);
    border: 1px solid var(--kohsanuk-rule);
    border-radius: var(--kohsanuk-radius-md);
    color: var(--kohsanuk-text);
    transition: border-color var(--kohsanuk-ease), box-shadow var(--kohsanuk-ease);
}
/* The overnight checkbox on the OPAQUE surfaces (/search, popup). Without this it kept the
   glass tokens — a near-transparent fill and a white border on a white card, invisible
   unchecked, plus a white focus ring (a WCAG 2.4.7 fail). Skinned like the fields above,
   and the focus ring matches .ks-solid .ks-more's. The checked state keeps its brand tint. */
.ks-solid .ks-check {
    background-color: var(--kohsanuk-surface);
    border-color: var(--kohsanuk-rule);
}
.ks-solid .ks-check__box { border-color: var(--kohsanuk-rule); }
.ks-solid .ks-check__in:focus-visible + .ks-check__box {
    outline: none;
    border-color: var(--kohsanuk-action);
    box-shadow: 0 0 0 3px var(--kohsanuk-action-soft);
}
/* background-color and color RESTATE the resting values on purpose. They are
   no-ops for every selector in this list — each of these fields already rests at
   surface/text — and they exist solely to pin the opaque skin against the glass
   FOCUS INVERT (--ks-g-field-focus-bg), whose select2 half is (0,3,0) and would
   otherwise reach these surfaces too and flip a themed field to literal white.
   Deleting them silently repaints /search and the popup on focus. */
.ks-solid .ks-g-inp:focus,
.ks-solid .ks-g-sel:focus,
.ks-solid .ks-g-inp:focus-visible,
.ks-solid .ks-g-sel:focus-visible,
.ks-solid .select2-container--focus .select2-selection--single,
.ks-solid .select2-container--open .select2-selection--single {
    outline: none;
    background-color: var(--kohsanuk-surface);
    color: var(--kohsanuk-text);
    border-color: var(--kohsanuk-action);
    box-shadow: 0 0 0 3px var(--kohsanuk-action-soft);
}

.ks-solid .ks-hero-search {
    max-width: none; margin: 0; padding: 0;
    background: none;
    -webkit-backdrop-filter: none; backdrop-filter: none;
    border: 0; border-radius: 0; box-shadow: none;
}
.ks-solid .ks-band--q { background: none; padding: 0; box-shadow: none; }
.ks-solid .ks-band--how { border-top-color: var(--kohsanuk-rule); }
.ks-solid .ks-g-lab { color: var(--kohsanuk-text); text-shadow: none; }
.ks-solid .ks-g-inp::placeholder { color: var(--kohsanuk-text-soft); }
/* The INK strokes, shared by the opaque skin and by any INVERTED (focused) glass
   field — a data: URI cannot read a custom property, so the only way to avoid a
   second copy of each glyph is to let both cases consume the same declaration.
   `.ks-g-sel:focus` is (0,2,0), which beats the base white-stroke rule at
   (0,1,0); on surfaces 2 and 3 `.ks-solid .ks-g-sel` in the same list already
   paints it, so the focused state there is a repaint with identical bytes. */
.ks-solid .ks-g-sel,
.ks-g-sel:focus,
.ks-g-sel:focus-visible {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8'><path d='M1 1l5 5 5-5' stroke='%23506880' stroke-width='2' fill='none' stroke-linecap='round'/></svg>");
}
.ks-solid .ks-g-inp--search,
.ks-g-inp--search:focus,
.ks-g-inp--search:focus-visible {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%23506880' stroke-width='2'><circle cx='7' cy='7' r='5.2'/><path d='M11 11l4 4' stroke-linecap='round'/></svg>");
}
.ks-solid .select2-selection--single:hover { background-color: var(--kohsanuk-surface); }
/* (0,2,1) — ties the glass hover exactly and wins on source order; without
   it the closed control flashes translucent glass on the opaque surfaces. */
.ks-solid .select2-selection__arrow { color: var(--kohsanuk-text-soft); }
.ks-solid .select2-selection__placeholder { color: var(--kohsanuk-text-soft); }
/* the legs are plain fields on this surface too — nothing joined, nothing half-open */
.ks-solid .ks-g-sel--l, .ks-solid .ks-g-sel--r,
.ks-solid .select2-selection--single.ks-sel--l,
.ks-solid .select2-selection--single.ks-sel--r {
    border: 1px solid var(--kohsanuk-rule); border-radius: var(--kohsanuk-radius-md);
}
/* The price gauge is glass only where there is a photograph to be glass ABOUT.
   On an opaque surface it restates as a plain inset card — same geometry, same
   histogram, same handles, no translucency and no lift. NO GREY: every neutral
   here was a slate/cream mix (card fill, both histogram states, the rail, the
   handle ring). They are now white + brand teal, with ORANGE reserved for the
   selected span, which is the one thing the palette rule allows it to mark. */
.ks-solid .ks-price {
    background: var(--kohsanuk-surface); box-shadow: none;
    border: 1px solid var(--kohsanuk-rule); border-radius: var(--kohsanuk-radius-md);
    color: var(--kohsanuk-text);
}
.ks-solid .ks-price__l { color: var(--kohsanuk-brand-dark); }
.ks-solid .ks-price__v { color: var(--kohsanuk-text); }
.ks-solid .ks-price__hist { border-bottom-color: var(--kohsanuk-rule); }
.ks-solid .ks-price__hist i { background: rgba(0, 151, 167, 0.22); }
.ks-solid .ks-price__hist i.lit { background: var(--kohsanuk-brand); }
.ks-solid .ks-price__tr { background: rgba(0, 151, 167, 0.24); }
.ks-solid .ks-price__sl input[type="range"]::-webkit-slider-thumb { box-shadow: 0 0 0 1.5px var(--kohsanuk-brand), 0 2px 6px rgba(0, 109, 122, 0.22); }
.ks-solid .ks-price__sl input[type="range"]::-moz-range-thumb { box-shadow: 0 0 0 1.5px var(--kohsanuk-brand), 0 2px 6px rgba(0, 109, 122, 0.22); }
.ks-solid .ks-price__sl input[type="range"]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 2px var(--kohsanuk-brand), 0 0 0 5px rgba(0, 151, 167, 0.25); }
.ks-solid .ks-price__sl input[type="range"]:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 2px var(--kohsanuk-brand), 0 0 0 5px rgba(0, 151, 167, 0.25); }
.ks-solid .ks-g-count { color: var(--kohsanuk-text-soft); text-shadow: none; }
.ks-solid .ks-g-count b { color: var(--kohsanuk-text); }
/* text-shadow:none is load-bearing — the hero's Reset carries a deep scrim
   shadow to survive the sand, which on a white surface is a grey smudge. */
.ks-solid .ks-g-reset {
    color: var(--kohsanuk-brand-dark); text-shadow: none;
    text-decoration-color: rgba(0, 109, 122, 0.45);
}
.ks-solid .ks-g-reset:hover, .ks-solid .ks-g-reset:focus { color: var(--kohsanuk-brand-dark); text-decoration-color: var(--kohsanuk-brand-dark); }
.ks-solid .ks-hint { color: var(--kohsanuk-brand-dark); text-shadow: none; }
/* literal teal alphas, not --kohsanuk-brand-tint/-soft — see the palette note
   in this section's header for why those two tokens are unsafe here */
.ks-solid .ks-g-notice { background: rgba(0, 151, 167, 0.08); border: 1px solid rgba(0, 151, 167, 0.18); }

/* ═══ SURFACE 3 — THE POPUP (every page that is not / or /search) ═══════
   Opaque white, no blur. The ONLY surface in this system that is a popup. */
/* Scroll lock — the opener sets this on <html>, the closer removes it. */
.ks-pop-open { overflow: hidden; }
.ks-pop-backdrop { position: fixed; inset: 0; background: rgba(18, 41, 57, 0.55); z-index: 1060; }
.ks-pop-backdrop[hidden] { display: none; }
.ks-pop {
    position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1061;
    width: calc(100% - 40px); max-width: 760px; max-height: 88vh; overflow: auto;
    background: var(--kohsanuk-surface); border-radius: var(--kohsanuk-radius-xl);
    box-shadow: var(--kohsanuk-shadow-popup);
    font-family: var(--kohsanuk-font-sans);
}
/* [hidden] on .ks-pop itself, NOT via the descendant guard above — an author
   `display` always beats the UA's [hidden]{display:none}, and this element has
   none, but stating it keeps the pair symmetrical with .ks-pop-backdrop. */
.ks-pop[hidden] { display: none; }
.ks-pop__hd {
    position: sticky; top: 0; z-index: 2;
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 18px 22px 14px; background: var(--kohsanuk-surface);
    border-bottom: 1px solid var(--kohsanuk-rule);
}
.ks-pop__ttl { margin: 0; font: 600 19px/1.2 var(--kohsanuk-font-sans); color: var(--kohsanuk-text); letter-spacing: -0.01em; }
.ks-pop__x {
    flex: 0 0 44px; width: 44px; height: 44px;   /* 44, NOT 40 — touch target */
    display: flex; align-items: center; justify-content: center;
    background: var(--kohsanuk-cream-100); border: none; border-radius: var(--kohsanuk-radius-lg);
    color: var(--kohsanuk-text-soft); font-size: 20px; line-height: 1; cursor: pointer;
}
.ks-pop__x:hover, .ks-pop__x:focus { background: var(--kohsanuk-cream-200); color: var(--kohsanuk-text); }
.ks-pop__x:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--kohsanuk-action-soft); }
.ks-pop__bd { padding: 18px 22px 22px; }

/* ═══ SURFACE 2 — THE /search REFINE BAR ═══════════════════════════════
   Inline at the top of the results page. NOT a popup and NOT a trigger: the
   standard results-page pattern, where the controls that produced the result
   set stay visible and editable while you scan it.

   Same form, same field names, same engine as the hero. Only two things differ,
   and both follow from where it sits:
     · it is on white page chrome rather than a photograph, so it takes the
       shared .ks-solid skin — glass over cream would be glass about nothing;
     · it is read alongside results and re-submitted repeatedly, so it is
       DENSER. The compaction the hero reserves for its 992–1399px tier is this
       surface's default at every width. */
/* THE FRAME. The mock did NOT solve this and could not have: its .ks-refine is a
   full-bleed WHITE band whose only edge is a bottom hairline. That reads as a
   panel in the mock, whose <body> is the hero scene, and dissolves completely on
   the real page, whose <body> is #ffffff — white band on white background, so
   the controls float unframed with nothing but a rule 340px below them. Porting
   it verbatim is what produced the reported symptom.

   So the band keeps the page's colour and the INNER becomes the card. It is
   framed exactly the way every other card on this site is framed — the token
   pair --cp-card-border / --cp-card-shadow (tour cards) and
   --st-card-border / --st-card-shadow (search tiles) both already resolve to
   --kohsanuk-brand-soft + --kohsanuk-shadow-card. Reused, not invented, so the
   refine bar reads as the same family of object as the result cards under it.

   The brand-soft border is on the CONTAINER only. The FIELDS keep the shared
   field recipe — white fill, --kohsanuk-rule border — so the two levels
   stay distinguishable and orange is still nothing but the focus marker. */
.ks-refine {
    background: var(--kohsanuk-surface);
    font-family: var(--kohsanuk-font-sans);
    /* Replaces the band's bottom hairline: the card now draws its own edge, and
       a full-width rule under a bordered card reads as a second, wrong frame. */
    padding: 18px 0 22px;
}
.ks-refine__in {
    max-width: 1120px; margin: 0 auto; padding: 16px 22px 18px;
    background: var(--kohsanuk-surface);
    border: 1px solid var(--kohsanuk-brand-soft);
    border-radius: var(--kohsanuk-radius-xl);
    box-shadow: var(--kohsanuk-shadow-card);
}
.ks-refine__hd { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 12px; }
.ks-refine__ttl {
    margin: 0; font: 600 13px/1.2 var(--kohsanuk-font-sans);
    text-transform: uppercase; letter-spacing: 0.14em; color: var(--kohsanuk-text-muted);
}
.ks-refine__n { color: var(--kohsanuk-brand-dark); font-size: 13.5px; }
.ks-refine__n b { color: var(--kohsanuk-text); font-weight: 600; }
/* PX, NOT REM — and this is the whole reason the bar's type looked broken.
   The mock is a standalone page at the browser default root of 16px, so its
   `0.7rem` label meant 11.2px. This site's bootstrap-styles.min.css sets
   html{font-size:10px}, so every rem the port carried over resolved at 62.5%:
   the labels landed at SEVEN pixels and the Search button at TEN. Restated in
   px at the size the mock actually specified. (The same defect is in the shared
   .ks-g-lab / .ks-g-count / .ks-g-go / .ks-g-reset base rules and therefore in
   the hero and the popup too — out of this lane's scope, flagged not fixed.) */
.ks-refine .ks-g-lab { font-size: 11px; margin-bottom: 5px; letter-spacing: 0.4px; }
.ks-refine .ks-g-inp,
.ks-refine .ks-g-sel,
.ks-refine .select2-selection--single { min-height: 44px; }
.ks-refine .ks-band--where { margin-top: 10px; }
.ks-refine .ks-band--how { margin-top: 10px; padding-top: 10px; }
.ks-refine .ks-price { padding: 6px 12px 5px; }
.ks-refine .ks-price__hist { height: 16px; margin-bottom: 3px; }

/* ─── THE TWO THINGS THE BAR EXISTS FOR ─────────────────────────────────
   Submitting, and saying how many trips are left. Both were the smallest
   objects on the surface. Neither gets more PADDING — this bar is re-submitted
   repeatedly while scanning results and 44px controls are the density contract
   — they get WEIGHT: real type sizes, and a fill that separates them from the
   nine neutral fields around them. */
.ks-refine .ks-foot { margin-top: 14px; align-items: center; }
/* 44px tall exactly as before. The growth is horizontal + typographic: the
   label was 10px on an 80px button, which is why it read as a chip rather than
   as the action the whole form points at. --kohsanuk-shadow-pop is the existing
   accent-tinted lift (0 6px 18px --kohsanuk-accent-shadow), so the CTA sits
   above the card the way the site's other orange actions do. */
.ks-refine .ks-g-go {
    min-height: 44px; padding: 11px 34px;
    font-size: 16px; letter-spacing: 0.01em;
    box-shadow: var(--kohsanuk-shadow-pop);
}
/* The lift belongs to the enabled CTA only: search-dialog.js disables this
   button whenever the count is zero, and a dead control must not still float. */
.ks-refine .ks-g-go[disabled] { box-shadow: none; }
/* THE RESULT COUNT — the feedback the entire form exists to produce, so it is
   stated as a readout rather than as a caption. Brand tint + brand-soft hairline
   is the pairing the form's own notice bar (.ks-g-notice) already uses, so this
   introduces no colour: teal says "this is the engine talking", and orange stays
   reserved for focus and the CTA.

   NOT display:flex — the count is a translated string with the number
   interpolated into it ("<b>168</b> trips match"), and locales put the number in
   the middle. Flex would turn each run of text either side into its own item and
   gap them apart. Inline-block keeps it one sentence.

   :empty is load-bearing. The span is empty on first paint, empty with JS off,
   and emptied again by showLoadError() — without this an empty bordered stub
   would sit in the footer in all three cases. */
.ks-refine .ks-g-count {
    display: inline-block;
    padding: 6px 14px;
    background: var(--kohsanuk-brand-tint);
    border: 1px solid var(--kohsanuk-brand-soft);
    border-radius: var(--kohsanuk-radius-pill);
    color: var(--kohsanuk-text);
    font-size: 14px; font-weight: 600; line-height: 1.25;
}
.ks-refine .ks-g-count:empty { display: none; }
.ks-refine .ks-g-count b { color: var(--kohsanuk-brand-dark); font-size: 16px; font-weight: 700; }
/* Reset is the count's row-mate; left at 0.85rem (=8.5px here) it would read as
   a rendering fault beside a 14px readout. */
.ks-refine .ks-g-reset { font-size: 13px; }
/* The refine bar is the one surface whose job is SUMMARISING the search, so it
   is the only one that marks which controls are doing the filtering. An active
   filter is already legible from the value sitting in the control, so it is
   marked with WEIGHT on the label — never by ringing the field orange. Orange
   is the focus marker and returns on :focus via the shared recipe, which is
   exactly where it belongs. */
.ks-refine .ks-f--on > .ks-g-lab { color: var(--kohsanuk-text); font-weight: 700; }

/* ═══ RESPONSIVE ════════════════════════════════════════════════════════
   Breakpoints are the repo's real set only: 991.98 / 767.98 / 575.98 / 400 /
   380 / 375, plus the two desktop compaction tiers below, which are justified
   by measured hero height budgets rather than by the site's layout grid and are
   flagged as such. (A previous revision added 760px and 560px under a comment
   claiming they were "the repo's existing set". They were not.) Nothing here is
   allowed to shrink a touch target to fit — width caps and reflow absorb the
   loss instead. */

/* 992–1399.98px: the tightest desktop budgets on the site. The cookie banner
   leaves 331px of usable panel height at 1024x768 and 363px at 1280x800; the
   roomy tier measures 378px. Everything here is COMPACTION, not re-layout:
   still four across, still three bands, nothing hidden. */
@media (min-width: 992px) and (max-width: 1399.98px) {
    .ks-hero-search { padding: 12px; }
    .ks-g-lab { font-size: 12px; margin-bottom: 5px; }
    .ks-band--where { margin-top: 10px; }
    .ks-band--how { margin-top: 10px; padding-top: 10px; }
    .ks-price { padding: 8px 12px 6px; }
    .ks-price__hist { height: 18px; margin-bottom: 4px; }
    .ks-foot { margin-top: 8px; }
}
/* 992–1199.98px only: 1024x768 is the single tightest viewport on the site.
   The tier above lands at 348px; these five declarations recover the last 17px. */
@media (min-width: 992px) and (max-width: 1199.98px) {
    .ks-hero-search { padding: 10px; }
    .ks-band--q { padding: 8px; }
    .ks-price { padding: 6px 12px 5px; }
    .ks-price__hist { height: 16px; margin-bottom: 3px; }
    .ks-foot { margin-top: 6px; }
}
/* .ks-search-grid's responsive ladder is NOT here: its base rule lives in the
   search-results section far below, so an override at this point in the file
   loses the cascade (equal specificity, earlier source). It now sits directly
   after that base rule. Same for .ks-tok / .ks-tok__x. */
/* 768–991.98: the hero CANNOT grow (inline min-height:50vh + .view{overflow:hidden})
   and clips SILENTLY. Measured headroom at 768x1024 was 2.8px, so the free-text
   box and the route pair each take a full row rather than sharing one. */
@media (min-width: 768px) and (max-width: 991.98px) {
    .ks-f--q { flex: 1 1 100%; }
    .ks-f--type { flex: 1 1 200px; }
    .ks-f--route { flex: 1 1 100%; }
}
/* <=767.98px: ONE control per row, always. A 2-up at 336px of inner width gives
   156px per control, below the 200px floor a labelled select needs for Thai
   place names. This is also the only breakpoint where the Advanced fold exists
   (see .ks-more above). */
@media (max-width: 767.98px) {
    .ks-hero-search {
        max-width: 100%; margin-top: 16px; padding: 12px;
        background: var(--ks-g-panel-bg-mob);
        border-radius: var(--kohsanuk-radius-lg);
    }
    .ks-band { gap: 12px; }
    .ks-band--q { padding: 8px; }
    .ks-band--where { margin-top: 12px; }
    .ks-band--how { margin-top: 12px; padding-top: 12px; }
    .ks-f,
    .ks-f--q, .ks-f--type, .ks-f--go, .ks-f--route,
    .ks-f--mode, .ks-f--guests, .ks-f--boat, .ks-f--price { flex: 1 1 100%; }
    .ks-g-go { width: 100%; font-size: 16.8px; }
    /* The route stacks. align-items MUST be reset: the base rule's flex-end
       aligns on the CROSS axis, which flips from vertical to horizontal when
       the direction becomes column — leaving both legs half-width and
       right-aligned. */
    .ks-route { flex-direction: column; gap: 10px; align-items: stretch; }
    .ks-foot { margin-top: 12px; }
    /* Full-screen sheet. Safe-area padding keeps the footer clear of the home bar. */
    .ks-pop {
        left: 0; top: 0; transform: none;
        width: 100%; max-width: none; height: 100%; max-height: none;
        border-radius: 0;
    }
    .ks-pop__bd { padding-bottom: calc(22px + env(safe-area-inset-bottom)); }
    /* PHONE VERTICAL RHYTHM (surface 2). The desktop bar is dense on purpose —
       it is read alongside results — but stacked into one column that density
       collapses into a wall of controls, worst of all directly under the
       "Refine these results" header, which stopped reading as a heading at all.
       Loosened here only. */
    /* The band's gutter tightens while the CARD's own padding stays loose: on a
       phone the frame has to earn its vertical cost, and the rhythm the block
       below buys is inside the card, not around it. */
    .ks-refine { padding: 12px 0 16px; }
    .ks-refine__in { padding: 16px 16px 18px; }
    .ks-refine__hd { margin-bottom: 20px; }
    .ks-refine .ks-band { gap: 18px; }
    .ks-refine .ks-band--where,
    .ks-refine .ks-band--how { margin-top: 18px; padding-top: 18px; }
    .ks-refine .ks-g-lab { margin-bottom: 10px; }
    .ks-refine .ks-g-go { margin-top: 4px; }
    .ks-refine .ks-more { margin-top: 16px; }
    .ks-refine .ks-foot { margin-top: 18px; }
}
@media (max-width: 575.98px) {
    .ks-price__v { font-size: 17px; }
    .ks-price__l { display: none; }  /* the full-domain reading is the first thing to go */
    /* .ks-search-grid / .ks-tok / .ks-tok__x moved below their base rules — see
       the note at the 991.98 band above. */
}
@media (max-width: 400px) {
    .ks-pop__hd, .ks-pop__bd { padding-left: 16px; padding-right: 16px; }
}
@media (max-width: 380px) {
    .ks-hero-search { padding: 10px; }
    .ks-g-lab { font-size: 12px; }
}

/* ─── REDUCED MOTION ────────────────────────────────────────────────────
   The chevron rotation is STATE, not decoration — it is the only non-textual
   signal that a menu is open, so it survives as a discrete flip rather than
   being removed with the transitions. */
@media (prefers-reduced-motion: reduce) {
    .ks-more__ic,
    .ks-price__hist i,
    .select2-selection__arrow svg { transition: none; }
    .select2-container--open .select2-selection__arrow svg { transform: rotate(180deg); }
}

/* ─── FORCED COLORS ─────────────────────────────────────────────────────
   Translucency and box-shadow both stop existing in forced-colors mode, and
   every state in the menu is expressed by one or the other — so `selected` and
   `highlighted` would become indistinguishable. Restate each with a system
   colour and, for `selected`, an outline (which forced-colors keeps). */
@media (forced-colors: active) {
    .ks-hero-search, .ks-menu--glass, .ks-menu--solid, .ks-price, .ks-refine, .ks-pop {
        background: Canvas !important; color: CanvasText !important;
        border: 1px solid CanvasText !important;
        -webkit-backdrop-filter: none !important; backdrop-filter: none !important;
        box-shadow: none !important;
    }
    .select2-selection--single { background: Field !important; color: FieldText !important; border: 1px solid FieldText !important; }
    .select2-results__option { border-bottom: 1px solid CanvasText !important; }
    .select2-results__option--highlighted { background: Highlight !important; color: HighlightText !important; }
    .select2-results__option[aria-selected="true"] { outline: 2px solid CanvasText; outline-offset: -3px; }
    .select2-results__option[aria-disabled="true"] { color: GrayText !important; }
    .ks-price__hist i { background: GrayText !important; }
    .ks-price__hist i.lit { background: Highlight !important; }
    .ks-price__fl { background: Highlight !important; }
    .ks-price__sl input[type="range"]::-webkit-slider-thumb { background: CanvasText !important; }
    .ks-price__sl input[type="range"]::-moz-range-thumb { background: CanvasText !important; }
    .ks-refine .ks-f--on > .ks-g-lab { color: CanvasText !important; text-decoration: underline; }
    /* The refine bar's frame moved from the full-bleed band to the inner card.
       The shared panel rule above borders every panel SURFACE, .ks-refine among
       them, which would now draw a full-width box around the card as well — so
       the band gives its border up and the card restates one. Declared after
       that rule, and .ks-refine-scoped, so neither lane treads on the other. */
    .ks-refine { border: 0 !important; }
    .ks-refine__in {
        background: Canvas !important; color: CanvasText !important;
        border: 1px solid CanvasText !important; box-shadow: none !important;
    }
    /* Tint and hairline both stop existing here, and they are the only things
       marking the count as a readout. Restate it as an outlined chip. */
    .ks-refine .ks-g-count { background: Canvas !important; border: 1px solid CanvasText !important; }
}

/* ═══ RESULTS-PAGE CHROME (unchanged by this rework) ════════════════════
   Kept verbatim: the token row and the results grid are not part of the search
   FORM and are still rendered by search/parts/result-tokens.blade.php and
   search/index.blade.php.

   NB the approved mock argues the refine bar ABSORBS the token row — a chip
   reading "Private" directly above a select reading "Private" states the same
   filter twice, and a populated control already IS the chip. If the markup
   change drops search/parts/result-tokens.blade.php, .ks-rbar / .ks-tok* below
   become dead and should be removed in that same change; they are retained here
   only because deleting styles for a partial that still exists would break the
   page. .ks-search-results / -count / -grid / -empty stay regardless. */
.ks-rbar {
    position: sticky; top: var(--ks-nav-h); z-index: 1020;
    padding: 12px 0; background: var(--kohsanuk-surface);
    border-bottom: 1px solid var(--kohsanuk-rule);
}
.ks-rbar__in { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.ks-rbar__lb {
    font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--kohsanuk-text-muted);
}
.ks-rbar__sp { flex: 1; }
.ks-tok {
    display: inline-flex; align-items: center; gap: 8px;
    min-height: 38px; padding: 0 6px 0 14px;
    background: var(--kohsanuk-cream-200); border: 1px solid var(--kohsanuk-rule);
    border-radius: var(--kohsanuk-radius-pill);
    font-family: inherit; font-size: 14px; font-weight: 500; color: var(--kohsanuk-text);
}
.ks-tok b { font-weight: 700; }
.ks-tok__x {
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; flex: none; border: 0; border-radius: 50%;
    background: var(--kohsanuk-rule); color: var(--kohsanuk-text-muted);
    font-size: 13px; line-height: 1; text-decoration: none; cursor: pointer;
}
.ks-tok__x:hover, .ks-tok__x:focus { background: var(--kohsanuk-text-muted); color: #fff; text-decoration: none; }
.ks-tok--edit {
    gap: 6px; padding: 0 16px; background: var(--kohsanuk-surface);
    border-color: var(--kohsanuk-brand); color: var(--kohsanuk-brand-dark);
    font-weight: 600; cursor: pointer;
}
/* left on --kohsanuk-brand-tint deliberately: this is pre-existing shipped code
   preserved verbatim. It carries the same classic-only-token hazard described
   in this section's header (coral under midnight-lagoon / driftwood), but
   fixing it is not this change's business — noted so it is not mistaken for
   new. */
.ks-tok--edit:hover, .ks-tok--edit:focus { background: var(--kohsanuk-brand-tint); }

.ks-search-results { padding: 22px 0 40px; }
.ks-search-count { margin: 0 0 14px; font-size: 14px; color: var(--kohsanuk-text-muted); }
/* TWO columns, not three, and the ladder mirrors .tour-card-grid on the category
   pages (category-pages.css:236 + :892) because .tour-card is the SAME component
   on both surfaces: an image-left / body-right horizontal card whose price row
   ("Join-in group from 2,200 ฿ /person") needs ~490px to set. A third column
   pushed the card to 321.7px at 1280 and the card's own overflow:hidden then
   clipped "/person" off 11 rows — silently, because the clipping ancestor is the
   card, so the text node's own scrollWidth still matched its clientWidth. */
.ks-search-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.ks-search-empty { padding: 44px 0 52px; text-align: center; }
.ks-search-empty h2 { margin: 0 0 6px; font: 600 23px/1.25 'Fraunces', Georgia, serif; color: var(--kohsanuk-text); }
.ks-search-empty p { max-width: 54ch; margin: 0 auto; font-size: 15px; color: var(--kohsanuk-text-muted); }

/* Pager separation. The pager used to sit at a MEASURED ZERO-pixel gap from the last
   card row — .ks-search-grid has no bottom margin and Bootstrap's .pagination has no
   top one — so it read as a fourth, malformed card rather than as the end of the list.

   40px is not a new step: it is the value .ks-search-results already sets as its own
   bottom padding (`padding: 22px 0 40px` above). Reusing it puts the pager in a
   symmetric 40px band — 40 above from this rule, 40 below from the section's padding —
   so the block is centred in its own whitespace instead of introducing a magic number
   that agrees with nothing else on the page. It also clears the grid's 18px inter-card
   gap by more than 2x, which is what makes the break read as "list ended" rather than
   "another row".

   Held at every width on purpose, with no responsive override: .ks-search-results'
   bottom padding has none either, so the symmetry above survives the 768px single-column
   switch. Measured at 1440 / 1024 / 390 — see the verification note in index.blade.php.

   Margin, not padding, and on the wrapper rather than the <ul>: the wrapper is what the
   hasPages() guard removes, so a single-page result set loses the spacing along with the
   pager instead of leaving a 40px void under the last row. No collapse risk — the
   preceding sibling grid has no bottom margin to collapse with. */
.ks-search-pager { margin-top: 40px; }

/* Responsive overrides for the search-results section MUST live here, after the
   base rules above — not up in the responsive bands earlier in this file. Media
   queries add no specificity, so an override placed before its own base rule
   ties on specificity and loses on source order. All four declarations below
   were previously in those earlier bands and were dead: the grid stayed at three
   columns down to 360px (84px cards, 42 clipped text nodes) and the touch
   targets never grew. */
/* 768, NOT 767.98: this band has to INCLUDE exactly 768px. Two rules earlier in
   this file are @media (max-width: 768px) and tighten the results gutter there,
   which drops the two-column card from 364px (769px and up, clean) to 339px —
   below the width the price row needs, so "from N ฿ /person" overflowed its card
   by 11px on 18 nodes at precisely the commonest tablet-portrait width. The card
   clips it silently: overflow:hidden lives on .tour-card, so the text node's own
   scrollWidth still equals its clientWidth and nothing reports the loss. 767.98
   would not have closed the gap — at a 768px viewport neither 767 nor 767.98
   matches. Measured clean at every width from 360 to 1440 after this change. */
@media (max-width: 768px) {
    .ks-search-grid { grid-template-columns: 1fr; }
}
@media (max-width: 575.98px) {
    /* Token row: 38 -> 44px and the remove target 26 -> 32px on touch. */
    .ks-tok { min-height: 44px; }
    .ks-tok__x { width: 32px; height: 32px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   SURFACE 2, PART 2 — /search: THE FACETS AS A LEFT SIDEBAR      (>=992px)
   ═══════════════════════════════════════════════════════════════════════════
   The refine bar was a full-width horizontal band above the results. Every
   filter change meant scrolling back up to a control that had left the screen,
   which is the one thing a results page must not ask for. So the facets move
   into a sticky left column beside the results and stay reachable, while the
   free-text box and the Search button stay a full-width row above BOTH columns
   — they are the search, not a refinement of it.

   SCOPING. Every rule below is under .ks-search-layout, which exists only in
   search/index.blade.php. The hero (/) and the popup (every other page) share
   this form partial and these ks-* classes and are deliberately unreachable
   from here — verified by measuring both before and after.

   THE ONE STRUCTURAL PROBLEM, AND WHY IT IS SOLVED THIS WAY.
   The form's DOM order is q, TYPE, go, …facets…, reset — type sits BETWEEN the
   two controls that have to stay in the top row. So no single wrapper can
   enclose "the sidebar" without also enclosing the free-text box, and the hero
   cannot give type up either: .ks-band--q is one glass row and q + type + go
   must share it. Both facts are fixed. The way out is that only ONE of the two
   regions needs to be a real box:
     · the top row does NOT — on .ks-solid the band already paints nothing
       (background:none, padding:0), so q and go can simply be placed cells;
     · the sidebar DOES — sticky, max-height and overflow all need an element.
   So the wrapper chain down to .ks-band--q becomes display:contents, which puts
   q / type / go / .ks-side directly into this grid as siblings, and .ks-side
   (added in the partial, inert everywhere else) is the box that carries the
   card, the stickiness and the scroll.
   ═══════════════════════════════════════════════════════════════════════════ */

/* INERT BY DEFAULT — hero, popup, and /search below 992px. display:contents
   keeps .ks-side out of the box tree so its children lay out as direct children
   of the form, exactly as they did before the wrapper existed. Same technique
   and same reason as .ks-adv. */
.ks-side { display: contents; }

/* THE COUNT IS STATED ONCE — but WHICH element states it is tier-dependent, and
   that is why the rule hiding the form's own readout is NOT here. It is inside
   the 992px block below, beside the .ks-foot it sits in.

   >=992px the sidebar auto-applies, so the engine may run the page-level figure
   (.ks-search-count[data-search-total], above the results) ahead of the grid: it
   is a preview of a navigation that is already scheduled. There the form's readout
   would be the same sentence twice, so it is hidden and the page-level element is
   the one that is live and the one that announces.

   <992px NOTHING replaces it. writeCount()'s page-level sink is gated on
   autoApplyOn(), so below the breakpoint the SERVER's figure stands until Search
   is pressed while the form's readout is the only element the engine keeps
   current. Hiding it at top level — which is what shipped — therefore deleted the
   tier's only live count outright: measured at 390px, the readout went "67 trips
   match" -> "19 trips match" inside a display:none box while the visible page
   figure stayed on the stale server string. It also deleted the only live REGION,
   because that span carries aria-live="polite" and a display:none subtree is never
   announced — so screen-reader users below 992px were told nothing at all.

   The invariant is one live element at every width, never two and never none:
   >=992px the page-level count (aria-live in index.blade.php), <992px the form's
   readout. Do not add aria-live to a second element. */

/* CLEARANCE UNDER THE FIXED NAVBAR, for the title band on THIS PAGE ONLY.
   .ks-nav is fixed and 64px tall, and .category-hero was starting at exactly
   y=64 — the two touched.

   It looks like a bug in the shared partial and is not. category-pages.css
   clears the navbar with `main#main { padding-top: 80px }`, and the partial's
   other two consumers — categories-index.blade.php and layouts/subindex.blade.php
   — both render inside that <main>, so they already sit at y=80 with 16px of
   clearance. Measured on /activities and /activities/snorkeling: hero top 80,
   navbar bottom 64, correct on both. /search is the odd one out because it
   extends layouts/web-app, which emits no <main> at all, so the one rule that
   was supposed to do this never applied to it.

   Fixing it in the shared partial would therefore have ADDED 16px to the two
   pages that are already right — regressing them to 32px — to fix the one that
   is wrong. So the fix is scoped here, and 16px is not a new number: it is the
   exact clearance the siblings get from that 80px, which lands /search's band at
   y=80 like theirs and makes the three consistent rather than the reverse. */
.ks-search-head { padding-top: 16px; }
/* ...and clearance UNDER it. The breadcrumb/title band's bottom edge and the free-text
   label's top edge were both landing on the same y (measured 0px of gap at 1440), so the
   label read as part of the band rather than as the start of the search. 28px is not a
   new number: it is the row-gap .ks-search-layout already uses between its own rows, so
   the band, the search row and the refine heading share one vertical rhythm. Set on the
   HEAD rather than on the layout because the head is the element that owns the space
   around itself here, and .ks-search-layout is neutralised below 992px. */
.ks-search-head { padding-bottom: 28px; }
/* :empty is load-bearing — index.blade.php renders this element empty at a server
   total of zero (the empty-state panel below says it in full) and the engine
   empties it again on a catalogue load error. */
.ks-search-count:empty { display: none; }
/* THIS ELEMENT HAS TWO WRITERS AND THEY MUST LOOK THE SAME. index.blade.php
   renders it as a plain escaped string — trans_choice('translate-search.n-results')
   is "67 trips", no markup, and {{ }} would escape any — while search-dialog.js
   writes "<b>5</b> trips match" from the first facet change onward.

   Styling that <b>, which is what shipped here, styled a child the server never
   produces: the page's headline count sat at 14px/400/muted on load and jumped to
   16px/700/teal the moment the user touched a control — and back again on the next
   apply, because applyNow() restores the server's plain string when no navigation
   follows. So the count re-styled itself on every cycle. The emphasis is dropped
   rather than the mismatch tolerated, and the <b> is explicitly neutralised
   because the UA's own font-weight:bold would otherwise keep half of it.

   The .ks-refine .ks-g-count b precedent does NOT transfer, and that was the
   mistake: the form's readout is rendered EMPTY by the blade and only ever written
   by the engine, so it has no server-rendered state to disagree with. This one has.

   TO PUT THE EMPHASISED FIGURE BACK, fix the server half first — wrap the number
   in <b> in search/index.blade.php so both writers emit the same shape — and only
   then restore, in BOTH mirrors:
       .ks-search-count b { color: var(--kohsanuk-brand-dark); font-size: 16px; font-weight: 700; }
   Never restore it without the Blade half; that is exactly the bug above. */
.ks-search-count b { font: inherit; color: inherit; }

@media (max-width: 991.98px) {
    /* BELOW THE SIDEBAR TIER NOTHING CHANGES. .ks-search-layout is only a
       wrapper here, so .ks-refine keeps the full-bleed band it has today and
       .ks-search-results keeps its own .container. The .container class on the
       wrapper is neutralised rather than omitted from the markup, because at
       >=992px it is exactly what aligns the two columns with the page gutter. */
    .ks-search-layout.container { width: 100%; max-width: none; padding: 0; }
}

@media (min-width: 992px) {
    .ks-search-layout {
        /* SIDEBAR WIDTH — set by the longest real option, measured rather than
           guessed. "Koh Tan & Koh Madsum (46)" sets at 207.3px in the control's
           16px font (the engine appends the live count, so the suffix is part of
           the string), and select2's closed control spends 58px of its own width
           on 2px of border, 14+4px of text padding and a 38px chevron column:
           265.3px of control. Add 2x16px of card padding, and 16px more for the
           facet list's own scrollbar — which is inside the card, so it eats
           control width exactly when the list is long enough to need it. Floor is
           314px. 328 leaves room for a three-digit count and for the longest
           label, "How do you want to travel" at 186.6px — labels are
           nowrap+ellipsis (.ks-g-lab), so a tight column would silently truncate
           them rather than wrap. */
        /* WIDENED for the two-column control pairs. 464 fits two ~205px fields plus the
           14px gap and 2x16px card padding; the old 328 only ever held one column. */
        --ks-side-w: 464px;
        /* The navbar is position:fixed and 64px tall — measured on this page,
           and the figure --ks-nav-h already carries above 992px. +16px so the
           card clears it instead of tucking its first control underneath. */
        --ks-side-top: calc(var(--ks-nav-h) + 16px);
        /* Bottom clearance for the sticky card. 24px normally; while the consent
           banner is up it is 16px off the viewport bottom and 149.6px tall
           (measured at both 1440 and 992 — it is max-width:760px and centred, so
           it overlaps this column's lower right corner), and the card must stop
           above it rather than put Reset underneath. .ks-cookie is rendered with
           v-if, so its presence in the DOM is a true test of whether the banner
           is showing. Browsers without :has() keep the 24px reserve and the
           banner overlaps the card's corner — which is what the full-width bar
           does today, so the fallback is the status quo, not a regression. */
        --ks-side-bot: 24px;

        display: grid;
        /* THREE columns, not two. The Search button belongs at the right end of
           the free-text row, which spans the full width, so it needs a track of
           its own; the results then span the two right-hand tracks. */
        grid-template-columns: var(--ks-side-w) minmax(0, 1fr) auto;
        /* Row 2 is the "REFINE" / "168 trips" line — one row, both columns, so
           the section label and the count sit on a shared baseline. Row 3 is
           flexible and holds the whole result list; the sidebar lives in it too
           and that is what gives the sticky card something to travel through, a
           sticky grid item being confined to its own grid area. Because the
           results span the flexible track they are skipped when the auto rows
           are sized, so row 2 stays as short as its own content. */
        grid-template-rows: auto auto 1fr;
        /* ONE gutter in both axes. There was no row-gap at all, so the free-text
           input's bottom and the "REFINE THESE RESULTS" heading's top were both
           at y=249 — a 0px seam where the page's two halves ran into each other.
           28px is not a new number: it is the column gutter this grid already
           declares, so the horizontal and vertical rhythm are the same measure.
           It also replaces the 14px margin-bottom that .ks-refine__hd was
           carrying to fake the row-2/row-3 gap — that margin was a workaround for
           the missing row-gap, and keeping both would have stacked 42px between
           the heading and the first control. */
        gap: 28px;
        align-items: start;
    }


    /* FLATTEN THE WRAPPERS so the fields become cells of the grid above. Each
       of these is a pure container on this surface: .ks-refine's band and
       .ks-refine__in's card are replaced by the sidebar card, and .ks-band--q
       already paints nothing under .ks-solid. */
    .ks-search-layout .ks-refine,
    .ks-search-layout .ks-refine__in,
    .ks-search-layout .ks-hero-search,
    .ks-search-layout .ks-band--q { display: contents; }

    /* ── ROW 1: the search itself, across both columns ─────────────────── */
    .ks-search-layout .ks-f--q  { grid-column: 1 / 3; grid-row: 1; }
    /* align-self:end because this cell has no label above it and would
       otherwise float to the top of a row sized by the labelled field. */
    .ks-search-layout .ks-f--go { grid-column: 3; grid-row: 1; align-self: end; }
    /* margin-bottom:0 — the grid's row-gap now spaces this off the sidebar below
       it, and .ks-refine__hd's own 12px base margin would stack on top of that.
       Zeroing it keeps the sidebar card's top edge within 6px of the first result
       card's (measured 337 vs 331 at 1440), which is closer than the 9px the
       horizontal-bar build shipped; leaving the margin in put it 17px low. */
    .ks-search-layout .ks-refine__hd { grid-column: 1; grid-row: 2; margin-bottom: 0; }

    /* ── ROW 3, LEFT: the sticky card ──────────────────────────────────── */
    /* EXACTLY ONE STICKY BOX IN THIS COLUMN, and it is the card — which is now also
       the only box in this grid cell.

       HISTORY, because it constrains what may be changed here. Type used to sit in
       this cell as a SECOND box: it lives between q and go in the shared partial, so
       it could not be moved inside .ks-side without breaking the hero's one-row band,
       and the card offset itself past type by a declared --ks-side-hd instead. Making
       type sticky too was tried and reverted: two sibling sticky boxes in one
       containing block release INDEPENDENTLY, each letting go when its own margin box
       reaches the bottom of the grid area, so a 639px card against an 81px type block
       gave 558px of scroll during which the card had released and type was still stuck,
       sliding DOWN THROUGH the card's interior and painting its label over the live
       price row (measured at 1440px; elementsFromPoint returned .ks-g-lab directly
       above .ks-price__hd). Equalising the two boxes fixes the detachment but needs a
       DEFINITE card height, and the card's natural height is a function of how many
       facets the chosen type has (331px at type=3, 448 bare, 608/688/768 at types
       1/2/4) — pinning it opened a white void of up to 437px above the Reset row.

       NONE OF THAT APPLIES NOW. The hero stopped rendering this form (the home page
       went back to its chip widget), which removed the constraint that kept type in
       the top band; type and the new category control were moved into .ks-side, so
       this cell holds one box, the card owns its own top edge, and --ks-side-hd is
       gone rather than merely kept in sync.

       WHAT IS STILL TRUE: do not add a second sticky box to this column. The failure
       above was a property of two independently-releasing sticky siblings, not of the
       particular element, so it would come back with any new one. */
    /* The card is framed the way the refine bar was, and the way the tour cards
       beside it are: --kohsanuk-brand-soft + --kohsanuk-shadow-card, the pair
       already shared by .ks-refine__in, .tour-card and the search tiles. No new
       colour, and orange stays the focus marker and the CTA only. */
    .ks-search-layout .ks-side {
        grid-column: 1; grid-row: 3;
        align-self: start;
        position: sticky; top: var(--ks-side-top);
        display: flex;
        flex-direction: column;
        /* NO TOP OFFSET ANY MORE. Type used to be a second box in this same grid
           cell, and the card had to push itself past it by --ks-side-hd (a declared
           81px) so the two stacked instead of overlapping. Type now lives INSIDE
           .ks-side, so the cell holds one box again, the card owns its own top edge,
           and the variable that had to be kept in sync with type's real height is
           gone rather than merely correct. */
        /* SCROLLS INDEPENDENTLY of the results. The card is capped at the viewport and
           keeps its own overflow, so a long results list no longer traps the filters
           (you can reach the last control without scrolling every result first). Two
           columns halve the panel's height, so on a normal viewport it fits with no
           scrollbar at all and this only engages when it genuinely cannot fit — the
           balance the earlier no-cap and hard-cap attempts each missed. min-height:0 is
           what lets a flex/grid item shrink below its content so overflow can act. */
        max-height: calc(100vh - var(--ks-side-top) - var(--ks-side-bot));
        min-height: 0;
        overflow-y: auto;
        overscroll-behavior: contain;
        padding: 16px 16px 4px;
        background: var(--kohsanuk-surface);
        border: 1px solid var(--kohsanuk-brand-soft);
        border-radius: var(--kohsanuk-radius-xl);
        box-shadow: var(--kohsanuk-shadow-card);
    }

    /* BAND 1b INSIDE THE CARD, above the scroll region.

       It is a direct child of .ks-side (not of .ks-adv), so it is the one part of the
       facet list that stays put while the rest scrolls — which is what you want of the
       two controls every other facet is conditioned on. Single column: .ks-band is a
       wrap-flex sized for a wide surface, and at 328px two 214px basis fields would each
       take a row anyway, so stating it removes the reflow rather than relying on it. */
    /* PAIR BASIS for the sidebar. The base flex-basis (214px) is tuned for the wider
       popup; in the 464px card two of them plus the 14px gap overflow by ~10px and wrap
       to one per row. calc(50% - 7px) makes exactly two share a row (7px = half the gap),
       and min-width:0 lets them shrink rather than force a wrap. Full-width fields keep
       their base flex:1 1 100% and are unaffected. */
    .ks-search-layout .ks-f--type,
    .ks-search-layout .ks-f--category,
    .ks-search-layout .ks-f--boat,
    .ks-search-layout .ks-f--guests {
        flex: 1 1 calc(50% - 7px);
        min-width: 0;
    }

    .ks-search-layout .ks-band--what {
        /* The card's 16px padding supplies the top gap; the base 14px would stack. */
        margin-top: 0;
        /* ROW-WRAP, not column: type+category share a row (each flex 1 1 214px), while
           price and the overnight checkbox are flex 1 1 100% and take their own rows.
           align-items:start so a paired field is not stretched to a taller sibling. */
        display: flex;
        flex-wrap: wrap;
        align-items: flex-start;
        gap: 14px;
        flex: 0 0 auto;
        margin-bottom: 14px;
    }

    /* THE FOLD, which no longer scrolls — see the card's height note above. It stays a
       real block (not display:contents) so it is one collapsible box, but it is sized by
       its content rather than by whatever the viewport had left over. */
    .ks-search-layout .ks-adv {
        /* A real scrolling box when OPEN — but only when open. The [hidden] twin that
           used to sit beside this selector (and the !important that went with it) existed
           to out-rank the old 768px force-open rule; both are gone, so the fold now
           collapses on the sidebar exactly as it does everywhere else. */
        display: block;
        flex: 0 0 auto;
        /* overflow-y and scrollbar-gutter both removed with the cap: with nothing
           clipping the list there is no scrollbar to reserve a gutter for, and reserving
           one would just narrow every control by 16px for no reason. */
        /* No top rule and no top margin. Both existed to separate the facet list
           from the type block that used to sit in the card's own top padding;
           with type outside the frame the list is the card's first content, and a
           hairline 14px under the card's top edge would read as a stray line
           across an otherwise empty band. The card's 16px of padding is the gap. */
        padding: 0 0 14px;
    }

    /* The bands stack. Named individually rather than as a bare .ks-band on
       purpose: .ks-band--q matches that too, and a display:flex here would
       out-rank its display:contents above on source order and pull the free-text
       box, type and Search back into one stacked box in the sidebar.

       flex-basis in a column container sets field HEIGHTS (the main axis flips),
       so every .ks-f--* basis from the horizontal layout has to be dropped rather
       than inherited — that is the whole reason for the .ks-f reset below. gap,
       not margins, so a structurally hidden facet leaves no hole behind it. */
    /* Fold bands are row-wrap too: .ks-band--how pairs boat+guests, .ks-band--where
       holds the route (full row) whose from+to pair INSIDE it. Travel style and
       overnight now live in the base band, not the fold. .ks-route keeps its own two-column split. */
    .ks-search-layout .ks-band--where,
    .ks-search-layout .ks-band--how {
        display: flex;
        flex-wrap: wrap;
        align-items: flex-start;
        gap: 14px;
    }
    .ks-search-layout .ks-route {
        display: flex;
        gap: 14px;
    }
    .ks-search-layout .ks-route__leg { flex: 1 1 0; min-width: 0; }
    /* Bands 2 and 3 are one continuous list in a single column, so the rule and
       the extra top margin that separated them horizontally would read as a
       false grouping. The list is already grouped by the card. */
    .ks-search-layout .ks-band--where,
    .ks-search-layout .ks-band--how {
        margin-top: 14px;
        padding-top: 0;
        border-top: 0;
    }
    /* WAS margin-top: 0, because this band used to be the FIRST thing in the scroll
       region and the card's own 16px padding supplied its top gap. The Advanced button
       sits above it now, so zero left the button's bottom border flush against the
       "WHERE ARE YOU STAYING" label — measured 0px at 1440. It takes the same 14px as
       band 3 above, so the two read as one evenly-spaced list. */

    /* THE FORM'S READOUT IS HIDDEN ON THIS TIER ONLY — the page-level count above
       the results is the live one here, and this would be the same sentence a few
       hundred pixels away. Below 992px it stays visible and keeps its aria-live,
       because down there nothing else is live. See the note at the head of this
       section; that is the whole reason this rule is inside the media query. */
    .ks-search-layout .ks-g-count { display: none; }

    /* RESET, pinned to the card's bottom edge. It stays with the controls that
       produced the filters, which is the half of the footer that belongs here —
       the count moved above the results. With the count hidden, space-between
       leaves Reset alone on the left, which also keeps it clear of the consent
       banner, a centred 760px box that intrudes on this column's lower right. */
    .ks-search-layout .ks-foot {
        flex: 0 0 auto;
        margin-top: 0;
        padding: 12px 0;
        border-top: 1px solid var(--kohsanuk-rule);
    }
    .ks-search-layout .ks-g-notice { flex: 0 0 auto; margin-bottom: 12px; }

    /* ── ROW 2-3, RIGHT: the results ───────────────────────────────────── */
    /* The wrapper is the .container now, so the inner one must not indent a
       second time; it keeps the class for the sub-992px layout. */
    .ks-search-layout > .ks-search-results {
        grid-column: 2 / -1;
        grid-row: 2 / -1;
        width: auto;
        max-width: none;
        padding: 0;
        margin: 0;
    }
    /* AUTO-APPLY IS IN FLIGHT — the cards on screen are about to be replaced.
       A navigation is not instant, and without this the only thing that moved
       when a facet changed was the count, several hundred pixels away from the
       control the user just touched. Fading the stale cards is the signal, and
       aria-busy is both the correct announcement for a region being replaced and
       the hook, so no invented class and no spinner. search-dialog.js sets it in
       setBusy(), and only ever on this surface.

       The COUNT is deliberately outside this selector: render() has already put
       the incoming figure there, so it is the one element that is currently
       right and it must stay legible while everything under it dims.

       pointer-events:none stops a click landing on a card that is one frame from
       being replaced by a different one. */
    .ks-search-layout .ks-search-results[aria-busy="true"] .ks-search-grid,
    .ks-search-layout .ks-search-results[aria-busy="true"] .ks-search-empty,
    .ks-search-layout .ks-search-results[aria-busy="true"] .ks-search-pager {
        opacity: .4;
        pointer-events: none;
        transition: opacity .12s ease-out;
    }

    /* ONE COLUMN of cards by default on this tier. The theme's .container is a stepped
       width — 769 / 1025 / 1341 (bootstrap-styles.min.css) — so between 992 and 1340 the
       content column is 745px or 1001px. The sidebar is --ks-side-w (464px from 1025 up,
       328px in the 992-1024.98 fallback below), and the tour card CLIPS text below ~341px
       (measured: 9 of 24 cards at 341px). A single card gets 1001 - 464 - 28 = 509px at
       1280, and 745 - 328 - 28 = 389px at 992 — both clear of the floor; a two-up grid on
       this tier would drop under it. Two-up only appears at >=1341px (below). */
    .ks-search-layout .ks-search-grid { grid-template-columns: 1fr; }
}

/* THE ONE TIER 464px DOES NOT FIT. Between 992 and 1024.98px the theme .container is
   only 769px (745px content), so a 464px sidebar + 28px gap leaves 253px for results —
   below the ~341px where the tour card starts clipping (see the width ladder below). This
   is the site's tightest viewport (1024x768 landscape iPad). Fall back to the pre-rework
   328px one-column sidebar here, which leaves 389px of results as it always did. From
   1025px up there is 1001px of content and the two-column math has 509px of headroom. */
@media (min-width: 992px) and (max-width: 1024.98px) {
    .ks-search-layout { --ks-side-w: 328px; }
    .ks-search-layout .ks-f--type,
    .ks-search-layout .ks-f--category,
    .ks-search-layout .ks-f--boat,
    .ks-search-layout .ks-f--guests { flex: 1 1 100%; }
}

/* 1341px is not a new breakpoint: it is the top step of the theme's own .container
   ladder, where the content column jumps to 1341px (1317px inside). That is the first
   width at which the 464px sidebar and a two-up card grid both fit — 1317 - 464 - 28 =
   825px of grid, so ~403px cards, clear of the 341px clipping floor. Keying the grid to
   the container's own step means the second column appears exactly when the room for it
   does. */
@media (min-width: 1341px) {
    .ks-search-layout .ks-search-grid { grid-template-columns: 1fr 1fr; }
}

/* The consent banner is up: hold the card above it. See --ks-side-bot. */
@media (min-width: 992px) {
    body:has(.ks-cookie) .ks-search-layout { --ks-side-bot: 184px; }
}
