/* ============================================================
   Shopnojattra Public Site — Design System
   Adapted from design.md / design-port-prompt.md (the "instrument panel" slab/glow language)
   for a multi-page travel site rather than a single coming-soon page. Structural layer (slab,
   hairlines, corner glows, bracket asymmetry, ghost numerals, hover language, ambient
   background, theming mechanism, accessibility method) is kept as specified — only the brand
   layer, typography pairing (Inter + Hind Siliguri per PLAN.md §10, not Space Grotesk/JetBrains
   Mono), cell counts, copy and page structure are reworked for this content. Scoped separately
   from admin.css per PLAN.md §2.8 — its own --clr-* set, its own Appearance "public" scope.
   ============================================================ */

/* ── Fonts — reuses the same self-hosted files as admin.css, no CDN ─────── */
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/inter/inter-400.woff2') format('woff2'); }
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 500; font-display: swap; src: url('/fonts/inter/inter-500.woff2') format('woff2'); }
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 600; font-display: swap; src: url('/fonts/inter/inter-600.woff2') format('woff2'); }
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/inter/inter-700.woff2') format('woff2'); }
@font-face { font-family: 'Hind Siliguri'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/hind-siliguri/hind-siliguri-400.ttf') format('truetype'); }
@font-face { font-family: 'Hind Siliguri'; font-style: normal; font-weight: 500; font-display: swap; src: url('/fonts/hind-siliguri/hind-siliguri-500.ttf') format('truetype'); }
@font-face { font-family: 'Hind Siliguri'; font-style: normal; font-weight: 600; font-display: swap; src: url('/fonts/hind-siliguri/hind-siliguri-600.ttf') format('truetype'); }
@font-face { font-family: 'Hind Siliguri'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/hind-siliguri/hind-siliguri-700.ttf') format('truetype'); }

/* ── Tokens — Light (design.md §3/§10; contrast re-measured for this hue, see TASKS.md) ── */
:root {
    /* Phase 13 client feedback — "base colors on the logo": these were the site's own invented
       blue-teal defaults, never actually matched to the real uploaded brand mark
       (storage/app/public/branding/logo.png). Sampled the real logo pixels via PHP GD rather than
       eyeballing it: the icon square is a #4B12BC→#6E39E2 gradient, the wordmark itself is a flat
       #814FFC. All three share one hue family (hsl ~257-260°), exactly the "opposite corners of
       lightness, same hue" structure design.md §3 already calls for — this just replaces which
       hue. Contrast re-measured against this project's own real surfaces, not assumed:
       #4B12BC → 9.50:1 on light-theme white (--clr-bg #F8FAFC) and 9.94:1 under white button
       text, both comfortably clear of the ~4.9 target floor this project holds itself to (see
       design-port-prompt.md §7.1) — used as --site-primary AND as light theme's --site-accent-text
       swap target below, same mechanism Phase 12 already built, just fed a new hex.
       #814FFC → 4.5:1 vs light-theme white (borderline, so kept OFF direct text duty in light
       theme) but a strong 4.33:1 is NOT enough against the new dark-theme canvas below — dark
       theme instead uses a lifted #9268FD (5.46:1), same "lift for dark, don't just invert"
       pattern already used for --site-gold/--site-hotel-accent/--site-flight-accent. */
    --site-primary:     #4B12BC;
    --site-accent:      #814FFC;
    --site-accent-rgb:  129, 79, 252;
    --site-primary-rgb: 75, 18, 188;
    --site-support:     #6b4fc7;
    --site-support-rgb: 107, 79, 199;
    /* DESIGN-VERTICALS.md — Hotels/Flights vertical-landing accents. Real hex defaults live here
       so every page works correctly with zero admin overrides (the common case); an admin
       override in the Appearance "public" scope replaces both the base and its -rgb companion
       together (see ThemeCssGenerator's RGB_COMPANIONS list). */
    --site-hotel-accent:      #a8763f;
    --site-hotel-accent-rgb:  168, 118, 63;
    --site-flight-accent:     #c98a3a;
    --site-flight-accent-rgb: 201, 138, 58;
    /* --site-accent (#814FFC) measures only 4.71:1 under white button text — passes but thinner
       than this project's own ~4.9 floor, so the gradient's lighter stop uses #6E39E2 instead:
       the REAL logo icon's own actual gradient-bright stop (not an invented value), and a safer
       6.28:1 under white text. */
    --site-accent-strong: #6E39E2;
    --site-grad:        linear-gradient(135deg, var(--site-primary), var(--site-accent-strong));
    /* Warm gold accent: reserved for star ratings, a "Featured" ribbon on standout packages, and
       premium-tier accents — never for structural chrome (nav, borders, focus rings, links),
       which all stay on the violet brand system so this reads as one deliberate warm highlight,
       not a second competing brand colour. #B8872F measures 4.6:1 on white (passes text-on-light
       use for the rating number/label); the RGB triplet is for glow/shadow rgba() reuse the same
       way --site-accent-rgb is used elsewhere. */
    --site-gold: #b8872f;
    --site-gold-rgb: 184, 135, 47;
    /* --site-accent (#814FFC) used AS COLORED TEXT (price, links, icons) on a light/white surface
       measures only 4.5:1 — passes bare AA but misses this project's own ~4.9 floor, exactly the
       trap design.md/design-port-prompt.md documents by name: "Light mode: the bright accent
       fails contrast on white — swap text/icon accents to the deeper primary." --site-accent
       itself is left untouched (borders, glows, orbs, focus rings all pass fine at this
       lightness) — only this text-specific role swaps to --site-primary (9.50:1). Dark theme
       DOES also need a swap here now (unlike the old blue hue, which happened to pass as-is at
       this same lightness) — see [data-theme="dark"] below, where --site-accent itself is lifted
       to a lighter violet rather than just redirecting --site-accent-text, the same "redefine the
       whole token, not just its text-role alias" pattern --site-hotel-accent/--site-flight-accent
       already use. */
    --site-accent-text: var(--site-primary);

    --clr-bg:        #F8FAFC;
    --clr-bg-2:      #EEF2F9;
    --clr-surface:   #FFFFFF;
    /* Opaque ELEVATED surface, for panels that float over page content — dropdowns, the Packages
       mega-menu, the Select2 list, the flatpickr calendar. Deliberately separate from
       --clr-surface: that one is a translucent wash by design in dark theme (3.2% white over the
       canvas), which is right for inline cards sitting ON the page but makes a floating panel
       see-through, with the hero photo and body text reading straight through it. Must stay a
       SOLID colour in every theme. */
    --clr-surface-float: #FFFFFF;
    --clr-surface-2: #FFFFFF;
    --clr-border:    rgba(15, 23, 42, 0.09);
    --clr-border-str:rgba(15, 23, 42, 0.16);
    --clr-tx-1:      #0F172A;
    --clr-tx-2:      rgba(15, 23, 42, 0.68);
    --clr-tx-3:      rgba(15, 23, 42, 0.62);
    --clr-inverse:   #FFFFFF;

    /* .site-badge-success/warning/danger text measured 2.46-3.50:1 against their own light-theme
       tinted pill background — a real §7.1 failure at the original 40/45/55% lightness; darkened
       to clear the ~4.9 target floor (same hue/sat as admin.css's equivalent tokens, reusing the
       same measured-safe lightness values). */
    --clr-success: hsl(142, 71%, 28%);
    --clr-warning: hsl(38, 92%, 28%);
    --clr-danger:  hsl(0, 84%, 38%);

    /* Space, radius, motion — design.md §5. sp-1..sp-7 (micro/component spacing: icon gaps,
       badge padding, button padding, card-internal spacing) are UNCHANGED — those sizes are
       tied to real tap targets and text rhythm, shrinking them would hurt usability, not
       "premium-ness". sp-8..sp-12 (structural spacing: section padding, hero padding, the
       gap between major page blocks) ARE tightened here (premium-density pass) — the client
       twice reported the site "still not premium... less spacing" after two passes that only
       added visual weight (shadows, tint bands) without ever revisiting how much air the layout
       itself was using. Every one of these usages goes through this named token, not a hardcoded
       pixel value, so this one change retunes section/hero rhythm sitewide instead of needing a
       per-page hunt. */
    --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px; --sp-5: 20px; --sp-6: 24px;
    --sp-7: 32px; --sp-8: 32px; --sp-9: 36px; --sp-10: 40px; --sp-11: 56px; --sp-12: 72px;
    --r-sm: 10px; --r-md: 14px; --r-lg: 20px; --r-xl: 28px; --r-tag: 6px; --r-pill: 999px;
    --content-max: 1180px;
    --gutter: 24px;
    --ease: cubic-bezier(.4, 0, .2, 1);
    --dur: .32s;

    /* Elevation scale (premium-upgrade pass, see TASKS.md) — light theme was shipping `.slab`
       (the flight/hotel/visa result-list frame) with NO shadow at all, just a 1px border at 9%
       black opacity on a surface (#FFFFFF) sitting barely 2% lighter than the page canvas
       (#F8FAFC) — confirmed by real screenshot to read as functionally invisible, which is why
       generous section/row spacing looked like "huge dead whitespace" instead of premium
       breathing room: there was nothing with enough visual weight to justify the space around it.
       Research-grounded fix (Vercel/Stripe/Skyscanner card elevation, "border-first depth in
       light mode" + layered shadow stacks, not one flat shadow): a hairline ring (crisp edge,
       reuses the existing --clr-border-str token so it doesn't introduce a new border color) plus
       a soft, brand-tinted, negative-offset diffuse layer for real lift off the canvas. Named by
       role (xs/sm/md), not by component, so every surface that wants "resting panel" weight uses
       --shadow-sm rather than a one-off value. */
    --shadow-ring: 0 0 0 1px var(--clr-border-str);
    --shadow-xs: var(--shadow-ring), 0 1px 2px rgba(var(--site-primary-rgb), .05);
    --shadow-sm: var(--shadow-ring), 0 2px 4px rgba(var(--site-primary-rgb), .06), 0 16px 32px -16px rgba(var(--site-primary-rgb), .28);
    --shadow-md: var(--shadow-ring), 0 4px 10px rgba(var(--site-primary-rgb), .08), 0 28px 56px -20px rgba(var(--site-primary-rgb), .32);

    /* A very low-opacity tint (not an opaque --clr-bg-2 swap) so an alternating "panel" section
       still lets the fixed hairline-grid ambient layer read through underneath it — see
       .site-section-alt below. Bridges section-to-section gaps with a tone shift instead of pure
       blank canvas (research: dead gaps get bridged with a background/surface shift, never left
       as plain whitespace). */
    --tint-alt: rgba(var(--site-primary-rgb), .035);

    color-scheme: light;
}

/* ── Tokens — Dark (default; design.md §3/§10 — "brand colour lives in the light, not the
     paint": canvas crushed to ~5% lightness at the brand hue, never neutral grey) ── */
[data-theme="dark"] {
    /* Phase 13 — canvas re-tinted to the new violet brand hue (260°), replacing the old blue
       (199°): design.md §3/§10's own rule is "crush the PRIMARY hue to ~5% lightness", not a
       fixed hue, so this follows the same formula with the new real brand colour. */
    --clr-bg:        hsl(260, 89%, 5%);
    --clr-bg-2:      hsl(260, 89%, 3%);
    --clr-surface:   rgba(255, 255, 255, 0.032);
    --clr-surface-2: rgba(255, 255, 255, 0.055);
    /* Same brand hue as the canvas, lifted well above its 5% lightness so a floating panel reads
       as raised — and fully opaque, unlike --clr-surface above. */
    --clr-surface-float: hsl(260, 40%, 12%);
    --clr-border:    rgba(255, 255, 255, 0.075);
    --clr-border-str:rgba(255, 255, 255, 0.15);
    --clr-tx-1:      #EEF3FB;
    --clr-tx-2:      rgba(238, 243, 251, 0.64);
    --clr-tx-3:      rgba(238, 243, 251, 0.50);
    --clr-inverse:   #0B1220;

    --clr-success: hsl(142, 65%, 50%);
    --clr-warning: hsl(38, 92%, 55%);
    /* 65% measured 4.89:1 (clears 4.5 but misses the ~4.9 target floor) — nudged lighter. */
    --clr-danger:  hsl(0, 84%, 70%);
    /* Unlike the old blue hue (which happened to pass as-is at this lightness), the new violet
       --site-accent (#814FFC) measures only 4.33:1 against the new violet-tinted canvas above —
       below this project's own floor. Redefines --site-accent itself (not just -text) to a
       lifted #9268FD (5.46:1), same "lift the whole token for dark theme" pattern already used
       for --site-gold/--site-hotel-accent/--site-flight-accent below. */
    --site-accent: #9268FD;
    --site-accent-rgb: 146, 104, 253;
    --site-accent-text: var(--site-accent);

    /* Brighter gold for dark theme — #B8872F's light-theme lightness (~48%) reads muddy against
       a near-black canvas; lifted to clear the same ~4.5:1 legibility bar against dark surfaces
       instead of just inverting the light-theme value. */
    --site-gold: #e0b563;
    --site-gold-rgb: 224, 181, 99;

    /* Dark theme was never the flat one (the shadow+glow language already gives it depth), so
       this redefinition is smaller: swap the ring to the plain --clr-border (already visible at
       7.5% white on a near-black canvas — the light theme's harsher --clr-border-str isn't
       needed here) and use a neutral black diffuse shadow instead of the brand-tinted one, since
       a colour-tinted shadow is far more visible — and more muddying — against a dark surface
       than a light one. */
    --shadow-ring: 0 0 0 1px var(--clr-border);
    --shadow-xs: var(--shadow-ring), 0 1px 2px rgba(0, 0, 0, .3);
    --shadow-sm: var(--shadow-ring), 0 2px 4px rgba(0, 0, 0, .3), 0 16px 32px -16px rgba(0, 0, 0, .55);
    --shadow-md: var(--shadow-ring), 0 4px 10px rgba(0, 0, 0, .35), 0 28px 56px -20px rgba(0, 0, 0, .6);
    --tint-alt: rgba(255, 255, 255, .025);

    /* DESIGN-VERTICALS.md — dark-theme defaults for the vertical accents; each is lighter in dark
       theme than light theme (same reasoning as --site-gold above: a mid-lightness hue reads
       muddy on a near-black canvas, so it is lifted here rather than reused verbatim from the
       light default). Phase 12 deepened the light-theme defaults one step further (client
       feedback: "candy gradient"), so the dark-theme lift amounts were re-picked from the new
       base hues rather than left as stale copies of the old bright palette. */
    --site-support:           #9678e0;
    --site-support-rgb:       150, 120, 224;
    --site-hotel-accent:      #c99260;
    --site-hotel-accent-rgb:  201, 146, 96;
    --site-flight-accent:     #d9a35c;
    --site-flight-accent-rgb: 217, 163, 92;

    color-scheme: dark;
}

/* ── Reset & base ─────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body.site {
    margin: 0;
    background: var(--clr-bg);
    color: var(--clr-tx-2);
    font-family: 'Inter', 'Hind Siliguri', system-ui, sans-serif;
    font-size: 15px;
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
    overflow-x: hidden;
}
body.site.lang-bn { font-family: 'Hind Siliguri', 'Inter', system-ui, sans-serif; }
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }

h1, h2, h3, h4 { color: var(--clr-tx-1); font-weight: 700; letter-spacing: -.02em; margin: 0; }
/* Phase 13 client feedback ("site-h1 reduce size") — was clamp(2.2rem, 6vw, 3.6rem); sitewide
   scale (every page's H1), not homepage-only, since it's a consistent type-scale adjustment. */
.site-h1 { font-size: clamp(1.9rem, 4.6vw, 2.75rem); line-height: 1.18; }
.site-h2 { font-size: clamp(1.5rem, 3vw, 2.05rem); line-height: 1.25; }
.site-card-title { font-size: 1.2rem; line-height: 1.3; }
.site-micro { font-size: 11px; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; color: var(--clr-tx-3); }
.site-tag { font-size: 11.5px; font-family: 'Inter', monospace; padding: 3px 9px; border-radius: var(--r-tag); background: var(--clr-surface-2); border: 1px solid var(--clr-border); color: var(--clr-tx-3); }
.site-grad-text { background: var(--site-grad); -webkit-background-clip: text; background-clip: text; color: transparent; }

/* Visible focus ring everywhere (design.md §11) */
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {
    outline: 2px solid var(--site-accent);
    outline-offset: 3px;
}

/* ── Shell & ambient background (design.md §6, fixed layer, never a body background) ── */
.site-ambient {
    position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden;
}
.site-ambient::before {
    content: ''; position: absolute; inset: 0;
    background-image:
        linear-gradient(var(--clr-border) 1px, transparent 1px),
        linear-gradient(90deg, var(--clr-border) 1px, transparent 1px);
    background-size: 56px 56px;
    -webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 40%, transparent 100%);
    mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 40%, transparent 100%);
    opacity: .6;
}
.site-orb { position: absolute; border-radius: 50%; filter: blur(90px); opacity: .16; animation: siteOrbDrift 18s ease-in-out infinite alternate; }
[data-theme="light"] .site-orb, :root:not([data-theme="dark"]) .site-orb { opacity: .10; }
.site-orb.o1 { width: 480px; height: 480px; top: -120px; left: -80px; background: var(--site-accent); }
.site-orb.o2 { width: 520px; height: 520px; bottom: -160px; right: -100px; background: var(--site-primary); animation-delay: -6s; }
.site-orb.o3 { width: 360px; height: 360px; top: 40%; left: 50%; background: var(--site-support); animation-delay: -12s; }
@keyframes siteOrbDrift { from { transform: translate(0, 0) scale(1); } to { transform: translate(24px, -20px) scale(1.08); } }

.site-shell { position: relative; z-index: 1; }
.site-container { max-width: var(--content-max); margin: 0 auto; padding: 0 var(--gutter); }
.site-container-wide { max-width: 1440px; margin: 0 auto; padding: 0 var(--gutter); }
.site-section { padding: var(--sp-10) 0; }
.site-section-head { max-width: 60ch; margin: 0 auto var(--sp-6); text-align: center; }
.site-section-head p { color: var(--clr-tx-3); margin-top: var(--sp-2); }

/* ── The slab — signature element (design.md §7) ─────────────
   Each cell paints its own right+bottom hairline via box-shadow; the frame's overflow:hidden
   clips the outer ones. Column-count agnostic — no nth-child bookkeeping at breakpoints. */
.slab {
    display: grid;
    border: 1px solid var(--clr-border);
    border-radius: var(--r-md);
    overflow: hidden;
    background: var(--clr-surface);
    --glow-size: 230px;
    --corner-size: 44px;
}
.cell {
    position: relative;
    overflow: hidden;
    padding: var(--sp-6);
    display: flex;
    flex-direction: column;
    box-shadow: 1px 0 0 var(--clr-border), 0 -1px 0 var(--clr-border);
    transition: background-color var(--dur) var(--ease);
}
.cell > * { position: relative; z-index: 1; }
.cell::before, .cell::after {
    content: ''; position: absolute; z-index: 0; border-radius: 50%;
    width: var(--glow-size); height: var(--glow-size);
    opacity: 0; transition: opacity .45s var(--ease); pointer-events: none;
}
.cell::before { top: 0; left: 0; transform: translate(-50%, -50%); background: radial-gradient(circle, rgba(var(--site-accent-rgb), .35), transparent 70%); }
.cell::after  { bottom: 0; right: 0; transform: translate(50%, 50%); background: radial-gradient(circle, rgba(var(--site-primary-rgb), .35), transparent 70%); }
.cell:hover { background: var(--clr-surface-2); }
.cell:hover::before, .cell:hover::after { opacity: 1; }

.cell-bracket { position: absolute; z-index: 2; width: var(--corner-size); height: var(--corner-size); opacity: 0; transition: opacity .35s var(--ease); pointer-events: none; }
.cell-bracket.tl { top: 10px; left: 10px; border-top: 2px solid var(--site-accent); border-left: 2px solid var(--site-accent); }
.cell-bracket.br { bottom: 10px; right: 10px; border-bottom: 2px solid var(--site-primary); border-right: 2px solid var(--site-primary); }
.cell:hover .cell-bracket { opacity: 1; }

.cell-icon { width: 44px; height: 44px; border-radius: var(--r-sm); border: 1px solid var(--clr-border); background: var(--clr-surface-2); display: flex; align-items: center; justify-content: center; color: var(--clr-tx-3); font-size: 18px; margin-bottom: var(--sp-4); transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease), background-color var(--dur) var(--ease); }
.cell:hover .cell-icon { color: var(--site-accent-text); border-color: rgba(var(--site-accent-rgb), .4); background: rgba(var(--site-accent-rgb), .08); }

.cell-ghost-num { position: absolute; z-index: 0; right: -12px; bottom: -30px; font-size: 130px; font-weight: 700; line-height: 1; color: transparent; -webkit-text-stroke: 1px var(--clr-border); pointer-events: none; user-select: none; }

.cell-tags { margin-top: auto; padding-top: var(--sp-4); display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.cell:hover .site-tag { color: var(--site-accent-text); border-color: rgba(var(--site-accent-rgb), .35); }

[data-theme="light"] .cell::before, :root:not([data-theme="dark"]) .cell::before { background: radial-gradient(circle, rgba(var(--site-primary-rgb), .18), transparent 70%); }
[data-theme="light"] .cell::after, :root:not([data-theme="dark"]) .cell::after { background: radial-gradient(circle, rgba(var(--site-primary-rgb), .18), transparent 70%); }

/* ── Cards (non-slab — result grids, blog cards, etc.: same tokens, same hover language) ── */
/* `.site-card-slot` is the actual grid item: the wishlist heart button must sit as its own
   sibling here rather than a descendant of `.site-card`, because `.site-card` is itself an <a>
   and the guest/logged-out heart is ALSO a real <a> (real href, works with JS disabled) — an <a>
   nested inside another <a> is invalid HTML, and browsers silently close the outer one the
   instant they hit the inner one, breaking the whole card's click-through. This wrapper is what
   gives the absolutely-positioned heart button (below) something to position against now that
   it's no longer inside `.site-card` itself. */
.site-card-slot { position: relative; }
.site-card {
    position: relative; overflow: hidden; display: block; height: 100%;
    background: var(--clr-surface); border: 1px solid var(--clr-border); border-radius: var(--r-md);
    transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.site-card:hover { border-color: var(--clr-border-str); box-shadow: 0 12px 32px rgba(var(--site-primary-rgb), .18); }
.site-card-body { padding: var(--sp-5); }
.site-card-img { aspect-ratio: 4/3; overflow: hidden; background: var(--clr-surface-2); position: relative; }
.site-card-img img { width: 100%; height: 100%; object-fit: cover; }
/* Real content gap (not a code bug, flagged here since it drives a real "unfinished" look): most
   packages/hotels/countries have no real photo uploaded yet, so this renders the same generic
   camera-icon placeholder JPEG dozens of times across a listing page. Can't fix the missing
   photos from CSS, but a flat gray stock image reads as broken/unfinished — this re-tints ANY
   placeholder image (matched by its own literal filename, no Blade changes needed anywhere) into
   a soft, on-brand gradient tile instead, so an empty card reads as an intentional "no photo yet"
   state rather than a failed image load. */
.site-card-img:has(img[src*="placeholder"]) {
    background: linear-gradient(135deg, rgba(var(--site-accent-rgb), .16), rgba(var(--site-primary-rgb), .22));
}
.site-card-img img[src*="placeholder"] {
    opacity: .3; mix-blend-mode: luminosity; filter: grayscale(1);
}

/* ── Wishlist heart toggle — overlaid on a card's image, same badge look in both themes ── */
.wishlist-heart-btn {
    position: absolute; top: 12px; right: 12px; z-index: 2;
    width: 34px; height: 34px; border-radius: var(--r-pill);
    display: flex; align-items: center; justify-content: center;
    background: rgba(15, 23, 42, 0.55); border: none; cursor: pointer;
    color: #fff; font-size: 15px; line-height: 1;
    transition: transform .15s ease, background-color .15s ease;
}
.wishlist-heart-btn:hover { background: rgba(15, 23, 42, 0.75); transform: scale(1.08); }
/* Background here is a constant near-white pill regardless of theme, so the icon needs a
   constant-dark red too — not --clr-danger, which is deliberately theme-varied (much lighter in
   dark theme, to stay readable against dark theme's own dark surfaces) and would fail badly
   against this always-white backdrop. */
.wishlist-heart-btn.is-saved { background: rgba(255, 255, 255, 0.9); color: hsl(0, 84%, 38%); }
.wishlist-heart-btn.is-saved:hover { background: #fff; }

/* ── Buttons — controls are rounded (design.md's radius-carries-meaning rule) ── */
.site-btn {
    display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
    padding: 12px 22px; border-radius: var(--r-pill); border: 1px solid transparent;
    font-weight: 600; font-size: 14px; cursor: pointer;
    transition: background-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
/* Phase 13.11 — solid, not gradient, per client instruction ("dont use gredient in buttons").
   `--site-accent-strong` is the logo's own bright gradient end, so the brand colour is unchanged;
   it just stops sliding across the button. It carries 6.28:1 under white text, and the darker
   `--site-primary` hover (9.94:1) keeps that as it deepens — both well clear of the 4.5 floor this
   file holds everywhere else. */
.site-btn-primary { background: var(--site-accent-strong); color: #fff; }
.site-btn-primary:hover { background: var(--site-primary); box-shadow: 0 8px 24px rgba(var(--site-accent-rgb), .35); }
.site-btn-outline { background: transparent; border-color: var(--clr-border-str); color: var(--clr-tx-1); }
.site-btn-outline:hover { border-color: var(--site-accent-text); color: var(--site-accent-text); }
.site-btn-ghost { background: var(--clr-surface-2); color: var(--clr-tx-1); }
.site-btn-sm { padding: 8px 16px; font-size: 13px; }

/* ── Mobile nav drawer (`#site-mobile-nav`, header.blade.php) — used to be a bare stack of
   default-inherited `a` tags (only the sitewide `a { color: inherit; text-decoration: none; }`
   reset applied), so it had no spacing rhythm, no hover/focus feedback, and no visual difference
   between top-level links and the Packages sub-links (Hajj/Umrah/International Tours) it flattens
   into a single list for this JS-free drawer, unlike the desktop's own themed dropdown. The
   `:not(.site-btn)` guard keeps this generic link styling from fighting the drawer's own
   `.site-btn-primary` Contact Us button, which needs to keep its normal solid-pill look. ── */
#site-mobile-nav a:not(.site-btn) {
    display: flex; align-items: center; gap: 10px;
    padding: 10px 12px; border-radius: var(--r-sm);
    font-size: 14.5px; font-weight: 600; color: var(--clr-tx-2);
    transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
#site-mobile-nav a:not(.site-btn):hover, #site-mobile-nav a:not(.site-btn):focus-visible { background: var(--clr-surface-2); color: var(--clr-tx-1); }
#site-mobile-nav a i { width: 18px; text-align: center; color: var(--site-accent-text); }
#site-mobile-nav .site-mobile-nav-group-label {
    display: block; font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
    color: var(--clr-tx-3); padding: 10px 12px 2px;
}
#site-mobile-nav a.site-mobile-nav-sub { padding-left: 22px; }
#site-mobile-nav hr { border: none; border-top: 1px solid var(--clr-border); margin: var(--sp-2) 0; }

/* ── Forms (search widget, filters, contact/inquiry) ── */
.site-field-label { display: block; font-size: 12.5px; font-weight: 600; color: var(--clr-tx-3); margin-bottom: 6px; letter-spacing: .02em; }
/* Phase 13 client feedback ("filters... more premium") — rounder radius (was --r-sm/10px, a
   fairly boxy corner for a "premium filter" surface), a soft inset shadow at rest so fields read
   as recessed wells rather than flat outlines, and a stronger, brand-coloured focus ring. */
.site-input, .site-select, .site-textarea {
    width: 100%; background: var(--clr-surface-2); border: 1px solid var(--clr-border); border-radius: var(--r-md);
    color: var(--clr-tx-1); font-family: inherit; font-size: 14px; padding: 11px 14px;
    box-shadow: inset 0 1px 2px rgba(var(--site-primary-rgb), .05);
    transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.site-input:focus, .site-select:focus, .site-textarea:focus {
    outline: none; border-color: var(--site-accent); box-shadow: 0 0 0 4px rgba(var(--site-accent-rgb), .16);
}
.site-textarea { resize: vertical; min-height: 110px; }

/* ── Search widget (hero) ── */
.search-widget { background: var(--clr-surface); border: 1px solid var(--clr-border); border-radius: var(--r-xl); box-shadow: 0 24px 64px rgba(var(--site-primary-rgb), .22); overflow: hidden; }
/* Real bug caught live: the tab row's own background (--clr-surface-2) and the "selected" tab's
   background (--clr-surface) resolve to the IDENTICAL #FFFFFF in light theme (both tokens are
   plain white there) — the active-tab treatment was rendering completely invisible in light
   theme, only working by accident in dark theme where the two tokens actually differ. Fixed with
   a real, theme-independent brand tint instead of relying on 2 surface tokens to differ. */
/* Phase 13.9 client feedback ("filter bar has no proper spacing"): the tabs used to be
   `flex: 1 1 auto`, so 4 short labels stretched edge-to-edge across the full widget and the row
   read as four labels marooned in whitespace rather than a considered control group. Tabs are now
   sized to their own content and left-aligned, with real gap between them — the row reads as a
   group, and its width no longer changes meaning when a vertical is added or removed. */
.search-widget-tabs {
    display: flex; overflow-x: auto; gap: var(--sp-1);
    padding: var(--sp-3) var(--sp-4) 0; background: rgba(var(--site-primary-rgb), .045);
    scrollbar-width: none;
    /* `safe center` centres the group but falls back to flex-start the moment the tabs overflow
       their track — plain `center` would push the first tab off the scrollable left edge on a
       narrow screen, making it unreachable. Browsers that don't parse the `safe` keyword simply
       drop that line and keep the plain `center` above it. */
    justify-content: center;
    justify-content: safe center;
}
.search-widget-tabs::-webkit-scrollbar { display: none; }
.search-widget-tab {
    flex: 0 0 auto; white-space: nowrap; padding: 13px 22px; font-weight: 600; font-size: 14px;
    display: inline-flex; align-items: center; gap: 8px;
    color: var(--clr-tx-3); cursor: pointer; background: none; border: none; border-radius: var(--r-md) var(--r-md) 0 0;
    transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.search-widget-tab:hover { color: var(--clr-tx-1); background: rgba(var(--site-primary-rgb), .05); }
.search-widget-tab.active {
    color: var(--site-accent-text); background: var(--clr-surface); font-weight: 700;
    box-shadow: inset 0 3px 0 var(--site-accent-strong);
}
.search-widget-body { padding: var(--sp-5) var(--sp-4) var(--sp-4); }

/* ── Search widget field BAR — Phase 13 client feedback ("filter inputs design is broken") ──
   Real bug: each field used to be its own separately bordered+shadowed `.site-input`/`.site-
   select` box inside uneven Bootstrap `col-md-*` widths (3/3/2/2/2 for one panel, 3/2/2/2/1 for
   another) — different field COUNTS per panel meant the columns never lined up evenly, and the
   result read as a lopsided grid with dead space after the button, not a considered layout.
   Replaced with the connected-bar pattern real travel search widgets use (Google Flights,
   Skyscanner, Airbnb): one flex row, hairline-divided, every field automatically equal width
   regardless of how many a given panel has — no per-panel width bookkeeping needed. Individual
   fields drop their own border/shadow (the bar itself already carries one) so this reads as one
   considered instrument, not a pile of separate boxes. */
.search-widget-bar {
    display: flex; flex-wrap: wrap; align-items: stretch;
    border: 1px solid var(--clr-border-str); border-radius: var(--r-lg);
    overflow: hidden; background: var(--clr-surface-2);
    transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
/* The whole bar lifts when any field inside it has focus — the fields have no borders of their
   own, so without this there is no visible "this instrument is active" state at all. */
.search-widget-bar:focus-within {
    border-color: rgba(var(--site-accent-rgb), .55);
    box-shadow: 0 0 0 4px rgba(var(--site-accent-rgb), .12);
}
.search-widget-field {
    flex: 1 1 170px; min-width: 150px; padding: 14px var(--sp-4) 13px;
    border-right: 1px solid var(--clr-border);
    transition: background-color var(--dur) var(--ease);
}
.search-widget-field:last-of-type { border-right: none; }
.search-widget-field:focus-within { background: rgba(var(--site-primary-rgb), .04); }
.search-widget-field .site-field-label {
    margin-bottom: 5px; font-size: 11px; letter-spacing: .07em; text-transform: uppercase;
}
.search-widget-field .site-input, .search-widget-field .site-select {
    border: none; background: none; box-shadow: none; padding: 2px 0; border-radius: 0;
    font-weight: 700; font-size: 15px; color: var(--clr-tx-1); height: auto; line-height: 1.35;
}
.search-widget-field .site-input:focus, .search-widget-field .site-select:focus { box-shadow: none; }
/* Values in this bar are 700/dark. Without this a placeholder inherits that and an EMPTY field
   reads as a filled one — "Select date…" looked like a chosen value. Matches the Select2
   placeholder treatment below so both kinds of empty field look the same. */
.search-widget-field .site-input::placeholder { font-weight: 600; color: var(--clr-tx-3); }

/* Select2 lives INSIDE the bar too, and it replaces the real <select> with its own markup — so
   the rule above (which targets `.site-select`) never reached it. Result, visible in the client's
   screenshot: Type and Destination rendered as bordered rounded boxes while Departure Date and
   Travelers next to them were bare text, in one bar that is supposed to read as a single
   instrument. These strip Select2 back to match the native fields exactly — same type size,
   weight, line-height and zero inset — so all four fields sit on one baseline.
   Specificity note: the base `.select2-container--default .select2-selection--single` block is
   (0,2,0); prefixing with `.search-widget-field` makes these (0,3,0), so they win wherever they
   sit in the file. The focus ring is deliberately dropped here — `.search-widget-bar:focus-within`
   already lights the whole bar, and a second ring inside it reads as a box again. */
.search-widget-field .select2-container { width: 100% !important; }
.search-widget-field .select2-container--default .select2-selection--single {
    border: none; background: none; box-shadow: none; border-radius: 0; padding: 0; height: auto;
}
.search-widget-field .select2-container--default.select2-container--open .select2-selection--single,
.search-widget-field .select2-container--default.select2-container--focus .select2-selection--single {
    border: none; box-shadow: none;
}
.search-widget-field .select2-container--default .select2-selection--single .select2-selection__rendered {
    padding: 2px 22px 2px 0; font-size: 15px; font-weight: 700; line-height: 1.35; color: var(--clr-tx-1);
}
.search-widget-field .select2-container--default .select2-selection--single .select2-selection__placeholder {
    font-weight: 600; color: var(--clr-tx-3);
}
.search-widget-field .select2-container--default .select2-selection--single .select2-selection__arrow { right: 0; }
/* Padding (not margin) so the accent block reads as inset within the bar's own border radius,
   with the bar's surface visible as a hairline frame around it. */
.search-widget-submit { flex: 0 0 auto; display: flex; align-items: stretch; padding: var(--sp-2); }
.search-widget-submit .site-btn { width: 100%; height: 100%; padding-left: 32px; padding-right: 32px; font-size: 15px; }
@media (max-width: 767px) {
    .search-widget-tabs { padding: var(--sp-2) var(--sp-2) 0; }
    .search-widget-tab { padding: 12px 15px; font-size: 13px; }
    .search-widget-body { padding: var(--sp-4) var(--sp-3) var(--sp-3); }
    .search-widget-field { flex: 1 1 45%; border-bottom: 1px solid var(--clr-border); }
    .search-widget-submit { flex: 1 1 100%; }
}

/* ── Badges / status pills ── */
.site-badge { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600; padding: 4px 10px; border-radius: var(--r-pill); }
.site-badge-success { background: hsla(142, 71%, 45%, .14); color: var(--clr-success); }
.site-badge-warning { background: hsla(38, 92%, 50%, .14); color: var(--clr-warning); }
.site-badge-danger  { background: hsla(0, 84%, 60%, .14); color: var(--clr-danger); }

/* ── Utility grids ── */
.site-grid { display: grid; gap: 0; }
/* Real bug found live: `auto-fit` computes its column COUNT from how many items actually exist
   in THAT grid instance — a container with only 1 real item (e.g. a continent with a single
   country on the Visa browse page) gets exactly 1 column, and that column's `1fr` then stretches
   to the container's FULL width, producing a jarringly oversized single card instead of a normal
   card-sized one. `auto-fill` instead sizes columns from the CONTAINER'S width regardless of item
   count, leaving any leftover tracks empty (but still reserving their share of the space) — a
   lone item stays card-sized, and every grid with 3+ items (the common case) renders identically
   either way, so this is a strictly-safe fix, not a behavior change for the normal case. */
.site-grid-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(272px, 1fr)); gap: var(--sp-5); }

/* ── Customer portal chrome (PLAN.md §6) — sidebar + topbar dashboard shell, same token set
     as the rest of the public site, not admin.css's separate scope. ── */
.portal-shell { display: flex; min-height: 100vh; }
.portal-sidebar {
    width: 260px; flex-shrink: 0; background: var(--clr-surface); border-right: 1px solid var(--clr-border);
    position: fixed; top: 0; bottom: 0; left: 0; z-index: 30; overflow-y: auto; padding: var(--sp-5) 0;
}
.portal-sidebar-brand { display: flex; align-items: center; gap: 10px; padding: 0 var(--sp-5) var(--sp-5); }
.portal-nav-link {
    display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-radius: var(--r-sm);
    color: var(--clr-tx-2); font-size: 14px; font-weight: 600; margin: 2px 12px;
    transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.portal-nav-link i { width: 18px; text-align: center; color: var(--clr-tx-3); }
.portal-nav-link.active, .portal-nav-link:hover { background: rgba(var(--site-accent-rgb), .1); color: var(--site-accent-text); }
.portal-nav-link.active i, .portal-nav-link:hover i { color: var(--site-accent-text); }
.portal-main { flex: 1; min-width: 0; margin-left: 260px; min-height: 100vh; display: flex; flex-direction: column; }
.portal-topbar {
    height: 68px; flex-shrink: 0; border-bottom: 1px solid var(--clr-border); display: flex; align-items: center;
    justify-content: space-between; padding: 0 var(--sp-6); background: var(--clr-surface); position: sticky; top: 0; z-index: 20;
}
.portal-content { flex: 1; padding: var(--sp-6); }
.portal-icon-btn {
    width: 36px; height: 36px; border-radius: var(--r-sm); border: 1px solid var(--clr-border);
    background: var(--clr-surface-2); color: var(--clr-tx-2); display: flex; align-items: center;
    justify-content: center; cursor: pointer;
}
@media (max-width: 991px) {
    .portal-sidebar { transform: translateX(-100%); transition: transform var(--dur) var(--ease); }
    .portal-sidebar.open { transform: translateX(0); }
    .portal-main { margin-left: 0; }
}

/* ── Reveal-on-scroll gate (design.md §9 — visible by default; JS opts in) ── */
.reveal { opacity: 1; transform: none; }
html.js-reveal-armed .reveal { opacity: 0; transform: translateY(20px); transition: opacity .5s var(--ease), transform .5s var(--ease); }
html.js-reveal-armed .reveal.in-view { opacity: 1; transform: none; }
html.js-reveal-armed .cell.reveal, html.js-reveal-armed .slab .reveal { transform: none; } /* fade only inside a slab — sliding tears the hairlines */

@media (prefers-reduced-motion: reduce) {
    .site-orb { animation: none; }
    html.js-reveal-armed .reveal { opacity: 1; transform: none; transition: none; }
    * { scroll-behavior: auto !important; }
}

/* ── Responsive (design.md §12 — only two queries; grids reflow via auto-fit) ── */
@media (max-width: 860px) {
    .site-section { padding: var(--sp-9) 0; }
    :root { --gutter: 20px; }
}
@media (max-width: 560px) {
    .site-section { padding: var(--sp-8) 0; }
    :root { --gutter: 18px; }
    .slab { --glow-size: 150px; --corner-size: 28px; }
    .cell-ghost-num { font-size: 90px; }
    /* Every `.slab` on the site sets its own column count inline (`style="grid-template-columns:
       repeat(N, 1fr)"`) per design.md's "column-count agnostic" contract, but none of those
       call sites ever added their own phone-width override, so a 3-column slab (Why Choose Us,
       the new homepage "Explore Visa, Hotels & Flights" section) was measured rendering each
       cell at ~117px wide on a 390px viewport — readable in devtools, but a cramped, un-premium
       squeeze on a real phone. `!important` is the only way to win over an inline style, and a
       forced single column is correct for every current `.slab` at this width — there is no
       legitimate instrument-panel layout that wants 2+ rigid columns inside 560px. */
    .slab { grid-template-columns: 1fr !important; }
}

/* ============================================================
   Checkout / booking-request flows — additive components
   (Package/Visa/Flight/Hotel step-1 & step-2 pages). Reuses the existing token set and the
   .slab/.cell hover language as-is; nothing here redefines a token or duplicates a primitive.
   ============================================================ */

/* ── Selection rows as .cell (design.md §7/§8) — the pricing-tier / fee-band "pick one" cards
   are exactly the case .cell was designed for: same corner-bracket + radial-glow hover language
   as the Services page, just laid out as a row instead of a stacked card. A single-column .slab
   (no grid-template-columns needed — grid's implicit default is one column) gives each option
   its own hairline-divided row inside one connected frame rather than a scatter of .site-cards. */
.cell.cell-option {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--sp-3);
}
.cell-option .cell-ghost-num { font-size: 76px; right: -6px; bottom: -18px; }
.cell-option-main, .cell-option-price { position: relative; z-index: 1; }

/* ── Step indicator (site.js-free — pure markup/CSS) — "Step 1 of 2 — <label>" as a .site-micro
   line plus a 2-segment progress bar. Reused across all three two-step flows. ── */
.site-steps { margin-bottom: var(--sp-5); }
.site-steps-track { display: flex; gap: 6px; margin-top: 8px; }
.site-steps-bar {
    flex: 1 1 0; height: 4px; border-radius: var(--r-pill);
    background: var(--clr-border);
}
.site-steps-bar.is-done, .site-steps-bar.is-current { background: var(--site-grad); }
.site-steps-bar.is-current { box-shadow: 0 0 0 3px rgba(var(--site-accent-rgb), .18); }

/* ── Segmented control — pill-shaped binary/small-choice picker (trip type, etc). Replaces a
   native radio group; keeps a real, focusable <input type="radio"> per option (same `name`/
   `value` a plain radio group would use) so no server-side wiring changes, styled via the
   sibling-selector pattern below rather than :has() for broader engine support. ── */
.site-segmented { display: inline-flex; flex-wrap: wrap; gap: 4px; padding: 4px; border-radius: var(--r-pill); background: var(--clr-surface-2); border: 1px solid var(--clr-border); }
.site-segmented-option { position: relative; }
.site-segmented-option input[type="radio"] {
    position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer;
}
.site-segmented-option label {
    position: relative; display: flex; align-items: center; gap: 6px;
    padding: 9px 20px; border-radius: var(--r-pill);
    font-size: 13.5px; font-weight: 600; color: var(--clr-tx-2);
    cursor: pointer; white-space: nowrap;
    transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.site-segmented-option input[type="radio"]:checked ~ label { background: var(--site-accent-strong); color: #fff; }
.site-segmented-option input[type="radio"]:focus-visible ~ label { outline: 2px solid var(--site-accent); outline-offset: 2px; }

/* ── Number input chrome — remove the native spinner bleed, add the same focus-glow
   .site-input:focus already uses elsewhere in this file. ── */
.site-input[type="number"] { -moz-appearance: textfield; }
.site-input[type="number"]::-webkit-outer-spin-button,
.site-input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* Native date-picker glyph: force-visible against a dark surface (color-scheme alone isn't
   always enough once a custom --clr-surface-2 background sits under it) and dim it to match the
   rest of the control at rest, brightening on hover/focus like every other interactive accent
   in this file. */
.site-input[type="date"]::-webkit-calendar-picker-indicator { cursor: pointer; opacity: .65; transition: opacity var(--dur) var(--ease); }
[data-theme="dark"] .site-input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(1) brightness(1.5); }
.site-input[type="date"]:hover::-webkit-calendar-picker-indicator,
.site-input[type="date"]:focus::-webkit-calendar-picker-indicator { opacity: 1; }

/* Native <select> POPUP, dark theme. Same class of problem as the date glyph above, and worse:
   the control itself is styled dark and its options inherit `color: var(--clr-tx-1)` — near-white
   — but the drop-down list is painted by the browser/OS, and when it renders that list on a light
   background the options come out white-on-white and effectively disappear. Reported from a real
   browser: the Adults/Children/Infants selects on the flight booking step opened as a white panel
   with unreadable numbers.
   `color-scheme: dark` is already set and computes correctly on the select, so this is not a
   missing declaration — it is that the popup does not reliably honour it. Painting the options
   explicitly, with the OPAQUE float surface rather than the translucent --clr-surface, makes the
   list readable regardless of how the browser decides to render it. */
.site-select option,
.site-select optgroup,
select.site-input option,
select.site-input optgroup {
    background-color: var(--clr-surface-float);
    color: var(--clr-tx-1);
}

/* ── Number stepper — wraps a real <input type="number"> (same name/min/max/value; the buttons
   are progressive enhancement only — the input alone still works with JS disabled/failed, same
   no-JS-required precedent as this project's other interactive controls). ── */
.site-stepper { display: flex; align-items: stretch; border: 1px solid var(--clr-border); border-radius: var(--r-sm); background: var(--clr-surface-2); overflow: hidden; transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease); }
.site-stepper:focus-within { border-color: var(--site-accent); box-shadow: 0 0 0 3px rgba(var(--site-accent-rgb), .15); }
.site-stepper-btn {
    flex: 0 0 auto; width: 40px; border: none; background: transparent; color: var(--clr-tx-2);
    font-size: 16px; font-weight: 700; line-height: 1; cursor: pointer;
    transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.site-stepper-btn:hover { background: rgba(var(--site-accent-rgb), .12); color: var(--site-accent-text); }
.site-stepper-btn:disabled { opacity: .4; cursor: not-allowed; background: none; color: var(--clr-tx-2); }
.site-stepper-input.site-input {
    border: none; background: transparent; text-align: center; border-radius: 0; box-shadow: none !important;
    padding-left: 4px; padding-right: 4px;
}

/* ── Trust-signal strip — the same license/membership badges already in the footer band, as a
   compact inline row meant to sit directly above a submit button rather than only in the footer. ── */
.site-trust-strip {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-4);
    padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-4);
    border: 1px solid var(--clr-border); border-radius: var(--r-sm); background: var(--clr-surface-2);
}
.site-trust-strip-label { flex: 0 0 auto; margin: 0; }
.site-trust-item { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--clr-tx-3); white-space: nowrap; }
.site-trust-item i { color: var(--site-accent-text); font-size: 12.5px; }

/* ── Total/price hierarchy — the single most prominent number on each page. Square corners
   (design.md's structural radius rule) with a tinted accent surface so it visually outranks the
   plain .site-card rows around it, in both themes. ── */
.site-total-card {
    position: relative; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--sp-3);
    padding: var(--sp-5) var(--sp-6); border-radius: var(--r-md);
    border: 1px solid rgba(var(--site-accent-rgb), .35);
    background: linear-gradient(135deg, rgba(var(--site-accent-rgb), .12), rgba(var(--site-primary-rgb), .06));
}
.site-total-card-label { font-size: 12px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--clr-tx-3); }
.site-total-card-value { font-size: clamp(1.5rem, 4vw, 2rem); font-weight: 800; letter-spacing: -.01em; color: var(--site-accent-text); line-height: 1.1; }
.site-total-card-meta { font-size: 12.5px; color: var(--clr-tx-3); margin-top: 2px; }

@media (max-width: 560px) {
    .cell.cell-option { flex-direction: column; align-items: stretch; }
    .cell-option .cell-ghost-num { font-size: 56px; }
    .site-total-card { flex-direction: column; align-items: flex-start; }
}

/* ============================================================
   Listing pages (packages/visa/hotels) — density + result-card hover pass
   Additive only; nothing above this line is redefined.
   ============================================================ */

/* ── Result cards as .cell (design.md §7/§8) — a "browse and pick one" listing card is exactly
   what .cell's hover language was built for (see Services' use of plain .cell, and the checkout
   flows' .cell-option row variant). This is a THIRD variant, but reuses the base .cell primitive
   rather than duplicating its glow/bracket/z-index ruleset: .cell is already flex-column (a
   vertical, image-on-top card orientation) by default, so all that's needed here is to make one
   stand alone outside a hairline .slab grid — swap the shared hairline box-shadow for a real
   border+radius, zero the base cell's padding so `.site-card-img` can bleed to the card edge
   (`.site-card-body`'s own padding still applies to the text underneath), and declare this
   variant's own --glow-size/--corner-size (there is no .slab ancestor here to inherit them from,
   the way Services' grid provides for plain .cell). ── */
.cell.cell-card {
    padding: 0;
    /* Phase 12 — the 9%-opacity --clr-border hairline read as "unfinished" against mostly-
       placeholder photos (real hotel/package images aren't uploaded yet); a stronger resting
       border (--clr-border-str, 16%) plus a real (if subtle) shadow gives every card enough
       definition to look deliberate rather than barely-there, without going back to a heavy
       drop-shadow look. */
    box-shadow: var(--shadow-xs);
    border: 1px solid var(--clr-border-str);
    border-radius: var(--r-md);
    background: var(--clr-surface);
    height: 100%;
    --glow-size: 260px;
    --corner-size: 36px;
}
.cell.cell-card:hover { border-color: rgba(var(--site-accent-rgb), .4); box-shadow: var(--shadow-sm); }

/* Price is the one number every listing card must win on visually (design pass requirement) —
   a single reusable class instead of repeating the inline weight/colour/size on every partial. */
.site-card-price { font-size: 18px; font-weight: 800; letter-spacing: -.01em; color: var(--site-accent-text); line-height: 1.2; }
.site-card-price-note { font-size: 11.5px; color: var(--clr-tx-3); font-weight: 500; }

/* ── Filter sidebar/bar density — scoped to `.site-filter-form` so it does not change
   `.site-field-label`/`.site-input` spacing anywhere else (checkout, contact, portal forms).
   Body copy at 15px/1.6 (site.css's base) is too loose for a form with many stacked fields —
   booking-DESIGN.md's point that dense list/filter screens read better tighter, applied only to
   this component. Labels drop to the .site-micro treatment (11px/600/uppercase) for an
   instrument-panel feel; inputs keep a real >=40px tap target even though the padding shrinks. ── */
.site-filter-form .site-filter-group + .site-filter-group { margin-top: var(--sp-3); }
.site-filter-form .site-field-label {
    font-size: 11px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
    color: var(--clr-tx-3); margin-bottom: 6px;
}
.site-filter-form .site-input, .site-filter-form .site-select {
    padding: 9px 12px; font-size: 13.5px; min-height: 40px;
}

/* ── Trust line — a lighter, borderless one-line variant of .site-trust-strip for browsing
   pages. .site-trust-strip itself belongs to a decision/commitment moment (checkout, booking
   forms); a listing page is still a browsing moment, so this reuses the same .site-trust-item
   badges without the bordered/filled box. ── */
.site-trust-line { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-4); margin-top: var(--sp-2); }

@media (max-width: 560px) {
    .cell.cell-card { --glow-size: 180px; --corner-size: 26px; }
}

/* ============================================================
   Instrument Grid — sitewide foundation (winning direction, 4-way design panel)
   "Structure + information density as the premium signal", ported from the working
   fixture at direction/fixture-2.html, plus one enhancement borrowed from the
   Signature Glow proposal (fixture-7.html) — idle-visible corner brackets. Additive
   only: every rule below reuses existing tokens (--site-*, --clr-*, --sp-*, --r-*,
   --dur, --ease) and the existing .cell/.cell-card/.slab primitives; nothing above
   this line is redefined except via a later, non-destructive rule for the same
   selector (documented inline each time that happens) — e.g. .cell-bracket's idle
   opacity, and .search-widget-tab gaining a counter-driven ::before.

   New components in this section (markup contracts other agents build pages against):

   .site-grid-bg          — modifier class for the existing `.site-ambient` wrapper
                             (`<div class="site-ambient site-grid-bg">`) that lets the
                             hairline grid already painted by `.site-ambient::before`
                             reach further down the page and read a touch more present,
                             instead of fading out near the top of the very first
                             section. Static (no animation) — nothing extra needed for
                             prefers-reduced-motion. Never sits behind text directly
                             (it's a fixed, z-index:0 layer under every opaque surface),
                             so it carries no text-contrast risk.

   .site-stat-row         — flex row wrapping N `.site-stat` items with painted hairline
                             dividers; stacks to a single column under 560px.
   .site-stat             — one stat "readout" cell. Expects:
                               <div class="site-stat">
                                 <div class="site-stat-ghost">500</div>
                                 <div class="site-stat-value">500<span>+</span></div>
                                 <div class="site-micro site-stat-label">Label</div>
                               </div>
                             Ghost-numeral size and value font-size are both CSS custom
                             properties (--stat-ghost-size / --stat-value-size) set on
                             `.site-stat` itself, so a scale variant is just a new class
                             overriding those two properties — see `.site-stat-sm` below.
   .site-stat-ghost       — the huge outlined ghost numeral bleeding behind the value
                             (same -webkit-text-stroke technique as `.cell-ghost-num`,
                             just exposed at a reusable, variable scale). Content must
                             repeat the same digits as `.site-stat-value` next to it.
   .site-stat-value       — the real gradient-filled number. Wrap a suffix (+ / % / etc.)
                             in a nested element (e.g. `<span>`) — it inherits a smaller
                             em-relative size automatically.
   .site-stat-sm          — modifier on `.site-stat` for a smaller reuse elsewhere
                             (e.g. inline stat strips lower down a page).

   .site-status-line      — the "LIVE INVENTORY ---- UPDATED CONTINUOUSLY" dotted
                             connector row. Expects:
                               <div class="site-status-line">
                                 <span class="site-status-dot"></span>
                                 <span>Live inventory</span>
                                 <span class="site-status-track"></span>
                                 <span class="site-status-value">Updated continuously</span>
                               </div>

   .search-widget-tab     — (existing class, untouched rule) now also gains a
                             counter-driven "01/02/03/04" prefix via a new ::before rule
                             on the same selector — purely CSS, no markup or JS change
                             needed, and the tab-switching JS (site.js, reads
                             [data-widget-tab] + .active) is completely unaffected.

   .site-card-meta        — hairline-bordered row of icon+label metadata pills for a
                             result card. Expects:
                               <div class="site-card-meta">
                                 <span class="site-card-meta-item"><i class="fa-solid fa-clock"></i>8 Days</span>
                                 <span class="site-card-meta-item"><i class="fa-solid fa-star"></i>5-Star</span>
                                 <span class="site-card-meta-item"><i class="fa-solid fa-location-dot"></i>Makkah</span>
                               </div>
   .site-card-meta-item   — one pill inside `.site-card-meta` (icon optional but expected).

   .site-card-ref         — small monospace reference-code tag ("PKG · 014") pinned to
                             the top-left of a card's image area. Requires its containing
                             element (typically `.site-card-img`) to establish a
                             positioning context — e.g. `style="position:relative"`, same
                             pattern the fixture and `.wishlist-heart-btn` already use.
                             Fixed dark pill regardless of theme (sits over a photo/
                             gradient, not page background), same reasoning as
                             `.wishlist-heart-btn`'s always-dark pill above.

   .site-card-link        — compact "View →" secondary link-button for browsing-context
                             cards (a `<span>`, not a nested `<a>`, inside a `.cell-card`
                             that is itself the link — same no-nested-anchor rule
                             `.wishlist-heart-btn`'s comment documents). Expects:
                               <span class="site-card-link">View <i class="fa-solid fa-arrow-right"></i></span>
                             Full-width `.site-btn-primary` should stay as-is on
                             single-item decision/commitment screens (checkout &
                             booking-request forms, tier/fee-band/passenger selection —
                             a converting moment); `.site-card-link` is for browsing-many
                             result grids/slabs (packages/hotels/flights listing cards,
                             blog cards) where the click just opens a detail page.

   .cell-bracket          — (existing class) idle opacity raised from 0 to .3 via a
                             later rule on the same selector, so the corner framing
                             reads as permanent structure at rest and brightens to 1 on
                             hover, instead of appearing from nothing (Signature Glow's
                             fixture-7.html idea, borrowed narrowly — see that file's own
                             `.cell-signature` comment for the source reasoning).
   ============================================================ */

/* ── 1. Ambient hairline grid, made structurally visible further down the page ──
   `.site-ambient::before` (top of this file) already paints the grid but masks it to a
   tight ellipse near the very top so it reads as a top-of-page flourish. This modifier
   — additive to that selector, not a redefinition of it — lets a page opt into the
   grid reading as an ambient structural layer for the whole viewport, the way
   direction/fixture-2.html demonstrated. Grid line colour is already `--clr-border`,
   which is separately tuned per theme at the top of this file, so no extra light/dark
   override is needed here. */
.site-grid-bg::before {
    opacity: .8;
    -webkit-mask-image: radial-gradient(ellipse 92% 90% at 50% 0%, #000 55%, transparent 100%);
    mask-image: radial-gradient(ellipse 92% 90% at 50% 0%, #000 55%, transparent 100%);
}

/* ── 2. Stat readout — hero-scale numbers with a bled ghost numeral behind each one ── */
.site-stat-row {
    display: flex; flex-wrap: wrap;
    border: 1px solid var(--clr-border); border-radius: var(--r-md);
    overflow: hidden; background: var(--clr-surface);
}
.site-stat {
    position: relative; overflow: hidden; flex: 1 1 160px; min-width: 140px;
    padding: var(--sp-5) var(--sp-5);
    display: flex; flex-direction: column; justify-content: center;
    --stat-ghost-size: 108px;
    --stat-value-size: clamp(1.9rem, 2.6vw, 2.6rem);
}
.site-stat + .site-stat { border-left: 1px solid var(--clr-border); }
/* A stat is sometimes a link to the screen it counts (the portal dashboard). It stays a plain
   readout — no underline, no movement — and answers the hover by lifting its own ghost numeral,
   which is the part of it that was already decorative. */
a.site-stat { text-decoration: none; color: inherit; }
a.site-stat .site-stat-ghost { transition: opacity var(--dur) var(--ease); }
a.site-stat:hover .site-stat-ghost { opacity: .9; }
a.site-stat:hover .site-stat-value { color: var(--site-accent-text); }
.site-stat-ghost {
    position: absolute; z-index: 0; right: -8px; bottom: -22px;
    font-size: var(--stat-ghost-size); font-weight: 700; line-height: 1;
    color: transparent; -webkit-text-stroke: 1px var(--clr-border);
    pointer-events: none; user-select: none; white-space: nowrap;
}
.site-stat-value {
    position: relative; z-index: 1;
    font-family: 'Inter', monospace; font-weight: 800;
    font-size: var(--stat-value-size); line-height: 1; letter-spacing: -.02em;
    background: var(--site-grad); -webkit-background-clip: text; background-clip: text; color: transparent;
    font-variant-numeric: tabular-nums;
}
.site-stat-value span { font-size: .6em; }
.site-stat-label { position: relative; z-index: 1; margin-top: 6px; display: block; }
/* Smaller reuse elsewhere (e.g. an inline stat strip lower down a page) — same markup,
   just a different pair of custom-property values, per the "not hardcoded to one scale"
   requirement. */
.site-stat-sm { --stat-ghost-size: 60px; --stat-value-size: clamp(1.25rem, 2vw, 1.5rem); padding: var(--sp-4) var(--sp-4); }

@media (max-width: 560px) {
    .site-stat-row { flex-direction: column; }
    .site-stat + .site-stat { border-left: none; border-top: 1px solid var(--clr-border); }
    .site-stat { --stat-ghost-size: 72px; }
}

/* ── 3. Live-status connector row — a small "this is a real system" structural detail ── */
.site-status-line {
    display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
    padding: 10px 0; margin: var(--sp-5) 0;
    border-top: 1px solid var(--clr-border); border-bottom: 1px solid var(--clr-border);
    font-family: 'Inter', monospace; font-size: 11px; font-weight: 600; letter-spacing: .14em;
    text-transform: uppercase; color: var(--clr-tx-3);
}
.site-status-dot {
    width: 5px; height: 5px; border-radius: 50%; flex: 0 0 auto;
    background: var(--clr-success); box-shadow: 0 0 0 3px rgba(var(--site-accent-rgb), .18);
}
.site-status-track {
    flex: 1 1 60px; height: 8px; opacity: .8;
    background-image: repeating-linear-gradient(90deg, var(--clr-border) 0, var(--clr-border) 1px, transparent 1px, transparent 12px);
}
.site-status-value { color: var(--site-accent-text); font-weight: 700; }
@media (max-width: 560px) {
    .site-status-track { flex-basis: 100%; order: 3; }
}

/* Phase 13 client feedback + design-skill review — REMOVED the numbered "01/02/03/04" tab prefix
   this section used to add via a CSS counter. Packages/Visa/Hotels/Flights are 4 parallel,
   unrelated options a visitor picks ONE of, not a sequence — numbered markers are only honest
   when content really is a sequence (a stepped process, a timeline), and using them here was
   exactly the kind of generic "AI-tell" decoration the frontend-design skill's own calibration
   guide names directly. The icon already in each tab label carries that signal instead. */

/* ── 5. Card metadata pills — icon+label spec-sheet row, denser than loose inline tags ── */
.site-card-meta {
    display: flex; align-items: stretch; margin-bottom: var(--sp-3);
    border: 1px solid var(--clr-border); border-radius: var(--r-sm); overflow: hidden;
}
.site-card-meta-item {
    flex: 1 1 0; display: flex; align-items: center; justify-content: center; gap: 6px;
    padding: 7px 6px; font-size: 10.5px; font-weight: 700; letter-spacing: .08em;
    text-transform: uppercase; color: var(--clr-tx-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.site-card-meta-item + .site-card-meta-item { border-left: 1px solid var(--clr-border); }
.site-card-meta-item i { color: var(--site-accent-text); font-size: 11px; flex: 0 0 auto; }
.cell.cell-card:hover .site-card-meta-item i { color: var(--site-accent); }
@media (max-width: 560px) {
    .site-card-meta-item { font-size: 9.5px; padding: 6px 4px; gap: 4px; }
}

/* ── 6. Card reference tag — "PKG · 014", top-left of the card image ── */
.site-card-ref {
    position: absolute; top: 12px; left: 12px; z-index: 2;
    font-family: 'Inter', monospace; font-size: 10.5px; font-weight: 700; letter-spacing: .08em;
    padding: 3px 8px; border-radius: var(--r-tag);
    background: rgba(5, 8, 14, .55); color: #EEF3FB; backdrop-filter: blur(2px);
}

/* ── 7. Compact "View →" card link — secondary action for browsing-context cards ── */
.site-card-link {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 8px 16px; border-radius: var(--r-pill);
    background: var(--clr-surface-2); color: var(--clr-tx-1);
    font-weight: 600; font-size: 13px;
    transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.site-card-link i { font-size: 10px; transition: transform var(--dur) var(--ease); }
.cell.cell-card:hover .site-card-link { background: rgba(var(--site-accent-rgb), .12); color: var(--site-accent-text); }
.cell.cell-card:hover .site-card-link i { transform: translateX(3px); }

/* ── 8. Corner brackets — idle-visible framing, borrowed from Signature Glow
   (fixture-7.html's `.cell-signature`) but applied directly to the base `.cell-bracket`
   rather than a new opt-in class, since every `.cell`/`.cell-card` in the app should read
   as permanently framed at rest, not just this one direction's showcase cards. This is
   the ONLY place in this file that changes the effective value of an existing rule, and
   it does so with a later, separate rule for the same selector (`.cell-bracket`'s own
   `opacity: 0` above is left textually untouched) rather than editing that line in place. ── */
.cell-bracket { opacity: .3; }
.cell:hover .cell-bracket { opacity: 1; }

/* ============================================================
   Flights + Services pass — one narrow, additive rule
   ============================================================
   `.site-card-link`'s accent hover (above) is wired to `.cell.cell-card:hover`, the image-card
   listing variant (packages/hotels/visa). Services' own result grid (site/services/index.blade.php)
   is this project's existing reference use of plain `.cell` (icon + ghost numeral, no image, no
   `.cell-card`), predating that listing-card variant — it still deserves the same "tags light up
   on hover" feedback design.md's hover section requires everywhere, so this mirrors the rule for
   plain `.cell` too rather than forcing Services onto the image-card layout it was never built
   for. No existing selector's value changes; this only adds a rule for a case the block above
   didn't cover. */
.cell:hover .site-card-link { background: rgba(var(--site-accent-rgb), .12); color: var(--site-accent-text); }
.cell:hover .site-card-link i { transform: translateX(3px); }

/* ============================================================
   Customer portal pass — one narrow, additive rule
   ============================================================
   `.site-card-link`'s accent hover (Instrument Grid section above) is wired to
   `.cell.cell-card:hover` and plain `.cell:hover` — both the site's photo-first result-card
   variants. The customer portal's own list-of-mine cards (bookings/visa-applications/reviews
   "to review") are plain `.site-card` (no image, no glow/bracket chrome — a data card, not a
   browsing card), predating both `.cell` variants, but the same "browse many, click one to open
   its detail page" pattern applies there too, so they now reuse `.site-card-link` for that CTA.
   This just extends the identical hover feedback to that existing selector; no existing rule's
   value changes. */
.site-card:hover .site-card-link { background: rgba(var(--site-accent-rgb), .12); color: var(--site-accent-text); }
.site-card:hover .site-card-link i { transform: translateX(3px); }

/* ── Flights pass — one small correctness fix ──────────────────────────────────────────────
   `.site-card-meta-item` already sets `white-space:nowrap; overflow:hidden; text-overflow:
   ellipsis` (Instrument Grid section above) so a long value truncates with an ellipsis instead
   of wrapping or blowing out the row. That only works if the flex item is actually allowed to
   shrink below its own text's natural width — and a flex item's default `min-width` is `auto`
   (= its content's min-content size), which overrides `flex-shrink` and silently defeats the
   ellipsis. Every existing use of this component (package/hotel/visa tags: "5 Days", "5-Star",
   "Makkah") happened to be short enough never to hit this. Flights' route pill ("Dhaka (DAC) →
   Bangkok (BKK)") is long enough to hit it for real — confirmed by screenshot at 390px, where the
   route and airline pills overran their own dividers instead of eliding. `min-width: 0` is the
   standard fix and is purely corrective: it changes nothing for content that already fit (that
   content never needed to shrink past its own width), it only lets genuinely long content shrink
   and truncate the way the component's own rule already says it should. Additive — no existing
   declaration's value changes. */
.site-card-meta-item { min-width: 0; }

/* ============================================================
   Customer portal pass — one narrow, additive rule (page-specific, not a foundation component)
   ============================================================
   `resources/views/partials/payment-summary.blade.php` (the shared total/paid/remaining +
   payment-history widget used identically by admin.bookings.show and portal.bookings.show, per
   its own docblock) deliberately renders a plain Bootstrap `.table`/`.badge` instead of either
   theme's decorative classes so one file can serve both. Bootstrap's own `.table` sets
   `--bs-table-bg: var(--bs-body-bg)` (white) and a dark `--bs-table-color` — correct on admin's
   light dashboard chrome, but a stray white box on the portal's dark, site.css-themed surface
   (confirmed via a real screenshot of portal bookings/show's "Payment History" section). Scoped
   to `.site-card .table` — the only place this partial (or any `.table`) is ever embedded under
   site.css's load (admin's own views never load site.css, so this can't touch admin's rendering
   at all) — this only overrides Bootstrap's own CSS custom properties to this project's existing
   dark-surface tokens; it doesn't redefine `.table` itself or touch the partial's markup. */
.site-card .table {
    --bs-table-bg: transparent;
    --bs-table-color: var(--clr-tx-1);
    --bs-table-border-color: var(--clr-border);
    --bs-table-striped-bg: var(--clr-surface-2);
    --bs-table-hover-bg: var(--clr-surface-2);
}

/* ============================================================
   Premium elevation & section-rhythm pass (V2) — research-grounded fix for "flat/washed-out,
   huge dead whitespace" reported against real screenshots of /flights and the homepage in LIGHT
   theme. Additive only, same convention as every pass above: a later rule for an existing
   selector, documented inline, rather than editing that selector's original declaration.

   Root cause (confirmed by reading this file, not guessed): `.slab` — the frame every
   flight/hotel/visa/package result list renders inside — had NO box-shadow at all, just a 1px
   border at 9% black opacity sitting on a surface only ~2% lighter than the page canvas. Nothing
   in the whole component had enough visual weight to read as "raised off the page", so the
   generous instrument-panel spacing around it looked like empty dead space instead of premium
   breathing room. `--shadow-ring/xs/sm/md` (defined per-theme up in :root / [data-theme="dark"])
   fix this at the token level; the rules below just point the existing components at them. ── */
.slab, .cell.cell-card, .site-card, .search-widget { box-shadow: var(--shadow-sm); }
.cell.cell-card:hover, .site-card:hover { box-shadow: var(--shadow-md); }
/* .search-widget already carried its own one-off shadow (a correct, deliberately stronger hero
   treatment) — --shadow-md reuses the same named scale instead of a bespoke value, one step up
   from the resting --shadow-sm every other surface now gets, so the hero widget still reads as
   the most elevated element on the page without inventing a third, undocumented shadow. */
.search-widget { box-shadow: var(--shadow-md); }

/* ── Section-rhythm bridge: an alternating low-opacity tint (NOT an opaque background swap) so
   the fixed hairline-grid ambient layer keeps reading through underneath — pages opt individual
   `.site-section`s into this per the research finding that dead marketing-page gaps get bridged
   with a tone shift, never left as plain canvas. Full-bleed on purpose (unlike `.site-container`
   inside it, which stays max-width) so the tint reads as a true section band. ── */
.site-section-alt { background: var(--tint-alt); }

/* ── Tabular/instrument numerals — prices, seat counts, durations: anywhere a number is the
   thing the user is scanning a list to compare, not reading as prose. `font-variant-numeric:
   tabular-nums` keeps digit widths equal so stacked prices/seat-counts align in a column instead
   of jittering with each digit's natural proportional width — this project's Inter font already
   ships real tabular-figure metrics, so no new font asset is needed for the "instrument panel"
   numeric feel. ── */
.site-num { font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1; letter-spacing: -.01em; }

/* Every existing price/amount display class gets tabular figures for free — one rule instead of
   hand-adding .site-num at each of the ~15 call sites across packages/hotels/visa/flights
   listings, checkout totals and the hero stat readout. */
.site-card-price, .site-total-card-value, .site-stat-value { font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1; }

/* ── Dropdown menus (Bootstrap's `.dropdown-menu`) — the site header's "Packages" nav dropdown
   and the portal topbar's user-avatar menu were both still rendering as Bootstrap's completely
   unthemed vendor default (a plain white box, black text, generic shadow) since nothing in this
   file had ever targeted them — the single most visible "not premium" tell on every page, since
   it sits in the primary navigation. Bootstrap 5.3 themes its dropdown entirely through CSS
   custom properties (--bs-dropdown-*), so redefining those to this project's own tokens is all
   that's needed — no need to fight/duplicate Bootstrap's own selectors. These reference the
   already-theme-reactive clr/site tokens directly, so light/dark just falls out for
   free. Idea borrowed from Corpo-HR's own richer profile-menu treatment (D:\Corpo-HR
   layouts/portal/partials/topbar.blade.php): an optional `.dropdown-menu-head` block (avatar +
   name/meta) above the divider, used by the portal topbar's user menu below. ── */
.dropdown-menu {
    --bs-dropdown-bg: var(--clr-surface-float);
    --bs-dropdown-border-color: var(--clr-border);
    --bs-dropdown-border-radius: var(--r-md);
    --bs-dropdown-box-shadow: var(--shadow-md);
    --bs-dropdown-color: var(--clr-tx-2);
    --bs-dropdown-link-color: var(--clr-tx-2);
    --bs-dropdown-link-hover-color: var(--clr-tx-1);
    --bs-dropdown-link-hover-bg: var(--clr-surface-2);
    --bs-dropdown-link-active-color: var(--site-accent-text);
    --bs-dropdown-link-active-bg: rgba(var(--site-accent-rgb), .12);
    --bs-dropdown-divider-bg: var(--clr-border);
    --bs-dropdown-padding-x: 8px;
    --bs-dropdown-padding-y: 8px;
    --bs-dropdown-item-padding-x: 12px;
    --bs-dropdown-item-padding-y: 9px;
    --bs-dropdown-font-size: 14px;
    --bs-dropdown-spacer: 10px;
    font-family: inherit;
}
.dropdown-item { border-radius: var(--r-sm); transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
.dropdown-item.text-danger:hover, .dropdown-item.text-danger:focus { background: hsla(0, 84%, 60%, .12) !important; color: var(--clr-danger) !important; }
.dropdown-toggle::after { vertical-align: 1px; opacity: .6; }
.dropdown-menu-head { display: flex; align-items: center; gap: 10px; padding: 6px 8px 12px; }
.dropdown-menu-head-name { font-size: 13.5px; font-weight: 700; color: var(--clr-tx-1); line-height: 1.3; }
.dropdown-menu-head-meta { font-size: 12px; color: var(--clr-tx-3); line-height: 1.3; }

/* ── Warm-gold star rating — the one place a 5-icon rating display is the entire point of the
   element (hotel star-class header, testimonial reviewer rating), as opposed to a small inline
   metadata pill icon (`.site-card-meta-item i`, left on the existing blue accent — it's one icon
   among several in a spec-sheet row, not a rating display). ── */
.site-star-rating { color: var(--site-gold); }

/* ── "Featured" ribbon — a small corner flag for admin-marked standout packages, using the new
   gold accent so it reads as a distinct merchandising signal, never confusable with the blue
   `.site-card-ref` reference tag it sits alongside.
   Phase 13, plan item 4 ("a corner ribbon badge... a wishlist heart top-right") — moved from
   top-right to top-left, since top-right is `.wishlist-heart-btn`'s corner and the two collided
   (caught live: "MOST POPULAR" rendered half-hidden behind the heart button). Top-left is also
   `.site-card-ref`'s corner, but the two only ever coexist on a single "most popular" card at a
   time — stacked underneath it (top: 44px, clearing the ref tag's own ~12-33px band) rather than
   fighting for the same 12px. */
.site-badge-featured {
    position: absolute; top: 44px; left: 12px; z-index: 2;
    display: inline-flex; align-items: center; gap: 5px;
    font-size: 10.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
    padding: 4px 10px; border-radius: var(--r-tag);
    background: var(--site-gold); color: #1a1206;
}

/* ============================================================
   Marquee — real, accessible, on-brand implementation of the motion contract design.md/
   design-port-prompt.md already specify (34s linear loop, pause on hover, edge-masked, content
   duplicated once and translated -50% for a seamless reset) previously written for that document's
   own single-page tech-stack-chip marquee and never built for this multi-page site. Built as one
   more instance of the existing "frame clips the outer edge, hairline-divided cells inside" slab
   construction (same mental model as every other `.slab`) rather than a row of floating logo
   boxes, so it reads as another instrument-panel readout, not a bolted-on carousel widget.
   ============================================================ */
.site-marquee {
    position: relative;
    overflow: hidden;
    border: 1px solid var(--clr-border);
    border-radius: var(--r-lg);
    background: var(--clr-surface);
    box-shadow: var(--shadow-sm);
    padding: var(--sp-5) 0;
    -webkit-mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
    mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
}
.site-marquee-track {
    display: flex;
    width: max-content;
    animation: siteMarqueeScroll 34s linear infinite;
}
/* :focus-within (not just :hover) so a keyboard user tabbing to a link inside the track — none
   exist today, but the contract should hold if one is ever added — can also stop the motion to
   read it; this is WCAG 2.2.2's "pause continuously-moving content" requirement, and using
   animation-play-state rather than a transform keeps every `.cell`-style hairline inside intact
   (design.md's hard rule: transforming a cell even slightly tears the shared hairlines). */
.site-marquee:hover .site-marquee-track, .site-marquee:focus-within .site-marquee-track { animation-play-state: paused; }
.site-marquee-group { display: flex; align-items: center; flex-shrink: 0; }
.site-marquee-item {
    display: flex; align-items: center; gap: 8px; flex-shrink: 0;
    padding: 0 var(--sp-7); white-space: nowrap;
    font-size: 13px; font-weight: 600; color: var(--clr-tx-2);
    border-right: 1px solid var(--clr-border);
}
.site-marquee-item i { color: var(--site-accent-text); font-size: 13px; }
@keyframes siteMarqueeScroll {
    from { transform: translateX(0); }
    to   { transform: translateX(-50%); }
}
/* Per design.md §9's existing policy for this project's other ambient motion (orbs/pulse/reveal):
   reduced-motion doesn't get a slowed-down version of the animation, it never arms at all — the
   row stays static and fully readable, just not scrolling. */
@media (prefers-reduced-motion: reduce) {
    .site-marquee-track { animation: none; }
}

/* ============================================================
   Select2 — site theme (premium-upgrade pass). Vendor select2.min.css ships unstyled-to-this-
   project defaults; admin's own select2-theme.css is NOT reused here even though it exists and
   is loaded on every admin page, because it targets admin.css's own token names (--clr-accent,
   --clr-input-bg, --radius-md, ...) which are never defined on public/portal pages — those rules
   would resolve to nothing and Select2 would fall back to unstyled browser defaults. This theme
   targets the exact same vendor classes but through this project's own --site-* / --clr-* tokens,
   so a Select2 dropdown reads as a native `.site-select` rather than a bolted-on widget.
   BUG FIX: the token pair above used to be written with no space around the slash between them.
   An asterisk immediately followed by a slash, anywhere in a CSS comment's text — even meant only
   as prose shorthand, never as real code — closes that comment right there (CSS comments don't
   nest and don't know "quoted example" from real syntax). That silently closed this whole comment
   two lines early, mid-sentence, which merged the rest of its own text with the next rule's
   selector below (the base `.select2-container--default .select2-selection--single` block) into
   one invalid selector — and a browser discards an entire rule when its selector fails to parse.
   Confirmed via computed style: every Select2 box site-wide rendered with the vendor's unstyled
   white background/grey border in BOTH themes — invisible-by-coincidence in light theme (vendor
   white reads the same as this theme's own white surface) but a glaring un-themed white box in
   dark theme. Adding the space was the fix; the sibling `__rendered`/`__placeholder`/`__arrow`
   rules right after were never affected (they sit outside the swallowed span).
   ============================================================ */
.select2-container--default .select2-selection--single {
    height: auto; background: var(--clr-surface-2); border: 1px solid var(--clr-border);
    border-radius: var(--r-sm); padding: 5px 0; transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
    color: var(--clr-tx-1); font-family: inherit; font-size: 14px; line-height: 1.6; padding-left: 14px; padding-right: 30px;
}
.select2-container--default .select2-selection--single .select2-selection__placeholder { color: var(--clr-tx-3); }
.select2-container--default .select2-selection--single .select2-selection__arrow { height: 100%; right: 8px; }
.select2-container--default .select2-selection--single .select2-selection__arrow b { border-color: var(--clr-tx-3) transparent transparent transparent; }
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {
    border-color: transparent transparent var(--site-accent) transparent;
}
.select2-container--default.select2-container--open .select2-selection--single,
.select2-container--default.select2-container--focus .select2-selection--single {
    border-color: var(--site-accent); box-shadow: 0 0 0 3px rgba(var(--site-accent-rgb), .15); outline: none;
}
.select2-dropdown {
    background: var(--clr-surface-float); border: 1px solid var(--clr-border-str); border-radius: var(--r-sm);
    box-shadow: var(--shadow-md); overflow: hidden;
}
.select2-search--dropdown { padding: 10px; background: var(--clr-surface); }
.select2-search--dropdown .select2-search__field {
    background: var(--clr-surface-2); border: 1px solid var(--clr-border); border-radius: var(--r-sm);
    color: var(--clr-tx-1); font-family: inherit; font-size: 13.5px; padding: 8px 10px; outline: none;
}
.select2-search--dropdown .select2-search__field:focus { border-color: var(--site-accent); }
.select2-results__option { color: var(--clr-tx-2); font-size: 14px; padding: 9px 14px; }
.select2-container--default .select2-results__option--highlighted[aria-selected] {
    background: rgba(var(--site-accent-rgb), .12); color: var(--site-accent-text);
}
.select2-container--default .select2-results__option[aria-selected="true"] {
    background: var(--clr-surface-2); color: var(--clr-tx-1); font-weight: 600;
}
.select2-results__message { color: var(--clr-tx-3); padding: 10px 14px; }
/* Dark theme's scrollbar inside the results list otherwise stays browser-default white-on-dark. */
.select2-results__options::-webkit-scrollbar { width: 8px; }
.select2-results__options::-webkit-scrollbar-thumb { background: var(--clr-border-str); border-radius: var(--r-pill); }
.site-card .table > thead { color: var(--clr-tx-3); font-size: 11.5px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; }

/* ============================================================
   Flatpickr — site theme (Phase 12 group G, hotel date availability)
   ============================================================
   Same technique as "Select2 — site theme" right above: vendor flatpickr.min.css loads BEFORE
   this file (layouts/site.blade.php) so this override wins the cascade, and only color/surface/
   border tokens are remapped here — flatpickr's own layout, spacing and grid math are left alone.
   The visible text input itself needs none of this: site.js's initFlatpickr() passes
   altInputClass equal to the real input's own class list (this project's plain .site-input),
   so the field that a customer actually sees and types near reuses the exact same input styling
   as every other field on the page for free. This block only covers the calendar dropdown that
   opens below it. Caution for future edits to this file: an asterisk immediately followed by a
   forward slash anywhere in a comment's prose closes the comment right there, no matter the
   intent, so this note deliberately never places those two characters next to each other. */
.flatpickr-calendar {
    background: var(--clr-surface-float); border: 1px solid var(--clr-border-str); border-radius: var(--r-sm);
    box-shadow: var(--shadow-md); font-family: inherit;
}
.flatpickr-calendar.arrowTop:before, .flatpickr-calendar.arrowTop:after { border-bottom-color: var(--clr-border); }
.flatpickr-calendar.arrowBottom:before, .flatpickr-calendar.arrowBottom:after { border-top-color: var(--clr-border); }
.flatpickr-months .flatpickr-month { color: var(--clr-tx-1); fill: var(--clr-tx-1); height: 44px; }
.flatpickr-current-month { font-size: 14px; padding: 8px 0 0 0; }
.flatpickr-current-month .flatpickr-monthDropdown-months {
    background: transparent; color: var(--clr-tx-1); font-weight: 600; appearance: auto;
}
.flatpickr-current-month .flatpickr-monthDropdown-months:hover { background: var(--clr-surface-2); }
.flatpickr-current-month .numInputWrapper span.arrowUp:after { border-bottom-color: var(--clr-tx-3); }
.flatpickr-current-month .numInputWrapper span.arrowDown:after { border-top-color: var(--clr-tx-3); }
.flatpickr-current-month input.cur-year { color: var(--clr-tx-1); font-weight: 600; background: transparent; }
.flatpickr-prev-month, .flatpickr-next-month { fill: var(--clr-tx-3); color: var(--clr-tx-3); }
.flatpickr-prev-month:hover, .flatpickr-next-month:hover { fill: var(--site-accent-text); color: var(--site-accent-text); }
.flatpickr-prev-month svg, .flatpickr-next-month svg { fill: inherit; }
.flatpickr-weekdays { background: transparent; }
span.flatpickr-weekday { background: transparent; color: var(--clr-tx-3); font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
.flatpickr-days { border-left-color: var(--clr-border); border-right-color: var(--clr-border); }
.flatpickr-day { color: var(--clr-tx-1); border-radius: var(--r-tag); }
.flatpickr-day.prevMonthDay, .flatpickr-day.nextMonthDay { color: var(--clr-tx-3); opacity: .5; }
.flatpickr-day.today { border-color: var(--site-accent); }
.flatpickr-day.today:hover, .flatpickr-day.today:focus { background: var(--site-accent); border-color: var(--site-accent); color: #fff; }
.flatpickr-day:hover, .flatpickr-day:focus {
    background: rgba(var(--site-accent-rgb), .14); border-color: rgba(var(--site-accent-rgb), .3); color: var(--clr-tx-1);
}
.flatpickr-day.selected, .flatpickr-day.selected:hover, .flatpickr-day.selected:focus,
.flatpickr-day.startRange, .flatpickr-day.startRange:hover, .flatpickr-day.endRange, .flatpickr-day.endRange:hover {
    background: var(--site-accent); border-color: var(--site-accent); color: #fff;
}
.flatpickr-day.inRange {
    background: rgba(var(--site-accent-rgb), .12); border-color: rgba(var(--site-accent-rgb), .12);
    box-shadow: -5px 0 0 rgba(var(--site-accent-rgb), .12), 5px 0 0 rgba(var(--site-accent-rgb), .12);
}
/* Unavailable dates (Site\HotelController::availability() reports available_rooms<=0, matching
   InternalDriver::searchHotels()'s own "no coverage row = excluded" rule verbatim) — visibly
   greyed out and non-interactive, never just "looks the same but silently rejects on submit". */
.flatpickr-day.flatpickr-disabled, .flatpickr-day.flatpickr-disabled:hover {
    color: var(--clr-tx-3); background: transparent; text-decoration: line-through; opacity: .4; cursor: not-allowed;
}
.numInputWrapper:hover { background: transparent; }

/* ============================================================
   Vertical landing pages — DESIGN-VERTICALS.md
   ============================================================
   Visa/Hotels/Flights each get their own accent identity so they read as their own premium
   landing page rather than the same template with different words. The remap below is scoped to
   a `data-vertical` attribute set once, high up the DOM (body or a top-level wrapper) — every
   existing component that already reads `--site-accent-text` / `--site-accent-rgb` / `--site-grad`
   (buttons, cell hover glows, card-link hover, focus rings) picks up the vertical's own color
   automatically, with no changes to those components themselves. Packages and the homepage never
   set this attribute, so they stay on the site's normal blue+gold identity untouched. */
/* `--site-accent-strong` is remapped here too, and it is NOT simply the vertical's accent.
   Since Phase 13.11 that token is the SOLID fill of every primary button (the gradient was
   dropped on client instruction), so it sits under WHITE text and has to clear 4.5:1 — measured,
   the raw accents do not: hotel #a8763f is 3.94:1 and flight #c98a3a only 2.92:1. The values
   below are each accent darkened on its own hue until it passes, so a Hotels button is genuinely
   that vertical's bronze and a Flights button genuinely its amber, while staying readable. Visa's
   #6b4fc7 already clears it at 5.89:1 and is used unchanged. */
[data-vertical="visa"] {
    --site-accent-text: var(--site-support);
    --site-accent-rgb: var(--site-support-rgb);
    --site-accent-strong: #6B4FC7;              /* 5.89:1 under white */
    --site-grad: linear-gradient(135deg, var(--site-primary), var(--site-support));
}
[data-vertical="hotel"] {
    --site-accent-text: var(--site-hotel-accent);
    --site-accent-rgb: var(--site-hotel-accent-rgb);
    --site-accent-strong: #996C39;              /* 4.61:1 under white */
    --site-grad: linear-gradient(135deg, var(--site-primary), var(--site-hotel-accent));
}
[data-vertical="flight"] {
    --site-accent-text: var(--site-flight-accent);
    --site-accent-rgb: var(--site-flight-accent-rgb);
    --site-accent-strong: #9F6C2C;              /* 4.51:1 under white */
    --site-grad: linear-gradient(135deg, var(--site-primary), var(--site-flight-accent));
}

/* ── Vertical hero — Phase 13 client feedback ("still so dark" / "when no image from database
   then no gradient"): colour is now earned by a real admin-uploaded photo, not shown by default.
   No banner configured (the common case for every vertical right now) → `.vertical-hero` alone
   renders as a plain section on the page's own normal theme tokens — no forced-dark text, no
   colour wash at all. A banner IS configured → the view also adds `.vertical-hero--photo` plus
   one of `--visa/--hotel/--flight/--brand`, which together supply the real photo, the dark-pinned
   text tokens a photo needs for guaranteed contrast, and a light, per-vertical-tinted scrim. ── */
.vertical-hero {
    position: relative;
    overflow: hidden;
    padding: var(--sp-11) 0 var(--sp-12);
    isolation: isolate;
}
/* Only the photo case gets forced-dark text/surface tokens — the no-photo case (no class below
   applied) stays on the page's own normal light/dark theme instead, per the component comment
   above. */
/* The photo comes in as a custom property rather than a literal `background-image` on the element,
   so a banner carrying optional dark-theme artwork can swap here with no JavaScript. Only the rule
   that MATCHES fetches its image — a browser never downloads the background of a rule whose
   selector does not apply — so the light file is not pulled in dark mode or vice versa.
   `--hero-photo-dark` is emitted by the view as the dark image when there is one and the ordinary
   image when there is not, so a single-image banner simply renders the same file in both themes. */
.vertical-hero--photo {
    background-image: var(--hero-photo);
    background-size: cover;
    background-position: center;
    --clr-tx-1: #EEF3FB;
    --clr-tx-2: rgba(238, 243, 251, .78);
    --clr-tx-3: rgba(238, 243, 251, .6);
    --clr-border: rgba(255, 255, 255, .14);
    --clr-border-str: rgba(255, 255, 255, .22);
    --clr-surface: rgba(255, 255, 255, .1);
    --clr-surface-2: rgba(255, 255, 255, .14);
}
[data-theme="dark"] .vertical-hero--photo { background-image: var(--hero-photo-dark, var(--hero-photo)); }
.vertical-hero--photo::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
}
/* Lighter than the pre-feedback version (client: "still so dark") — a bottom-weighted scrim,
   tinted per vertical so the 4 sections keep their own identity, but never opaque enough to hide
   the actual photo underneath it. */
.vertical-hero--photo.vertical-hero--visa::before {
    background: linear-gradient(180deg, rgba(75, 18, 188, .22) 0%, rgba(10, 10, 23, .3) 55%, rgba(10, 10, 23, .62) 100%);
}
.vertical-hero--photo.vertical-hero--hotel::before {
    background: linear-gradient(180deg, rgba(75, 18, 188, .2) 0%, rgba(12, 10, 8, .28) 55%, rgba(12, 10, 8, .6) 100%);
}
.vertical-hero--photo.vertical-hero--flight::before {
    background: linear-gradient(180deg, rgba(75, 18, 188, .2) 0%, rgba(6, 12, 20, .28) 55%, rgba(6, 12, 20, .6) 100%);
}
.vertical-hero--photo.vertical-hero--brand::before {
    background: linear-gradient(180deg, rgba(75, 18, 188, .22) 0%, rgba(9, 1, 24, .3) 55%, rgba(9, 1, 24, .62) 100%);
}
.vertical-hero-eyebrow { color: var(--site-accent-text); }

/* ── Homepage split hero + real photo slider — Phase 13 client feedback ("mix option 1 and 2":
   split layout, no full-bleed colour wash, real photos with a slider). Content sits on the
   page's own normal theme (no forced-dark tokens, no gradient wash — the whole point of this
   direction), a real image panel sits alongside it. `.hero-slide`s crossfade — becomes an actual
   slider automatically once more than one real Hero banner exists (only one does today; see
   HomeController@index's own note), never invented/stock photos. No banner at all → the image
   panel is simply omitted by the view and the content column goes full-width. ── */
.hero-split { display: grid; grid-template-columns: 1.1fr 1fr; gap: var(--sp-8); align-items: center; }
.hero-image-panel {
    position: relative; isolation: isolate;
    border-radius: var(--r-xl); overflow: hidden; aspect-ratio: 4/3;
    box-shadow: var(--shadow-md);
}
/* NOTE: no background is painted here. The uploaded hero artwork is a PNG whose upper area is
   fully transparent (sampled: alpha 127 across the top, a solid violet ground strip along the
   bottom), so it composites onto whatever is behind the panel. That is why it reads strongly on
   the dark canvas and weakly on the light one. Fixing that with a tinted panel was tried and
   rejected by the client — no gradient wash here. */
.hero-slide { position: absolute; inset: 0; opacity: 0; transition: opacity 1.1s var(--ease); }
.hero-slide.is-active { opacity: 1; }
/* `display: contents` so a <picture> wrapper (used when a banner has a dark-theme variant) adds no
   box of its own — the <img> keeps filling the slide exactly as it does without the wrapper. */
.hero-slide picture { display: contents; }
.hero-slide img { width: 100%; height: 100%; object-fit: cover; }
.hero-slide-dots { position: absolute; z-index: 2; bottom: var(--sp-4); left: 50%; transform: translateX(-50%); display: flex; gap: 8px; }
.hero-slide-dot {
    width: 8px; height: 8px; border-radius: var(--r-pill); padding: 0; border: none; cursor: pointer;
    background: rgba(255, 255, 255, .55); transition: background-color var(--dur) var(--ease), width var(--dur) var(--ease);
}
.hero-slide-dot.is-active { background: #fff; width: 22px; }
@media (max-width: 900px) {
    .hero-split { grid-template-columns: 1fr; }
    .hero-image-panel { order: -1; aspect-ratio: 16/9; }
}

/* ── Search widget overlap — an opt-in modifier that pulls the widget up into the hero band
   above it by a multiple of the base spacing unit, per the reference sites' own "the search card
   floats half-in/half-out of the photo" pattern. Every vertical's own search form markup is
   untouched; this only changes the wrapping element's position. ── */
.search-widget--overlap { margin-top: calc(var(--sp-9) * -1); position: relative; z-index: 2; }
@media (max-width: 768px) {
    .search-widget--overlap { margin-top: calc(var(--sp-7) * -1); }
}

/* ============================================================
   Phase 13 — Foundation components (structural redesign, informed by Turie/Gotur/Garuda/Anygo
   reference templates — see PLAN.md §13 for the full source list and the design-language
   reconciliation against design.md/design-port-prompt.md). Every component below is built on the
   EXISTING Instrument Grid grammar already established above (square slabs, hairline dividers,
   glow/bracket hover, the --site- / --clr- / --sp- / --r- token families) — nothing here
   introduces a competing visual language. Additive only; nothing above this line is redefined.

   New components in this section (markup contracts other stages build pages against):

   .page-banner            — generic secondary-page header for pages that are NOT one of the
                              3 accent verticals (Packages, Blog, Contact, static pages) — a
                              plain primary-gradient-mesh band (same photo/color-wash technique
                              as .vertical-hero, just without a vertical accent) with a centered
                              title + optional one-line subtitle/breadcrumb. Expects:
                                <section class="page-banner">
                                  <div class="site-container text-center">
                                    <nav class="page-banner-crumb">Home / Packages</nav>
                                    <h1 class="site-h1">Packages</h1>
                                    <p class="page-banner-sub">One line of context</p>
                                  </div>
                                </section>
                              `.page-banner--sm` is a shorter variant for pages that don't need
                              full hero weight (Contact, static pages).

   .site-filter-pill-row    — horizontal scrollable row of filter/category pills (Turie's
                              category-shelf + sort-bar pattern). Expects:
                                <div class="site-filter-pill-row">
                                  <button class="site-filter-pill is-active">All</button>
                                  <button class="site-filter-pill">Tourist</button>
                                  ...
                                </div>
                              `.site-filter-pill-row--scroll` adds horizontal scroll + edge mask
                              for a long list instead of wrapping.

   .site-card-rating-row    — star rating + review count row for a result card, sits directly
                              under the image. Expects:
                                <div class="site-card-rating-row">
                                  <span class="site-star-rating">★★★★★</span>
                                  <span class="site-card-rating-count">(24 reviews)</span>
                                </div>

   .site-card-list          — modifier on `.cell.cell-card` (add alongside the existing class)
                              that switches the card from vertical image-on-top to a horizontal
                              list row (image left, content right) — the grid/list view toggle
                              from the plan's item 3. Falls back to stacked on narrow widths.

   .detail-gallery          — detail-page image gallery: one large image + a thumbnail column,
                              with a "View all N photos" overlay on the last thumbnail. Expects:
                                <div class="detail-gallery">
                                  <div class="detail-gallery-main"><img ...></div>
                                  <div class="detail-gallery-thumbs">
                                    <div class="detail-gallery-thumb"><img ...></div>
                                    <div class="detail-gallery-thumb">
                                      <img ...>
                                      <span class="detail-gallery-more">+12</span>
                                    </div>
                                  </div>
                                </div>

   .booking-widget-tabs     — Book vs. Inquiry tab toggle for the sticky booking/checkout
                              sidebar. Same tab mechanics as `.search-widget-tabs` (reuses that
                              pattern rather than inventing a second one), scaled down.

   .booking-stepper         — a labeled row (Adults/Children/Infants + optional per-unit price
                              hint) wrapping the EXISTING, already-JS-wired `.site-stepper`
                              control (public/js/site.js initStepper(), also used by the hotel
                              booking form) rather than a second, decorative-only +/- widget.
                              Expects:
                                <div class="booking-stepper">
                                  <div class="booking-stepper-label">Adults <small>Ages 13+</small></div>
                                  <div class="site-stepper">
                                    <button type="button" class="site-stepper-btn" data-stepper-decrement>&minus;</button>
                                    <input type="number" class="site-input site-stepper-input" value="1" min="0">
                                    <button type="button" class="site-stepper-btn" data-stepper-increment>&plus;</button>
                                  </div>
                                </div>

   .booking-timeslot-row    — a row of radio-style time-slot pills (reuses `.site-filter-pill`'s
                              look at a smaller scale via `.site-filter-pill-row--compact`).

   .booking-extra           — one checkbox row with a trailing price, for the sticky widget's
                              optional Extra Services list.

   .checkout-summary-card   — sticky order-summary card for checkout/apply flows: thumbnail row,
                              icon meta rows with a "change" link, a price breakdown list, and a
                              Subtotal/Total footer. Expects:
                                <div class="checkout-summary-card">
                                  <div class="checkout-summary-head">...thumbnail + title...</div>
                                  <div class="checkout-summary-row">...</div>
                                  <div class="checkout-summary-price-list">
                                    <div class="checkout-summary-price-row"><span>Base fare</span><span>$120</span></div>
                                  </div>
                                  <div class="checkout-summary-total"><span>Total</span><span>$120</span></div>
                                </div>
   ============================================================ */

/* ── 1. Page banner — generic secondary-page header ──
   Same technique as .vertical-hero above (gradient-mesh band, dark-pinned text tokens, --photo
   modifier support) but with ONE neutral primary/accent mesh instead of a per-vertical accent,
   since Packages/Blog/Contact/static pages don't belong to a single vertical. Deliberately a
   separate, self-contained ruleset rather than sharing .vertical-hero's selector — the two are
   independent components with independent modifier classes (--photo on each is a different
   color mix), so merging them would couple two things that should be free to diverge. */
/* Phase 13.9 — restructured to match .vertical-hero, which was given this same treatment after the
   client's "gradient-mesh looking so bad... also other pages hero section also" and "when no image
   from database then no gradient". `.page-banner` had been left behind on the old always-dark
   radial-mesh backdrop, so Packages/Blog/Contact still rendered the exact thing that was rejected.
   The base is now structural only: no colour, no forced-dark tokens, no scrim. Colour arrives ONLY
   with `--photo`, i.e. only when an admin has actually uploaded a banner image for the page. With
   no photo the banner sits on the page's own light/dark theme, like the rest of the page. */
.page-banner {
    position: relative;
    overflow: hidden;
    isolation: isolate;
    padding: var(--sp-9) 0 var(--sp-8);
    text-align: center;
    border-bottom: 1px solid var(--clr-border);
}
/* The no-photo case still gets a whisper of brand light so the band reads as a header rather than
   as the first paragraph of the page — a tint, not a wash, and it inherits the page's own theme. */
.page-banner::before {
    content: '';
    position: absolute; inset: 0; z-index: -1;
    background: linear-gradient(180deg, rgba(var(--site-primary-rgb), .05), transparent 78%);
}
.page-banner--photo {
    background-size: cover; background-position: center;
    border-bottom: none;
    --clr-tx-1: #EEF3FB;
    --clr-tx-2: rgba(238, 243, 251, .72);
    --clr-tx-3: rgba(238, 243, 251, .55);
    --clr-border: rgba(255, 255, 255, .14);
    --clr-surface: rgba(255, 255, 255, .08);
    --clr-surface-2: rgba(255, 255, 255, .12);
}
.page-banner--photo::before {
    background:
        radial-gradient(ellipse 65% 70% at 15% 0%, rgba(var(--site-accent-rgb), .32), transparent 60%),
        radial-gradient(ellipse 60% 60% at 90% 25%, rgba(var(--site-primary-rgb), .4), transparent 60%),
        linear-gradient(180deg, rgba(6, 10, 20, .55), rgba(6, 10, 20, .78));
}
.page-banner--sm { padding: var(--sp-7) 0; }
.page-banner-crumb { font-size: 13px; font-weight: 600; color: var(--clr-tx-3); margin-bottom: var(--sp-3); }
.page-banner-crumb a:hover { color: var(--clr-tx-1); }
.page-banner-sub { margin: var(--sp-3) auto 0; max-width: 56ch; color: var(--clr-tx-2); }

/* ── 2. Filter pill row ── */
.site-filter-pill-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.site-filter-pill-row--scroll {
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    -webkit-mask-image: linear-gradient(to right, transparent, #000 3%, #000 97%, transparent);
    mask-image: linear-gradient(to right, transparent, #000 3%, #000 97%, transparent);
}
.site-filter-pill-row--scroll::-webkit-scrollbar { display: none; }
.site-filter-pill-row--compact { gap: var(--sp-1); }
.site-filter-pill {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; gap: 6px;
    padding: 8px 16px; border-radius: var(--r-pill);
    border: 1px solid var(--clr-border-str); background: var(--clr-surface);
    font-size: 13.5px; font-weight: 600; color: var(--clr-tx-2); cursor: pointer;
    transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.site-filter-pill:hover { border-color: rgba(var(--site-accent-rgb), .4); color: var(--clr-tx-1); }
.site-filter-pill.is-active { background: var(--site-accent-strong); border-color: transparent; color: #fff; }
/* Count riding inside a pill. It sits on a tinted chip rather than plain text so the number
   never reads as part of the label ("Pending 3" vs "Pending" + 3). */
.site-filter-pill-count {
    font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums;
    padding: 1px 6px; border-radius: var(--r-pill);
    background: var(--clr-surface-2); color: var(--clr-tx-3);
}
.site-filter-pill.is-active .site-filter-pill-count { background: rgba(255, 255, 255, .22); color: #fff; }

/* ── 3. Result-card enrichments — rating row + list-view variant ── */
.site-card-rating-row { display: flex; align-items: center; gap: 6px; font-size: 13px; margin-bottom: var(--sp-2); }
.site-card-rating-row .site-star-rating { font-size: 12px; letter-spacing: 1px; }
.site-card-rating-count { color: var(--clr-tx-3); font-size: 12.5px; }

.cell.cell-card.site-card-list { flex-direction: row; align-items: stretch; }
.cell.cell-card.site-card-list .site-card-img { flex: 0 0 260px; aspect-ratio: auto; }
.cell.cell-card.site-card-list .site-card-body { flex: 1 1 auto; display: flex; flex-direction: column; }
.cell.cell-card.site-card-list .site-card-desc { color: var(--clr-tx-3); font-size: 13.5px; margin: var(--sp-2) 0; }
@media (max-width: 700px) {
    .cell.cell-card.site-card-list { flex-direction: column; }
    .cell.cell-card.site-card-list .site-card-img { flex-basis: auto; aspect-ratio: 4/3; }
}

/* ── 4. Detail-page gallery ── */
.detail-gallery { display: grid; grid-template-columns: 2fr 1fr; gap: var(--sp-3); border-radius: var(--r-md); overflow: hidden; }
.detail-gallery-main { position: relative; border-radius: var(--r-md); overflow: hidden; background: var(--clr-surface-2); aspect-ratio: 4/3; }
.detail-gallery-main img { width: 100%; height: 100%; object-fit: cover; }
.detail-gallery-thumbs { display: grid; grid-template-rows: 1fr 1fr; gap: var(--sp-3); }
.detail-gallery-thumb { position: relative; border-radius: var(--r-md); overflow: hidden; background: var(--clr-surface-2); }
.detail-gallery-thumb img { width: 100%; height: 100%; object-fit: cover; }
.detail-gallery-more {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    background: rgba(6, 10, 20, .55); color: #fff; font-weight: 700; font-size: 18px;
    cursor: pointer;
}
@media (max-width: 700px) {
    .detail-gallery { grid-template-columns: 1fr; }
    .detail-gallery-thumbs { grid-template-columns: repeat(3, 1fr); grid-template-rows: none; }
}

/* ── 5. Sticky booking widget enrichments ── */
.booking-widget-tabs { display: flex; border-bottom: 1px solid var(--clr-border); margin-bottom: var(--sp-4); }
.booking-widget-tab {
    flex: 1 1 auto; text-align: center; padding: 10px 12px;
    font-weight: 600; font-size: 13.5px; color: var(--clr-tx-3);
    background: none; border: none; border-bottom: 2px solid transparent; cursor: pointer;
}
.booking-widget-tab.active { color: var(--site-accent-text); border-bottom-color: var(--site-accent); }

.booking-stepper { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-3) 0; }
.booking-stepper + .booking-stepper { border-top: 1px solid var(--clr-border); }
.booking-stepper-label { font-size: 13.5px; font-weight: 600; color: var(--clr-tx-1); }
.booking-stepper-label small { display: block; font-weight: 400; color: var(--clr-tx-3); font-size: 12px; }
/* The stepper CONTROL itself is `.site-stepper` (existing component, above) at its normal size —
   this just caps its width so 3 stacked rows (Adults/Children/Infants) don't each stretch full
   width the way a lone one might elsewhere. */
.booking-stepper .site-stepper { flex: 0 0 auto; width: 112px; }
.booking-stepper .site-stepper-input { width: 32px; }

.booking-extra { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-2) 0; font-size: 13.5px; }
.booking-extra-label { display: flex; align-items: center; gap: var(--sp-2); color: var(--clr-tx-2); }
.booking-extra-price { font-weight: 700; color: var(--clr-tx-1); }

/* ── 6. Checkout / order-summary card ── */
.checkout-summary-card {
    background: var(--clr-surface); border: 1px solid var(--clr-border-str); border-radius: var(--r-md);
    box-shadow: var(--shadow-sm); padding: var(--sp-5);
    position: sticky; top: var(--sp-5);
}
.checkout-summary-head { display: flex; gap: var(--sp-3); align-items: center; padding-bottom: var(--sp-4); border-bottom: 1px solid var(--clr-border); }
.checkout-summary-thumb { width: 64px; height: 64px; border-radius: var(--r-sm); overflow: hidden; flex: 0 0 auto; background: var(--clr-surface-2); }
.checkout-summary-thumb img { width: 100%; height: 100%; object-fit: cover; }
.checkout-summary-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); padding: var(--sp-2) 0; font-size: 13.5px; color: var(--clr-tx-2); }
.checkout-summary-row-change { color: var(--site-accent-text); font-weight: 600; font-size: 12.5px; }
.checkout-summary-price-list { margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--clr-border); }
.checkout-summary-price-row { display: flex; align-items: center; justify-content: space-between; padding: 4px 0; font-size: 13.5px; color: var(--clr-tx-2); }
.checkout-summary-total {
    display: flex; align-items: center; justify-content: space-between;
    margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--clr-border-str);
    font-weight: 800; font-size: 17px; color: var(--clr-tx-1);
}
@media (max-width: 992px) {
    .checkout-summary-card { position: static; }
}

/* ============================================================
   Phase 13.8 — Homepage hero polish ("more premium... text writing effect, floating plane
   icons, slider placeholders, tour vibe"). Every motion rule here is gated on
   prefers-reduced-motion, matching this file's existing policy for orbs/marquee/reveal: reduced
   motion gets the finished state immediately, never a slowed-down version.
   ============================================================ */

/* ── Hero shell — positioning context for the decorative layer below, which must never sit over
   real content or intercept clicks. ── */
.hero-section { position: relative; overflow: hidden; }
.hero-section > .site-container { position: relative; z-index: 1; }

/* ── Decorative flight layer: floating plane glyphs + one dashed flight-path arc. Purely
   ornamental (aria-hidden in the markup, pointer-events off here), so it can never block the
   search widget or the headline behind it. ── */
.hero-deco { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.hero-deco-plane { position: absolute; color: rgba(var(--site-accent-rgb), .22); will-change: transform; }
/* Positions are deliberately pinned to the page's outer GUTTERS (under ~7% / over ~93%), which is
   the band outside `.site-container`'s max-width on a wide screen — the only region guaranteed to
   be clear of real content on every hero layout, centred or split. p2/p5 sit further in and are
   dropped on narrower screens (below), where that band no longer exists. */
.hero-deco-plane.p1 { top: 11%; left: 3.5%;  font-size: 22px; animation: heroFloatA 11s ease-in-out infinite; }
.hero-deco-plane.p2 { top: 47%; left: 1.5%;  font-size: 15px; animation: heroFloatB 14s ease-in-out infinite; }
.hero-deco-plane.p3 { top: 79%; left: 5%;    font-size: 18px; animation: heroFloatA 16s ease-in-out infinite reverse; }
.hero-deco-plane.p4 { top: 21%; right: 4.5%; font-size: 19px; animation: heroFloatA 13s ease-in-out infinite reverse; }
.hero-deco-plane.p5 { top: 55%; right: 1.5%; font-size: 14px; animation: heroFloatB 17s ease-in-out infinite; }
.hero-deco-plane.p6 { top: 86%; right: 6%;   font-size: 16px; animation: heroFloatB 12s ease-in-out infinite reverse; }
/* Over a real uploaded photo these read as clutter on someone's image rather than decoration. */
.vertical-hero--photo .hero-deco { display: none; }
.hero-deco-path { position: absolute; top: 8%; left: 0; width: 100%; height: 84%; }
.hero-deco-path path { stroke: rgba(var(--site-accent-rgb), .28); stroke-width: 1.5; stroke-dasharray: 5 9; fill: none; }
@keyframes heroFloatA {
    0%, 100% { transform: translate3d(0, 0, 0) rotate(-8deg); }
    50%      { transform: translate3d(14px, -18px, 0) rotate(4deg); }
}
@keyframes heroFloatB {
    0%, 100% { transform: translate3d(0, 0, 0) rotate(6deg); }
    50%      { transform: translate3d(-16px, -12px, 0) rotate(-6deg); }
}
/* Below the container's own max-width the outer gutter narrows and then disappears, so the icons
   that sit furthest in would start drifting across real copy. They are dropped in two steps
   rather than dimmed — a glyph sitting on a paragraph is a legibility problem at any opacity. */
@media (max-width: 1200px) {
    .hero-deco-plane.p2, .hero-deco-plane.p5 { display: none; }
}
@media (max-width: 900px) {
    .hero-deco-plane.p3, .hero-deco-plane.p6 { display: none; }
    .hero-deco-plane.p1 { top: 6%; }
    .hero-deco-plane.p4 { top: 9%; }
}

/* ── Hero headline "writing" effect. The full text is ALWAYS in the HTML — site.js only ever
   opts IN to animating it (and measures/reserves the final height first so nothing below jumps
   while it types), so a script failure or reduced-motion preference simply shows the finished
   headline, per design.md's hard "motion must never be able to hide content" rule. ── */
.site-h1.is-typing::after {
    content: ''; display: inline-block; width: 3px; height: .82em; margin-left: 5px;
    vertical-align: -.06em; background: var(--site-accent); border-radius: 1px;
    animation: heroCaret .9s steps(1) infinite;
}
@keyframes heroCaret { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }

/* ── Slider upgrades: slow Ken Burns push on the active slide, plus prev/next controls alongside
   the existing dots. The caption band that used to sit here is gone on client instruction — it
   overlaid a dark scrim across the bottom of the artwork, which fought the illustration rather
   than adding anything. The banner title still does real work as the image's alt text. ── */
.hero-slide img { transition: transform 8s linear; }
.hero-slide.is-active img { transform: scale(1.01); }
.hero-slide-nav {
    position: absolute; z-index: 3; top: 50%; transform: translateY(-50%);
    width: 36px; height: 36px; border-radius: var(--r-pill); border: none; cursor: pointer;
    background: rgba(255, 255, 255, .82); color: #1a1030; font-size: 13px;
    display: flex; align-items: center; justify-content: center;
    box-shadow: 0 4px 14px rgba(6, 2, 20, .22);
    transition: background-color var(--dur) var(--ease);
}
.hero-slide-nav:hover { background: #fff; }
.hero-slide-nav.prev { left: var(--sp-3); }
.hero-slide-nav.next { right: var(--sp-3); }

/* ── Placeholder slides — shown when an admin has no ACTIVE Hero banner uploaded yet, so the
   panel still reads as a designed, working slider rather than a missing block. Deliberately
   icon-only: a visibly empty "no photo yet" state, never invented stock photography or fake
   marketing copy. ── */
.hero-slide-placeholder {
    display: flex; align-items: center; justify-content: center;
    background:
        radial-gradient(ellipse 70% 60% at 30% 25%, rgba(var(--site-accent-rgb), .22), transparent 62%),
        linear-gradient(135deg, rgba(var(--site-primary-rgb), .16), rgba(var(--site-accent-rgb), .12));
}
/* Inset hairline frame — the same "deliberate structural panel" cue the Instrument Grid uses
   elsewhere, dashed here to read as a reserved slot rather than a finished surface. Without it a
   lone centred glyph on a tint just reads as a broken image. */
.hero-slide-placeholder::before {
    content: ''; position: absolute; inset: var(--sp-4); pointer-events: none;
    border: 1px dashed rgba(var(--site-primary-rgb), .2); border-radius: var(--r-lg);
}
.hero-slide-placeholder i {
    display: grid; place-items: center; width: 128px; height: 128px; border-radius: var(--r-pill);
    font-size: 44px; color: rgba(var(--site-primary-rgb), .34);
    background: radial-gradient(circle, rgba(255, 255, 255, .6), rgba(255, 255, 255, 0) 70%);
    box-shadow: inset 0 0 0 1px rgba(var(--site-primary-rgb), .14),
                0 0 0 14px rgba(var(--site-primary-rgb), .05);
}
[data-theme="dark"] .hero-slide-placeholder::before { border-color: rgba(255, 255, 255, .16); }
[data-theme="dark"] .hero-slide-placeholder i {
    color: rgba(255, 255, 255, .34);
    background: radial-gradient(circle, rgba(255, 255, 255, .1), rgba(255, 255, 255, 0) 70%);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .16), 0 0 0 14px rgba(255, 255, 255, .04);
}

/* The dots and arrows are tuned for white-on-photo. With no photo behind them that reads as
   near-invisible, so the empty-state panel re-tones both to the brand ink instead. */
.hero-image-panel--empty .hero-slide-dot { background: rgba(var(--site-primary-rgb), .28); }
.hero-image-panel--empty .hero-slide-dot.is-active { background: var(--site-accent-strong); }
.hero-image-panel--empty .hero-slide-nav { background: rgba(255, 255, 255, .7); color: var(--site-primary); }
.hero-image-panel--empty .hero-slide-nav:hover { background: #fff; }
[data-theme="dark"] .hero-image-panel--empty .hero-slide-dot { background: rgba(255, 255, 255, .3); }
[data-theme="dark"] .hero-image-panel--empty .hero-slide-dot.is-active { background: var(--site-accent); }
[data-theme="dark"] .hero-image-panel--empty .hero-slide-nav { background: rgba(255, 255, 255, .16); color: #fff; }
[data-theme="dark"] .hero-image-panel--empty .hero-slide-nav:hover { background: rgba(255, 255, 255, .28); }

@media (prefers-reduced-motion: reduce) {
    .hero-deco-plane { animation: none; }
    .hero-slide img, .hero-slide.is-active img { transition: none; transform: none; }
    .site-h1.is-typing::after { animation: none; }
}

/* ============================================================
   Phase 13.9 — Packages mega-menu (two-pane header dropdown)
   Left pane: real published package categories. Right pane: that category's genuinely most-booked
   packages (HeaderPackagesComposer). Every package on this site currently resolves to the same
   shipped placeholder image, so the cards use an icon tile rather than a thumbnail — four
   identical placeholder photos would read as broken, an icon reads as designed.
   ============================================================ */
/* Centred on the PAGE, not hung off the "Packages" button. `.site-mega` drops to `position:
   static` so the absolutely-positioned menu resolves against the <header> instead (which is
   already `position: relative`), and the header spans the full width — so centring there centres
   the panel on the site. The toggle carries `data-bs-display="static"` for the same reason: it
   stops Bootstrap's Popper from writing inline `transform`/`inset` that would override this. */
.site-mega { position: static; }
.site-mega-menu {
    --bs-dropdown-padding-x: 0; --bs-dropdown-padding-y: 0;
    width: min(760px, calc(100vw - 32px)); max-width: none; overflow: hidden;
    /* Painted explicitly rather than left to --bs-dropdown-bg. This panel floats over the hero —
       an ambient grid, drifting orbs and the slider photo — so it has to be genuinely opaque, and
       it is large enough that a thin dropdown shadow does not read as "above" the page. Both
       surfaces here are solid tokens (never rgba), so nothing shows through in either theme. */
    background: var(--clr-surface-float);
    border: 1px solid var(--clr-border-str);
    border-radius: var(--r-lg);
    box-shadow: 0 24px 64px rgba(var(--site-primary-rgb), .2), 0 2px 8px rgba(6, 2, 20, .08);
}
/* Background is already theme-correct via --clr-surface-float; only the shadow needs redoing,
   since a brand-tinted glow reads as haze rather than elevation on a near-black canvas. */
[data-theme="dark"] .site-mega-menu {
    box-shadow: 0 24px 64px rgba(0, 0, 0, .55), 0 0 0 1px rgba(255, 255, 255, .04);
}
/* The `[data-bs-popper]` selector is not redundant: Bootstrap ships
   `.dropdown-menu[data-bs-popper] { left: 0 }` and still sets that attribute even with
   `data-bs-display="static"`. At (0,2,0) it outranks a lone `.site-mega-menu`, so the centring
   silently lost and the panel rendered at x=-380 — off the left edge of the page. Measured, not
   assumed. Both selectors are listed so this holds whether or not the attribute is present. */
.site-mega .site-mega-menu,
.site-mega .site-mega-menu[data-bs-popper] {
    top: 100%; left: 50%; right: auto; transform: translateX(-50%); margin-top: 8px;
}
.site-mega-grid { display: grid; grid-template-columns: 232px 1fr; }

/* Left pane */
.site-mega-nav {
    display: flex; flex-direction: column; gap: 2px; padding: var(--sp-3);
    background: rgba(var(--site-primary-rgb), .045);
    border-right: 1px solid var(--clr-border);
}
.site-mega-nav-item {
    display: flex; align-items: center; gap: 10px; padding: 11px 12px;
    border-radius: var(--r-md); text-decoration: none; color: var(--clr-tx-2);
    font-size: 14px; font-weight: 600;
    transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.site-mega-nav-item:hover { background: rgba(var(--site-primary-rgb), .08); color: var(--clr-tx-1); }
.site-mega-nav-item.is-active { background: var(--clr-surface); color: var(--site-accent-text); box-shadow: var(--shadow-xs); }
.site-mega-nav-icon {
    flex: 0 0 auto; width: 30px; height: 30px; border-radius: var(--r-sm);
    display: grid; place-items: center; font-size: 13px;
    background: rgba(var(--site-accent-rgb), .12); color: var(--site-accent-text);
}
.site-mega-nav-label { flex: 1 1 auto; }
.site-mega-nav-count {
    flex: 0 0 auto; min-width: 22px; text-align: center; padding: 1px 6px;
    border-radius: var(--r-pill); font-size: 11px; font-weight: 700;
    background: rgba(var(--site-primary-rgb), .1); color: var(--clr-tx-3);
}
.site-mega-nav-item.is-active .site-mega-nav-count { background: var(--site-accent-strong); color: #fff; }

/* Right pane */
.site-mega-panels { padding: var(--sp-4); }
.site-mega-panel { display: none; }
.site-mega-panel.is-active { display: block; }
.site-mega-panel-head {
    display: flex; align-items: center; justify-content: space-between;
    margin-bottom: var(--sp-3); padding-bottom: var(--sp-2);
    border-bottom: 1px solid var(--clr-border);
}
.site-mega-all {
    display: inline-flex; align-items: center; gap: 6px; text-decoration: none;
    font-size: 13px; font-weight: 700; color: var(--site-accent-text);
}
.site-mega-all i { font-size: 10px; transition: transform var(--dur) var(--ease); }
.site-mega-all:hover i { transform: translateX(3px); }
.site-mega-card {
    display: flex; align-items: center; gap: 12px; padding: 10px;
    border-radius: var(--r-md); text-decoration: none; color: inherit;
    transition: background-color var(--dur) var(--ease);
}
.site-mega-card + .site-mega-card { margin-top: 2px; }
.site-mega-card:hover { background: rgba(var(--site-primary-rgb), .06); }
.site-mega-card-icon {
    flex: 0 0 auto; width: 44px; height: 44px; border-radius: var(--r-sm);
    display: grid; place-items: center; font-size: 17px;
    color: rgba(var(--site-primary-rgb), .5);
    background: linear-gradient(135deg, rgba(var(--site-primary-rgb), .13), rgba(var(--site-accent-rgb), .1));
}
.site-mega-card-body { display: block; min-width: 0; }
.site-mega-card-title {
    display: block; font-size: 14px; font-weight: 700; color: var(--clr-tx-1); line-height: 1.3;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.site-mega-card-meta {
    display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 3px;
    font-size: 12px; color: var(--clr-tx-3);
}
.site-mega-card-meta i { font-size: 10px; margin-right: 3px; }
.site-mega-card-price { font-weight: 700; color: var(--site-accent-text); }
.site-mega-empty { margin: 0; padding: var(--sp-4) 0; font-size: 13px; color: var(--clr-tx-3); }

/* Below the lg breakpoint the desktop nav is replaced by the mobile drawer, so the two-pane
   layout never renders there — but keep it from breaking if the breakpoint ever moves. */
@media (max-width: 575px) {
    .site-mega-grid { grid-template-columns: 1fr; }
    .site-mega-nav { flex-direction: row; overflow-x: auto; border-right: none; border-bottom: 1px solid var(--clr-border); }
}

/* ── Results toolbar (Phase 13.11) ──────────────────────────────────────────────────────────
   The count on the left, refine controls (Sort By, Star Rating) on the right, above a results
   grid. These controls used to live inside the search bar itself, which was wrong twice over:
   they refine results rather than search for them, and at 7 fields the hotels bar wrapped to a
   second row with one field stretched across the whole of it. The selects sit outside the form
   in the DOM and re-join it with the HTML `form` attribute, so nothing about the submitted query
   changes. */
.site-results-bar {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
    gap: var(--sp-4); margin-bottom: var(--sp-5);
}
.site-results-bar-controls { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); }
.site-results-bar-field {
    display: flex; align-items: center; gap: 8px;
    padding: 6px 12px 6px 14px; border: 1px solid var(--clr-border-str);
    border-radius: var(--r-pill); background: var(--clr-surface);
    transition: border-color var(--dur) var(--ease);
}
.site-results-bar-field:focus-within { border-color: rgba(var(--site-accent-rgb), .55); }
.site-results-bar-field .site-field-label { margin: 0; white-space: nowrap; font-size: 11px; letter-spacing: .06em; text-transform: uppercase; }
/* Same "strip Select2 back to bare text" treatment the search bar uses — without it these render
   as a bordered box inside an already-bordered pill. */
.site-results-bar-field .select2-container { width: auto !important; min-width: 118px; }
.site-results-bar-field .select2-container--default .select2-selection--single {
    border: none; background: none; box-shadow: none; border-radius: 0; padding: 0; height: auto;
}
.site-results-bar-field .select2-container--default.select2-container--open .select2-selection--single,
.site-results-bar-field .select2-container--default.select2-container--focus .select2-selection--single {
    border: none; box-shadow: none;
}
.site-results-bar-field .select2-container--default .select2-selection--single .select2-selection__rendered {
    padding: 0 20px 0 0; font-size: 14px; font-weight: 700; line-height: 1.5; color: var(--clr-tx-1);
}
.site-results-bar-field .select2-container--default .select2-selection--single .select2-selection__arrow { right: 0; }
.site-results-bar-field .site-select { border: none; background: none; box-shadow: none; padding: 0; font-weight: 700; font-size: 14px; }
@media (max-width: 575px) {
    .site-results-bar { flex-direction: column; align-items: stretch; }
    .site-results-bar-controls { justify-content: space-between; }
}

/* ── Theme switching ────────────────────────────────────────────────────────────────────────
   Deliberately NO blanket transition here. An earlier attempt put a `transition` on every element
   and pseudo-element (with !important) for the duration of the switch; transitioning
   background-image and box-shadow across the whole tree at once cost more than it bought and read
   as a flash rather than a fade — measurably worse than the instant switch it replaced. The
   instant repaint is kept. The only thing that gets special handling is a themed banner image,
   and that is handled in site.js by preloading the incoming file BEFORE swapping, so the swap
   happens in one frame with nothing blank in between.
   ──────────────────────────────────────────────────────────────────────────────────────────── */
/* ── Flight fare row (Phase 13.16) ───────────────────────────────────────────────────────────
   A departure board, not a stack of cards. Every fare is one row of the same slab, and the four
   columns below are a real grid, so airline, route, cabin and price line up down the WHOLE list
   and the eye can compare fares by scanning a column.

   What this replaces: the row was a wrapping flexbox (`flex-wrap: wrap` with `flex: 1 1 340px`
   on the middle block), which means every row solved its own layout independently — nothing
   lined up with the row above it, and the right edge came out ragged because price and button
   stacked at whatever width was left over. That is the misalignment; a grid fixes it by
   construction rather than by tuning basis values.
   ─────────────────────────────────────────────────────────────────────────────────────────── */
.flight-row {
    display: grid;
    grid-template-columns: 46px minmax(190px, 1.5fr) minmax(150px, .9fr) minmax(132px, auto);
    align-items: center;
    gap: var(--sp-5);
    padding: var(--sp-5) var(--sp-6);
}
.flight-row-mark {
    width: 46px; height: 46px; border-radius: var(--r-sm);
    display: grid; place-items: center; font-size: 17px;
    border: 1px solid var(--clr-border); background: var(--clr-surface-2); color: var(--clr-tx-3);
    transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.cell:hover .flight-row-mark { color: var(--site-accent-text); border-color: rgba(var(--site-accent-rgb), .4); background: rgba(var(--site-accent-rgb), .08); }
.flight-row-airline { font-size: 14.5px; font-weight: 700; color: var(--clr-tx-1); line-height: 1.3; }

/* Route connector — origin and destination either side of a hairline the plane sits on. Kept
   from the previous row because it genuinely reads as a flight rather than as "A to B" text. */
.flight-route { display: flex; align-items: center; gap: var(--sp-2); margin-top: 5px; font-size: 13px; color: var(--clr-tx-2); }
.flight-route-code { font-weight: 700; color: var(--clr-tx-1); font-variant-numeric: tabular-nums; }
/* Two line segments with the icon BETWEEN them, rather than one line with the icon knocked out
   over it. The knock-out version painted `var(--clr-surface)` behind the glyph, which is a solid
   white in light theme but `rgba(255,255,255,.03)` in dark — so in dark mode it hid nothing and
   the hairline ran straight through the plane. Splitting the line removes the need for any
   knock-out colour at all, so there is nothing left to get wrong per theme or on hover. */
.flight-route-line { flex: 0 0 46px; display: flex; align-items: center; gap: 4px; }
.flight-route-line::before,
.flight-route-line::after { content: ''; flex: 1 1 auto; height: 1px; background: var(--clr-border-str); }
.flight-route-line i { flex: 0 0 auto; font-size: 10px; color: var(--site-accent-text); }

.flight-row-facts { display: flex; flex-direction: column; gap: 5px; font-size: 12.5px; color: var(--clr-tx-2); }
.flight-row-fact { display: flex; align-items: center; gap: 7px; }
.flight-row-fact i { width: 13px; text-align: center; font-size: 11px; color: var(--clr-tx-3); }
.flight-row-fact .site-num { font-weight: 700; color: var(--clr-tx-1); }

/* Fixed-width, right-aligned so the price column and the buttons share one clean edge. */
.flight-row-buy { text-align: right; display: flex; flex-direction: column; align-items: flex-end; gap: var(--sp-2); }
.flight-row-price { font-size: 18px; font-weight: 800; letter-spacing: -.01em; color: var(--site-accent-text); line-height: 1.15; font-variant-numeric: tabular-nums; }
.flight-row-price-note { font-size: 11px; color: var(--clr-tx-3); font-weight: 500; }

@media (max-width: 860px) {
    /* Two columns: identity spans the top, facts and price sit beneath it. The mark keeps its
       own column so the left edge still reads as a single rail down the list. */
    .flight-row { grid-template-columns: 46px 1fr; gap: var(--sp-3) var(--sp-4); }
    .flight-row-id { grid-column: 2; }
    .flight-row-facts { grid-column: 2; }
    .flight-row-buy { grid-column: 2; align-items: flex-start; text-align: left; flex-direction: row; align-items: center; justify-content: space-between; width: 100%; }
}

/* The idle corner brackets (section 8 above, `.cell-bracket { opacity: .3 }`) are right on a
   360px card, where they read as framing. Across a 17-row fare list they become 34 faint marks
   and read as artifacts — design.md principle 3 exactly: "decoration scales with its container;
   an effect tuned for a 360px card floods a 100px one". The row keeps the hover bracket, which
   is the part that carries meaning, and drops the resting one. */
.flight-row .cell-bracket { opacity: 0; }
.flight-row:hover .cell-bracket { opacity: 1; }

/* ── Spec grid — label/value quick facts (Phase 13.16) ───────────────────────────────────────
   The detail pages had no component for "here are the six things you need to know", so each one
   improvised: the visa page borrowed `.site-filter-pill` (an INTERACTIVE filter control) for
   static facts and then cancelled the affordance with `cursor:default` three times over, and the
   hotel page simply had nowhere to put its city/stars/rooms at all.

   Built in the slab's own language — one bordered frame, cells divided by hairlines, square
   structural corners — so facts read as one instrument panel rather than a scatter of chips. No
   hover: these are readouts, not controls, and per design.md hover is reserved for things you
   can act on. ─────────────────────────────────────────────────────────────────────────────── */
.site-spec {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
    border: 1px solid var(--clr-border);
    border-radius: var(--r-md);
    overflow: hidden;
    background: var(--clr-surface);
}
.site-spec-item {
    padding: var(--sp-4) var(--sp-5);
    box-shadow: 1px 0 0 var(--clr-border), 0 1px 0 var(--clr-border);
    display: flex; flex-direction: column; gap: 4px; min-width: 0;
}
.site-spec-label {
    display: flex; align-items: center; gap: 7px;
    font-size: 11px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
    color: var(--clr-tx-3);
}
.site-spec-label i { font-size: 11px; color: var(--site-accent-text); }
.site-spec-value {
    font-size: 15px; font-weight: 700; color: var(--clr-tx-1); line-height: 1.35;
    overflow-wrap: anywhere;
}
.site-spec-value .site-num { font-variant-numeric: tabular-nums; }

/* ── Data table (Phase 13.16) ────────────────────────────────────────────────────────────────
   Extracted from two identical hand-rolled tables (visa fee bands, package pricing tiers) that
   between them carried ~22 inline styles. Numeric columns get tabular figures so prices line up
   digit-for-digit down the column — the whole reason a fare table beats a list of chips. ─── */
.site-table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.site-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.site-table th {
    text-align: left; padding: 10px 14px; font-weight: 700; font-size: 11px;
    letter-spacing: .07em; text-transform: uppercase; color: var(--clr-tx-3);
    border-bottom: 1px solid var(--clr-border-str); white-space: nowrap;
}
.site-table td { padding: 11px 14px; border-bottom: 1px solid var(--clr-border); color: var(--clr-tx-2); }
.site-table tbody tr:last-child td { border-bottom: none; }
.site-table .is-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.site-table td.is-num { font-weight: 700; color: var(--clr-tx-1); }
.site-table .is-primary { color: var(--site-accent-text); }

/* Phase 13.29 — the modifiers the portal's four hand-rolled list tables (invoices, notifications,
   reviews, booking detail) were each re-specifying inline on every cell. They belong on this
   component rather than a second portal-only one: the markup is the same table, so a second
   component would only have reintroduced the drift this one was extracted to end. */
.site-table .is-key { color: var(--clr-tx-1); font-weight: 600; }
.site-table .is-right { text-align: right; white-space: nowrap; }
.site-table .is-nowrap { white-space: nowrap; }
.site-table tbody tr { transition: background-color var(--dur) var(--ease); }
.site-table tbody tr:hover { background: rgba(var(--site-accent-rgb), .04); }
/* A secondary line inside a cell (a failure reason under a notification's title) — a block span,
   not a div, so it stays valid inside the inline content these cells already hold. */
.site-table .cell-note { display: block; font-size: 11.5px; font-weight: 400; margin-top: 2px; }
.site-table .cell-note--danger { color: var(--clr-danger); }
/* Below this width the columns word-wrap into unreadable stacks, so the SCROLLER takes over.
   Only applies inside .site-table-scroll: the narrow fee/pricing tables this component was first
   built for sit in tight sidebars and must stay free to shrink. */
.site-table-scroll > .site-table { min-width: 620px; }

/* ── Accordion (Phase 13.16) ─────────────────────────────────────────────────────────────────
   Built on <details>/<summary> so it opens with no JavaScript and is keyboard-operable for free.
   The site's one existing <details> (the visa FAQ) killed the native marker with
   `list-style:none` and put nothing back, leaving no way to tell an open row from a closed one —
   this supplies a real chevron that turns on open. Motion here answers the person's own click,
   which is the kind design.md permits. ──────────────────────────────────────────────────── */
.site-accordion { display: flex; flex-direction: column; gap: var(--sp-2); }
.site-accordion-item {
    border: 1px solid var(--clr-border); border-radius: var(--r-sm);
    background: var(--clr-surface); overflow: hidden;
    transition: border-color var(--dur) var(--ease);
}
.site-accordion-item[open] { border-color: rgba(var(--site-accent-rgb), .35); }
.site-accordion-head {
    display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
    padding: var(--sp-4) var(--sp-5); cursor: pointer;
    font-weight: 600; font-size: 14.5px; color: var(--clr-tx-1);
    list-style: none;                         /* Firefox */
    transition: color var(--dur) var(--ease);
}
.site-accordion-head::-webkit-details-marker { display: none; }   /* Safari/Chrome */
.site-accordion-head:hover { color: var(--site-accent-text); }
.site-accordion-head::after {
    content: '\f078';                          /* fa-chevron-down */
    font-family: 'Font Awesome 6 Free'; font-weight: 900;
    font-size: 11px; color: var(--clr-tx-3); flex: 0 0 auto;
    transition: transform var(--dur) var(--ease), color var(--dur) var(--ease);
}
.site-accordion-item[open] .site-accordion-head::after { transform: rotate(180deg); color: var(--site-accent-text); }
.site-accordion-body {
    padding: 0 var(--sp-5) var(--sp-5);
    font-size: 14px; color: var(--clr-tx-2); line-height: 1.65; max-width: 72ch;
}
@media (prefers-reduced-motion: reduce) {
    .site-accordion-head::after { transition: none; }
}

/* Document-requirement cells inside a visa-type card. Two columns at most: the list is scannable
   prose, not a card grid, so letting it auto-fit to 4+ narrow columns would shred the sentences. */
.doc-req-slab { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.doc-req-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.doc-req-item { display: flex; align-items: flex-start; gap: 10px; font-size: 13px; color: var(--clr-tx-2); }
.doc-req-mark {
    flex: 0 0 auto; width: 18px; height: 18px; margin-top: 1px; border-radius: var(--r-pill);
    display: grid; place-items: center; font-size: 7px;
    background: var(--clr-surface-2); color: var(--clr-tx-3);
}
/* A required document is the norm here, not an alarm — the old treatment put a red "Required"
   badge beside almost every line, which made a routine checklist read as a wall of errors.
   Required is marked with a quiet accent asterisk; only the EXCEPTION is spelled out in words. */
.doc-req-mark.is-required { background: rgba(var(--site-accent-rgb), .12); color: var(--site-accent-text); }
.doc-req-name { font-weight: 600; color: var(--clr-tx-1); }
.doc-req-optional {
    margin-left: 6px; font-size: 10.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
    color: var(--clr-tx-3);
}
.doc-req-desc { display: block; margin-top: 3px; font-size: 12.5px; color: var(--clr-tx-3); line-height: 1.5; }

/* ── Hotel detail: gallery empty state, amenity checklist, room rows (Phase 13.16) ───────── */

/* When a hotel has no real uploaded photo. Sized like a caption, not like a photo — the old page
   let the fallback asset render full-width and unconstrained, so a placeholder occupied a third
   of the page and read as "this is what the hotel looks like". */
.detail-gallery-empty {
    display: flex; align-items: center; justify-content: center; gap: var(--sp-3);
    padding: var(--sp-7) var(--sp-5);
    border: 1px dashed var(--clr-border-str); border-radius: var(--r-md);
    color: var(--clr-tx-3); font-size: 13.5px; font-weight: 600;
}
.detail-gallery-empty i { font-size: 18px; }

.amenity-list {
    list-style: none; margin: 0; padding: 0;
    display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: var(--sp-2) var(--sp-5);
}
.amenity-item { display: flex; align-items: flex-start; gap: 9px; font-size: 14px; color: var(--clr-tx-2); }
.amenity-item i { margin-top: 4px; font-size: 10px; color: var(--site-accent-text); flex: 0 0 auto; }

/* Same grid discipline as .flight-row: fixed columns so the price column is a straight line down
   the list and rooms can be compared without reading each one. */
.room-row {
    display: grid;
    grid-template-columns: 46px minmax(160px, 1fr) minmax(170px, auto);
    align-items: center; gap: var(--sp-5);
    padding: var(--sp-5) var(--sp-6);
}
.room-row-mark {
    width: 46px; height: 46px; border-radius: var(--r-sm);
    display: grid; place-items: center; font-size: 17px;
    border: 1px solid var(--clr-border); background: var(--clr-surface-2); color: var(--clr-tx-3);
    transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.cell:hover .room-row-mark { color: var(--site-accent-text); border-color: rgba(var(--site-accent-rgb), .4); background: rgba(var(--site-accent-rgb), .08); }
.room-row-name { font-size: 15px; font-weight: 700; color: var(--clr-tx-1); line-height: 1.3; }
.room-row-fact { display: flex; align-items: center; gap: 7px; margin-top: 5px; font-size: 12.5px; color: var(--clr-tx-2); }
.room-row-fact i { font-size: 11px; color: var(--clr-tx-3); }
.room-row-fact .site-num { font-weight: 700; color: var(--clr-tx-1); }
.room-row-buy { display: flex; align-items: center; justify-content: flex-end; gap: var(--sp-4); text-align: right; }
.room-row-price { font-size: 18px; font-weight: 800; letter-spacing: -.01em; color: var(--site-accent-text); line-height: 1.15; font-variant-numeric: tabular-nums; }
.room-row-price-note { font-size: 11px; color: var(--clr-tx-3); font-weight: 500; }
/* Same container-scale reasoning as .flight-row: a resting bracket on every row of a list is
   noise rather than framing. */
.room-row .cell-bracket { opacity: 0; }
.room-row:hover .cell-bracket { opacity: 1; }

@media (max-width: 700px) {
    .room-row { grid-template-columns: 46px 1fr; }
    .room-row-buy { grid-column: 2; justify-content: space-between; }
}

/* ── Package detail: itinerary timeline, included hotels, remarks (Phase 13.17) ──────────────
   The itinerary was a grid of equal cells, each carrying a big outlined ghost numeral. Two things
   were wrong with that: a grid says "these are parallel options" when a day-by-day itinerary is
   the one thing on this site that genuinely IS a sequence, and the numerals bled off the corner of
   short cells as stray marks rather than reading as day numbers.

   A vertical rail with numbered markers states the sequence directly. (The design guidance against
   01/02/03 markers is about decoration on non-sequential content — here the numbers ARE the
   content.) ─────────────────────────────────────────────────────────────────────────────────── */
/* `list-style: none` and the margin/padding reset are load-bearing, not tidiness: this is a real
   <ol> (the days are an ordered sequence, and that belongs in the markup so it reads as one to a
   screen reader), so without them the browser's own "1." marker prints beside the rail marker. */
.itinerary {
    position: relative; list-style: none; margin: 0;
    display: flex; flex-direction: column; gap: var(--sp-5);
    padding: 0 0 0 44px;
}
/* The rail stops at the centre of the first and last markers rather than running the full height,
   so it reads as spanning day 1 to day N instead of trailing off past both ends. */
.itinerary::before {
    content: ''; position: absolute; left: 15px; top: 15px; bottom: 15px;
    width: 1px; background: var(--clr-border-str);
}
.itinerary-day { position: relative; }
.itinerary-marker {
    position: absolute; left: -44px; top: 0;
    width: 31px; height: 31px; border-radius: var(--r-pill);
    display: grid; place-items: center;
    font-size: 11.5px; font-weight: 800; font-variant-numeric: tabular-nums;
    background: var(--clr-surface); color: var(--site-accent-text);
    border: 1px solid rgba(var(--site-accent-rgb), .4);
    /* Opaque ring so the rail does not show through the marker in either theme — the same
       knock-out mistake the flight route connector made, avoided here by painting a ring in the
       page's own background colour rather than a translucent surface token. */
    box-shadow: 0 0 0 4px var(--clr-bg);
}
.itinerary-title { font-size: 15px; font-weight: 700; color: var(--clr-tx-1); line-height: 1.35; padding-top: 5px; }
.itinerary-day-label { color: var(--clr-tx-3); font-weight: 700; }
.itinerary-desc { margin: 6px 0 0; font-size: 13.5px; color: var(--clr-tx-2); line-height: 1.6; max-width: 72ch; }
.itinerary-meta { margin-top: var(--sp-3); }

/* Included hotels — compact rows in one slab rather than separately bordered boxes. */
.pkg-hotel-slab { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.pkg-hotel { flex-direction: row; align-items: center; gap: var(--sp-3); padding: var(--sp-4) var(--sp-5); }
.pkg-hotel-thumb {
    width: 48px; height: 48px; border-radius: var(--r-sm); overflow: hidden; flex: 0 0 auto;
    background: var(--clr-surface-2); display: grid; place-items: center;
    color: var(--clr-tx-3); font-size: 16px;
}
.pkg-hotel-thumb img { width: 100%; height: 100%; object-fit: cover; }
.pkg-hotel-name { font-size: 14px; font-weight: 700; color: var(--clr-tx-1); line-height: 1.3; }
.pkg-hotel-city { font-size: 12.5px; color: var(--clr-tx-3); margin-top: 2px; }
.pkg-hotel .site-star-rating { font-size: 10px; margin-top: 3px; }
.pkg-hotel .cell-bracket { opacity: 0; }
.pkg-hotel:hover .cell-bracket { opacity: 1; }

/* Variant header: name and tags on the left, the price and CTA as one right-hand block. The old
   layout pushed a stat readout and a button to the right with `justify-content:space-between` and
   nothing in between, so a wide screen opened a corridor of empty space across the card. */
.variant-head {
    display: flex; align-items: flex-start; justify-content: space-between;
    flex-wrap: wrap; gap: var(--sp-5); margin-bottom: var(--sp-5);
}
.variant-buy { display: flex; flex-direction: column; align-items: flex-end; gap: var(--sp-3); text-align: right; }
.variant-price { font-size: clamp(1.4rem, 3vw, 1.75rem); font-weight: 800; letter-spacing: -.01em; color: var(--site-accent-text); line-height: 1.1; font-variant-numeric: tabular-nums; }
.variant-price-note { font-size: 12px; color: var(--clr-tx-3); }
@media (max-width: 600px) {
    .variant-head { flex-direction: column; }
    .variant-buy { align-items: flex-start; text-align: left; width: 100%; }
    .variant-buy .site-btn { width: 100%; justify-content: center; }
}

/* Remarks — what's included/excluded, grouped by category. Cells in one slab so the groups read
   as one list of terms rather than a scatter of unrelated cards. */
.remark-slab { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.remark-body { font-size: 13.5px; color: var(--clr-tx-2); line-height: 1.6; margin: 0; }
.remark-body + .remark-body { margin-top: var(--sp-3); }

/* ── Package gallery — main image with selectable thumbnails (Phase 13.18) ───────────────────
   The e-commerce pattern: one large frame, a strip of thumbnails beneath it, clicking a thumbnail
   swaps the main image. Distinct from `.detail-gallery` (a fixed composed collage) because a
   package can carry several shots that each deserve a full look, not a 2-up crop.

   Progressive enhancement: the main image is server-rendered and every thumbnail is a real
   <button> inside the same figure, so with JS off the page still shows the hero and the strip —
   nothing is hidden behind script. ────────────────────────────────────────────────────────── */
/* Thumbnails run down the SIDE, not underneath. Two reasons: a full-width 16/9 frame plus a
   full-width strip beneath it consumed roughly 700px of vertical space before a single fact about
   the package, and a vertical strip is the arrangement that keeps the main image and its
   alternatives in one glance instead of pushing the page down. The main frame is also 3/2 rather
   than 16/9 — less letterboxed, and noticeably shorter at the same width. */
.pkg-gallery {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 92px;
    gap: var(--sp-3);
    /* `start`, not the default `stretch`: the rail sizes to its own thumbnails instead of being
       stretched to whatever height the current image happens to have. */
    align-items: start;
}
.pkg-gallery-main {
    position: relative; border-radius: var(--r-lg); overflow: hidden;
    /* Fixed WIDTH, automatic height — the frame takes whatever height the image's own proportions
       give it, so nothing is ever cropped. Any fixed height here (with `object-fit: cover`) cuts
       the top and bottom off the picture, which is wrong for a gallery: the point is to see the
       whole shot. The trade-off accepted deliberately is that the frame's height changes with the
       selected image; `min-height` keeps a very short image from collapsing the rail beside it. */
    width: 100%;
    min-height: 220px;
    background: var(--clr-surface-2);
}
/* `height: auto`, not 100% + cover — this is what actually prevents the crop. */
.pkg-gallery-main img { width: 100%; height: auto; display: block; }
/* Counter sits over the image so it does not add a row of chrome under the strip. */
.pkg-gallery-count {
    position: absolute; right: var(--sp-4); bottom: var(--sp-4); z-index: 1;
    padding: 5px 11px; border-radius: var(--r-pill);
    background: rgba(6, 2, 20, .62); color: #fff;
    font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums;
    backdrop-filter: blur(3px);
}
/* The grid's default `align-items: stretch` gives this column exactly the main frame's height,
   so the strip never drives the gallery taller than the image — extra thumbnails scroll inside it
   instead. 2px of padding keeps the selected ring from clipping against the scroll box. */
.pkg-gallery-strip {
    display: flex; flex-direction: column; gap: var(--sp-3);
    overflow-y: auto; overflow-x: hidden; scrollbar-width: none;
    padding: 2px;
}
.pkg-gallery-strip::-webkit-scrollbar { display: none; }
.pkg-gallery-thumb {
    position: relative; padding: 0; border: none; cursor: pointer;
    border-radius: var(--r-md); overflow: hidden; aspect-ratio: 4/3;
    flex: 0 0 auto;
    background: var(--clr-surface-2);
    box-shadow: 0 0 0 1px var(--clr-border);
    transition: box-shadow var(--dur) var(--ease), opacity var(--dur) var(--ease);
    opacity: .72;
}
.pkg-gallery-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pkg-gallery-thumb:hover { opacity: 1; }
/* Selected state is a ring, not a transform — design.md: interaction changes light, never layout. */
.pkg-gallery-thumb.is-active { opacity: 1; box-shadow: 0 0 0 2px var(--site-accent-strong); }
.pkg-gallery-thumb:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(var(--site-accent-rgb), .55); }

@media (prefers-reduced-motion: reduce) {
    .pkg-gallery-thumb { transition: none; }
}

/* ── Form field error + required marker (Phase 13.19) ────────────────────────────────────────
   Extracted from 38 copies of an identical inline <div> and 28 of an identical asterisk <span>
   across the four booking flows. Beyond the duplication, having it as a class means the error
   treatment can be fixed in one place — and it needed fixing: the inline version was plain
   12px text with no icon and no spacing rhythm, so a rejected field read as a stray sentence
   rather than as something wrong with the input above it. */
.site-field-error {
    display: flex; align-items: flex-start; gap: 6px;
    margin-top: 6px; font-size: 12.5px; line-height: 1.45; color: var(--clr-danger);
}
.site-field-error::before {
    content: '\f06a';                      /* fa-circle-exclamation */
    font-family: 'Font Awesome 6 Free'; font-weight: 900;
    font-size: 11px; line-height: 1.5; flex: 0 0 auto;
}
/* Block variant for an error that belongs to a whole group rather than one input (the flight
   flow's `segments` rule, for instance) — it needs to read as a banner, not a field footnote. */
/* Derived from --clr-danger with color-mix rather than a new hardcoded rgb triplet: there is no
   --clr-danger-rgb token, and inventing one would mean keeping two representations of the same
   colour in step across both themes. color-mix keeps this correct automatically when the danger
   token is re-tuned, which it already is per theme (light 38% lightness, dark 70%). */
.site-field-error--block {
    margin: 0 0 var(--sp-4);
    padding: 10px var(--sp-4);
    border: 1px solid color-mix(in srgb, var(--clr-danger) 40%, transparent);
    border-radius: var(--r-sm);
    background: color-mix(in srgb, var(--clr-danger) 10%, transparent);
}
.site-req { color: var(--clr-danger); }

/* Narrow screens: a 92px side rail costs too much of the width, so the strip returns to a
   horizontal row beneath the image — scrolling sideways there rather than stacking tall. */
@media (max-width: 700px) {
    .pkg-gallery { grid-template-columns: 1fr; }
    .pkg-gallery-strip {
        flex-direction: row; overflow-x: auto; overflow-y: hidden;
    }
    .pkg-gallery-thumb { width: 88px; }
}

/* ── Language switch (Phase 13.21) ───────────────────────────────────────────────────────────
   A two-sided segmented control rather than a single link showing only the OTHER language. The
   old control had a real problem beyond looks: it printed "BN" while you were reading English,
   so the label named the destination, not the state — and with nothing to compare it against,
   which one you were currently in was a guess. Here both languages are always visible and the
   active one is filled, so the control states the current language and the alternative at once.

   Structurally it is two links, not a toggle button: each side is a real href to
   /language/<locale>, so it keeps working with JavaScript off. site.js only intercepts the click
   to swap the page in place. */
.site-lang-switch {
    display: inline-flex; align-items: center;
    padding: 2px; gap: 2px;
    border: 1px solid var(--clr-border-str); border-radius: var(--r-pill);
    background: var(--clr-surface-2);
    line-height: 1;
}
.site-lang-switch-option {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 34px; padding: 5px 9px;
    border-radius: var(--r-pill);
    font-size: 11.5px; font-weight: 700; letter-spacing: .04em;
    color: var(--clr-tx-3); text-decoration: none;
    transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.site-lang-switch-option:hover { color: var(--clr-tx-1); }
.site-lang-switch-option.is-active {
    background: var(--site-accent-strong); color: #fff;
}
/* The active side is the current language, so it is not a link — `pointer-events: none` would
   hide it from assistive tech, so it stays focusable and is marked aria-current instead. */
.site-lang-switch-option.is-active { cursor: default; }

/* ── Note panel + empty state (Phase 13.22) ──────────────────────────────────────────────────
   Two patterns the portal re-invented inline on nearly every screen: a short explanatory panel
   (previously a bordered div with 4-5 inline styles, written differently each time) and the
   "nothing here yet" placeholder, which across 13 portal views was a bare em-dash in a styled
   <p> — a character that reads as a value, not as an absence, and told the customer nothing
   about why the section was blank or what to do next.

   The note's tone comes from a modifier, not from a colour written at the call site, so the
   whole family stays consistent and dark-mode-correct in one place. Tints use color-mix against
   the live surface so each variant sits at the same distance from its background in both
   themes — "contrast is measured, not eyeballed" (design.md, principle 5). ───────────────── */
.site-note {
    display: flex; align-items: flex-start; gap: var(--sp-4);
    padding: var(--sp-4) var(--sp-5);
    border: 1px solid var(--clr-border);
    border-radius: var(--r-md);
    background: var(--clr-surface-2);
}
.site-note-icon {
    flex: 0 0 auto; width: 30px; height: 30px;
    display: flex; align-items: center; justify-content: center;
    border-radius: var(--r-sm);
    background: rgba(var(--site-accent-rgb), .12);
    color: var(--site-accent-text);
    font-size: 13px;
}
.site-note-body { min-width: 0; }
.site-note-title { font-size: 14px; font-weight: 700; color: var(--clr-tx-1); margin: 0 0 3px; }
.site-note-text { font-size: 13.5px; line-height: 1.55; color: var(--clr-tx-2); margin: 0; }
.site-note--info {
    border-color: color-mix(in srgb, var(--site-accent-strong) 32%, transparent);
    background: color-mix(in srgb, var(--site-accent-strong) 7%, var(--clr-surface));
}
.site-note--warn {
    border-color: color-mix(in srgb, var(--clr-warning) 38%, transparent);
    background: color-mix(in srgb, var(--clr-warning) 9%, var(--clr-surface));
}
.site-note--warn .site-note-icon { background: color-mix(in srgb, var(--clr-warning) 16%, transparent); color: var(--clr-warning); }
.site-note--danger {
    border-color: color-mix(in srgb, var(--clr-danger) 38%, transparent);
    background: color-mix(in srgb, var(--clr-danger) 8%, var(--clr-surface));
}
.site-note--danger .site-note-icon { background: color-mix(in srgb, var(--clr-danger) 14%, transparent); color: var(--clr-danger); }

/* Empty state. Centred, quiet, and always says what would appear here — plus, where the caller
   supplies one, the action that would fill it. */
.site-empty {
    display: flex; flex-direction: column; align-items: center; text-align: center;
    gap: var(--sp-3);
    padding: var(--sp-8) var(--sp-5);
    color: var(--clr-tx-3);
}
.site-empty-icon {
    width: 46px; height: 46px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 50%;
    background: var(--clr-surface-2);
    border: 1px solid var(--clr-border);
    font-size: 17px; color: var(--clr-tx-3);
}
.site-empty-title { font-size: 14.5px; font-weight: 700; color: var(--clr-tx-2); margin: 0; }
.site-empty-text { font-size: 13px; line-height: 1.55; margin: 0; max-width: 42ch; }
.site-empty--inline { padding: var(--sp-5) var(--sp-4); }
.site-empty--inline .site-empty-icon { width: 34px; height: 34px; font-size: 13px; }

/* ── Portal page header (Phase 13.22) ────────────────────────────────────────────────────────
   Every portal screen opened with its own hand-built flex row — title on the left, whatever
   actions on the right — each with its own margins and its own idea of the sizes. One component
   now, so the eyebrow/title/action rhythm is identical on all of them and mobile wrapping is
   solved once instead of thirteen times. ─────────────────────────────────────────────────── */
.portal-page-head {
    display: flex; flex-wrap: wrap; align-items: flex-start;
    justify-content: space-between; gap: var(--sp-4);
    margin-bottom: var(--sp-6);
}
.portal-page-head-main { min-width: 0; }
.portal-page-head-title { font-size: 22px; font-weight: 800; letter-spacing: -.01em; color: var(--clr-tx-1); margin: 5px 0 0; }
.portal-page-head-sub { font-size: 13.5px; color: var(--clr-tx-3); margin: 5px 0 0; }
.portal-page-head-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }

/* Key/value rail used down the side of a detail screen — the portal's counterpart to
   `.site-spec`, for when the facts stack vertically rather than sitting in a grid. */
.portal-kv { display: flex; flex-direction: column; }
.portal-kv-row {
    display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-4);
    padding: 9px 0;
    border-bottom: 1px solid var(--clr-border);
}
.portal-kv-row:last-child { border-bottom: none; }
.portal-kv-key { font-size: 12.5px; color: var(--clr-tx-3); flex: 0 0 auto; }
.portal-kv-val { font-size: 13.5px; font-weight: 600; color: var(--clr-tx-1); text-align: right; overflow-wrap: anywhere; }
.portal-kv-val.is-num { font-variant-numeric: tabular-nums; }

/* ── Pagination (Phase 13.22) ────────────────────────────────────────────────────────────────
   Laravel's bootstrap-5 paginator view ships Bootstrap's own blue link colours, which had no
   relationship to the site's accent and — being defined for a light card — left the "Showing X
   to Y of Z" line nearly unreadable on the dark portal background. Rather than fork the vendor
   view, the same markup is re-tokenised here: variables first (Bootstrap reads these at paint,
   so the per-vertical accent remap keeps working), then the few rules its variables don't cover.

   The active page is a filled accent chip rather than an outline, because on a grid of cards
   the paginator is the only thing on its row and an outline-only state was easy to miss. ─── */
.pagination {
    --bs-pagination-color: var(--clr-tx-2);
    --bs-pagination-bg: var(--clr-surface);
    --bs-pagination-border-color: var(--clr-border);
    --bs-pagination-hover-color: var(--clr-tx-1);
    --bs-pagination-hover-bg: var(--clr-surface-2);
    --bs-pagination-hover-border-color: var(--clr-border-str);
    --bs-pagination-focus-color: var(--clr-tx-1);
    --bs-pagination-focus-bg: var(--clr-surface-2);
    --bs-pagination-focus-box-shadow: 0 0 0 3px rgba(var(--site-accent-rgb), .25);
    --bs-pagination-active-color: #fff;
    --bs-pagination-active-bg: var(--site-accent-strong);
    --bs-pagination-active-border-color: var(--site-accent-strong);
    --bs-pagination-disabled-color: var(--clr-tx-3);
    --bs-pagination-disabled-bg: var(--clr-surface);
    --bs-pagination-disabled-border-color: var(--clr-border);
    --bs-pagination-border-radius: var(--r-sm);
    --bs-pagination-font-size: 13.5px;
    margin-bottom: 0;
}
/* Laravel's paginator wraps its result count in `.results` inside a flex row; the vendor markup
   colours it with Bootstrap's muted grey, which fails contrast on the dark portal surface. */
.pagination-wrap, nav[role="navigation"] .text-muted, nav[role="navigation"] p small,
nav[role="navigation"] .small { color: var(--clr-tx-3) !important; }
nav[role="navigation"] .fw-semibold { color: var(--clr-tx-1); }

/* ── Portal list rows (Phase 13.22) ──────────────────────────────────────────────────────────
   The dashboard, the invoice list and the notification list each built the same row by hand —
   a bordered box, a title that ellipses, a meta line, something on the right — with the padding,
   radius and border repeated inline every time and drifting between them. One component, so a
   row reads identically wherever the portal lists things.

   Hover is a border/background shift only. The row is a link, and design.md's principle 6 is
   that interaction never disturbs layout: nothing here moves. ───────────────────────────── */
.portal-list { display: flex; flex-direction: column; gap: var(--sp-2); }
.portal-list-item {
    display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
    padding: var(--sp-3) var(--sp-4);
    border: 1px solid var(--clr-border); border-radius: var(--r-sm);
    background: var(--clr-surface);
    color: inherit; text-decoration: none;
    transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
a.portal-list-item:hover {
    border-color: rgba(var(--site-accent-rgb), .45);
    background: var(--clr-surface-2);
}
.portal-list-item-main { min-width: 0; flex: 1 1 auto; }
.portal-list-item-title {
    font-size: 14px; font-weight: 600; color: var(--clr-tx-1);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.portal-list-item-meta {
    display: flex; align-items: center; flex-wrap: wrap; gap: 4px 10px;
    margin-top: 3px; font-size: 12.5px; color: var(--clr-tx-3);
}
.portal-list-item-meta .is-num { font-variant-numeric: tabular-nums; }
.portal-list-item-side { flex: 0 0 auto; display: flex; align-items: center; gap: var(--sp-2); }

/* ── Document library — file manager (Phase 13.23) ───────────────────────────────────────────
   A real folders-and-files view for the customer's own documents. The layout is a tile grid
   rather than a table because the two things in it are of different kinds — a folder you enter,
   a file you open — and a tile can carry a type icon and a thumbnail where a table row cannot.

   Tiles are square-cornered like every other structural surface in this system (design.md,
   "radius carries meaning"); only their actions, being controls, are rounded. Hover changes
   colour and nothing else — principle 6, interaction never disturbs layout — which also keeps
   a tile's own dropdown menu from jumping out from under the cursor. ─────────────────────── */
.lib-toolbar {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border: 1px solid var(--clr-border); border-radius: var(--r-md);
    background: var(--clr-surface);
    margin-bottom: var(--sp-4);
}
.lib-crumbs { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; min-width: 0; font-size: 13.5px; }
.lib-crumb {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 4px 9px; border-radius: var(--r-sm);
    color: var(--clr-tx-2); text-decoration: none; font-weight: 600;
    transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
a.lib-crumb:hover { background: var(--clr-surface-2); color: var(--clr-tx-1); }
.lib-crumb.is-current { color: var(--clr-tx-1); cursor: default; }
.lib-crumb-sep { color: var(--clr-tx-3); font-size: 10px; }

.lib-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: var(--sp-3);
}
.lib-tile {
    position: relative;
    display: flex; flex-direction: column; gap: var(--sp-2);
    padding: var(--sp-4);
    border: 1px solid var(--clr-border); border-radius: var(--r-md);
    background: var(--clr-surface);
    min-width: 0;
    transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.lib-tile:hover { border-color: rgba(var(--site-accent-rgb), .45); background: var(--clr-surface-2); }
/* The tile's whole face is the open target, laid over the tile so the menu button — which comes
   later in the stacking order — stays clickable without nesting an <a> inside an <a>. */
.lib-tile-open { position: absolute; inset: 0; z-index: 1; border-radius: inherit; }
.lib-tile-icon {
    width: 40px; height: 40px;
    display: flex; align-items: center; justify-content: center;
    border-radius: var(--r-sm); font-size: 17px;
    background: rgba(var(--site-accent-rgb), .12); color: var(--site-accent-text);
}
.lib-tile--file .lib-tile-icon { background: var(--clr-surface-2); color: var(--clr-tx-2); }
.lib-tile-name {
    font-size: 13.5px; font-weight: 700; color: var(--clr-tx-1); line-height: 1.35;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
    overflow-wrap: anywhere;
}
.lib-tile-meta { font-size: 11.5px; color: var(--clr-tx-3); display: flex; flex-wrap: wrap; gap: 4px 8px; }
.lib-tile-menu { position: absolute; top: var(--sp-2); right: var(--sp-2); z-index: 2; }
.lib-tile-menu .btn {
    width: 28px; height: 28px; padding: 0; border: none; border-radius: var(--r-sm);
    display: flex; align-items: center; justify-content: center;
    background: transparent; color: var(--clr-tx-3); font-size: 13px;
}
.lib-tile-menu .btn:hover { background: var(--clr-surface-2); color: var(--clr-tx-1); }
.lib-tile-badges { display: flex; flex-wrap: wrap; gap: 4px; }

/* Preview strip for image files — the reason a tile beats a row. Fixed aspect, cover-cropped:
   these are scans of documents, so the top-left of the page is the recognisable part. */
.lib-thumb {
    aspect-ratio: 16 / 10; width: 100%;
    border-radius: var(--r-sm); overflow: hidden;
    background: var(--clr-surface-2);
    border: 1px solid var(--clr-border);
}
.lib-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Upload drop zone inside the upload modal. */
.lib-drop {
    display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
    padding: var(--sp-6) var(--sp-4); text-align: center;
    border: 1px dashed var(--clr-border-str); border-radius: var(--r-md);
    background: var(--clr-surface-2);
    color: var(--clr-tx-3); font-size: 13px;
    cursor: pointer;
    transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.lib-drop:hover, .lib-drop.is-over {
    border-color: var(--site-accent-strong);
    background: color-mix(in srgb, var(--site-accent-strong) 8%, var(--clr-surface));
}
.lib-drop-icon { font-size: 22px; color: var(--site-accent-text); }
.lib-drop-name { font-weight: 700; color: var(--clr-tx-1); font-size: 13.5px; overflow-wrap: anywhere; }

/* Bootstrap modal, re-tokenised. Its default white panel and Bootstrap-blue focus ring were
   the only unthemed surfaces left in the portal. */
.modal-content {
    background: var(--clr-surface-float);
    color: var(--clr-tx-1);
    border: 1px solid var(--clr-border-str);
    border-radius: var(--r-lg);
}
.modal-header, .modal-footer { border-color: var(--clr-border); }
.modal-title { font-size: 16px; font-weight: 700; }
.modal .btn-close { filter: var(--icon-invert, none); }
[data-theme="dark"] .modal .btn-close { filter: invert(1) grayscale(100%) brightness(200%); }

/* Attach-from-library picker, used on the four checkout forms. */
.lib-pick { display: flex; flex-direction: column; gap: var(--sp-2); max-height: 320px; overflow-y: auto; }
.lib-pick-item {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: 10px var(--sp-4);
    border: 1px solid var(--clr-border); border-radius: var(--r-sm);
    background: var(--clr-surface);
    cursor: pointer;
    transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.lib-pick-item:hover { border-color: rgba(var(--site-accent-rgb), .45); }
.lib-pick-item input { flex: 0 0 auto; }
.lib-pick-item.is-checked { border-color: var(--site-accent-strong); background: color-mix(in srgb, var(--site-accent-strong) 6%, var(--clr-surface)); }
.lib-pick-body { min-width: 0; }
.lib-pick-name { font-size: 13.5px; font-weight: 600; color: var(--clr-tx-1); overflow-wrap: anywhere; }
.lib-pick-meta { font-size: 11.5px; color: var(--clr-tx-3); }

@media (max-width: 560px) {
    .lib-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: var(--sp-2); }
    .lib-tile { padding: var(--sp-3); }
}

/* Destructive action button. Its own colour, not the accent — a delete confirmation's primary
   button must not look like every other primary button on the site. */
.site-btn-danger { background: var(--clr-danger); border-color: transparent; color: #fff; }
.site-btn-danger:hover { background: color-mix(in srgb, var(--clr-danger) 85%, #000); color: #fff; }

/* ── Profile avatar (Phase 13.23) ────────────────────────────────────────────────────────────
   One component for the customer's picture, used at four sizes across the portal (topbar button,
   account dropdown, profile editor, and the admin customer screen). The initial-letter circle
   the portal already drew is kept as the no-picture state rather than a generic silhouette — a
   letter identifies an account at a glance in a way a grey person icon does not. ──────────── */
.profile-avatar {
    position: relative; flex: 0 0 auto;
    width: var(--avatar-size, 88px); height: var(--avatar-size, 88px);
    display: flex; align-items: center; justify-content: center;
    border-radius: 50%; overflow: hidden;
    background: rgba(var(--site-accent-rgb), .15);
    color: var(--site-accent-text);
    font-weight: 700; font-size: calc(var(--avatar-size, 88px) * .4);
    line-height: 1;
    border: 1px solid var(--clr-border);
}
.profile-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.profile-avatar--sm { --avatar-size: 36px; }
.profile-avatar--md { --avatar-size: 38px; }

.profile-avatar-row { display: flex; align-items: center; gap: var(--sp-5); flex-wrap: wrap; }
.profile-avatar-actions { min-width: 0; flex: 1 1 260px; }

/* ── Package: flights, inclusions, policies (Phase 13.24) ────────────────────────────────────
   Three blocks the package page could not render before because nothing held the data.

   The flight leg reuses the route-line idea already proven on the flight results row, but as a
   list item rather than a grid row: a package's legs are read in sequence (out, then internal,
   then back), and a grid of equal columns said "pick one of these" instead. ──────────────── */
.pkg-flight-list { display: flex; flex-direction: column; gap: var(--sp-2); }
.pkg-flight {
    display: grid;
    grid-template-columns: 34px minmax(0, 1fr) auto;
    align-items: center; gap: var(--sp-4);
    padding: var(--sp-4) var(--sp-5);
    border: 1px solid var(--clr-border); border-radius: var(--r-md);
    background: var(--clr-surface);
}
.pkg-flight-dir {
    width: 34px; height: 34px;
    display: flex; align-items: center; justify-content: center;
    border-radius: var(--r-sm);
    background: rgba(var(--site-accent-rgb), .12); color: var(--site-accent-text);
    font-size: 13px;
}
/* The return leg's plane points back, so the row reads as a direction before it is read as text. */
.pkg-flight--return .pkg-flight-dir i { transform: scaleX(-1); }
.pkg-flight-body { min-width: 0; }
.pkg-flight-route {
    display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
    font-size: 14.5px; font-weight: 700; color: var(--clr-tx-1);
}
.pkg-flight-route .sep { color: var(--clr-tx-3); font-size: 11px; }
.pkg-flight-meta {
    display: flex; align-items: center; flex-wrap: wrap; gap: 4px 12px;
    margin-top: 4px; font-size: 12.5px; color: var(--clr-tx-3);
}
.pkg-flight-meta .is-num { font-variant-numeric: tabular-nums; }
.pkg-flight-side { flex: 0 0 auto; text-align: right; }

/* What's included / what's not. Two columns of ticks and crosses, one list each — the shape
   this is universally read in, and the reason it must not be a slab of equal cells. */
.pkg-incl-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: var(--sp-5);
}
.pkg-incl-col { min-width: 0; }
.pkg-incl-head {
    display: flex; align-items: center; gap: 8px;
    font-size: 14px; font-weight: 700; color: var(--clr-tx-1);
    padding-bottom: var(--sp-3); margin-bottom: var(--sp-3);
    border-bottom: 1px solid var(--clr-border);
}
.pkg-incl-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.pkg-incl-item {
    display: grid; grid-template-columns: 20px minmax(0, 1fr);
    align-items: start; gap: 10px;
    font-size: 14px; line-height: 1.5; color: var(--clr-tx-2);
}
.pkg-incl-mark { font-size: 12px; line-height: 1.5; }
.pkg-incl--in .pkg-incl-mark { color: var(--clr-success); }
.pkg-incl--out .pkg-incl-mark { color: var(--clr-danger); }
/* An excluded line is struck through nowhere — greyed instead. Strike-through on a whole list
   is unreadable at 14px, and the cross already carries the meaning. */
.pkg-incl--out .pkg-incl-item { color: var(--clr-tx-3); }

/* Policy clauses: one slab of cells, because they are clauses of one agreement. */
.pkg-policy-body {
    font-size: 14px; line-height: 1.65; color: var(--clr-tx-2);
    margin: var(--sp-2) 0 0; white-space: pre-line;
}

@media (max-width: 560px) {
    .pkg-flight { grid-template-columns: 34px minmax(0, 1fr); gap: var(--sp-3); }
    .pkg-flight-side { grid-column: 2; text-align: left; }
}

/* ── Preview banner (Phase 13.25) ────────────────────────────────────────────────────────────
   Shown on the real public page when a signed-in admin is viewing something the public cannot
   reach. Fixed to the top of the viewport, not inline at the top of the document: the whole
   point is that it stays true while you scroll a long page, so an unpublished draft can never be
   mistaken for the live site just because you scrolled past the notice.

   It is the one place on the public site that deliberately ignores the brand accent — a warning
   that adopts the site's own colours stops reading as a warning. ─────────────────────────── */
.site-preview-bar {
    position: fixed;
    inset: 0 0 auto 0;
    z-index: 2000;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-4);
    padding: 9px var(--sp-5);
    background: var(--clr-warning);
    color: #1a1205;
    font-size: 13px;
    font-weight: 600;
    text-align: center;
    box-shadow: 0 2px 12px rgba(0, 0, 0, .18);
}
.site-preview-bar strong { font-weight: 800; }
.site-preview-bar a {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
    font-weight: 700;
    white-space: nowrap;
}
/* Pushes the page down by the bar's height so it never covers the header it sits above. */
body.has-preview-bar { padding-top: 38px; }
@media (max-width: 560px) {
    .site-preview-bar { font-size: 12px; padding: 8px var(--sp-4); }
    body.has-preview-bar { padding-top: 54px; }
}

/* ══════════════════════════════════════════════════════════════════════════════════════════
   CUSTOMER PORTAL — account summary widgets (Phase 13.29)
   Everything below layers on the existing --clr- and --site- token sets and the Instrument Grid
   grammar already in use: square-cornered panels, hairline dividers, glow-not-lift on hover.
   No new colour values, no charting library — the portal loads none and one percentage does
   not justify adding one.
   ══════════════════════════════════════════════════════════════════════════════════════════ */

/* ── "Needs your attention" row ───────────────────────────────────────────────────────────
   Work items, not statistics: each is a link, coloured by urgency rather than by brand, and
   the builder omits any tile whose count is zero so this strip is never reassurance-shaped. ── */
/* Flex rather than an auto-fit grid: with only one or two outstanding items an auto-fit grid
   stretched each tile across half the page, which made two small counts read as the loudest
   thing on the dashboard. These grow to fill a row but stop at a sane card width. */
.portal-attention-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-3);
    margin-bottom: var(--sp-5);
}
.portal-attention {
    flex: 1 1 260px;
    max-width: 380px;
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border: 1px solid var(--clr-border);
    border-left-width: 3px;
    border-radius: var(--r-sm);
    background: var(--clr-surface);
    text-decoration: none;
    transition: box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.portal-attention:hover { box-shadow: var(--shadow-sm); }
.portal-attention:hover .portal-attention-go { opacity: .75; transform: translateX(2px); }
.portal-attention-icon {
    width: 34px; height: 34px; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    border-radius: var(--r-tag);
    font-size: 14px;
}
.portal-attention-body { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
.portal-attention-count { font-size: 19px; font-weight: 800; color: var(--clr-tx-1); font-variant-numeric: tabular-nums; }
.portal-attention-label { font-size: 12.5px; font-weight: 500; color: var(--clr-tx-2); }
.portal-attention-meta { font-size: 11.5px; color: var(--clr-tx-3); margin-top: 2px; font-variant-numeric: tabular-nums; }
.portal-attention-go {
    margin-left: auto; font-size: 12px; color: var(--clr-tx-3);
    opacity: 0; transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.portal-attention--danger  { border-left-color: var(--clr-danger); }
.portal-attention--danger  .portal-attention-icon { background: rgba(220, 38, 38, .12); color: var(--clr-danger); }
.portal-attention--warning { border-left-color: var(--clr-warning); }
.portal-attention--warning .portal-attention-icon { background: rgba(217, 119, 6, .12); color: var(--clr-warning); }
.portal-attention--accent  { border-left-color: var(--site-accent); }
.portal-attention--accent  .portal-attention-icon { background: rgba(var(--site-accent-rgb), .12); color: var(--site-accent-text); }

/* ── Next trip ────────────────────────────────────────────────────────────────────────────
   The one panel on this page that earns extra visual weight: it is the thing the customer
   actually came to look at. Accent arrives as a tint and an edge, never as a filled block. ── */
.portal-trip {
    display: block;
    padding: var(--sp-5);
    border: 1px solid var(--clr-border);
    border-radius: var(--r-md);
    background:
        linear-gradient(135deg, rgba(var(--site-accent-rgb), .07), transparent 58%),
        var(--clr-surface);
    text-decoration: none;
    transition: box-shadow var(--dur) var(--ease);
}
.portal-trip:hover { box-shadow: var(--shadow-md); }
.portal-trip-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4); }
.portal-trip-headings { min-width: 0; }
.portal-trip-title {
    font-size: 19px; font-weight: 800; letter-spacing: -.01em;
    color: var(--clr-tx-1); margin: var(--sp-1) 0 0;
}
.portal-trip-sub { font-size: 13px; color: var(--clr-tx-3); margin: var(--sp-1) 0 0; }
.portal-trip-dot { opacity: .5; margin: 0 4px; }
.portal-trip-count { flex-shrink: 0; text-align: center; line-height: 1; }
.portal-trip-countdown {
    display: block; font-size: 34px; font-weight: 800;
    color: var(--site-accent-text); font-variant-numeric: tabular-nums;
}
.portal-trip-countdown-label {
    display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .07em;
    color: var(--clr-tx-3); margin-top: 4px;
}

.portal-trip-dates {
    display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-3);
    margin-top: var(--sp-4); padding-top: var(--sp-4);
    border-top: 1px solid var(--clr-border);
}
.portal-trip-date { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 600; color: var(--clr-tx-1); }
.portal-trip-date i { color: var(--site-accent-text); font-size: 12px; }
/* Takes the slack so the status badge sits hard right regardless of how many dates exist. */
.portal-trip-rail { flex: 1 1 24px; min-width: 24px; height: 1px; background: var(--clr-border-str); }

/* ── Trip checklist ───────────────────────────────────────────────────────────────────────
   A row of real stages. Each mark is driven by its own column, so an unticked step after a
   ticked one is a true statement about this booking, not a rendering artefact. ───────────── */
.portal-steps {
    list-style: none; margin: var(--sp-4) 0 0; padding: 0;
    display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--sp-3);
}
/* align-items:flex-start, and the label wraps instead of truncating: at four steps across this
   card the columns are ~155px, and an ellipsis turned "Documents submitted" into "Document…"
   while its "Not required" note was squeezed onto two lines beside it. */
.portal-step { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0 8px; min-width: 0; }
.portal-step-mark {
    width: 19px; height: 19px; flex-shrink: 0; margin-top: 1px;
    display: flex; align-items: center; justify-content: center;
    border: 1.5px solid var(--clr-border-str); border-radius: 50%;
    font-size: 9px; color: transparent;
}
.portal-step.is-done .portal-step-mark { background: var(--clr-success); border-color: var(--clr-success); color: #fff; }
.portal-step.is-skipped .portal-step-mark { border-style: dashed; color: var(--clr-tx-3); }
.portal-step-label { font-size: 12.5px; line-height: 1.35; color: var(--clr-tx-3); min-width: 0; }
.portal-step.is-done .portal-step-label { color: var(--clr-tx-1); font-weight: 600; }
/* On its own line under the label, never competing with it for the column's width. */
.portal-step-note { display: block; width: 100%; font-size: 10.5px; color: var(--clr-tx-3); opacity: .8; padding-left: 27px; margin-top: 1px; }

/* ── Payment ring ─────────────────────────────────────────────────────────────────────────
   conic-gradient, so the figure is painted by the same stylesheet that themes it and needs no
   script to redraw when the theme flips. ─────────────────────────────────────────────────── */
.portal-ring-row { display: flex; align-items: center; gap: var(--sp-4); margin-bottom: var(--sp-4); }
.portal-ring {
    --pct: 0;
    width: 86px; height: 86px; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    border-radius: 50%;
    background: conic-gradient(var(--clr-success) calc(var(--pct) * 1%), var(--clr-bg-2) 0);
    position: relative;
}
/* The hole is a real punched-out disc in the card's own colour, not a lighter tint, so the
   ring reads identically on both themes. */
.portal-ring::before {
    content: ''; position: absolute; inset: 9px;
    border-radius: 50%; background: var(--clr-surface-float);
}
.portal-ring-value {
    position: relative; font-size: 18px; font-weight: 800;
    color: var(--clr-tx-1); font-variant-numeric: tabular-nums;
}
.portal-ring-caption { font-size: 12.5px; color: var(--clr-tx-3); margin: 0; }

/* ── Document readiness segmented bar ─────────────────────────────────────────────────────
   Segments are sized by flex-grow against the real counts, so the bar is a true proportion
   without a percentage having to be computed per segment. ───────────────────────────────── */
.portal-segbar {
    display: flex; gap: 2px; height: 9px;
    border-radius: var(--r-pill); overflow: hidden;
    background: var(--clr-bg-2); margin-bottom: var(--sp-4);
}
.portal-seg { display: block; height: 100%; }
.portal-seg--ok { background: var(--clr-success); }
.portal-seg--wait { background: var(--site-accent); }
/* NOT --clr-warning: that token is deliberately crushed to 28% lightness so warning TEXT clears
   contrast on a light tinted pill, which as a solid 9px fill reads as olive-brown rather than as
   a warning (confirmed against a real light-theme screenshot). Same hue and saturation, lifted to
   a lightness that actually reads as amber in the role it is used in here. */
.portal-seg--todo { background: hsl(38, 92%, 45%); }
.portal-seg-legend { display: flex; flex-direction: column; gap: 7px; }
.portal-seg-legend > span { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--clr-tx-3); }
.portal-seg-legend b { margin-left: auto; color: var(--clr-tx-1); font-variant-numeric: tabular-nums; }
.portal-seg-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; display: inline-block; }
.portal-seg-done {
    display: flex; align-items: center; gap: 7px;
    margin: var(--sp-4) 0 0; font-size: 12.5px; color: var(--clr-success);
}

@media (max-width: 575.98px) {
    .portal-attention-row { grid-template-columns: 1fr; }
    .portal-trip { padding: var(--sp-4); }
    .portal-trip-head { flex-direction: column; gap: var(--sp-3); }
    .portal-trip-count { text-align: left; }
    .portal-trip-countdown { font-size: 28px; }
    .portal-steps { grid-template-columns: 1fr; gap: 10px; }
}
