/* SalonOS — install wizard styling. Same palette/token approach the
   public site and admin area will use later (see salonos-v3-spec.md):
   purple/pink/blue/green, light/dark toggle via [data-theme]. */

:root {
    --sos-primary: #7C3AED;
    --sos-secondary: #EC4899;
    --sos-accent: #0EA5E9;
    --sos-success: #10B981;
    --sos-bg: #F7F5FF;
    --sos-surface: #FFFFFF;
    --sos-text: #1F1B2E;
    --sos-muted: #6B6478;
    --sos-border: #E4DEF5;
    /* Milestone 95 — the salon's chosen brand font (see
       salon_font_choices() in includes/functions.php), overridden
       per-request by salon_font_style_tag()'s inline <style> block,
       same "override a :root default via a later, more specific inline
       tag" technique this file's colour variables already use. */
    --sos-font: 'Poppins', system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}

[data-theme="dark"] {
    --sos-bg: #15121F;
    --sos-surface: #1E1A2E;
    --sos-text: #F1EEFA;
    --sos-muted: #A79FC0;
    --sos-border: #322B49;
}

* { box-sizing: border-box; }

body {
    background: linear-gradient(160deg, var(--sos-bg) 0%, var(--sos-bg) 60%, color-mix(in srgb, var(--sos-secondary) 8%, var(--sos-bg)) 100%);
    color: var(--sos-text);
    font-family: var(--sos-font);
    min-height: 100vh;
    /* Bug fix — a thin horizontal scrollbar was showing up on every
       public page (reported live on the homepage and on the Cookie
       Policy page, but really any page — both share the same
       book/partials/footer.php, and .pub-footer below is the actual
       cause, not anything specific to either page). .pub-footer and
       .pub-hero-fullscreen both use the standard "100vw + calc(50% -
       50vw) margins" full-bleed trick to break out of .pub-shell's
       centered max-width. That trick has one well-known gotcha: 100vw
       is the width of the whole viewport INCLUDING the vertical
       scrollbar's own width, while the 50% it's paired with is the
       width of the layout viewport EXCLUDING it — so on any real page
       tall enough to need a vertical scrollbar (i.e. almost every page
       on this site), the full-bleed element ends up a few pixels
       (the scrollbar's own width, ~15-17px) wider than the visible
       window, and THAT sliver is what was showing as a second,
       horizontal scrollbar. The full-bleed elements are already
       designed to visually bleed past the edge of the content column,
       so there's nothing genuinely there to scroll to — hiding the
       overflow this causes is the correct fix, not a workaround; it
       doesn't clip anything a visitor could otherwise see, since nothing
       on this site is meant to be reached by scrolling sideways.
    */
    overflow-x: hidden;
}

/* Milestone 94 — every real page heading, site-wide, now picks up the
   salon's own brand colour rather than staying plain neutral text — a
   deliberate widening of Milestone 93's colour fix, requested directly
   by the user. A tag selector so it reaches every h1-h6 regardless of
   which Bootstrap size-utility class dresses it (e.g. <h1 class="h4
   mb-3">) — those only change font-size, not the element. Any heading
   that genuinely needs a different colour (the plain-gradient hero's
   own white h1, on a primary/secondary background where primary text
   would be unreadable) gets its own explicit override below/near its
   own rule, which a plain tag selector like this one always loses to
   on ordinary CSS specificity grounds. */
h1, h2, h3, h4, h5, h6 { color: var(--sos-primary); }

.sos-shell {
    max-width: 760px;
    margin: 0 auto;
    padding: 2.5rem 1rem 4rem;
}

.sos-brand {
    display: flex;
    align-items: center;
    gap: .6rem;
    font-weight: 700;
    font-size: 1.35rem;
    margin-bottom: 1.75rem;
}

.sos-brand .dot {
    width: 12px; height: 12px; border-radius: 50%;
    background: linear-gradient(135deg, var(--sos-primary), var(--sos-secondary));
    display: inline-block;
}

.sos-card {
    background: var(--sos-surface);
    border: 1px solid var(--sos-border);
    border-radius: 1rem;
    padding: 2rem;
    box-shadow: 0 12px 32px -18px rgba(124, 58, 237, .25);
}

.sos-stepper {
    display: flex;
    gap: .5rem;
    margin-bottom: 1.75rem;
}

.sos-step {
    flex: 1;
    text-align: center;
    padding: .5rem .25rem;
    border-radius: .6rem;
    font-size: .8rem;
    font-weight: 600;
    background: var(--sos-bg);
    border: 1px solid var(--sos-border);
    color: var(--sos-muted);
}

.sos-step.is-active {
    background: linear-gradient(135deg, var(--sos-primary), var(--sos-secondary));
    color: #fff;
    border-color: transparent;
}

.sos-step.is-done {
    color: var(--sos-success);
    border-color: var(--sos-success);
}

.btn-sos {
    background: linear-gradient(135deg, var(--sos-primary), var(--sos-secondary));
    border: none;
    color: #fff;
    font-weight: 600;
}
.btn-sos:hover, .btn-sos:focus { color: #fff; filter: brightness(1.08); }

.sos-check-row { display: flex; justify-content: space-between; align-items: center; padding: .5rem 0; border-bottom: 1px dashed var(--sos-border); }
.sos-check-row:last-child { border-bottom: none; }
.sos-badge-ok { color: var(--sos-success); font-weight: 600; }
.sos-badge-fail { color: #E11D48; font-weight: 600; }

.form-control, .form-select {
    background: var(--sos-bg);
    border: 1px solid var(--sos-border);
    color: var(--sos-text);
}
.form-control:focus, .form-select:focus {
    background: var(--sos-bg);
    color: var(--sos-text);
    border-color: var(--sos-primary);
    box-shadow: 0 0 0 .2rem color-mix(in srgb, var(--sos-primary) 25%, transparent);
}
/* Fix — same root cause as the list-group/table background fix above:
   Bootstrap gives a :disabled form-control/form-select its own default
   background colour (a higher-specificity rule than the plain
   .form-control one above, so it wins regardless of load order), which
   in dark theme renders as a visibly different, oddly-tinted box rather
   than just looking greyed-out. First hit on admin/services.php's
   "Deposit amount" field (disabled until "Requires a deposit" is
   ticked), but applied sitewide since any other disabled field would
   have the exact same mismatch. Dimmed via opacity instead of a
   different background, so "disabled" still reads clearly without
   reintroducing a mismatched fill colour. */
.form-control:disabled, .form-select:disabled {
    background: var(--sos-bg);
    color: var(--sos-text);
    opacity: .5;
}

/* Milestone 50 — book/schedule.php's date picker was reported as too
   small/fiddly to tap; form-control-lg (Bootstrap) already enlarges the
   field itself, this on top of it enlarges Chrome/Edge/Safari's own tiny
   calendar-icon indicator inside it to match (Firefox has no such
   pseudo-element to target — its native date input is already a
   reasonable size by default, so nothing more is needed there). */
.pub-date-input::-webkit-calendar-picker-indicator {
    transform: scale(1.35);
    margin-inline-start: .3rem;
    cursor: pointer;
}

/* Chrome/Edge/Safari paint autofilled fields with their own yellow
   background + dark text via an internal UA style — a plain
   `background`/`color` rule (even with !important) can't override it.
   The only thing that beats it is painting our own theme colour over
   the top with an inset box-shadow large enough to fully cover the
   field, plus -webkit-text-fill-color to recolour the text itself.
   Uses var(--sos-bg)/var(--sos-text) so it matches whichever theme
   (light/dark) is active, same as every other themed input. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
    -webkit-box-shadow: 0 0 0 1000px var(--sos-bg) inset !important;
    box-shadow: 0 0 0 1000px var(--sos-bg) inset !important;
    -webkit-text-fill-color: var(--sos-text) !important;
    caret-color: var(--sos-text);
    /* Chrome re-applies its own autofill paint after a delay unless the
       transition is pushed out this far — a standard part of this fix. */
    transition: background-color 5000s ease-in-out 0s;
}

.theme-toggle {
    border: 1px solid var(--sos-border);
    background: var(--sos-surface);
    border-radius: 2rem;
    padding: .35rem .8rem;
    font-size: .8rem;
    color: var(--sos-muted);
}

a.sos-link { color: var(--sos-primary); font-weight: 600; text-decoration: none; }
a.sos-link:hover { text-decoration: underline; }

/* ---------------------------------------------------------------------
   Public site + admin area — wider layout than the wizard's narrow
   .sos-shell, same colour tokens/theme toggle above. Milestone 17 added
   admin/site-settings.php, the first (and only) screen that actually
   sets hero_style/hero_image_path — .pub-hero-fullscreen below is that
   template's real CSS; .pub-hero (the plain gradient) stays the default
   and is unchanged.
   --------------------------------------------------------------------- */

.pub-shell { max-width: 1080px; margin: 0 auto; padding: 0 1rem 4rem; }

.pub-nav {
    display: flex; justify-content: space-between; align-items: center;
    padding: 1.25rem 0; flex-wrap: wrap; gap: .75rem;
}
.pub-brand { display: inline-flex; align-items: center; gap: .6rem; }
.pub-brand-logo { max-height: 36px; max-width: 160px; object-fit: contain; }
/* Milestone 94 — the business-name text itself now defaults to brand
   primary colour too (previously plain neutral text) — reused for the
   same business-name text in admin/staff partials/header.php as well,
   not just the public site's own nav. render_brand_name_html()
   (includes/functions.php) overrides this per-segment via its own
   inline style="color:..." when the owner has set up individually-
   coloured words — an inline style always wins over this class rule,
   so the two features layer correctly without conflicting. */
.pub-brand-name { color: var(--sos-primary); }

/* Milestone 32 — the salon's logo again, this time as a small centred
   header above the "Staff login"/"Owner login" card title
   (staff/login.php, admin/login.php), rather than the nav-sized
   .pub-brand-logo — bigger since it's the sole visual anchor of the
   card, not sitting next to a business-name wordmark. */
.pub-login-logo { display: block; max-height: 64px; max-width: 220px; object-fit: contain; margin: 0 auto 1rem; }
.pub-nav-links { display: flex; gap: 1.25rem; align-items: center; flex-wrap: wrap; }
/* Milestone 94 — a menu link's resting colour now also picks up brand
   colour (primary), not just its hover/active state; hover/active moved
   to secondary so there's still a visible colour shift on interaction
   rather than the two states becoming indistinguishable. Same swap
   applied to every other nav-link family below (.adm-nav-links,
   .staff-nav-links, .adm-sidebar, .acct-sidebar, .staff-sidebar). */
.pub-nav-links a { color: var(--sos-primary); text-decoration: none; font-weight: 500; font-size: .95rem; }
.pub-nav-links a:hover, .pub-nav-links a.is-active { color: var(--sos-secondary); }
.pub-nav-links a.is-active { font-weight: 700; }
.pub-nav-links a.btn-sos { color: #fff; padding: .5rem 1.1rem; border-radius: 2rem; }

/* Milestone 34 — <992px: Services/Our team/Waitlist drop out of the
   always-visible row (into #pubNavDropdown instead, see header.php) and
   Login/Register collapse to icon-only. The burger toggle itself is
   desktop-hidden and only appears at this same width. All untouched at
   >=992px, where the nav looks exactly as it did before. */
.pub-nav-toggle {
    display: none;
    background: none; border: none; color: var(--sos-text);
    font-size: 1.15rem; line-height: 1; padding: .5rem .6rem;
    border-radius: .5rem; cursor: pointer;
}
.pub-nav-toggle:hover { color: var(--sos-primary); }

.pub-nav-backdrop { display: none; }

/* Milestone 36 — off-canvas panel (was a drop-down under the nav row
   through Milestone 35): fixed to the viewport, slides in from the right
   via transform rather than display/height, so the .25s transition
   actually animates. Shared by both the guest links and (new this
   milestone) the account-nav links — header.php switches its contents
   on $accountNav, not its markup/CSS. Untouched at >=992px (display:none
   below is overridden inside the media query only). */
.pub-nav-dropdown { display: none; }

/* Milestone 35 — plain icon theme toggle for the public nav (book/partials/
   header.php only — admin/staff/setup keep their existing .theme-toggle
   pill, not touched here). Both icons are always in the DOM; which one
   shows is driven purely by the html[data-theme] attribute the inline
   head script (and sosToggleTheme(), footer.php) already sets, so the
   icon flips the instant the attribute changes with no extra JS. */
.pub-theme-toggle {
    background: none; border: none; color: var(--sos-text);
    font-size: 1.05rem; line-height: 1; padding: .5rem .4rem;
    cursor: pointer;
}
.pub-theme-toggle:hover { color: var(--sos-primary); }
.pub-theme-icon-dark { display: none; }
html[data-theme="dark"] .pub-theme-icon-light { display: none; }
html[data-theme="dark"] .pub-theme-icon-dark { display: inline; }

@media (max-width: 991.98px) {
    .pub-nav-link-collapsible { display: none; }
    .pub-nav-link-text { display: none; }
    .pub-nav-toggle { display: inline-flex; align-items: center; justify-content: center; }

    /* Milestone 37 — brand + icon row was wrapping onto two lines at
       mobile widths (worse once logged in, since "Log out" renders as
       text rather than an icon like Login/Register do). Force a single
       row: the icon row never wraps or shrinks (flex: 0 0 auto, gap
       closed right up), and the brand is the one thing allowed to give
       up space — its logo/name shrink first, and a long business name
       ellipsizes (.pub-brand-name) rather than pushing the icons around
       or wrapping the row. */
    .pub-nav { flex-wrap: nowrap; }
    .pub-brand { gap: .4rem; min-width: 0; flex: 1 1 auto; overflow: hidden; }
    .pub-brand-logo { max-height: 22px; max-width: 72px; }
    .pub-brand-name {
        font-size: .95rem;
        min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
    }
    .pub-nav-links { gap: .35rem; flex-wrap: nowrap; flex: 0 0 auto; }

    .pub-nav-backdrop {
        display: block;
        position: fixed; inset: 0;
        background: rgba(0, 0, 0, .4);
        opacity: 0; pointer-events: none;
        transition: opacity .25s ease;
        z-index: 1040;
    }
    .pub-nav-backdrop.is-open { opacity: 1; pointer-events: auto; }

    .pub-nav-dropdown {
        display: flex; flex-direction: column; gap: .1rem;
        position: fixed; top: 0; right: 0; bottom: 0;
        width: min(78vw, 320px);
        padding: 4.5rem 1.25rem 1.5rem;
        background: var(--sos-surface); border-left: 1px solid var(--sos-border);
        box-shadow: -12px 0 32px rgba(0, 0, 0, .3);
        transform: translateX(100%);
        transition: transform .25s ease;
        overflow-y: auto;
        z-index: 1050;
    }
    .pub-nav-dropdown.is-open { transform: translateX(0); }

    .pub-nav-dropdown-close {
        position: absolute; top: 1rem; right: 1rem;
        background: none; border: none; color: var(--sos-text);
        font-size: 1.2rem; line-height: 1; padding: .4rem;
        cursor: pointer;
    }
    .pub-nav-dropdown-close:hover { color: var(--sos-primary); }

    .pub-nav-dropdown a {
        color: var(--sos-primary); text-decoration: none; font-weight: 500; font-size: .95rem;
        padding: .65rem .25rem;
    }
    .pub-nav-dropdown a:hover, .pub-nav-dropdown a.is-active { color: var(--sos-secondary); }
    .pub-nav-dropdown a.is-active { font-weight: 700; }
    .pub-nav-dropdown a:not(:last-child) { border-bottom: 1px solid var(--sos-border); }
}

.pub-hero {
    background: linear-gradient(135deg, var(--sos-primary), var(--sos-secondary));
    color: #fff; border-radius: 1.25rem; padding: 3rem 2rem; text-align: center; margin-bottom: 2.5rem;
}
/* Milestone 94 — explicit override: the new global h1-h6 rule above
   would otherwise win over .pub-hero's inherited color:#fff (a directly-
   targeted rule always beats plain inheritance, regardless of
   specificity), turning this heading brand-primary-on-brand-primary-
   gradient — unreadable. Stays white on purpose as the BASE colour.
   Milestone 96 — the user asked for this heading to "copy colours/fonts
   from nav logo": the font part needs nothing further (--sos-font,
   Milestone 95, already applies globally via body, this heading
   included); for colour, index.php now runs the business name through
   the same render_brand_name_html() the nav bar itself uses, which — if
   the owner has actually set up individually-coloured word segments
   (Milestone 94's "blue Salon / orange OS" feature) — emits each word as
   its own inline `style="color:..."` span, and an inline style always
   wins over this class rule regardless of source order. So: a salon
   with real colour segments configured sees those exact colours here,
   matching the nav; a salon with no segments set (the common case) still
   falls through to this rule's plain white, avoiding the unreadable
   primary-on-primary-gradient outcome documented above for that specific
   case — text_shadow below is an extra legibility safety net for any
   segment colour that ends up close in tone to the gradient behind it. */
.pub-hero h1 { font-weight: 700; margin-bottom: .5rem; color: #fff; text-shadow: 0 1px 10px rgba(0,0,0,.3); }

/* Milestone 96 — the centred icon above .pub-hero's h1 (see
   book/partials/hero-icon.php): the salon's own logo, shown larger than
   its nav-bar size, or — with no logo uploaded yet — a circular badge in
   the exact primary→secondary gradient the nav's own fallback ".dot"
   indicator already uses, just scaled up with a Font Awesome glyph
   inside instead of staying an empty dot. */
.pub-hero-icon { display: flex; align-items: center; justify-content: center; margin: 0 auto .9rem; }
.pub-hero-icon img { max-height: 72px; max-width: 220px; object-fit: contain; }
.pub-hero-icon-fallback {
    width: 64px; height: 64px; border-radius: 50%;
    background: linear-gradient(135deg, var(--sos-primary), var(--sos-secondary));
    display: flex; align-items: center; justify-content: center;
    color: #fff; font-size: 1.6rem;
}

/* Milestone 97 — .pub-hero-content is a plain wrapper around the icon/
   heading/tagline/description (added purely so there's a single element
   to hang the optional .pub-hero-panel modifier off; no styling of its
   own is needed since .pub-hero/.pub-hero-fullscreen already handle
   centring). .pub-hero-panel is the owner-toggleable (Admin → Site
   settings → Homepage template) translucent backdrop: a soft, blurred
   dark panel that sits behind that content, giving text a consistent,
   readable base regardless of what colour the heading/segments end up
   being or how busy a background photo is — the exact "soften a brand-
   colour clash" case the user asked for. backdrop-filter degrades
   gracefully (older browsers just show the plain rgba() fill with no
   blur, still perfectly readable, never broken). */
.pub-hero-panel {
    background: rgba(0, 0, 0, .32);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border-radius: 1rem;
    padding: 1.5rem 2rem;
    display: inline-block;
    max-width: 100%;
    /* Requested directly by the user: without this, the panel's own
       rounded edge sat right up against the "Book an appointment" button
       below it with no breathing room — .pub-hero-content itself carries
       no margin (see its own doc comment above) so this only affects the
       hero when the panel is actually switched on, not the plain-text
       default. */
    margin-bottom: 1.5rem;
}

/* Milestone 17 — the "full-viewport background image" template. Breaks
   out of .pub-shell's 1080px max-width via the standard negative-margin
   full-bleed trick (calc(50% - 50vw) on each side cancels the shell's own
   centering) so the image genuinely spans the whole browser width, not
   just the content column. min-height (not height) fills most of the
   viewport while still growing for extra-long business names/taglines
   without clipping content. The dark linear-gradient layered into the
   same background-image (rather than a separate overlay element) keeps
   the markup identical to the default hero — same h1/p/a structure. */
.pub-hero-fullscreen {
    width: 100vw;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    border-radius: 0;
    min-height: 80vh;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    padding: 3rem 1.5rem;
}
.pub-hero p { opacity: .92; max-width: 46rem; margin: 0 auto 1.5rem; }
.pub-hero .btn-light { font-weight: 600; }

.pub-section-title { font-weight: 700; margin: 2.5rem 0 1rem; }

.pub-card {
    background: var(--sos-surface); border: 1px solid var(--sos-border); border-radius: .9rem;
    padding: 1.25rem; height: 100%;
}

/* Milestone 66 — long-form static/blog body text (About us, Terms,
   Cookie Policy, Refunds Policy, and blog post bodies) needed real
   paragraph/heading/list spacing that plain Bootstrap defaults don't
   give — .pub-card alone is fine for short cards, but a multi-paragraph
   legal page inside one read like a wall of text. Scoped to only the
   elements it needs to restyle so it composes cleanly inside an
   existing .pub-card without fighting any other component's spacing. */
.pub-prose h2 { font-size: 1.25rem; font-weight: 700; margin: 1.75rem 0 .75rem; }
.pub-prose h2:first-child { margin-top: 0; }
.pub-prose h3 { font-size: 1.05rem; font-weight: 700; margin: 1.25rem 0 .5rem; }
.pub-prose p { margin: 0 0 1rem; line-height: 1.65; }
.pub-prose ul, .pub-prose ol { margin: 0 0 1rem; padding-left: 1.4rem; line-height: 1.65; }
.pub-prose li { margin-bottom: .35rem; }
.pub-prose a { color: var(--sos-primary); }
.pub-prose blockquote {
    margin: 0 0 1rem; padding: .5rem 1rem; border-left: 3px solid var(--sos-primary);
    color: var(--sos-muted); font-style: italic;
}
.pub-prose img { border-radius: .6rem; margin: 0 0 1rem; }
.pub-prose strong { color: var(--sos-text); }

/* Blog listing — the featured-image thumbnail on each card in
   book/blog.php's grid. */
.pub-blog-thumb {
    width: 100%; aspect-ratio: 16 / 9; object-fit: cover; border-radius: .6rem .6rem 0 0;
    margin: -1.25rem -1.25rem .9rem; display: block; max-width: calc(100% + 2.5rem);
}
.pub-blog-meta { color: var(--sos-muted); font-size: .82rem; }

/* Service category accordion (book/index.php) — Bootstrap's own accordion
   re-themes fine under data-bs-theme (same mechanism the theme toggle
   already relies on), this just matches its corners/border/surface colour
   to the rest of the public site's .pub-card look rather than Bootstrap's
   own smaller default radius. */
.pub-service-accordion .accordion-item {
    background: var(--sos-surface); border: 1px solid var(--sos-border); border-radius: .9rem;
    overflow: hidden; margin-bottom: .75rem;
}
.pub-service-accordion .accordion-item:last-child { margin-bottom: 0; }
.pub-service-accordion .accordion-button {
    background: var(--sos-surface); color: inherit; font-weight: 600; font-size: 1rem;
    box-shadow: none;
}
.pub-service-accordion .accordion-button:not(.collapsed) {
    background: var(--sos-surface); color: var(--sos-primary); box-shadow: none;
}
.pub-service-accordion .accordion-button:focus { box-shadow: none; border-color: var(--sos-border); }
.pub-service-accordion .accordion-body { padding-top: .5rem; }

/* Bug fix — book/account.php's own Booking history table (Date/Time/
   Service/Staff/Status/Deposit/Full payment/Tip — 8 columns) never fit a
   phone width at any column count; below 992px it's replaced with this
   accordion instead (see book/account.php's own comment where it's built)
   — same real Bootstrap accordion component as .pub-service-accordion
   above, re-themed the same way, just with its own summary layout since a
   booking's collapsed row needs to show a date AND a status pill side by
   side rather than a single line of text. */
.pub-booking-accordion .accordion-item {
    background: var(--sos-surface); border: 1px solid var(--sos-border); border-radius: .9rem;
    overflow: hidden; margin-bottom: .6rem;
}
.pub-booking-accordion .accordion-item:last-child { margin-bottom: 0; }
.pub-booking-accordion .accordion-button {
    background: var(--sos-surface); color: inherit; box-shadow: none; padding: .9rem 1rem;
}
.pub-booking-accordion .accordion-button:not(.collapsed) { background: var(--sos-surface); box-shadow: none; }
.pub-booking-accordion .accordion-button:focus { box-shadow: none; border-color: var(--sos-border); }
.pub-booking-accordion-summary {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
    gap: .5rem .75rem; width: 100%; padding-right: .5rem; font-size: .92rem;
}
.pub-booking-accordion-date { font-weight: 600; }
.pub-booking-accordion .accordion-body { padding: 0 1rem 1rem; }
.pub-booking-accordion-row {
    display: flex; justify-content: space-between; gap: 1rem;
    padding: .5rem 0; border-bottom: 1px dashed var(--sos-border); font-size: .92rem;
}
.pub-booking-accordion-row:last-child { border-bottom: none; padding-bottom: 0; }

/* Milestone 26 — "Meet the team" cards are now clickable links through
   to a staff member's public profile page (book/staff.php); this
   modifier keeps the same .pub-card look while adding a hover cue and
   overriding the default <a> colour/underline. */
.pub-card-link {
    color: inherit;
    transition: border-color .15s ease, transform .15s ease;
}
.pub-card-link:hover {
    border-color: var(--sos-primary);
    transform: translateY(-2px);
    color: inherit;
}

.pub-service-row {
    display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem;
    padding: .85rem 0; border-bottom: 1px dashed var(--sos-border);
}
.pub-service-row:last-child { border-bottom: none; }
.pub-service-price { font-weight: 700; color: var(--sos-primary); white-space: nowrap; }

/* Bug fix — on a phone, .pub-service-row's own flex layout (name+
   description+duration stacked on the left, price+button on the right)
   couldn't fit both sides on one line at typical service-name lengths, so
   the two columns wrapped onto each other in an unpredictable, "messy"
   order (name, then price, then description, then button, depending on
   exact text length) with no consistent reading order. Below 992px this
   is replaced with a second, nested accordion — one collapsed row per
   service inside each already-open category — so a phone only ever sees
   one guaranteed-single-line thing per service (name, truncated with an
   ellipsis rather than wrapped, plus its price) until tapped open to
   reveal the description/duration/deposit line and the Add to basket
   button/badge. Same real Bootstrap accordion component as
   .pub-service-accordion/.pub-booking-accordion above, just flattened
   (no boxed card look, transparent background, dashed row separator like
   .pub-service-row itself) since it's nested one level inside a category
   that's already boxed. >=992px keeps the original .pub-service-row
   layout untouched — there's room there for everything on one line
   without truncating anything. */
.pub-service-item-accordion .accordion-item {
    border-bottom: 1px dashed var(--sos-border);
}
.pub-service-item-accordion .accordion-item:last-child { border-bottom: none; }
.pub-service-item-accordion .accordion-button {
    background: transparent; color: inherit; box-shadow: none; padding: .85rem 0; font-weight: 600;
}
.pub-service-item-accordion .accordion-button:not(.collapsed) { background: transparent; color: var(--sos-primary); box-shadow: none; }
.pub-service-item-accordion .accordion-button:focus { box-shadow: none; }
.pub-service-item-summary {
    display: flex; align-items: center; gap: .6rem; width: 100%; min-width: 0; padding-right: .5rem;
}
.pub-service-item-name {
    flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pub-service-item-price { flex-shrink: 0; white-space: nowrap; font-weight: 700; color: var(--sos-primary); }
.pub-service-item-accordion .accordion-body { padding: 0 0 1rem; font-weight: normal; }

/* Milestone 51 — the service accordion's already-in-basket indicator
   (book/index.php): a plain confirmation, not a button — removing a
   service happens from the "Your basket" summary's own "Remove" link
   further down the page, so this no longer needs to be interactive. */
.pub-in-basket-badge {
    display: inline-flex; align-items: center; gap: .35rem;
    color: var(--sos-success); font-weight: 600; font-size: .85rem;
}

/* Milestone 52 — the "Booking with: [avatar] [name]" confirmation banner
   (book/partials/staff-context-banner.php), shown on both book/index.php
   and book/schedule.php so a stylist chosen from their own profile page
   stays visibly acknowledged across both steps, not just carried along
   invisibly in the session. Tinted the same way as .pub-stylist-choice
   (book/schedule.php) so the two read as the same kind of "here's a
   decision already made for you" element, not two unrelated styles. */
.pub-staff-context-banner {
    display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
    background: color-mix(in srgb, var(--sos-primary) 6%, var(--sos-surface));
    border: 1px solid color-mix(in srgb, var(--sos-primary) 25%, var(--sos-border));
    border-radius: .9rem;
    padding: .65rem 1rem;
}
.pub-staff-context-text { line-height: 1.25; }

/* Milestone 78 fix — on a narrow screen, the avatar + "Booking with"
   text + "Not X? Choose someone else" trigger (a nowrap text link, plus
   its own switch widget on book/index.php) couldn't all fit on one row,
   overflowing past the card's right edge instead of wrapping — flex-wrap
   above lets it drop to a second line, but the trigger's own `ms-auto`
   (a Bootstrap utility applied inline in the markup) would then just
   right-align on that new line at whatever width its content needs,
   which still looked cramped. Below this banner's own breakpoint, drop
   ms-auto's margin and let the trigger/link take the full row instead,
   left-aligned like everything else that wraps. */
@media (max-width: 575.98px) {
    .pub-staff-context-banner > .pub-staff-switch.ms-auto,
    .pub-staff-context-banner > a.ms-auto {
        margin-left: 0 !important;
        width: 100%;
    }
    .pub-staff-context-banner .pub-staff-switch-list {
        left: 0; right: auto; width: 100%; max-width: none;
    }
}

/* Milestone 53 — the banner's "Not X? Choose someone else" switch
   dropdown (book/partials/staff-context-banner.php). Reuses .pub-staff-
   select-list/.pub-staff-select-option's look (book/schedule.php's own
   picker) but right-aligned and only as wide as its content, since the
   trigger here is a small text link, not a full-width box. */
.pub-staff-switch { position: relative; flex-shrink: 0; }
.pub-staff-switch-trigger {
    background: none; border: none; padding: 0; color: var(--sos-primary);
    font-weight: 600; font-size: .85rem; cursor: pointer; white-space: nowrap;
}
.pub-staff-switch-trigger:hover { text-decoration: underline; }
.pub-staff-switch-trigger .pub-staff-switch-chevron,
.pub-staff-switch-trigger i { transition: transform .15s ease; }
.pub-staff-switch-trigger.is-open i { transform: rotate(180deg); }
.pub-staff-switch-list {
    left: auto; right: 0; width: max-content; min-width: 14rem; max-width: 16rem;
}

.pub-staff-avatar {
    width: 64px; height: 64px; border-radius: 50%; object-fit: cover;
    background: linear-gradient(135deg, var(--sos-primary), var(--sos-accent));
    display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; font-size: 1.3rem;
}

/* Milestone 26 — a smaller variant for a booking-history table row
   (book/bookings.php, book/account.php, book/dashboard.php), where a
   full 64px avatar would dwarf the rest of the row. Same photo-or-
   initial markup as index.php's "Meet the team" section, just sized
   down. */
.pub-staff-avatar-sm { width: 28px; height: 28px; font-size: .8rem; flex-shrink: 0; }

.pub-hours-row { display: flex; justify-content: space-between; padding: .35rem 0; border-bottom: 1px dashed var(--sos-border); font-size: .9rem; }
.pub-hours-row:last-child { border-bottom: none; }

/* Milestone 59 — homepage "Find us" map (index.php), driven by
   Integrations > Maps. A responsive 16:9 wrapper around the Google Maps
   Embed iframe rather than a fixed pixel height, so it scales cleanly
   from mobile up to the widest desktop layout this site supports. */
.pub-map-card { max-width: 100%; }
.pub-map-frame { position: relative; width: 100%; padding-top: 45%; border-radius: .6rem; overflow: hidden; }
.pub-map-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* Milestone 47 — book/schedule.php's "Who with?" stylist choice, pulled
   out of the plain form row it used to share with the date picker (see
   that file's own doc comment) into its own visually distinct block —
   a tinted background/border in the site's primary colour so it reads
   as a deliberate decision point rather than just another form field. */
.pub-stylist-choice {
    background: color-mix(in srgb, var(--sos-primary) 6%, var(--sos-surface));
    border: 1px solid color-mix(in srgb, var(--sos-primary) 25%, var(--sos-border));
    border-radius: .9rem;
    padding: 1rem 1.1rem;
}
.pub-stylist-choice .form-label { color: var(--sos-primary); }

/* Milestone 48 — the real .pub-stylist-choice <select> is visually
   hidden (not display:none — a display:none form control still submits
   fine, but keeping it off-screen rather than un-rendered is the more
   conservative choice here) and driven by the custom listbox below it,
   because a plain <option> can never contain an avatar image/icon —
   see book/schedule.php's own Milestone 48 doc comment. */
.pub-sr-only {
    position: absolute !important; width: 1px !important; height: 1px !important;
    padding: 0 !important; margin: -1px !important; overflow: hidden !important;
    clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; border: 0 !important;
}

.pub-staff-select { position: relative; }
.pub-staff-select-trigger {
    width: 100%; display: flex; align-items: center; justify-content: space-between; gap: .75rem;
    background: var(--sos-surface); border: 1px solid var(--sos-border); border-radius: .6rem;
    padding: .55rem .9rem; font-size: 1.05rem; font-weight: 600; color: var(--sos-text); text-align: left;
    cursor: pointer;
}
.pub-staff-select-trigger:hover, .pub-staff-select-trigger.is-open { border-color: var(--sos-primary); }
.pub-staff-select-current { display: flex; align-items: center; gap: .6rem; min-width: 0; }
.pub-staff-select-current span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pub-staff-select-chevron {
    color: var(--sos-primary); flex-shrink: 0; transition: transform .15s ease;
}
.pub-staff-select-trigger.is-open .pub-staff-select-chevron { transform: rotate(180deg); }
.pub-staff-select-list {
    position: absolute; z-index: 20; left: 0; right: 0; margin-top: .35rem; padding: .35rem;
    background: var(--sos-surface); border: 1px solid var(--sos-border); border-radius: .6rem;
    box-shadow: 0 .5rem 1.5rem rgba(0, 0, 0, .25); max-height: 16rem; overflow-y: auto;
    list-style: none;
}
.pub-staff-select-option {
    display: flex; align-items: center; gap: .6rem; padding: .5rem .6rem; border-radius: .45rem;
    cursor: pointer; font-weight: 500; color: inherit; text-decoration: none;
}
.pub-staff-select-option:hover { background: color-mix(in srgb, var(--sos-primary) 10%, transparent); }
.pub-staff-select-option.is-selected {
    background: color-mix(in srgb, var(--sos-primary) 16%, transparent); color: var(--sos-primary); font-weight: 700;
}
/* The "Any available" row's stand-in avatar — a plain group icon rather
   than a photo/initial, since it isn't a real person. */
.pub-staff-avatar-any { background: var(--sos-border); color: var(--sos-muted); font-size: .75rem; }

.pub-slot-btn {
    border: 1px solid var(--sos-border); background: var(--sos-bg); color: var(--sos-text);
    border-radius: .6rem; padding: .5rem .4rem; font-weight: 600; font-size: .9rem; width: 100%;
}
.pub-slot-btn:hover { border-color: var(--sos-primary); color: var(--sos-primary); }

/* Milestone 55 — an already-booked time slot (book/schedule.php),
   shown rather than hidden: the time itself stays legible but dimmed,
   with a bold purple X washed over the whole tile so it reads at a
   glance as "taken", not clickable (position:relative/flex centering
   here, not a real button — cursor/aria communicate that, not colour
   alone). */
.pub-slot-btn-booked {
    position: relative; cursor: not-allowed; overflow: hidden;
}
.pub-slot-btn-booked .pub-slot-time { opacity: .35; }
.pub-slot-btn-booked .pub-slot-x {
    position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
    color: var(--sos-primary); font-size: 1.4rem;
    background: color-mix(in srgb, var(--sos-primary) 14%, transparent);
}

/* Milestone 19 — a real 3-column public-site footer (About / Explore /
   Contact), full-bleed out of .pub-shell the same way .pub-hero-fullscreen
   already does, since a footer band reads as more "real site" when it
   spans the full viewport rather than staying boxed inside the content
   column. Opening hours was dropped from the original 4-column draft —
   the homepage already has its own "Opening hours" section, and
   repeating it in the footer on every page was pure duplication. */
.pub-footer {
    margin-top: 3rem;
    /* Cancels .pub-shell's own 4rem bottom padding (needed on pages that
       have no .pub-footer at all, e.g. the staff portal's own footer
       partial) — without this, that padding still applied AFTER this
       footer band on every page that does include it, leaving a fixed
       ~64px dead gap below the footer on every single one. */
    margin-bottom: -4rem;
    width: 100vw;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    padding: 2.5rem 1.5rem 1.5rem;
    background: var(--sos-surface);
    border-top: 1px solid var(--sos-border);
    color: var(--sos-muted);
    font-size: .9rem;
}
.pub-footer-inner { max-width: 1080px; margin: 0 auto; }
.pub-footer-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 2rem;
    margin-bottom: 2rem;
}
.pub-footer-heading {
    font-weight: 700; color: var(--sos-primary); margin-bottom: .75rem;
    font-size: .82rem; text-transform: uppercase; letter-spacing: .04em;
}
.pub-footer-links, .pub-footer-contact { display: flex; flex-direction: column; }
.pub-footer-links a, .pub-footer-contact a { color: var(--sos-muted); text-decoration: none; padding: .2rem 0; }
.pub-footer-links a:hover, .pub-footer-contact a:hover { color: var(--sos-primary); }
.pub-footer-contact span { padding: .2rem 0; }
.pub-footer-bottom {
    border-top: 1px solid var(--sos-border);
    padding-top: 1.25rem;
    display: flex; justify-content: space-between; flex-wrap: wrap; gap: .5rem;
    font-size: .8rem;
}
@media (max-width: 767px) {
    .pub-footer-grid { grid-template-columns: 1fr; gap: 1.5rem; }
}

/* Milestone 159 — the quiet "Powered by SalonOS" credit link shared by the
   public footer's own .pub-footer-bottom and the new admin footer band
   below. Deliberately styled to blend in rather than stand out: it just
   inherits whatever muted colour the surrounding footer text already uses,
   with an underline only on hover/focus as the one hint that it's a real
   link — no brand colour, no separate visual weight. Always opens in a
   new tab (target="_blank" rel="noopener noreferrer" in the markup) so
   following it never navigates the salon's own site away. */
.sos-credit-link { color: inherit; text-decoration: none; }
.sos-credit-link:hover, .sos-credit-link:focus { text-decoration: underline; }

/* Milestone 159 — a footer band for the admin area, matching .pub-footer's
   own full-bleed look (same background/border/colour treatment, same
   glass-mode + reduced-transparency handling below) but a single quiet row
   rather than the public site's 3-column marketing grid — admin already
   has its own nav for real navigation, so this only ever needs to carry
   the copyright line and the credit link. */
.adm-footer {
    margin-top: 3rem;
    width: 100vw;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    padding: 1.1rem 1.5rem;
    background: var(--sos-surface);
    border-top: 1px solid var(--sos-border);
    color: var(--sos-muted);
    font-size: .8rem;
}
.adm-footer-inner {
    max-width: 1240px; margin: 0 auto;
    display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: .5rem;
}

.pub-step-indicator { display: flex; gap: .5rem; margin-bottom: 1.5rem; font-size: .85rem; color: var(--sos-muted); }
.pub-step-indicator .is-current { color: var(--sos-primary); font-weight: 700; }

/* ---------------------------------------------------------------------
   Admin area — same tokens/theme mechanism again, a bit wider still
   than the public site since dashboards and the calendar benefit from
   the extra room.
   --------------------------------------------------------------------- */

.adm-shell { max-width: 1240px; margin: 0 auto; padding: 0 1rem 4rem; }

/* Milestone 12 — desktop sidebar + slim always-visible top strip. The
   top strip (brand, "+ New booking", "Hi, name", logout, theme toggle)
   is global identity/actions, shown at every width. The page-nav links
   themselves render twice from the same $navItems array (see
   admin/partials/header.php): .adm-sidebar (a vertical column, >=992px)
   and .adm-nav-links-mobile (the original horizontal wrapping bar,
   <992px) — exactly one of the two is ever visible at a given width, so
   mobile stays pixel-identical to Milestone 9a's already-verified 390px
   layout, and the sidebar is purely additive on top of it. */
.adm-topbar {
    display: flex; justify-content: space-between; align-items: center;
    padding: 1.1rem 0; flex-wrap: wrap; gap: .75rem;
    max-width: 1240px; margin: 0 auto; padding-left: 1rem; padding-right: 1rem;
    border-bottom: 1px solid var(--sos-border);
}
.adm-topbar-actions { display: flex; gap: 1.1rem; align-items: center; flex-wrap: wrap; }
.adm-topbar-actions a { color: var(--sos-text); text-decoration: none; font-weight: 500; font-size: .9rem; }
.adm-topbar-actions a:hover { color: var(--sos-primary); }

.adm-nav-links { display: flex; gap: 1.1rem; align-items: center; flex-wrap: wrap; }
.adm-nav-links a { color: var(--sos-primary); text-decoration: none; font-weight: 500; font-size: .9rem; }
.adm-nav-links a:hover, .adm-nav-links a.is-active { color: var(--sos-secondary); }
.adm-nav-links a.is-active { font-weight: 700; }

.adm-nav-links-mobile {
    padding: .85rem 1rem; margin: 0 auto; max-width: 1240px;
    border-bottom: 1px solid var(--sos-border);
}

.adm-sidebar { display: none; }
.adm-main { min-width: 0; }

@media (min-width: 992px) {
    .adm-nav-links-mobile { display: none; }

    .adm-shell { display: flex; align-items: flex-start; gap: 2rem; }
    .adm-sidebar {
        /* Milestone 134 follow-up — gap and per-link padding both tightened
           at the user's own direct request ("reduce spacing between links"),
           down from .35rem/.5rem .75rem. */
        display: flex; flex-direction: column; gap: .1rem;
        width: 200px; flex: 0 0 200px;
        position: sticky; top: 1.5rem;
        padding-top: 1.5rem;
    }
    .adm-sidebar a {
        padding: .3rem .75rem; border-radius: .5rem;
        color: var(--sos-primary); text-decoration: none; font-weight: 500; font-size: .92rem;
    }
    .adm-sidebar a:hover { background: var(--sos-bg); color: var(--sos-secondary); }
    .adm-sidebar a.is-active { background: var(--sos-bg); color: var(--sos-secondary); font-weight: 700; }

    .adm-main { flex: 1 1 auto; padding-top: 1.5rem; }
}

.adm-stat-card {
    background: var(--sos-surface); border: 1px solid var(--sos-border); border-radius: .9rem;
    padding: 1.1rem; height: 100%; position: relative;
}
.adm-stat-card .adm-stat-value { font-size: 1.7rem; font-weight: 700; color: var(--sos-primary); }
.adm-stat-card .adm-stat-label { color: var(--sos-muted); font-size: .85rem; }
.adm-stat-icon {
    position: absolute; top: 1rem; right: 1rem; font-size: 1.3rem;
    color: color-mix(in srgb, var(--sos-primary) 35%, transparent);
}

.adm-appt-card {
    background: var(--sos-bg); border: 1px solid var(--sos-border); border-radius: .6rem;
    padding: .6rem .7rem; margin-bottom: .5rem; font-size: .85rem;
}
.adm-appt-card .adm-appt-time { font-weight: 700; color: var(--sos-primary); }
.adm-appt-col { min-height: 4rem; }
.adm-appt-col-header { font-weight: 700; display: flex; align-items: center; gap: .4rem; margin-bottom: .6rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Milestone 126 — the day-view team-column layout (admin/calendar.php,
   staff/index.php's "Team calendar"). Replaces the previous Bootstrap
   col-md-<floor(12/n)> grid maths, which had two problems: it left
   visible dead space down the right edge whenever staff count didn't
   divide evenly into 12 (5 staff -> col-md-2 * 5 = 10/12 columns, an
   eighth of the row left blank), and above 6 staff the forced min of
   col-md-2 pushed the total past 12 and the grid silently wrapped onto
   uneven rows. A plain equal-flex row fixes both: each column grows to
   fill whatever width is actually available when there's room for it,
   and shrinks down to one fixed readable minimum before wrapping onto a
   new, still full-width row once there isn't — tidy whether a salon has
   3 staff working today or 30.

   Milestone 128 — a plain flex-grow:1 with no ceiling meant a column left
   alone on the last, otherwise-empty row (a team of 5 wrapping 4 + 1, say)
   stretched to fill the ENTIRE row width on its own — a single "No
   bookings."/appointment list rendered nearly 1500px wide, far past a
   comfortable reading width, purely because there was nothing left in
   that row to share the space with. A max-width caps how far any single
   column can stretch (roughly what four columns would each get on a
   typical wide desktop layout), so a lone last-row column just sits at a
   normal column width instead of ballooning — every column still grows
   to fill available space up to that cap when there's room, and still
   shrinks/wraps exactly as before once there isn't. */
.adm-cal-daycols { display: flex; flex-wrap: wrap; gap: 1rem; }
.adm-cal-daycol { flex: 1 1 200px; min-width: 200px; max-width: 360px; }
.adm-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }

.adm-status-badge { font-size: .7rem; font-weight: 700; padding: .1rem .5rem; border-radius: 1rem; display: inline-block; }
.adm-status-booked, .adm-status-confirmed { background: color-mix(in srgb, var(--sos-accent) 18%, transparent); color: var(--sos-accent); }
.adm-status-completed { background: color-mix(in srgb, var(--sos-success) 18%, transparent); color: var(--sos-success); }
.adm-status-cancelled, .adm-status-no_show { background: color-mix(in srgb, #E11D48 18%, transparent); color: #E11D48; }
/* Milestone 122 — a kiosk check-in (appointments.checked_in_at) overrides
   the displayed status/badge for a still-live booking (see
   appointment_status_badge()'s own doc comment) — var(--sos-primary)
   deliberately distinguishes it from the accent (booked/confirmed),
   success (completed), and red (cancelled/no-show) colours already in
   use, since it's a distinct in-between state, not a synonym for any of
   those. */
.adm-status-checked_in { background: color-mix(in srgb, var(--sos-primary) 18%, transparent); color: var(--sos-primary); }

/* Milestone 160 — the reviews moderation queue's own status pill
   (admin/reviews.php) — the same .adm-status-badge shape as every other
   status pill above, three new colour keys ('pending'/'approved'/
   'rejected' aren't reused from appointments' own set to avoid any
   accidental overlap in meaning). */
.adm-status-pending { background: color-mix(in srgb, #D97706 18%, transparent); color: #D97706; }
.adm-status-approved { background: color-mix(in srgb, var(--sos-success) 18%, transparent); color: var(--sos-success); }
.adm-status-rejected { background: color-mix(in srgb, #E11D48 18%, transparent); color: #E11D48; }

/* Milestone 110 — the "Staff working today" roster list
   (staff_roster_panel_html(), includes/functions.php): a plain divided
   row per staff member rather than a card, since this is a scannable
   name list, not something that needs its own tinted box per entry the
   way an appointment card does. .adm-roster-off is muted so the "not
   working" column doesn't visually compete with the "working" one for
   attention — the working list is the one worth reading first. */
.adm-roster-row { padding: .4rem 0; border-bottom: 1px solid var(--sos-border); font-size: .9rem; }
.adm-roster-row:last-child { border-bottom: none; }
.adm-roster-off { opacity: .7; }

/* Originally added just for admin/index.php's "Today's schedule" list,
   then extended (same request, follow-up) to every other .adm-appt-card
   listing in the app: admin/calendar.php's per-staff day columns,
   admin/clients.php's booking-history list, and staff/index.php's "My
   Day"/"Team calendar" columns — each card's own background now tints
   toward its status's colour, not just the small pill badge in the
   corner, so the status reads at a glance across the whole row wherever
   this card style is used. Deliberately a new class name
   (.adm-appt-tint-*) rather than reusing appointment_status_badge()'s own
   .adm-status-* classes directly on the card: those are bare selectors
   (not scoped under .adm-status-badge), so putting one straight on the
   card would also recolour all of its text via that rule's own `color:`
   property, not just tint the background — this avoids that entirely. A
   much stronger mix than the 18% badge tint above (30%), since the whole
   point here is a background that's genuinely close to the status
   colour, not another subtle pill. */
.adm-appt-card.adm-appt-tint-booked, .adm-appt-card.adm-appt-tint-confirmed {
    background: color-mix(in srgb, var(--sos-accent) 30%, var(--sos-bg));
    border-color: color-mix(in srgb, var(--sos-accent) 55%, var(--sos-border));
}
.adm-appt-card.adm-appt-tint-completed {
    background: color-mix(in srgb, var(--sos-success) 30%, var(--sos-bg));
    border-color: color-mix(in srgb, var(--sos-success) 55%, var(--sos-border));
}
.adm-appt-card.adm-appt-tint-cancelled, .adm-appt-card.adm-appt-tint-no_show {
    background: color-mix(in srgb, #E11D48 30%, var(--sos-bg));
    border-color: color-mix(in srgb, #E11D48 55%, var(--sos-border));
}

/* Milestone 129 — admin/calendar.php day view: each booking card can now
   be collapsed to one compact line (time + client name + status badge)
   with the service name hidden, or expanded to show it — scoped to just
   this one day-view listing (a new .adm-appt-card-summary/-details/
   -toggle set of classes), not the shared .adm-appt-card base rules
   above, which every other listing using this card style (admin/index.php's
   "Today's schedule", admin/clients.php's booking history, staff/index.php)
   is untouched by, since none of them render this new markup at all. */
.adm-appt-card-client { color: var(--sos-text); }
.adm-appt-card-details { margin-top: .3rem; }
.adm-appt-card-toggle {
    background: transparent; border: none; color: var(--sos-muted); font-size: .75rem;
    line-height: 1; padding: .15rem .3rem; cursor: pointer;
}
.adm-appt-card-toggle:hover { color: var(--sos-primary); }
/* The bulk "Compact view" switch collapses every card's details by
   default (this rule)... */
.adm-cal-daycols.adm-compact .adm-appt-card-details { display: none; }
/* ...but a card's own is-expanded/is-collapsed class (set by clicking
   that one card's own arrow) always overrides it, in either direction —
   both rules below match on exactly as many classes as the rule above
   (three), so which one wins is decided by source order, not
   specificity: these two are declared after it deliberately, so a
   manually-toggled card is never fought back into the wrong state by the
   bulk switch's own rule above. */
.adm-appt-card.is-expanded .adm-appt-card-details { display: block; }
.adm-appt-card.is-collapsed .adm-appt-card-details { display: none; }

/* Milestone 39 — staff/calendar.php's month-grid view marks each day's
   bookings with a small solid-colour status dot rather than the full
   pill badge (too wide for a calendar cell). Reuses the existing
   .adm-dot circle (already used elsewhere for a staff member's own
   colour swatch, always via an inline background style) as the shape,
   plus the same .adm-status-* class names appointment_status_badge()
   already returns — but solid, not the 18%-tint used for text-pill
   backgrounds, since a tint is too faint to read at 10px. The extra
   .adm-cal-dot class keeps this solid-colour override scoped to the
   calendar page only, so it can't affect any other .adm-dot usage
   (every other call site sets its own inline background anyway, which
   would win regardless, but this keeps intent explicit). */
.adm-cal-dot { margin-right: .25rem; vertical-align: middle; }
.adm-cal-dot.adm-status-booked, .adm-cal-dot.adm-status-confirmed { background: var(--sos-accent); }
.adm-cal-dot.adm-status-completed { background: var(--sos-success); }
.adm-cal-dot.adm-status-cancelled, .adm-cal-dot.adm-status-no_show { background: #E11D48; }
.adm-cal-dot.adm-status-checked_in { background: var(--sos-primary); }

/* Milestone 39 — staff/calendar.php's month table: a fixed 7-column grid
   (Sunday-first, matching DAY_NAMES/the opening-hours grid convention),
   each cell tall enough for a day number plus a few small booking links
   without the row height ballooning unpredictably per month. */
.adm-month-table { table-layout: fixed; }
.adm-month-cell { height: 6.5rem; padding: .35rem .4rem; vertical-align: top; overflow: hidden; }
.adm-month-cell-blank { background: color-mix(in srgb, var(--sos-border) 25%, transparent); }
.adm-month-cell-today { background: color-mix(in srgb, var(--sos-primary) 10%, transparent); border: 1px solid var(--sos-primary) !important; }
.adm-month-daynum { font-weight: 700; font-size: .8rem; margin-bottom: .2rem; color: var(--sos-muted); }
.adm-month-appt { line-height: 1.3; margin-bottom: .15rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.adm-month-appt:hover { color: var(--sos-primary) !important; }
@media (max-width: 767.98px) {
    .adm-month-cell { height: 4.5rem; font-size: .8rem; }
}

/* Milestone 88 — admin/closed-dates.php reuses the .adm-month-table/
   .adm-month-cell grid above verbatim, extended with: a plain-looking
   button standing in for the day-number link (clicking it opens the
   "mark this day closed" modal instead of navigating anywhere — the only
   month-grid page where the day number is an action, not a link) and a
   distinct tint + small badge for a date with an actual closed_dates
   override, so it reads apart from a normal weekly-closed day at a
   glance. admin/calendar.php's own month view reuses just the badge
   class to flag the same dates there too. */
.adm-month-daynum-btn {
    background: none; border: none; padding: 0; font: inherit; color: inherit;
    cursor: pointer; text-decoration: none;
}
.adm-month-daynum-btn:hover { color: var(--sos-primary); text-decoration: underline; }
.adm-month-cell-closed {
    background: color-mix(in srgb, #E11D48 12%, var(--sos-bg));
    border-color: color-mix(in srgb, #E11D48 40%, var(--sos-border)) !important;
}
.adm-closed-badge {
    display: inline-block; font-size: .65rem; font-weight: 700; padding: .1rem .4rem;
    border-radius: 1rem; background: color-mix(in srgb, #E11D48 18%, transparent); color: #E11D48;
    margin-bottom: .2rem;
}
.adm-weekly-closed-label { font-size: .7rem; color: var(--sos-muted); }

/* Milestone 24 — a client's own profile photo, shown wherever the salon
   looks up a specific customer (admin/clients.php's list + detail,
   admin/appointment.php, staff/appointment.php) and on the client's own
   book/account.php. A generic person icon stands in when no photo has
   been uploaded yet, rather than an initial-letter avatar — clients
   (unlike staff) have no assigned colour to draw one from. */
.adm-client-avatar { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
.adm-client-avatar-fallback {
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--sos-bg); border: 1px solid var(--sos-border);
    color: var(--sos-muted); font-size: 1.2rem;
}
.adm-client-avatar-lg { width: 64px; height: 64px; font-size: 1.8rem; }

/* Milestone 107 — admin/clients.php's restructure: a full-width list of
   clients (no more side-by-side "list here, edit panel there" columns),
   each row expanding open in place into the same card's own edit form +
   booking/service history rather than that living in a permanently
   visible sidebar panel. */
.adm-client-list { display: flex; flex-direction: column; gap: .6rem; }
.adm-client-item {
    background: var(--sos-surface); border: 1px solid var(--sos-border);
    border-radius: .75rem; overflow: hidden;
}
.adm-client-row {
    display: flex; align-items: center; gap: .75rem; padding: .75rem 1rem;
    color: var(--sos-text); text-decoration: none;
}
.adm-client-row:hover, .adm-client-row:focus { background: var(--sos-bg); color: var(--sos-text); }
.adm-client-item.is-open .adm-client-row { background: color-mix(in srgb, var(--sos-primary) 8%, transparent); }
.adm-client-chevron { color: var(--sos-muted); transition: transform .15s ease; flex-shrink: 0; }
.adm-client-item.is-open .adm-client-chevron { transform: rotate(180deg); color: var(--sos-primary); }
.adm-client-expanded {
    padding: 1.1rem 1.25rem 1.25rem; border-top: 1px solid var(--sos-border);
}
/* A flex child needs this to actually shrink below its content's natural
   width — without it, .text-truncate on a long email/name has nothing to
   truncate against and just pushes the row wider instead. Bootstrap
   itself ships no .min-w-0 utility, so this project defines its own. */
.min-w-0 { min-width: 0; }

/* Milestone 109 — dormant client account closure. Same amber/warning
   accent as .pub-eotm-badge/.staff-eotm-banner elsewhere in this file
   (not the salon's own brand colours) — this is a caution state, not
   part of the salon's own visual identity, the same reasoning that
   applies there. */
.adm-dormancy-badge {
    display: inline-flex; align-items: center; font-size: .7rem; font-weight: 700;
    padding: .2rem .55rem; border-radius: 1rem;
    background: color-mix(in srgb, #F59E0B 18%, transparent); color: #F59E0B;
}
.adm-dormancy-notice {
    background: color-mix(in srgb, #F59E0B 12%, var(--sos-bg));
    border: 1px solid color-mix(in srgb, #F59E0B 40%, var(--sos-border));
    border-radius: .6rem; padding: .7rem .9rem; font-size: .85rem;
}
.adm-dormancy-notice i { color: #F59E0B; }

/* Milestone 85 — admin/staff.php's staff-list row avatar: a real photo,
   or a coloured-initial fallback using the same staff.colour every
   calendar/public page already uses — unlike .adm-client-avatar-fallback
   just above, which shows a generic icon since a client has no colour
   field of their own. */
.adm-staff-avatar-sm {
    width: 32px; height: 32px; border-radius: 50%; object-fit: cover; flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
    color: #fff; font-weight: 700; font-size: .85rem;
}

/* Milestone 18 — admin/services.php and admin/staff.php were both switched
   from cramped side-by-side columns to full-width stacked panels, so their
   tables/lists now have real room to breathe. These two classes give every
   row a bit more vertical padding and a subtle divider so the extra width
   reads as "spacious," not just "wide." */
.adm-table-spaced > thead > tr > th,
.adm-table-spaced > tbody > tr > td {
    padding: .9rem 1rem;
}
.adm-table-spaced > tbody > tr:not(:last-child) > td {
    border-bottom: 1px solid var(--sos-border);
}
.adm-list-spaced .list-group-item {
    padding: .85rem .25rem .85rem 1rem;
}

/* Fix — reported alongside Milestone 79's build: Bootstrap's own default
   dark-theme list-group-item and table backgrounds (--bs-list-group-bg /
   --bs-table-bg) render as a solid fill that's a visibly different shade
   from this project's own .sos-card surface colour (--sos-surface), so
   every row inside a card reads as its own separate tinted box rather
   than blending into the card underneath it — a real, sitewide styling
   mismatch this sandbox's CDN block hid until now (Bootstrap only loads
   for real on the live site). Neutralised sitewide, not just on
   admin/services.php: every .list-group-item and .table row now sits on
   plain transparent background, so only the existing border-bottom
   dividers (.adm-table-spaced, list-group-flush's own rule) separate one
   row from the next, matching how every .sos-card itself already looks. */
.list-group-item,
.table,
.table > :not(caption) > * > * {
    background-color: transparent;
}

/* Milestone 42 — drag-to-reorder the admin staff table (plain HTML5
   drag-and-drop, no library). The handle is the only part of the row
   that visually invites dragging; the row itself still has its normal
   text-selection cursor everywhere else. */
.adm-drag-handle {
    cursor: grab;
    width: 2.5rem;
}
.adm-staff-row.adm-dragging {
    opacity: .4;
}

/* Milestone 79 — the same drag-to-reorder treatment extended to
   admin/services.php's category list and service table (both share the
   handle styling above; each needs its own dragging-state selector
   since they're different element types/classes). */
.adm-cat-row.adm-dragging, .adm-svc-row.adm-dragging {
    opacity: .4;
}

/* Milestone 117 — same drag-to-reorder treatment, this time for
   admin/kiosk.php's/frontdesk/kiosk.php's announcement slides table
   (includes/kiosk_control_body.php). Shares .adm-drag-handle above. */
.adm-slide-row.adm-dragging {
    opacity: .4;
}

/* Milestone 18 — a small role pill next to "Hi, {name}" in both the admin
   top strip (admin/partials/header.php, always "Owner") and the staff
   portal's own nav (staff/partials/header.php, the logged-in staff
   member's real role — Manager/Front Desk/Stylist). Sits in the same
   identity strip as the name/logout link on both desktop and mobile,
   since neither .adm-topbar nor .pub-nav is hidden at any breakpoint. */
.adm-role-pill {
    font-size: .7rem;
    font-weight: 700;
    padding: .2rem .65rem;
    border-radius: 1rem;
    background: color-mix(in srgb, var(--sos-primary) 16%, transparent);
    color: var(--sos-primary);
    display: inline-block;
    text-transform: uppercase;
    letter-spacing: .02em;
}

/* Milestone 82 — the public "Meet the team" pill (index.php's homepage
   cards, book/staff.php's individual profile), split out from
   .adm-role-pill so it can vary its colour by label instead of always
   being the one flat --sos-primary tint every .adm-role-pill still is.
   .pub-role-pill carries the shape (identical to .adm-role-pill's own);
   one of the 8 .pub-pill-N classes below is added alongside it, chosen
   per-label by includes/functions.php's staff_pill_class() — 8 distinct
   hues (STAFF_PILL_PALETTE_SIZE there must match the count here), the
   project's 4 existing theme colours plus 4 more picked to sit clearly
   apart from those 4 and from each other, each used the same light-tint-
   background/solid-text treatment .adm-role-pill already established. */
.pub-role-pill {
    font-size: .7rem;
    font-weight: 700;
    padding: .2rem .65rem;
    border-radius: 1rem;
    display: inline-block;
    text-transform: uppercase;
    letter-spacing: .02em;
}
.pub-pill-0 { background: color-mix(in srgb, var(--sos-primary) 16%, transparent); color: var(--sos-primary); }
.pub-pill-1 { background: color-mix(in srgb, var(--sos-secondary) 16%, transparent); color: var(--sos-secondary); }
.pub-pill-2 { background: color-mix(in srgb, var(--sos-accent) 16%, transparent); color: var(--sos-accent); }
.pub-pill-3 { background: color-mix(in srgb, var(--sos-success) 16%, transparent); color: var(--sos-success); }
.pub-pill-4 { background: color-mix(in srgb, #F59E0B 16%, transparent); color: #F59E0B; }
.pub-pill-5 { background: color-mix(in srgb, #14B8A6 16%, transparent); color: #14B8A6; }
.pub-pill-6 { background: color-mix(in srgb, #EF4444 16%, transparent); color: #EF4444; }
.pub-pill-7 { background: color-mix(in srgb, #6366F1 16%, transparent); color: #6366F1; }

/* Milestone 25 — the client account area (book/account.php "My bookings",
   book/account-photo.php "My photo", book/account-privacy.php "Privacy &
   data") gained a proper sidebar: a vertical .acct-sidebar column, sticky,
   desktop-only (>=992px). Scoped to its own .acct-* classes rather than
   reusing .adm-*, since this sits inside the public site's .pub-shell
   (1080px), not the admin area's wider .adm-shell. Milestone 36 — the
   <992px half of this (a horizontal .acct-nav-links-mobile bar, always
   visible) is gone; mobile now shares the guest nav's burger + off-canvas
   .pub-nav-dropdown panel instead (header.php switches its contents on
   $accountNav) — this sidebar is the only piece left here. */
.acct-sidebar { display: none; }
.acct-main { min-width: 0; }

@media (min-width: 992px) {
    .acct-shell { display: flex; align-items: flex-start; gap: 2rem; }
    .acct-sidebar {
        display: flex; flex-direction: column; gap: .35rem;
        width: 180px; flex: 0 0 180px;
        position: sticky; top: 1.5rem;
    }
    .acct-sidebar a {
        padding: .5rem .75rem; border-radius: .5rem;
        color: var(--sos-primary); text-decoration: none; font-weight: 500; font-size: .92rem;
    }
    .acct-sidebar a:hover { background: var(--sos-bg); color: var(--sos-secondary); }
    .acct-sidebar a.is-active { background: var(--sos-bg); color: var(--sos-secondary); font-weight: 700; }

    .acct-main { flex: 1 1 auto; }
}

/* Milestone 12 (revised) — the staff portal (staff/index.php "My day"/
   "Team calendar", staff/profile.php "My profile") gained the same
   sidebar treatment as the admin area and the client account area
   above: one shared $navItems array (see staff/partials/header.php)
   rendered twice — a horizontal .staff-nav-links row (mobile, <992px)
   and a vertical .staff-sidebar column (desktop, >=992px, sticky) —
   only one of which is ever visible at a given width. The top strip
   (brand + "Staff Portal" label, "Hi, name", role pill, log out, theme
   toggle) stays global identity/actions and is always visible, same
   split as the admin area. Scoped to its own .staff-* classes rather
   than reusing .adm-*/.acct-*, since this sits inside the public
   site's .pub-shell, same reasoning as .acct-* above. */
.staff-nav-links { display: flex; gap: 1.1rem; align-items: center; flex-wrap: wrap; padding-bottom: 1rem; }
.staff-nav-links a { color: var(--sos-primary); text-decoration: none; font-weight: 500; font-size: .9rem; }
.staff-nav-links a:hover, .staff-nav-links a.is-active { color: var(--sos-secondary); }
.staff-nav-links a.is-active { font-weight: 700; }

.staff-sidebar { display: none; }
.staff-main { min-width: 0; }

@media (min-width: 992px) {
    .staff-nav-links-mobile { display: none; }

    .staff-shell { display: flex; align-items: flex-start; gap: 2rem; }
    .staff-sidebar {
        /* Milestone 134 follow-up — gap and per-link padding both tightened
           at the user's own direct request ("reduce spacing between links"),
           down from .35rem/.5rem .75rem — same change as .adm-sidebar's own,
           applied here since Front Desk AND the staff/manager portal both
           share this one class. */
        display: flex; flex-direction: column; gap: .1rem;
        width: 180px; flex: 0 0 180px;
        position: sticky; top: 1.5rem;
    }
    .staff-sidebar a {
        padding: .3rem .75rem; border-radius: .5rem;
        color: var(--sos-primary); text-decoration: none; font-weight: 500; font-size: .92rem;
    }
    .staff-sidebar a:hover { background: var(--sos-bg); color: var(--sos-secondary); }
    .staff-sidebar a.is-active { background: var(--sos-bg); color: var(--sos-secondary); font-weight: 700; }

    .staff-main { flex: 1 1 auto; }
}

/* Milestone 103 — Employee of the Month. A small fixed gold/amber accent
   for both pieces below, deliberately NOT drawn from the salon's own
   colour_primary/secondary/accent palette (unlike .pub-pill-N above) —
   a trophy reads as an award regardless of whatever brand colours this
   particular salon has chosen, the same reasoning STAFF_PILL_PALETTE
   itself doesn't apply here. */
.pub-eotm-badge {
    display: inline-flex; align-items: center; gap: .35rem;
    font-size: .7rem; font-weight: 700; padding: .2rem .65rem; border-radius: 1rem;
    background: color-mix(in srgb, #F59E0B 18%, transparent); color: #F59E0B;
}
.staff-eotm-banner {
    display: flex; align-items: center; gap: .85rem;
    background: linear-gradient(135deg, color-mix(in srgb, #F59E0B 22%, var(--sos-surface)), color-mix(in srgb, #F59E0B 8%, var(--sos-surface)));
    border: 1px solid color-mix(in srgb, #F59E0B 45%, var(--sos-border));
    border-radius: .9rem; padding: .9rem 1.2rem; margin-bottom: 1.25rem;
}
.staff-eotm-banner i.fa-trophy { color: #F59E0B; font-size: 1.5rem; flex-shrink: 0; }

/* Milestone 68 — a floating "back to top" button, requested for
   "everywhere" (public site, admin area, staff portal). One shared
   definition here since assets/css/setup.css is the one stylesheet
   already loaded by all three realms (book/admin/staff
   partials/header.php each link it) — no need for three copies of this
   rule. The button element + the scroll-listener/smooth-scroll JS that
   drives it are still added separately in each realm's own footer
   partial (this project has no shared JS file across realms), but all
   three use this one class and so stay visually identical.
   Hidden by default via opacity/visibility (not display:none, so the
   fade below has something to animate) and toggled purely by JS adding
   .is-visible once the page has scrolled down some distance — plain
   CSS/JS, no framework, consistent with the rest of this project. */
.sos-back-to-top {
    position: fixed;
    right: 1.25rem;
    bottom: 1.25rem;
    z-index: 1030;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: 50%;
    border: none;
    background: linear-gradient(135deg, var(--sos-primary), var(--sos-secondary));
    color: #fff;
    font-size: 1.05rem;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 10px 24px -8px rgba(124, 58, 237, .45);
    opacity: 0;
    visibility: hidden;
    transform: translateY(.5rem);
    transition: opacity .2s ease, transform .2s ease, visibility .2s;
    cursor: pointer;
}
.sos-back-to-top:hover, .sos-back-to-top:focus { filter: brightness(1.08); color: #fff; }
.sos-back-to-top.is-visible { opacity: 1; visibility: visible; transform: translateY(0); }
@media (max-width: 575.98px) {
    .sos-back-to-top { right: 1rem; bottom: 1rem; width: 2.5rem; height: 2.5rem; font-size: .95rem; }
}

/* Milestone 106 — the internal messaging widget (includes/messaging_widget.php),
   included on every admin/staff/frontdesk page. Sits a little higher than
   .sos-back-to-top above (bottom: 5.5rem vs 1.25rem) so the two never
   overlap on a page that's both scrolled down and has the messenger open —
   .sos-back-to-top only ever appears once scrolled, but is positioned lower,
   directly beneath the message bubble, rather than the two competing for
   the same corner. */
.sos-msg-widget { position: fixed; right: 1.25rem; bottom: 5.5rem; z-index: 1035; }
.sos-msg-bubble {
    position: relative;
    width: 3.25rem;
    height: 3.25rem;
    border-radius: 50%;
    border: none;
    background: linear-gradient(135deg, var(--sos-primary), var(--sos-secondary));
    color: #fff;
    font-size: 1.25rem;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 10px 24px -8px rgba(124, 58, 237, .5);
    cursor: pointer;
}
.sos-msg-bubble:hover, .sos-msg-bubble:focus { filter: brightness(1.08); color: #fff; }
.sos-msg-badge {
    position: absolute;
    top: -.3rem;
    right: -.3rem;
    min-width: 1.35rem;
    height: 1.35rem;
    padding: 0 .3rem;
    border-radius: 999px;
    /* Fix — the user asked for "a red notice" and reported never seeing
       one even after two rounds of polling-speed fixes proved (via a real
       end-to-end Playwright run: two separate logins, one genuinely
       messaging the other, no manual DOM poking) that the badge's
       underlying show/hide logic and timing were already correct — the
       badge really was appearing within a few seconds of a new message.
       The actual gap was this line: it painted the badge in the salon's
       own branded --sos-secondary colour (a pink/magenta, e.g. #EC4899
       on this salon's palette), which shifts with every salon's theme and
       never reads as "red" the way a notification badge conventionally
       does (phone app badges, Gmail, etc.) — so a real badge could be
       sitting there and still not register as "a red notice" to someone
       glancing at it. Hard-coded to Bootstrap's own standard danger red
       (the same #dc3545 this project's .alert-danger/.btn-danger already
       use elsewhere, already loaded on every page via the Bootstrap
       bundle) instead of a theme variable, specifically so it stays a
       consistent, unmistakable alert red regardless of which salon/theme
       colours are in play. */
    background: #dc3545;
    color: #fff;
    font-size: .7rem;
    font-weight: 700;
    line-height: 1.35rem;
    text-align: center;
    box-shadow: 0 0 0 2px var(--sos-bg);
}
.sos-msg-panel {
    position: absolute;
    right: 0;
    bottom: 4rem;
    width: 21rem;
    max-width: calc(100vw - 2.5rem);
    height: 28rem;
    max-height: calc(100vh - 8rem);
    background: var(--sos-surface);
    border: 1px solid var(--sos-border);
    border-radius: 1rem;
    box-shadow: 0 20px 45px -12px rgba(0, 0, 0, .35);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
.sos-msg-panel-header {
    display: flex;
    align-items: center;
    gap: .4rem;
    padding: .65rem .75rem;
    border-bottom: 1px solid var(--sos-border);
    background: linear-gradient(135deg, var(--sos-primary), var(--sos-secondary));
    color: #fff;
    flex: 0 0 auto;
}
.sos-msg-panel-title { flex: 1 1 auto; font-weight: 600; font-size: .95rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sos-msg-icon-btn {
    border: none;
    background: transparent;
    color: #fff;
    width: 1.9rem;
    height: 1.9rem;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    opacity: .9;
}
.sos-msg-icon-btn:hover, .sos-msg-icon-btn:focus { opacity: 1; background: rgba(255, 255, 255, .18); }
.sos-msg-body { flex: 1 1 auto; overflow-y: auto; min-height: 0; }
.sos-msg-list { display: flex; flex-direction: column; }
.sos-msg-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    width: 100%;
    border: none;
    border-bottom: 1px solid var(--sos-border);
    background: transparent;
    color: var(--sos-text);
    text-align: left;
    padding: .65rem .85rem;
    cursor: pointer;
}
.sos-msg-row:hover, .sos-msg-row:focus { background: var(--sos-bg); }
.sos-msg-row.has-unread .sos-msg-row-name { font-weight: 700; }
.sos-msg-row-main { display: flex; flex-direction: column; min-width: 0; }
.sos-msg-row-name { font-size: .9rem; }
.sos-msg-row-preview { font-size: .78rem; color: var(--sos-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 12rem; }
.sos-msg-row-side { display: flex; flex-direction: column; align-items: flex-end; gap: .25rem; flex: 0 0 auto; }
.sos-msg-row-time { font-size: .7rem; color: var(--sos-muted); white-space: nowrap; }
.sos-msg-unread-dot {
    min-width: 1.1rem;
    height: 1.1rem;
    padding: 0 .3rem;
    border-radius: 999px;
    background: var(--sos-secondary);
    color: #fff;
    font-size: .68rem;
    font-weight: 700;
    line-height: 1.1rem;
    text-align: center;
}
.sos-msg-empty { padding: 1.5rem 1rem; margin: 0; text-align: center; color: var(--sos-muted); font-size: .85rem; }
.sos-msg-thread { display: flex; flex-direction: column; gap: .4rem; padding: .75rem; }
.sos-msg-bubble-row { display: flex; }
.sos-msg-bubble-row.mine { justify-content: flex-end; }
.sos-msg-bubble-row.theirs { justify-content: flex-start; }
/* Wraps the sender-name label and the bubble itself so the name sits
   above the bubble and stacks on the same side of the thread as it,
   rather than beside it — .sos-msg-bubble-row stays a plain horizontal
   flex row (its justify-content is what pushes "mine" bubbles right and
   "theirs" left), so the max-width that used to live on the bubble text
   itself now lives here, one level up. */
.sos-msg-bubble-col { display: flex; flex-direction: column; max-width: 78%; }
.sos-msg-bubble-row.mine .sos-msg-bubble-col { align-items: flex-end; }
.sos-msg-bubble-row.theirs .sos-msg-bubble-col { align-items: flex-start; }
.sos-msg-bubble-name { font-size: .7rem; font-weight: 600; color: var(--sos-muted); margin: 0 .35rem .15rem; }
.sos-msg-bubble-text {
    padding: .45rem .7rem;
    border-radius: .9rem;
    font-size: .85rem;
    line-height: 1.35;
    word-wrap: break-word;
    white-space: pre-wrap;
}
/* Was linear-gradient(135deg, var(--sos-primary), var(--sos-secondary))
   — the same two-tone brand combo used for buttons/headers elsewhere,
   which reads fine on those but turned "messy" once real message bubbles
   started stacking up a thread full of them. Flattened to a plain fill
   of --sos-primary alone: it's still on-brand (same purple used for
   headings/links throughout) but a single solid colour, and — unlike
   --sos-bg/--sos-surface/--sos-text/--sos-border — --sos-primary is NOT
   redefined inside [data-theme="dark"] above, so this reads identically,
   with the same white-text contrast, in both themes with no extra rule
   needed here. */
.sos-msg-bubble-row.mine .sos-msg-bubble-text { background: var(--sos-primary); color: #fff; border-bottom-right-radius: .25rem; }
.sos-msg-bubble-row.theirs .sos-msg-bubble-text { background: var(--sos-bg); color: var(--sos-text); border: 1px solid var(--sos-border); border-bottom-left-radius: .25rem; }
/* A message that's nothing but emoji (see EMOJI_ONLY_RE in
   messaging_widget.php) gets sized up the way most chat apps render
   emoji-only messages — small single characters otherwise get lost at
   the normal .85rem text size. */
.sos-msg-bubble-text.emoji-only { font-size: 2rem; line-height: 1.2; padding: .3rem .5rem; }
/* Three rounds on this one: .85rem/.6rem read as clipped, 1.75rem/1.5rem
   (screenshot-confirmed clear of the corner) read as too much empty space
   on the right ("too far left" — the button visibly floating away from
   the edge rather than sitting in it naturally). Settled in between, at
   a value that still clears .sos-msg-panel's 1rem border-radius with
   real margin (not a razor-thin one, per the earlier rounds' own notes)
   but doesn't leave the button looking stranded mid-panel. The
   min-width:0 fix below and the appearance:none reset on the button
   itself are unrelated to this exact number and stay as-is. */
.sos-msg-send-form { position: relative; display: flex; gap: .5rem; padding: .6rem 1.5rem 1.25rem .6rem; border-top: 1px solid var(--sos-border); flex: 0 0 auto; }
.sos-msg-send-input {
    flex: 1 1 auto;
    min-width: 0;
    border: 1px solid var(--sos-border);
    border-radius: 999px;
    padding: .45rem .9rem;
    background: var(--sos-bg);
    color: var(--sos-text);
    font-size: .85rem;
}
.sos-msg-send-input:focus { outline: none; border-color: var(--sos-primary); }
.sos-msg-send-btn {
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    box-sizing: border-box;
    border: none;
    border-radius: 50%;
    width: 2.3rem;
    height: 2.3rem;
    flex: 0 0 auto;
    background: linear-gradient(135deg, var(--sos-primary), var(--sos-secondary));
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}
.sos-msg-send-btn:hover, .sos-msg-send-btn:focus { filter: brightness(1.08); color: #fff; }
/* Emoji picker — requested directly by the user. .sos-msg-emoji-btn reuses
   .sos-msg-icon-btn's own circular sizing/hover treatment (same as the
   header's back/new/delete/close buttons) so it looks native to the rest
   of the widget's chrome rather than a bolted-on extra. The panel itself
   anchors to .sos-msg-send-form's own `position: relative` (added above)
   via `bottom: 100%` — floats directly above the input row with no
   hardcoded pixel offsets to keep in sync if the send-form's own height
   ever changes — and stays comfortably inside .sos-msg-panel's fixed
   height, so its own overflow:hidden never needs to clip it. */
.sos-msg-emoji-btn { color: var(--sos-muted); flex: 0 0 auto; }
.sos-msg-emoji-btn:hover, .sos-msg-emoji-btn:focus, .sos-msg-emoji-btn[aria-expanded="true"] { color: var(--sos-text); background: var(--sos-bg); opacity: 1; }
.sos-msg-emoji-panel {
    position: absolute;
    left: .6rem;
    right: .6rem;
    bottom: calc(100% + .5rem);
    max-height: 11rem;
    overflow-y: auto;
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    gap: .15rem;
    padding: .5rem;
    background: var(--sos-surface);
    border: 1px solid var(--sos-border);
    border-radius: .75rem;
    box-shadow: 0 12px 28px -10px rgba(0, 0, 0, .4);
}
.sos-msg-emoji-item {
    border: none;
    background: transparent;
    border-radius: .4rem;
    font-size: 1.15rem;
    line-height: 1;
    padding: .3rem 0;
    cursor: pointer;
}
.sos-msg-emoji-item:hover, .sos-msg-emoji-item:focus { background: var(--sos-bg); }
/* Milestone 118 — a second, generic emoji picker for the Kiosk Control
   slide composer (includes/kiosk_control_body.php's Heading/Body text
   fields), not just a rename of .sos-msg-emoji-*: that one anchors its
   panel ABOVE a fixed-height chat input strip (bottom: 100%), while this
   one anchors BELOW an ordinary form field sitting in the normal page
   flow, so the two need opposite anchoring and can't safely share a
   class without one of them fighting the other's positioning. .sos-emoji-
   item's own look (font-size/padding/hover) is copied from .sos-msg-emoji-
   item on purpose, so an emoji picker looks and feels the same wherever
   it shows up in the app. */
.sos-emoji-trigger { color: var(--sos-muted); }
.sos-emoji-trigger:hover, .sos-emoji-trigger:focus, .sos-emoji-trigger[aria-expanded="true"] { color: var(--sos-primary); }
.sos-emoji-panel {
    position: absolute;
    left: 0;
    right: 0;
    top: calc(100% + .25rem);
    z-index: 5;
    max-height: 11rem;
    overflow-y: auto;
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    gap: .15rem;
    padding: .5rem;
    background: var(--sos-surface);
    border: 1px solid var(--sos-border);
    border-radius: .75rem;
    box-shadow: 0 12px 28px -10px rgba(0, 0, 0, .4);
}
.sos-emoji-item {
    border: none;
    background: transparent;
    border-radius: .4rem;
    font-size: 1.15rem;
    line-height: 1;
    padding: .3rem 0;
    cursor: pointer;
}
.sos-emoji-item:hover, .sos-emoji-item:focus { background: var(--sos-bg); }
@media (max-width: 575.98px) {
    .sos-msg-widget { right: 1rem; bottom: 4.75rem; }
    .sos-msg-bubble { width: 2.9rem; height: 2.9rem; font-size: 1.1rem; }
    .sos-msg-panel { width: calc(100vw - 2rem); height: calc(100vh - 7.5rem); max-height: none; bottom: 3.6rem; }
}

/* Milestone 125 — "People Onsite", the small panel at the bottom of the
   left nav sidebar (includes/onsite_panel.php, shared between the admin
   and front-desk .adm-sidebar/.staff-sidebar columns — see that file's
   own doc comment). Plain sos-* names, not adm-*/staff-*, since it's the
   exact same markup in both realms, not something realm-specific. Only
   ever shown inside the desktop (>=992px) sidebar — those columns are
   display:none below that width already (see .adm-sidebar/.staff-sidebar
   above), so this needs no media query of its own. */
.sos-onsite-panel {
    margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--sos-border);
}
/* Milestone 134 follow-up — the panel now renders FIRST in every one of
   its three sidebars (above the page-nav links — admin, Front Desk, AND
   the staff/manager portal, all at the user's own direct request), so its
   divider needs to sit below it instead of above it everywhere now —
   otherwise the very first thing in each sidebar would be a stray top
   border with nothing above it to separate from. */
.adm-sidebar .sos-onsite-panel, .staff-sidebar .sos-onsite-panel {
    margin-top: 0; padding-top: 0; border-top: 0;
    margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--sos-border);
}
.sos-onsite-heading {
    font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
    color: var(--sos-muted); margin: 0 0 .75rem; padding: 0 .75rem;
}
.sos-onsite-group { margin-bottom: .9rem; padding: 0 .75rem; }
.sos-onsite-group:last-child { margin-bottom: 0; }
.sos-onsite-group-label { font-size: .72rem; font-weight: 600; color: var(--sos-muted); margin: 0 0 .35rem; }
.sos-onsite-list { list-style: none; margin: 0; padding: 0; }
.sos-onsite-row { font-size: .85rem; color: var(--sos-text); padding: .2rem 0; }
.sos-onsite-link {
    display: flex; align-items: center; gap: .4rem; color: var(--sos-text); text-decoration: none;
}
.sos-onsite-link:hover { color: var(--sos-secondary); }
.sos-onsite-check-icon { color: var(--sos-primary); font-size: .8rem; }
.sos-onsite-empty { font-size: .78rem; color: var(--sos-muted); font-style: italic; margin: 0; }
/* Milestone 127 — the "Off duty"/"Back on duty" buttons on each Staff
   row and in the new "Off duty today" group. Deliberately tiny and
   understated (a plain bordered pill, not a solid brand-coloured
   button) — this sits in a small sidebar column next to a name, not a
   primary action, and it's one people will click often through a shift. */
.sos-onsite-duty-btn {
    font-size: .68rem; line-height: 1; padding: .2rem .5rem; border-radius: 999px;
    border: 1px solid var(--sos-border); background: transparent; color: var(--sos-muted);
    white-space: nowrap; cursor: pointer;
}
.sos-onsite-duty-btn:hover { color: var(--sos-primary); border-color: var(--sos-primary); }
.sos-onsite-duty-btn:disabled { opacity: .5; cursor: default; }
/* Milestone 127 addendum 3 — the Staff group's collapse/expand toggle and
   the collapsible wrapper it controls (which now nests the "Off duty
   today" group inside the "Staff" group, so one toggle shortens both at
   once — see includes/onsite_panel.php's own doc comment). The nested
   .sos-onsite-group no longer needs its own left/right padding, since
   it's already inside a padded parent by this point — without this
   override it would visibly double-indent versus every other group. */
.sos-onsite-collapse-btn {
    background: transparent; border: none; color: var(--sos-muted); font-size: .7rem;
    line-height: 1; padding: .1rem .2rem; cursor: pointer;
}
.sos-onsite-collapse-btn:hover { color: var(--sos-primary); }
#sosOnsiteStaffCollapsible .sos-onsite-group { padding-left: 0; padding-right: 0; margin-top: .75rem; margin-bottom: 0; }

/* =====================================================================
   Milestone 143 — "glass" component layer (glassmorphism).
   The user asked for a frosted-glass look on new UX going forward:
   translucent surfaces that let the page's brand-tinted background
   gradient show through, a soft blur behind them, a hairline
   semi-transparent border, and a gentle lift on hover. Deliberately a
   small, opt-in set of classes (.sos-glass, .sos-glass-card,
   .btn-glass, .btn-glass-primary, .sos-glass-list) rather than a
   restyle of .admin-card/.btn-sos — existing pages keep rendering
   exactly as before; only surfaces that explicitly adopt these classes
   change. A whole-app retrofit is its own agreed milestone (see
   claude/working-preferences.md). Everything is driven off the same
   --sos-* tokens, so it follows the salon's brand colours and the
   light/dark toggle for free; backdrop-filter has a plain translucent
   fallback on the few older engines (kiosk TVs) that lack it.
   ===================================================================== */
:root {
    --sos-glass-bg: rgba(255, 255, 255, 0.52);
    --sos-glass-bg-strong: rgba(255, 255, 255, 0.72);
    --sos-glass-border: rgba(255, 255, 255, 0.65);
    --sos-glass-shadow: 0 8px 32px rgba(31, 27, 46, 0.10);
    --sos-glass-shadow-hover: 0 14px 40px rgba(31, 27, 46, 0.16);
    --sos-glass-highlight: rgba(255, 255, 255, 0.85);
}
[data-theme="dark"] {
    --sos-glass-bg: rgba(14, 10, 32, 0.56);
    --sos-glass-bg-strong: rgba(14, 10, 32, 0.76);
    --sos-glass-border: rgba(255, 255, 255, 0.14);
    --sos-glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
    --sos-glass-shadow-hover: 0 14px 40px rgba(0, 0, 0, 0.45);
    --sos-glass-highlight: rgba(255, 255, 255, 0.22);
}

.sos-glass {
    background: var(--sos-glass-bg);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    backdrop-filter: blur(14px) saturate(140%);
    border: 1px solid var(--sos-glass-border);
    box-shadow: var(--sos-glass-shadow), inset 0 1px 0 var(--sos-glass-highlight);
    border-radius: 1rem;
}
.sos-glass-card {
    background: var(--sos-glass-bg);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    backdrop-filter: blur(14px) saturate(140%);
    border: 1px solid var(--sos-glass-border);
    box-shadow: var(--sos-glass-shadow), inset 0 1px 0 var(--sos-glass-highlight);
    border-radius: 1rem;
    padding: 1.25rem;
    position: relative;
    overflow: hidden;
    transition: box-shadow .2s ease, transform .2s ease;
}
.sos-glass-card::before {
    /* a soft brand-coloured glow in one corner, so the panel reads as
       glass over a lit background rather than a flat grey box */
    content: "";
    position: absolute; inset: -40% auto auto -20%;
    width: 60%; aspect-ratio: 1;
    background: radial-gradient(closest-side, color-mix(in srgb, var(--sos-primary) 22%, transparent), transparent 70%);
    pointer-events: none;
}
.sos-glass-card:hover { box-shadow: var(--sos-glass-shadow-hover), inset 0 1px 0 var(--sos-glass-highlight); }
.sos-glass-card > * { position: relative; }

.sos-glass-eyebrow {
    display: inline-flex; align-items: center; gap: .4rem;
    font-size: .72rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
    color: var(--sos-primary);
    background: color-mix(in srgb, var(--sos-primary) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--sos-primary) 25%, transparent);
    border-radius: 999px; padding: .25rem .65rem;
}

.btn-glass {
    background: var(--sos-glass-bg-strong);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    border: 1px solid var(--sos-glass-border);
    color: var(--sos-text);
    font-weight: 600;
    border-radius: .75rem;
    box-shadow: 0 2px 10px rgba(31, 27, 46, 0.08), inset 0 1px 0 var(--sos-glass-highlight);
    transition: transform .15s ease, box-shadow .15s ease, background .15s ease;
}
.btn-glass:hover, .btn-glass:focus {
    color: var(--sos-primary);
    transform: translateY(-1px);
    box-shadow: 0 6px 18px rgba(31, 27, 46, 0.14), inset 0 1px 0 var(--sos-glass-highlight);
}
.btn-glass:active { transform: translateY(0); }
.btn-glass-primary {
    background: linear-gradient(135deg, color-mix(in srgb, var(--sos-primary) 88%, white), color-mix(in srgb, var(--sos-secondary) 88%, white));
    border: 1px solid rgba(255, 255, 255, 0.45);
    color: #fff;
    font-weight: 600;
    border-radius: .75rem;
    box-shadow: 0 6px 20px color-mix(in srgb, var(--sos-primary) 35%, transparent), inset 0 1px 0 rgba(255, 255, 255, 0.45);
    transition: transform .15s ease, box-shadow .15s ease, filter .15s ease;
}
.btn-glass-primary:hover, .btn-glass-primary:focus {
    color: #fff;
    transform: translateY(-1px);
    filter: brightness(1.06);
    box-shadow: 0 10px 26px color-mix(in srgb, var(--sos-primary) 45%, transparent), inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.btn-glass-primary:active { transform: translateY(0); }

/* A compact, scrollable preview list inside a glass card (used by the
   wizard's suggested-menu offer). */
.sos-glass-list { max-height: 18rem; overflow-y: auto; padding-right: .25rem; }
.sos-glass-list-cat { font-weight: 600; color: var(--sos-primary); font-size: .8rem; margin: .6rem 0 .2rem; }
.sos-glass-list-cat:first-child { margin-top: 0; }
.sos-glass-list-row {
    display: flex; justify-content: space-between; gap: .75rem; align-items: baseline;
    font-size: .82rem; padding: .22rem 0; border-bottom: 1px dashed var(--sos-glass-border);
}
.sos-glass-list-row:last-child { border-bottom: none; }
.sos-glass-list-row .meta { color: var(--sos-muted); white-space: nowrap; font-variant-numeric: tabular-nums; }

/* Milestone 145 — "Form outstanding" badge (consent_outstanding_badge_html()),
   shown beside the status badge on appointment cards and pages. Amber so
   it reads as "action needed", distinct from every status colour. */
.sos-consent-badge { background: color-mix(in srgb, #F59E0B 20%, transparent); color: #B45309; }
[data-theme="dark"] .sos-consent-badge { color: #FBBF24; }
.sos-q-row { padding: .5rem .6rem; margin-bottom: .5rem; border: 1px solid var(--sos-glass-border); border-radius: .6rem; background: var(--sos-glass-bg); }
/* Milestone 146 — room/equipment unit badge on cards (resources_label_html()). */
.sos-resource-badge { background: color-mix(in srgb, #64748B 18%, transparent); color: #475569; }
[data-theme="dark"] .sos-resource-badge { color: #CBD5E1; }
/* Milestone 152 — group booking badge on cards (group_badge_html()) and the
   member rows of the group form. Violet so a party reads as one at a glance. */
.sos-group-badge { background: color-mix(in srgb, #8B5CF6 18%, transparent); color: #6D28D9; }
[data-theme="dark"] .sos-group-badge { color: #C4B5FD; }
.sos-group-row .form-control-plaintext { padding-top: .375rem; }
/* Milestone 153 — home-visit badge on cards (travel_badge_html()). Teal, so
   "we go to them" reads differently from a room (grey) or a group (violet). */
.sos-travel-badge { background: color-mix(in srgb, #0D9488 18%, transparent); color: #0F766E; }
[data-theme="dark"] .sos-travel-badge { color: #5EEAD4; }

/* =====================================================================
   Milestone 155 — the glass retrofit; Milestone 157 — made to pop.
   Every layout header emits body.sos-theme-glass while Site settings →
   Look & feel is on "Glass" (the default); "Classic" emits
   sos-theme-classic and none of the rules below apply, so the pre-155
   look is one radio button away.

   157's recipe, agreed in discussion with the owner (158 adds three
   alternative backdrops alongside it — see below):
     backdrop  — three big brand-derived orbs (--sos-orb-N, lifted HSL
                 from includes/functions.php) on body::before/::after,
                 drifting on a 60 s loop while body.sos-glass-motion is
                 present (Site settings switch; prefers-reduced-motion
                 stills it anyway), plus a 3% SVG grain against banding.
     panels    — ~58% white, blur(18px) saturate(160%), translucent
                 white rim, inset top highlight, a diagonal sheen as an
                 extra background layer (no pseudo-elements, so nothing
                 needs overflow:hidden), brand-tinted shadow.
     tiers     — chrome (topbar / sidebars / public nav) is the most
                 transparent and most blurred; cards one step more
                 opaque; things inside cards carry no fill or blur.
     blur only on containers — never on the hundreds of .adm-appt-card
                 in a calendar (they're translucent tints, no filter).
     buttons   — .btn-sos gradient + brand glow + lift; outline buttons
                 become glass pills.
     text      — a shade darker / heavier on translucent surfaces.

   Milestone 158 — three alternatives to the Orbs backdrop above, picked
   from Site settings → Look & feel (ui_glass_backdrop, includes/
   functions.php's ui_glass_backdrop()/UI_GLASS_BACKDROPS, emitted as a
   sos-backdrop-<style> class alongside sos-theme-glass). Every style is
   still driven off the exact same --sos-orb-1/2/3 (+ -dark) custom
   properties orbs already uses — this setting only rearranges them into
   a different shape, it never changes their colour source, so a salon's
   own brand palette shows through whichever style the owner picks:
     mesh       — calmer: more, smaller soft blobs (some blended pairs of
                  orbs via color-mix) scattered further apart than the
                  three big orbs, so no single glow dominates.
     aurora     — more dramatic: 2-3 broad soft diagonal bands (plain
                  angled linear-gradients, not rotated elements) that read
                  as flowing colour rather than distinct glowing circles.
     geometric  — angular/modern: a faint two-direction diagonal grid
                  (repeating-linear-gradient hairlines) plus a couple of
                  soft colour washes, deliberately less organic than the
                  other three.
   All four reuse the SAME body::before/::after pseudo-elements and the
   SAME sos-orb-drift-a/-b keyframes/motion toggle below — only the
   `background` each style paints onto those two layers differs, so no
   new animation or reduced-motion logic was needed for this milestone.
   .sos-backdrop-swatch-* (further down, near the picker's own styles)
   are small static previews of the same four recipes for the Site
   settings radio picker.
   ===================================================================== */

:root {
    /* orb fallbacks for the pages that don't emit salon_colour_style_tag()
       (the setup wizard, the generic reset pages) */
    --sos-orb-1: hsl(262 83% 62%);
    --sos-orb-2: hsl(330 85% 62%);
    --sos-orb-3: hsl(199 89% 62%);
    --sos-orb-1-dark: hsl(262 83% 52%);
    --sos-orb-2-dark: hsl(330 85% 52%);
    --sos-orb-3-dark: hsl(199 89% 52%);
    --sos-glass-sheen: linear-gradient(135deg, rgba(255,255,255,.55) 0%, rgba(255,255,255,.12) 38%, rgba(255,255,255,0) 60%);
    --sos-glass-rim: rgba(255, 255, 255, .62);
    --sos-glass-tint-shadow: 0 10px 30px color-mix(in srgb, var(--sos-primary) 14%, transparent), 0 2px 6px rgba(31, 27, 46, .06);
    --sos-glass-tint-shadow-hover: 0 18px 44px color-mix(in srgb, var(--sos-primary) 22%, transparent), 0 4px 10px rgba(31, 27, 46, .08);
    --sos-glass-grain: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 .05 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
}
[data-theme="dark"] {
    --sos-glass-sheen: linear-gradient(135deg, rgba(255,255,255,.14) 0%, rgba(255,255,255,.04) 38%, rgba(255,255,255,0) 60%);
    --sos-glass-rim: rgba(255, 255, 255, .2);
    --sos-glass-tint-shadow: 0 12px 34px rgba(0, 0, 0, .45), 0 0 0 1px rgba(0,0,0,.2);
    --sos-glass-tint-shadow-hover: 0 20px 50px rgba(0, 0, 0, .55), 0 0 0 1px rgba(0,0,0,.2);
}

/* --- Milestone 158 — small STATIC (no drift/motion, no fixed/vmax
       sizing) previews of the four backdrop recipes below, for the
       Site settings → Look & feel swatch picker. Same colour source
       (--sos-orb-*) as the live backdrop, just scaled to sit inside a
       small square swatch box rather than the whole viewport — every
       size below is a percentage of that box, not vmax, precisely so a
       swatch looks right whatever pixel size the picker renders it at.
       Put on the swatch element itself (e.g. a small rounded <span>
       inside each radio label); .sos-backdrop-swatch supplies the
       shared box/overflow, the -orbs/-mesh/-aurora/-geometric modifier
       supplies the actual picture. */
.sos-backdrop-swatch {
    display: block;
    width: 100%;
    height: 100%;
    border-radius: inherit;
    overflow: hidden;
}
.sos-backdrop-swatch-orbs {
    background:
        radial-gradient(70% 70% at 20% 25%, color-mix(in srgb, var(--sos-orb-1) 65%, transparent), transparent 70%),
        radial-gradient(65% 65% at 80% 30%, color-mix(in srgb, var(--sos-orb-2) 55%, transparent), transparent 70%),
        radial-gradient(60% 60% at 55% 90%, color-mix(in srgb, var(--sos-orb-3) 55%, transparent), transparent 70%),
        var(--sos-bg);
}
.sos-backdrop-swatch-mesh {
    background:
        radial-gradient(45% 45% at 15% 20%, color-mix(in srgb, var(--sos-orb-1) 48%, transparent), transparent 70%),
        radial-gradient(40% 40% at 45% 60%, color-mix(in srgb, var(--sos-orb-2) 42%, transparent), transparent 70%),
        radial-gradient(45% 45% at 78% 25%, color-mix(in srgb, var(--sos-orb-3) 45%, transparent), transparent 70%),
        radial-gradient(38% 38% at 65% 82%, color-mix(in srgb, var(--sos-orb-1) 32%, var(--sos-orb-3)), transparent 70%),
        var(--sos-bg);
}
.sos-backdrop-swatch-aurora {
    background:
        linear-gradient(120deg, transparent 5%, color-mix(in srgb, var(--sos-orb-1) 55%, transparent) 30%, transparent 55%),
        linear-gradient(120deg, transparent 40%, color-mix(in srgb, var(--sos-orb-2) 48%, transparent) 62%, transparent 85%),
        linear-gradient(120deg, transparent 15%, color-mix(in srgb, var(--sos-orb-3) 45%, transparent) 48%, transparent 78%),
        var(--sos-bg);
}
.sos-backdrop-swatch-geometric {
    background:
        repeating-linear-gradient(45deg, color-mix(in srgb, var(--sos-orb-1) 22%, transparent) 0px, color-mix(in srgb, var(--sos-orb-1) 22%, transparent) 2px, transparent 2px, transparent 14px),
        repeating-linear-gradient(-45deg, color-mix(in srgb, var(--sos-orb-2) 18%, transparent) 0px, color-mix(in srgb, var(--sos-orb-2) 18%, transparent) 2px, transparent 2px, transparent 14px),
        radial-gradient(60% 60% at 25% 20%, color-mix(in srgb, var(--sos-orb-1) 30%, transparent), transparent 70%),
        radial-gradient(55% 55% at 75% 85%, color-mix(in srgb, var(--sos-orb-3) 32%, transparent), transparent 70%),
        var(--sos-bg);
}

/* --- the lit backdrop: base colour on body, orbs + grain on two fixed
       pseudo-elements behind everything (z-index -1) ------------------ */
body.sos-theme-glass {
    background: var(--sos-bg);
    color: #14102a;
    isolation: isolate;
}
[data-theme="dark"] body.sos-theme-glass { color: var(--sos-text); }
body.sos-theme-glass::before,
body.sos-theme-glass::after {
    content: "";
    position: fixed;
    inset: -20vmax;
    z-index: -1;
    pointer-events: none;
    will-change: transform;
}
/* --- Orbs (Milestone 157's original, now the "orbs" backdrop style) - */
body.sos-theme-glass.sos-backdrop-orbs::before {
    background:
        radial-gradient(42vmax 42vmax at 18% 22%, color-mix(in srgb, var(--sos-orb-1) 62%, transparent), transparent 70%),
        radial-gradient(38vmax 38vmax at 82% 30%, color-mix(in srgb, var(--sos-orb-2) 52%, transparent), transparent 70%);
}
body.sos-theme-glass.sos-backdrop-orbs::after {
    background:
        var(--sos-glass-grain),
        radial-gradient(40vmax 40vmax at 55% 92%, color-mix(in srgb, var(--sos-orb-3) 50%, transparent), transparent 70%);
    background-size: 160px 160px, auto;
}
[data-theme="dark"] body.sos-theme-glass.sos-backdrop-orbs::before {
    background:
        radial-gradient(44vmax 44vmax at 18% 22%, color-mix(in srgb, var(--sos-orb-1-dark) 70%, transparent), transparent 70%),
        radial-gradient(40vmax 40vmax at 82% 30%, color-mix(in srgb, var(--sos-orb-2-dark) 60%, transparent), transparent 70%);
}
[data-theme="dark"] body.sos-theme-glass.sos-backdrop-orbs::after {
    background:
        var(--sos-glass-grain),
        radial-gradient(42vmax 42vmax at 55% 92%, color-mix(in srgb, var(--sos-orb-3-dark) 58%, transparent), transparent 70%);
    background-size: 160px 160px, auto;
}

/* --- Mesh (158) — calmer: more, smaller, further-spread soft blobs,
       two of them a blended pair of orbs rather than one solid orb --- */
body.sos-theme-glass.sos-backdrop-mesh::before {
    background:
        radial-gradient(22vmax 22vmax at 15% 20%, color-mix(in srgb, var(--sos-orb-1) 42%, transparent), transparent 70%),
        radial-gradient(20vmax 20vmax at 45% 65%, color-mix(in srgb, var(--sos-orb-2) 38%, transparent), transparent 70%),
        radial-gradient(22vmax 22vmax at 78% 25%, color-mix(in srgb, var(--sos-orb-3) 40%, transparent), transparent 70%),
        radial-gradient(18vmax 18vmax at 65% 82%, color-mix(in srgb, var(--sos-orb-1) 30%, var(--sos-orb-3)), transparent 70%);
}
body.sos-theme-glass.sos-backdrop-mesh::after {
    background:
        var(--sos-glass-grain),
        radial-gradient(20vmax 20vmax at 30% 85%, color-mix(in srgb, var(--sos-orb-2) 36%, transparent), transparent 70%),
        radial-gradient(18vmax 18vmax at 88% 60%, color-mix(in srgb, var(--sos-orb-3) 34%, transparent), transparent 70%),
        radial-gradient(20vmax 20vmax at 8% 55%, color-mix(in srgb, var(--sos-orb-1) 32%, var(--sos-orb-2)), transparent 70%);
    background-size: 160px 160px, auto, auto, auto;
}
[data-theme="dark"] body.sos-theme-glass.sos-backdrop-mesh::before {
    background:
        radial-gradient(24vmax 24vmax at 15% 20%, color-mix(in srgb, var(--sos-orb-1-dark) 52%, transparent), transparent 70%),
        radial-gradient(22vmax 22vmax at 45% 65%, color-mix(in srgb, var(--sos-orb-2-dark) 48%, transparent), transparent 70%),
        radial-gradient(24vmax 24vmax at 78% 25%, color-mix(in srgb, var(--sos-orb-3-dark) 50%, transparent), transparent 70%),
        radial-gradient(20vmax 20vmax at 65% 82%, color-mix(in srgb, var(--sos-orb-1-dark) 38%, var(--sos-orb-3-dark)), transparent 70%);
}
[data-theme="dark"] body.sos-theme-glass.sos-backdrop-mesh::after {
    background:
        var(--sos-glass-grain),
        radial-gradient(22vmax 22vmax at 30% 85%, color-mix(in srgb, var(--sos-orb-2-dark) 46%, transparent), transparent 70%),
        radial-gradient(20vmax 20vmax at 88% 60%, color-mix(in srgb, var(--sos-orb-3-dark) 44%, transparent), transparent 70%),
        radial-gradient(22vmax 22vmax at 8% 55%, color-mix(in srgb, var(--sos-orb-1-dark) 40%, var(--sos-orb-2-dark)), transparent 70%);
    background-size: 160px 160px, auto, auto, auto;
}

/* --- Aurora (158) — dramatic: 2-3 broad soft diagonal bands (plain
       angled linear-gradients — a rotated pseudo-element would fight the
       drift keyframes' own transform, so the angle lives in the gradient
       itself, not in a static transform) reading as flowing colour --- */
body.sos-theme-glass.sos-backdrop-aurora::before {
    background:
        linear-gradient(118deg, transparent 5%, color-mix(in srgb, var(--sos-orb-1) 48%, transparent) 30%, transparent 55%),
        linear-gradient(118deg, transparent 40%, color-mix(in srgb, var(--sos-orb-2) 40%, transparent) 62%, transparent 85%);
}
body.sos-theme-glass.sos-backdrop-aurora::after {
    background:
        var(--sos-glass-grain),
        linear-gradient(118deg, transparent 15%, color-mix(in srgb, var(--sos-orb-3) 42%, transparent) 48%, transparent 78%);
    background-size: 160px 160px, auto;
}
[data-theme="dark"] body.sos-theme-glass.sos-backdrop-aurora::before {
    background:
        linear-gradient(118deg, transparent 5%, color-mix(in srgb, var(--sos-orb-1-dark) 56%, transparent) 30%, transparent 55%),
        linear-gradient(118deg, transparent 40%, color-mix(in srgb, var(--sos-orb-2-dark) 48%, transparent) 62%, transparent 85%);
}
[data-theme="dark"] body.sos-theme-glass.sos-backdrop-aurora::after {
    background:
        var(--sos-glass-grain),
        linear-gradient(118deg, transparent 15%, color-mix(in srgb, var(--sos-orb-3-dark) 50%, transparent) 48%, transparent 78%);
    background-size: 160px 160px, auto;
}

/* --- Geometric (158) — angular/modern: a faint two-direction diagonal
       grid of hairlines plus a couple of soft colour washes, kept
       deliberately less organic than the other three styles --------- */
body.sos-theme-glass.sos-backdrop-geometric::before {
    background:
        repeating-linear-gradient(45deg, color-mix(in srgb, var(--sos-orb-1) 16%, transparent) 0px, color-mix(in srgb, var(--sos-orb-1) 16%, transparent) 1px, transparent 1px, transparent 72px),
        repeating-linear-gradient(-45deg, color-mix(in srgb, var(--sos-orb-2) 14%, transparent) 0px, color-mix(in srgb, var(--sos-orb-2) 14%, transparent) 1px, transparent 1px, transparent 72px),
        radial-gradient(55vmax 55vmax at 25% 20%, color-mix(in srgb, var(--sos-orb-1) 22%, transparent), transparent 70%);
}
body.sos-theme-glass.sos-backdrop-geometric::after {
    background:
        var(--sos-glass-grain),
        radial-gradient(50vmax 50vmax at 75% 85%, color-mix(in srgb, var(--sos-orb-3) 26%, transparent), transparent 70%);
    background-size: 160px 160px, auto;
}
[data-theme="dark"] body.sos-theme-glass.sos-backdrop-geometric::before {
    background:
        repeating-linear-gradient(45deg, color-mix(in srgb, var(--sos-orb-1-dark) 20%, transparent) 0px, color-mix(in srgb, var(--sos-orb-1-dark) 20%, transparent) 1px, transparent 1px, transparent 72px),
        repeating-linear-gradient(-45deg, color-mix(in srgb, var(--sos-orb-2-dark) 18%, transparent) 0px, color-mix(in srgb, var(--sos-orb-2-dark) 18%, transparent) 1px, transparent 1px, transparent 72px),
        radial-gradient(58vmax 58vmax at 25% 20%, color-mix(in srgb, var(--sos-orb-1-dark) 30%, transparent), transparent 70%);
}
[data-theme="dark"] body.sos-theme-glass.sos-backdrop-geometric::after {
    background:
        var(--sos-glass-grain),
        radial-gradient(52vmax 52vmax at 75% 85%, color-mix(in srgb, var(--sos-orb-3-dark) 34%, transparent), transparent 70%);
    background-size: 160px 160px, auto;
}
@keyframes sos-orb-drift-a {
    0%   { transform: translate3d(0, 0, 0) scale(1); }
    33%  { transform: translate3d(4vmax, 3vmax, 0) scale(1.06); }
    66%  { transform: translate3d(-3vmax, 5vmax, 0) scale(.97); }
    100% { transform: translate3d(0, 0, 0) scale(1); }
}
@keyframes sos-orb-drift-b {
    0%   { transform: translate3d(0, 0, 0) scale(1); }
    50%  { transform: translate3d(-5vmax, -3vmax, 0) scale(1.08); }
    100% { transform: translate3d(0, 0, 0) scale(1); }
}
body.sos-theme-glass.sos-glass-motion::before { animation: sos-orb-drift-a 60s ease-in-out infinite; }
body.sos-theme-glass.sos-glass-motion::after  { animation: sos-orb-drift-b 75s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
    body.sos-theme-glass.sos-glass-motion::before,
    body.sos-theme-glass.sos-glass-motion::after { animation: none; }
}

/* --- cards & panels (the legacy classes take the glass recipe) ------ */
body.sos-theme-glass .sos-card,
body.sos-theme-glass .pub-card,
body.sos-theme-glass .adm-stat-card,
body.sos-theme-glass .pub-map-card,
body.sos-theme-glass .pub-hero-panel,
body.sos-theme-glass .card,
body.sos-theme-glass .modal-content,
body.sos-theme-glass .dropdown-menu,
body.sos-theme-glass .sos-msg-panel,
body.sos-theme-glass .adm-dormancy-notice,
body.sos-theme-glass .sos-q-row,
body.sos-theme-glass .sos-glass,
body.sos-theme-glass .sos-glass-card {
    background: var(--sos-glass-sheen), var(--sos-glass-bg);
    -webkit-backdrop-filter: blur(18px) saturate(160%);
    backdrop-filter: blur(18px) saturate(160%);
    border: 1px solid var(--sos-glass-rim);
    box-shadow: var(--sos-glass-tint-shadow), inset 0 1px 0 var(--sos-glass-highlight), inset 0 -1px 0 rgba(255,255,255,.08);
    transition: box-shadow .25s ease, transform .25s ease, border-color .25s ease;
}
body.sos-theme-glass .sos-card,
body.sos-theme-glass .pub-card,
body.sos-theme-glass .adm-stat-card,
body.sos-theme-glass .card { border-radius: 1.1rem; }
body.sos-theme-glass .modal-content { border-radius: 1.1rem; }
body.sos-theme-glass .dropdown-menu { border-radius: .8rem; }
/* the 157 glow corner on .sos-glass-card is now the sheen — drop the old ::before glow */
body.sos-theme-glass .sos-glass-card::before { display: none; }
/* lift on the cards a person hovers over (not the big form cards) */
body.sos-theme-glass .pub-card:hover,
body.sos-theme-glass .adm-stat-card:hover,
body.sos-theme-glass .sos-glass-card:hover {
    transform: translateY(-2px);
    border-color: color-mix(in srgb, #fff 80%, var(--sos-primary));
    box-shadow: var(--sos-glass-tint-shadow-hover), inset 0 1px 0 var(--sos-glass-highlight);
}
[data-theme="dark"] body.sos-theme-glass .pub-card:hover,
[data-theme="dark"] body.sos-theme-glass .adm-stat-card:hover,
[data-theme="dark"] body.sos-theme-glass .sos-glass-card:hover { border-color: color-mix(in srgb, var(--sos-primary) 55%, transparent); }
body.sos-theme-glass .sos-card:hover { box-shadow: var(--sos-glass-tint-shadow-hover), inset 0 1px 0 var(--sos-glass-highlight); }
body.sos-theme-glass .adm-stat-card .adm-stat-value {
    font-size: 2rem; font-weight: 800; letter-spacing: -.02em;
    background: linear-gradient(135deg, var(--sos-primary), var(--sos-secondary));
    -webkit-background-clip: text; background-clip: text; color: transparent;
}
[data-theme="dark"] body.sos-theme-glass .adm-stat-card .adm-stat-value {
    background: linear-gradient(135deg, color-mix(in srgb, var(--sos-primary) 55%, white), color-mix(in srgb, var(--sos-secondary) 60%, white));
    -webkit-background-clip: text; background-clip: text;
}
/* sidebar / nav link colour: brand in light, a lifted brand tint in dark
   so it reads over the orbs */
[data-theme="dark"] body.sos-theme-glass .adm-sidebar a,
[data-theme="dark"] body.sos-theme-glass .staff-sidebar a { color: color-mix(in srgb, var(--sos-primary) 45%, white); }
[data-theme="dark"] body.sos-theme-glass .adm-sidebar a.is-active,
[data-theme="dark"] body.sos-theme-glass .staff-sidebar a.is-active { color: #fff; }
/* dark + glass: anything that uses the brand colour as INK gets a lifted
   tint (a navy or deep-green brand is unreadable on a dark panel) */
[data-theme="dark"] body.sos-theme-glass h1, [data-theme="dark"] body.sos-theme-glass h2, [data-theme="dark"] body.sos-theme-glass h3,
[data-theme="dark"] body.sos-theme-glass h4, [data-theme="dark"] body.sos-theme-glass h5, [data-theme="dark"] body.sos-theme-glass h6,
[data-theme="dark"] body.sos-theme-glass a.sos-link,
[data-theme="dark"] body.sos-theme-glass .pub-nav-links a:not(.btn),
[data-theme="dark"] body.sos-theme-glass .pub-service-price,
[data-theme="dark"] body.sos-theme-glass .pub-step-indicator .is-current,
[data-theme="dark"] body.sos-theme-glass .pub-footer-links a,
[data-theme="dark"] body.sos-theme-glass .pub-prose a,
[data-theme="dark"] body.sos-theme-glass .pub-theme-toggle,
[data-theme="dark"] body.sos-theme-glass .adm-topbar-actions a:not(.btn),
[data-theme="dark"] body.sos-theme-glass .adm-nav-links a,
[data-theme="dark"] body.sos-theme-glass .staff-nav-links a,
[data-theme="dark"] body.sos-theme-glass .adm-appt-card .adm-appt-time,
[data-theme="dark"] body.sos-theme-glass .adm-appt-card-toggle,
[data-theme="dark"] body.sos-theme-glass .adm-month-appt,
[data-theme="dark"] body.sos-theme-glass .adm-month-daynum-btn,
[data-theme="dark"] body.sos-theme-glass .sos-glass-list-cat { color: color-mix(in srgb, var(--sos-primary) 45%, white); }
body.sos-theme-glass h1, body.sos-theme-glass h2, body.sos-theme-glass h3,
body.sos-theme-glass .h1, body.sos-theme-glass .h2, body.sos-theme-glass .h3 { font-weight: 700; letter-spacing: -.01em; }
/* a card inside a card is a section, not another slab of glass */
body.sos-theme-glass .sos-card .sos-card,
body.sos-theme-glass .sos-card .pub-card,
body.sos-theme-glass .sos-card .card,
body.sos-theme-glass .sos-glass-card .sos-card,
body.sos-theme-glass .sos-glass-card .pub-card,
body.sos-theme-glass .sos-card .sos-glass-card,
body.sos-theme-glass .pub-card .sos-glass-card,
body.sos-theme-glass .card .card {
    background: color-mix(in srgb, var(--sos-glass-bg-strong) 45%, transparent);
    -webkit-backdrop-filter: none; backdrop-filter: none;
    box-shadow: none; transform: none;
    border-color: var(--sos-glass-border);
}

/* --- chrome: top bars, sidebar, public nav, footers (tier 1: most
       transparent, most blurred — content visibly slides underneath) --- */
body.sos-theme-glass .adm-topbar,
body.sos-theme-glass .adm-nav-links-mobile {
    background: color-mix(in srgb, var(--sos-glass-bg) 78%, transparent);
    -webkit-backdrop-filter: blur(22px) saturate(170%);
    backdrop-filter: blur(22px) saturate(170%);
    border-bottom: 1px solid var(--sos-glass-rim);
    box-shadow: 0 6px 24px color-mix(in srgb, var(--sos-primary) 8%, transparent);
}
body.sos-theme-glass .adm-topbar { max-width: none; padding-left: max(1rem, calc(50% - 620px + 1rem)); padding-right: max(1rem, calc(50% - 620px + 1rem)); }
@media (min-width: 992px) {
    body.sos-theme-glass .adm-sidebar,
    body.sos-theme-glass .staff-sidebar {
        background: var(--sos-glass-sheen), color-mix(in srgb, var(--sos-glass-bg) 80%, transparent);
        -webkit-backdrop-filter: blur(22px) saturate(170%);
        backdrop-filter: blur(22px) saturate(170%);
        border: 1px solid var(--sos-glass-rim);
        box-shadow: var(--sos-glass-tint-shadow), inset 0 1px 0 var(--sos-glass-highlight);
        border-radius: 1.1rem; padding: .6rem .3rem;
    }
    body.sos-theme-glass .adm-sidebar { margin-top: 1.5rem; }
    body.sos-theme-glass .adm-sidebar a,
    body.sos-theme-glass .staff-sidebar a { padding-left: .55rem; padding-right: .55rem; border-radius: .6rem; transition: background .15s ease, transform .15s ease; }
    body.sos-theme-glass .adm-sidebar a:hover,
    body.sos-theme-glass .staff-sidebar a:hover { background: color-mix(in srgb, var(--sos-primary) 10%, transparent); transform: translateX(2px); }
    body.sos-theme-glass .adm-sidebar a.is-active,
    body.sos-theme-glass .staff-sidebar a.is-active {
        background: linear-gradient(135deg, color-mix(in srgb, var(--sos-primary) 22%, transparent), color-mix(in srgb, var(--sos-secondary) 14%, transparent));
        box-shadow: inset 0 1px 0 var(--sos-glass-highlight);
    }
}
body.sos-theme-glass .staff-eotm-banner { -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); border: 1px solid var(--sos-glass-rim); }
body.sos-theme-glass .pub-nav {
    background: var(--sos-glass-sheen), color-mix(in srgb, var(--sos-glass-bg) 80%, transparent);
    -webkit-backdrop-filter: blur(22px) saturate(170%);
    backdrop-filter: blur(22px) saturate(170%);
    border: 1px solid var(--sos-glass-rim);
    box-shadow: var(--sos-glass-tint-shadow), inset 0 1px 0 var(--sos-glass-highlight);
    border-radius: 999px; padding: .7rem 1.2rem; margin-top: 1rem; margin-bottom: 1rem;
}
body.sos-theme-glass .pub-nav-dropdown,
body.sos-theme-glass .pub-staff-select-list,
body.sos-theme-glass .pub-staff-switch-list {
    background: var(--sos-glass-sheen), var(--sos-glass-bg-strong);
    -webkit-backdrop-filter: blur(20px) saturate(160%);
    backdrop-filter: blur(20px) saturate(160%);
    border: 1px solid var(--sos-glass-rim);
    border-radius: .9rem;
    box-shadow: var(--sos-glass-tint-shadow);
}
body.sos-theme-glass .pub-footer {
    background: color-mix(in srgb, var(--sos-glass-bg) 70%, transparent);
    -webkit-backdrop-filter: blur(22px) saturate(170%);
    backdrop-filter: blur(22px) saturate(170%);
    border-top: 1px solid var(--sos-glass-rim);
}
body.sos-theme-glass .adm-footer {
    background: color-mix(in srgb, var(--sos-glass-bg) 70%, transparent);
    -webkit-backdrop-filter: blur(22px) saturate(170%);
    backdrop-filter: blur(22px) saturate(170%);
    border-top: 1px solid var(--sos-glass-rim);
}
body.sos-theme-glass .pub-hero { border-radius: 1.25rem; overflow: hidden; box-shadow: var(--sos-glass-tint-shadow); }

/* --- tables, forms, pills, badges, alerts (tier 3: no fill, hairlines) */
body.sos-theme-glass .table { --bs-table-bg: transparent; --bs-table-striped-bg: transparent; --bs-table-hover-bg: color-mix(in srgb, var(--sos-primary) 6%, transparent); }
body.sos-theme-glass .table > :not(caption) > * > * { background-color: transparent; border-bottom-color: color-mix(in srgb, var(--sos-glass-rim) 60%, transparent); padding-top: .6rem; padding-bottom: .6rem; }
body.sos-theme-glass .table thead th { color: var(--sos-muted); font-weight: 600; font-size: .78rem; letter-spacing: .05em; text-transform: uppercase; border-bottom-color: color-mix(in srgb, var(--sos-primary) 25%, transparent); }
body.sos-theme-glass .table-striped > tbody > tr:nth-of-type(odd) > * { background-color: transparent; }
body.sos-theme-glass .form-control,
body.sos-theme-glass .form-select,
body.sos-theme-glass .input-group-text {
    background-color: color-mix(in srgb, var(--sos-glass-bg-strong) 70%, transparent);
    border-color: var(--sos-glass-rim);
    color: var(--sos-text);
    border-radius: .7rem;
    box-shadow: inset 0 1px 2px rgba(31, 27, 46, .05);
}
body.sos-theme-glass .form-control:focus,
body.sos-theme-glass .form-select:focus {
    background-color: var(--sos-glass-bg-strong);
    border-color: color-mix(in srgb, var(--sos-primary) 60%, transparent);
    box-shadow: 0 0 0 .25rem color-mix(in srgb, var(--sos-primary) 18%, transparent), 0 6px 18px color-mix(in srgb, var(--sos-primary) 14%, transparent);
    color: var(--sos-text);
}
body.sos-theme-glass .form-control::placeholder { color: var(--sos-muted); opacity: .8; }
body.sos-theme-glass .form-check-input:checked { background-color: var(--sos-primary); border-color: var(--sos-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--sos-primary) 18%, transparent); }
body.sos-theme-glass .nav-pills .nav-link {
    background: color-mix(in srgb, var(--sos-glass-bg-strong) 70%, transparent);
    border: 1px solid var(--sos-glass-rim);
    color: var(--sos-text);
    border-radius: 999px;
    margin-right: .35rem; margin-bottom: .35rem;
}
body.sos-theme-glass .nav-pills .nav-link.active {
    background: linear-gradient(135deg, var(--sos-primary), var(--sos-secondary));
    border-color: transparent; color: #fff;
    box-shadow: 0 6px 18px color-mix(in srgb, var(--sos-primary) 35%, transparent);
}
body.sos-theme-glass .alert {
    -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
    border: 1px solid var(--sos-glass-rim);
    border-radius: .9rem;
}
body.sos-theme-glass .adm-status-badge,
body.sos-theme-glass .badge { box-shadow: inset 0 1px 0 var(--sos-glass-highlight); }

/* --- buttons: the one solid, saturated thing on the page ------------- */
body.sos-theme-glass .btn-sos {
    border-radius: .75rem;
    box-shadow: 0 8px 22px color-mix(in srgb, var(--sos-primary) 38%, transparent), inset 0 1px 0 rgba(255,255,255,.35);
    transition: transform .15s ease, box-shadow .15s ease, filter .15s ease;
}
body.sos-theme-glass .btn-sos:hover, body.sos-theme-glass .btn-sos:focus {
    transform: translateY(-1px);
    box-shadow: 0 12px 28px color-mix(in srgb, var(--sos-primary) 48%, transparent), inset 0 1px 0 rgba(255,255,255,.35);
}
body.sos-theme-glass .btn-sos:active { transform: translateY(0); }
body.sos-theme-glass .btn-outline-secondary,
body.sos-theme-glass .btn-outline-danger,
body.sos-theme-glass .btn-outline-primary,
body.sos-theme-glass .btn-glass { border-radius: .75rem; }
body.sos-theme-glass .btn-outline-secondary,
body.sos-theme-glass .btn-glass {
    background: color-mix(in srgb, var(--sos-glass-bg-strong) 70%, transparent);
    border-color: var(--sos-glass-rim); color: var(--sos-text);
    box-shadow: inset 0 1px 0 var(--sos-glass-highlight);
}
body.sos-theme-glass .btn-outline-secondary:hover,
body.sos-theme-glass .btn-glass:hover { background: color-mix(in srgb, var(--sos-primary) 12%, var(--sos-glass-bg-strong)); color: var(--sos-text); border-color: color-mix(in srgb, var(--sos-primary) 40%, var(--sos-glass-rim)); }
body.sos-theme-glass .pub-slot-btn { background: color-mix(in srgb, var(--sos-glass-bg-strong) 70%, transparent); border-color: var(--sos-glass-rim); border-radius: .7rem; box-shadow: inset 0 1px 0 var(--sos-glass-highlight); }
body.sos-theme-glass .pub-slot-btn:hover { background: color-mix(in srgb, var(--sos-primary) 14%, var(--sos-glass-bg-strong)); transform: translateY(-1px); }

/* --- calendars & lists: translucent tints, NO backdrop-filter on the
       many small cards; the blur belongs to the day column container --- */
body.sos-theme-glass .adm-cal-daycol {
    background: color-mix(in srgb, var(--sos-glass-bg) 60%, transparent);
    -webkit-backdrop-filter: blur(18px) saturate(160%);
    backdrop-filter: blur(18px) saturate(160%);
    border: 1px solid var(--sos-glass-rim);
    border-radius: 1rem; padding: .75rem;
    box-shadow: var(--sos-glass-tint-shadow), inset 0 1px 0 var(--sos-glass-highlight);
}
body.sos-theme-glass .adm-appt-card {
    background: color-mix(in srgb, var(--sos-glass-bg-strong) 75%, transparent);
    border: 1px solid var(--sos-glass-rim);
    box-shadow: inset 0 1px 0 var(--sos-glass-highlight);
}
body.sos-theme-glass .adm-appt-card.adm-appt-tint-booked, body.sos-theme-glass .adm-appt-card.adm-appt-tint-confirmed {
    background: color-mix(in srgb, var(--sos-accent) 26%, var(--sos-glass-bg-strong));
    border-color: color-mix(in srgb, var(--sos-accent) 45%, var(--sos-glass-rim));
}
body.sos-theme-glass .adm-appt-card.adm-appt-tint-completed {
    background: color-mix(in srgb, var(--sos-success) 26%, var(--sos-glass-bg-strong));
    border-color: color-mix(in srgb, var(--sos-success) 45%, var(--sos-glass-rim));
}
body.sos-theme-glass .adm-appt-card.adm-appt-tint-cancelled, body.sos-theme-glass .adm-appt-card.adm-appt-tint-no_show {
    background: color-mix(in srgb, #E11D48 26%, var(--sos-glass-bg-strong));
    border-color: color-mix(in srgb, #E11D48 45%, var(--sos-glass-rim));
}
body.sos-theme-glass .adm-month-cell,
body.sos-theme-glass .adm-client-item,
body.sos-theme-glass .adm-roster-row {
    background: color-mix(in srgb, var(--sos-glass-bg) 55%, transparent);
    border-color: color-mix(in srgb, var(--sos-glass-rim) 60%, transparent);
}
body.sos-theme-glass .adm-month-cell-today { box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--sos-primary) 55%, transparent); }

/* --- setup wizard shell ---------------------------------------------- */
body.sos-theme-glass .sos-step { background: color-mix(in srgb, var(--sos-glass-bg-strong) 70%, transparent); border: 1px solid var(--sos-glass-rim); }

/* Reduced transparency / old engines: the frosting degrades to a solid
   surface, the orbs stay as a plain soft backdrop; the layout never
   depends on backdrop-filter. */
@media (prefers-reduced-transparency: reduce) {
    body.sos-theme-glass .sos-card, body.sos-theme-glass .pub-card, body.sos-theme-glass .adm-stat-card, body.sos-theme-glass .card,
    body.sos-theme-glass .adm-topbar, body.sos-theme-glass .adm-sidebar, body.sos-theme-glass .staff-sidebar, body.sos-theme-glass .pub-nav, body.sos-theme-glass .pub-footer, body.sos-theme-glass .adm-footer,
    body.sos-theme-glass .sos-glass-card, body.sos-theme-glass .sos-glass, body.sos-theme-glass .adm-cal-daycol, body.sos-theme-glass .modal-content, body.sos-theme-glass .dropdown-menu {
        -webkit-backdrop-filter: none; backdrop-filter: none; background: var(--sos-surface);
    }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    body.sos-theme-glass .sos-card, body.sos-theme-glass .pub-card, body.sos-theme-glass .adm-stat-card, body.sos-theme-glass .card,
    body.sos-theme-glass .adm-topbar, body.sos-theme-glass .adm-sidebar, body.sos-theme-glass .staff-sidebar, body.sos-theme-glass .pub-nav, body.sos-theme-glass .pub-footer, body.sos-theme-glass .adm-footer,
    body.sos-theme-glass .sos-glass-card, body.sos-theme-glass .sos-glass, body.sos-theme-glass .adm-cal-daycol, body.sos-theme-glass .modal-content, body.sos-theme-glass .dropdown-menu {
        background: color-mix(in srgb, var(--sos-surface) 92%, transparent);
    }
}

/* ----------------------------------------------------------------------
   Milestone 160 — customer feedback & star reviews. Both the input
   widget (book/review.php's submission form) and the read-only display
   (book/reviews.php's public list, admin/reviews.php's moderation
   queue) reuse .pub-card/.adm-* wrappers for everything except the
   stars themselves, so no new entry is needed in the glass fallback
   lists just above — only genuinely new "frosted container" classes
   need adding there, and this feature introduces none.
   ---------------------------------------------------------------------- */

/* The clickable 1-5 star input — five radio inputs in reverse DOM order
   (5,4,3,2,1) under a row-reverse flex container, so the classic
   "checked/hovered ~ label" general-sibling trick lights up every star
   up to and including the chosen one without any JS at all. */
.sos-star-rating {
    display: flex;
    flex-direction: row-reverse;
    gap: .35rem;
    justify-content: flex-start;
}
.sos-star-rating input { position: absolute; opacity: 0; width: 0; height: 0; }
.sos-star-rating label {
    font-size: 2rem;
    line-height: 1;
    color: var(--sos-border);
    cursor: pointer;
    transition: color .15s, transform .1s;
}
.sos-star-rating label:hover { transform: scale(1.08); }
.sos-star-rating input:checked ~ label,
.sos-star-rating label:hover,
.sos-star-rating label:hover ~ label {
    color: #F5B400;
}
.sos-star-rating input:focus-visible ~ label { outline: 2px solid var(--sos-primary); outline-offset: 2px; border-radius: 4px; }

/* The plain read-only stars — review_render_stars(), includes/reviews.php. */
.sos-star-display { color: #F5B400; letter-spacing: .1rem; }
.sos-star-display .fa-regular { color: var(--sos-border); }

/* Milestone 162 — the aggregate-rating "trust badge" reused across
   several new surfaces (homepage hero, public footer, About us):
   .pub-hero-rating is the hero-specific variant, sitting on the same
   brand-gradient background as .pub-hero itself (both the default and
   Milestone 17's fullscreen-photo variant), so it needs its own
   light-on-dark treatment rather than the plain .sos-star-display
   colours (whose empty-star grey and card-only assumptions would be
   invisible against a gradient or photo). A brighter gold than
   .sos-star-display's own #F5B400 for the filled stars, and translucent
   white (not var(--sos-border), a light grey with no contrast here) for
   the empty ones — same "translucent panel" language as .pub-hero-panel
   just above, but sized as a small pill rather than a full backdrop. */
.pub-hero-rating {
    display: inline-flex; align-items: center; gap: .5rem;
    background: rgba(255, 255, 255, .16);
    border: 1px solid rgba(255, 255, 255, .35);
    border-radius: 999px;
    padding: .4rem 1rem;
    margin: .25rem 0 1rem;
    font-size: .85rem;
    color: #fff;
    text-decoration: none;
}
.pub-hero-rating:hover, .pub-hero-rating:focus { color: #fff; background: rgba(255, 255, 255, .24); }
.pub-hero-rating .sos-star-display { color: #FFD666; letter-spacing: .05rem; }
.pub-hero-rating .sos-star-display .fa-regular { color: rgba(255, 255, 255, .35); }

/* The quieter variant for the public footer and About us — sits on the
   ordinary page background, so the plain .sos-star-display colours
   already read fine; this just adds the small pill/link chrome and a
   centred one-line layout. */
.pub-rating-strip {
    display: flex; align-items: center; justify-content: center; gap: .5rem;
    text-align: center; font-size: .85rem; color: var(--sos-muted);
    text-decoration: none; margin-bottom: 1rem;
}
.pub-rating-strip:hover, .pub-rating-strip:focus { color: var(--sos-text); }
.pub-rating-strip .sos-star-display { font-size: .8rem; }

/* One review in book/reviews.php's public list / admin's moderation
   queue — a slightly denser variant of .pub-card rather than a new
   container recipe. */
.sos-review-item + .sos-review-item { margin-top: 1rem; }
.sos-review-owner-reply {
    margin-top: .75rem;
    padding: .75rem 1rem;
    border-left: 3px solid var(--sos-primary);
    background: var(--sos-bg);
    border-radius: 0 .5rem .5rem 0;
    font-size: .9rem;
}
